wip
This commit is contained in:
258
panda/python/xcp.py
Normal file
258
panda/python/xcp.py
Normal file
@@ -0,0 +1,258 @@
|
||||
import sys
|
||||
import time
|
||||
import struct
|
||||
from enum import IntEnum
|
||||
|
||||
class COMMAND_CODE(IntEnum):
|
||||
CONNECT = 0xFF
|
||||
DISCONNECT = 0xFE
|
||||
GET_STATUS = 0xFD
|
||||
SYNCH = 0xFC
|
||||
GET_COMM_MODE_INFO = 0xFB
|
||||
GET_ID = 0xFA
|
||||
SET_REQUEST = 0xF9
|
||||
GET_SEED = 0xF8
|
||||
UNLOCK = 0xF7
|
||||
SET_MTA = 0xF6
|
||||
UPLOAD = 0xF5
|
||||
SHORT_UPLOAD = 0xF4
|
||||
BUILD_CHECKSUM = 0xF3
|
||||
TRANSPORT_LAYER_CMD = 0xF2
|
||||
USER_CMD = 0xF1
|
||||
DOWNLOAD = 0xF0
|
||||
DOWNLOAD_NEXT = 0xEF
|
||||
DOWNLOAD_MAX = 0xEE
|
||||
SHORT_DOWNLOAD = 0xED
|
||||
MODIFY_BITS = 0xEC
|
||||
SET_CAL_PAGE = 0xEB
|
||||
GET_CAL_PAGE = 0xEA
|
||||
GET_PAG_PROCESSOR_INFO = 0xE9
|
||||
GET_SEGMENT_INFO = 0xE8
|
||||
GET_PAGE_INFO = 0xE7
|
||||
SET_SEGMENT_MODE = 0xE6
|
||||
GET_SEGMENT_MODE = 0xE5
|
||||
COPY_CAL_PAGE = 0xE4
|
||||
CLEAR_DAQ_LIST = 0xE3
|
||||
SET_DAQ_PTR = 0xE2
|
||||
WRITE_DAQ = 0xE1
|
||||
SET_DAQ_LIST_MODE = 0xE0
|
||||
GET_DAQ_LIST_MODE = 0xDF
|
||||
START_STOP_DAQ_LIST = 0xDE
|
||||
START_STOP_SYNCH = 0xDD
|
||||
GET_DAQ_CLOCK = 0xDC
|
||||
READ_DAQ = 0xDB
|
||||
GET_DAQ_PROCESSOR_INFO = 0xDA
|
||||
GET_DAQ_RESOLUTION_INFO = 0xD9
|
||||
GET_DAQ_LIST_INFO = 0xD8
|
||||
GET_DAQ_EVENT_INFO = 0xD7
|
||||
FREE_DAQ = 0xD6
|
||||
ALLOC_DAQ = 0xD5
|
||||
ALLOC_ODT = 0xD4
|
||||
ALLOC_ODT_ENTRY = 0xD3
|
||||
PROGRAM_START = 0xD2
|
||||
PROGRAM_CLEAR = 0xD1
|
||||
PROGRAM = 0xD0
|
||||
PROGRAM_RESET = 0xCF
|
||||
GET_PGM_PROCESSOR_INFO = 0xCE
|
||||
GET_SECTOR_INFO = 0xCD
|
||||
PROGRAM_PREPARE = 0xCC
|
||||
PROGRAM_FORMAT = 0xCB
|
||||
PROGRAM_NEXT = 0xCA
|
||||
PROGRAM_MAX = 0xC9
|
||||
PROGRAM_VERIFY = 0xC8
|
||||
|
||||
ERROR_CODES = {
|
||||
0x00: "Command processor synchronization",
|
||||
0x10: "Command was not executed",
|
||||
0x11: "Command rejected because DAQ is running",
|
||||
0x12: "Command rejected because PGM is running",
|
||||
0x20: "Unknown command or not implemented optional command",
|
||||
0x21: "Command syntax invalid",
|
||||
0x22: "Command syntax valid but command parameter(s) out of range",
|
||||
0x23: "The memory location is write protected",
|
||||
0x24: "The memory location is not accessible",
|
||||
0x25: "Access denied, Seed & Key is required",
|
||||
0x26: "Selected page not available",
|
||||
0x27: "Selected page mode not available",
|
||||
0x28: "Selected segment not valid",
|
||||
0x29: "Sequence error",
|
||||
0x2A: "DAQ configuration not valid",
|
||||
0x30: "Memory overflow error",
|
||||
0x31: "Generic error",
|
||||
0x32: "The slave internal program verify routine detects an error",
|
||||
}
|
||||
|
||||
class CONNECT_MODE(IntEnum):
|
||||
NORMAL = 0x00,
|
||||
USER_DEFINED = 0x01,
|
||||
|
||||
class GET_ID_REQUEST_TYPE(IntEnum):
|
||||
ASCII = 0x00,
|
||||
ASAM_MC2_FILE = 0x01,
|
||||
ASAM_MC2_PATH = 0x02,
|
||||
ASAM_MC2_URL = 0x03,
|
||||
ASAM_MC2_UPLOAD = 0x04,
|
||||
# 128-255 user defined
|
||||
|
||||
class CommandTimeoutError(Exception):
|
||||
pass
|
||||
|
||||
class CommandCounterError(Exception):
|
||||
pass
|
||||
|
||||
class CommandResponseError(Exception):
|
||||
def __init__(self, message, return_code):
|
||||
super().__init__()
|
||||
self.message = message
|
||||
self.return_code = return_code
|
||||
|
||||
def __str__(self):
|
||||
return self.message
|
||||
|
||||
class XcpClient():
|
||||
def __init__(self, panda, tx_addr: int, rx_addr: int, bus: int=0, timeout: float=0.1, debug=False, pad=True):
|
||||
self.tx_addr = tx_addr
|
||||
self.rx_addr = rx_addr
|
||||
self.can_bus = bus
|
||||
self.timeout = timeout
|
||||
self.debug = debug
|
||||
self._panda = panda
|
||||
self._byte_order = ">"
|
||||
self._max_cto = 8
|
||||
self._max_dto = 8
|
||||
self.pad = pad
|
||||
|
||||
def _send_cto(self, cmd: int, dat: bytes = b"") -> None:
|
||||
tx_data = (bytes([cmd]) + dat)
|
||||
|
||||
# Some ECUs don't respond if the packets are not padded to 8 bytes
|
||||
if self.pad:
|
||||
tx_data = tx_data.ljust(8, b"\x00")
|
||||
|
||||
if self.debug:
|
||||
print("CAN-CLEAR: TX")
|
||||
self._panda.can_clear(self.can_bus)
|
||||
if self.debug:
|
||||
print("CAN-CLEAR: RX")
|
||||
self._panda.can_clear(0xFFFF)
|
||||
if self.debug:
|
||||
print(f"CAN-TX: {hex(self.tx_addr)} - 0x{bytes.hex(tx_data)}")
|
||||
self._panda.can_send(self.tx_addr, tx_data, self.can_bus)
|
||||
|
||||
def _recv_dto(self, timeout: float) -> bytes:
|
||||
start_time = time.time()
|
||||
while time.time() - start_time < timeout:
|
||||
msgs = self._panda.can_recv() or []
|
||||
if len(msgs) >= 256:
|
||||
print("CAN RX buffer overflow!!!", file=sys.stderr)
|
||||
for rx_addr, _, rx_data, rx_bus in msgs:
|
||||
if rx_bus == self.can_bus and rx_addr == self.rx_addr:
|
||||
rx_data = bytes(rx_data) # convert bytearray to bytes
|
||||
if self.debug:
|
||||
print(f"CAN-RX: {hex(rx_addr)} - 0x{bytes.hex(rx_data)}")
|
||||
|
||||
pid = rx_data[0]
|
||||
if pid == 0xFE:
|
||||
err = rx_data[1]
|
||||
err_desc = ERROR_CODES.get(err, "unknown error")
|
||||
dat = rx_data[2:]
|
||||
raise CommandResponseError(f"{hex(err)} - {err_desc} {dat}", err)
|
||||
|
||||
return bytes(rx_data[1:])
|
||||
time.sleep(0.001)
|
||||
|
||||
raise CommandTimeoutError("timeout waiting for response")
|
||||
|
||||
# commands
|
||||
def connect(self, connect_mode: CONNECT_MODE=CONNECT_MODE.NORMAL) -> dict:
|
||||
self._send_cto(COMMAND_CODE.CONNECT, bytes([connect_mode]))
|
||||
resp = self._recv_dto(self.timeout)
|
||||
assert len(resp) == 7, f"incorrect data length: {len(resp)}"
|
||||
self._byte_order = ">" if resp[1] & 0x01 else "<"
|
||||
self._slave_block_mode = resp[1] & 0x40 != 0
|
||||
self._max_cto = resp[2]
|
||||
self._max_dto = struct.unpack(f"{self._byte_order}H", resp[3:5])[0]
|
||||
return {
|
||||
"cal_support": resp[0] & 0x01 != 0,
|
||||
"daq_support": resp[0] & 0x04 != 0,
|
||||
"stim_support": resp[0] & 0x08 != 0,
|
||||
"pgm_support": resp[0] & 0x10 != 0,
|
||||
"byte_order": self._byte_order,
|
||||
"address_granularity": 2**((resp[1] & 0x06) >> 1),
|
||||
"slave_block_mode": self._slave_block_mode,
|
||||
"optional": resp[1] & 0x80 != 0,
|
||||
"max_cto": self._max_cto,
|
||||
"max_dto": self._max_dto,
|
||||
"protocol_version": resp[5],
|
||||
"transport_version": resp[6],
|
||||
}
|
||||
|
||||
def disconnect(self) -> None:
|
||||
self._send_cto(COMMAND_CODE.DISCONNECT)
|
||||
resp = self._recv_dto(self.timeout)
|
||||
assert len(resp) == 0, f"incorrect data length: {len(resp)}"
|
||||
|
||||
def get_id(self, req_id_type: GET_ID_REQUEST_TYPE = GET_ID_REQUEST_TYPE.ASCII) -> dict:
|
||||
if req_id_type > 255:
|
||||
raise ValueError("request id type must be less than 255")
|
||||
self._send_cto(COMMAND_CODE.GET_ID, bytes([req_id_type]))
|
||||
resp = self._recv_dto(self.timeout)
|
||||
return {
|
||||
# mode = 0 means MTA was set
|
||||
# mode = 1 means data is at end (only CAN-FD has space for this)
|
||||
"mode": resp[0],
|
||||
"length": struct.unpack(f"{self._byte_order}I", resp[3:7])[0],
|
||||
"identifier": resp[7:] if self._max_cto > 8 else None
|
||||
}
|
||||
|
||||
def get_seed(self, mode: int = 0) -> bytes:
|
||||
if mode > 255:
|
||||
raise ValueError("mode must be less than 255")
|
||||
self._send_cto(COMMAND_CODE.GET_SEED, bytes([0, mode]))
|
||||
|
||||
# TODO: add support for longer seeds spread over multiple blocks
|
||||
ret = self._recv_dto(self.timeout)
|
||||
length = ret[0]
|
||||
return ret[1:length+1]
|
||||
|
||||
def unlock(self, key: bytes) -> bytes:
|
||||
# TODO: add support for longer keys spread over multiple blocks
|
||||
self._send_cto(COMMAND_CODE.UNLOCK, bytes([len(key)]) + key)
|
||||
return self._recv_dto(self.timeout)
|
||||
|
||||
def set_mta(self, addr: int, addr_ext: int = 0) -> bytes:
|
||||
if addr_ext > 255:
|
||||
raise ValueError("address extension must be less than 256")
|
||||
# TODO: this looks broken (missing addr extension)
|
||||
self._send_cto(COMMAND_CODE.SET_MTA, bytes([0x00, 0x00, addr_ext]) + struct.pack(f"{self._byte_order}I", addr))
|
||||
return self._recv_dto(self.timeout)
|
||||
|
||||
def upload(self, size: int) -> bytes:
|
||||
if size > 255:
|
||||
raise ValueError("size must be less than 256")
|
||||
if not self._slave_block_mode and size > self._max_dto - 1:
|
||||
raise ValueError("block mode not supported")
|
||||
|
||||
self._send_cto(COMMAND_CODE.UPLOAD, bytes([size]))
|
||||
resp = b""
|
||||
while len(resp) < size:
|
||||
resp += self._recv_dto(self.timeout)[:size - len(resp) + 1]
|
||||
return resp[:size] # trim off bytes with undefined values
|
||||
|
||||
def short_upload(self, size: int, addr_ext: int, addr: int) -> bytes:
|
||||
if size > 6:
|
||||
raise ValueError("size must be less than 7")
|
||||
if addr_ext > 255:
|
||||
raise ValueError("address extension must be less than 256")
|
||||
self._send_cto(COMMAND_CODE.SHORT_UPLOAD, bytes([size, 0x00, addr_ext]) + struct.pack(f"{self._byte_order}I", addr))
|
||||
return self._recv_dto(self.timeout)[:size] # trim off bytes with undefined values
|
||||
|
||||
def download(self, data: bytes) -> bytes:
|
||||
size = len(data)
|
||||
if size > 255:
|
||||
raise ValueError("size must be less than 256")
|
||||
if not self._slave_block_mode and size > self._max_dto - 2:
|
||||
raise ValueError("block mode not supported")
|
||||
|
||||
self._send_cto(COMMAND_CODE.DOWNLOAD, bytes([size]) + data)
|
||||
return self._recv_dto(self.timeout)[:size]
|
||||
Reference in New Issue
Block a user