Release 260111
This commit is contained in:
871
panda/python/__init__.py
Normal file
871
panda/python/__init__.py
Normal file
@@ -0,0 +1,871 @@
|
||||
# python library to interface with panda
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import usb1
|
||||
import struct
|
||||
import hashlib
|
||||
import binascii
|
||||
from functools import wraps, partial
|
||||
from itertools import accumulate
|
||||
|
||||
from opendbc.car.structs import CarParams
|
||||
|
||||
from .base import BaseHandle
|
||||
from .constants import FW_PATH, McuType
|
||||
from .dfu import PandaDFU
|
||||
from .spi import PandaSpiHandle, PandaSpiException, PandaProtocolMismatch
|
||||
from .usb import PandaUsbHandle
|
||||
from .utils import logger
|
||||
|
||||
__version__ = '0.0.10'
|
||||
|
||||
CANPACKET_HEAD_SIZE = 0x6
|
||||
DLC_TO_LEN = [0, 1, 2, 3, 4, 5, 6, 7, 8, 12, 16, 20, 24, 32, 48, 64]
|
||||
LEN_TO_DLC = {length: dlc for (dlc, length) in enumerate(DLC_TO_LEN)}
|
||||
PANDA_BUS_CNT = 3
|
||||
|
||||
|
||||
def calculate_checksum(data):
|
||||
res = 0
|
||||
for b in data:
|
||||
res ^= b
|
||||
return res
|
||||
|
||||
def pack_can_buffer(arr, fd=False):
|
||||
snds = [b'']
|
||||
for address, dat, bus in arr:
|
||||
assert len(dat) in LEN_TO_DLC
|
||||
#logger.debug(" W 0x%x: 0x%s", address, dat.hex())
|
||||
|
||||
extended = 1 if address >= 0x800 else 0
|
||||
data_len_code = LEN_TO_DLC[len(dat)]
|
||||
header = bytearray(CANPACKET_HEAD_SIZE)
|
||||
word_4b = address << 3 | extended << 2
|
||||
header[0] = (data_len_code << 4) | (bus << 1) | int(fd)
|
||||
header[1] = word_4b & 0xFF
|
||||
header[2] = (word_4b >> 8) & 0xFF
|
||||
header[3] = (word_4b >> 16) & 0xFF
|
||||
header[4] = (word_4b >> 24) & 0xFF
|
||||
header[5] = calculate_checksum(header[:5] + dat)
|
||||
|
||||
snds[-1] += header + dat
|
||||
if len(snds[-1]) > 256: # Limit chunks to 256 bytes
|
||||
snds.append(b'')
|
||||
|
||||
return snds
|
||||
|
||||
def unpack_can_buffer(dat):
|
||||
ret = []
|
||||
|
||||
while len(dat) >= CANPACKET_HEAD_SIZE:
|
||||
data_len = DLC_TO_LEN[(dat[0]>>4)]
|
||||
|
||||
header = dat[:CANPACKET_HEAD_SIZE]
|
||||
|
||||
bus = (header[0] >> 1) & 0x7
|
||||
address = (header[4] << 24 | header[3] << 16 | header[2] << 8 | header[1]) >> 3
|
||||
|
||||
if (header[1] >> 1) & 0x1:
|
||||
# returned
|
||||
bus += 128
|
||||
if header[1] & 0x1:
|
||||
# rejected
|
||||
bus += 192
|
||||
|
||||
# we need more from the next transfer
|
||||
if data_len > len(dat) - CANPACKET_HEAD_SIZE:
|
||||
break
|
||||
|
||||
assert calculate_checksum(dat[:(CANPACKET_HEAD_SIZE+data_len)]) == 0, "CAN packet checksum incorrect"
|
||||
|
||||
data = dat[CANPACKET_HEAD_SIZE:(CANPACKET_HEAD_SIZE+data_len)]
|
||||
dat = dat[(CANPACKET_HEAD_SIZE+data_len):]
|
||||
|
||||
ret.append((address, data, bus))
|
||||
|
||||
return (ret, dat)
|
||||
|
||||
|
||||
def ensure_version(desc, lib_field, panda_field, fn):
|
||||
@wraps(fn)
|
||||
def wrapper(self, *args, **kwargs):
|
||||
lib_version = getattr(self, lib_field)
|
||||
panda_version = getattr(self, panda_field)
|
||||
if lib_version != panda_version:
|
||||
raise RuntimeError(f"{desc} packet version mismatch: panda's firmware v{panda_version}, library v{lib_version}. Reflash panda.")
|
||||
return fn(self, *args, **kwargs)
|
||||
return wrapper
|
||||
ensure_can_packet_version = partial(ensure_version, "CAN", "CAN_PACKET_VERSION", "can_version")
|
||||
ensure_can_health_packet_version = partial(ensure_version, "CAN health", "CAN_HEALTH_PACKET_VERSION", "can_health_version")
|
||||
ensure_health_packet_version = partial(ensure_version, "health", "HEALTH_PACKET_VERSION", "health_version")
|
||||
|
||||
|
||||
|
||||
class Panda:
|
||||
|
||||
SERIAL_DEBUG = 0
|
||||
SERIAL_ESP = 1
|
||||
SERIAL_LIN1 = 2
|
||||
SERIAL_LIN2 = 3
|
||||
SERIAL_SOM_DEBUG = 4
|
||||
|
||||
USB_VIDS = (0xbbaa, 0x3801) # 0x3801 is comma's registered VID
|
||||
USB_PIDS = (0xddee, 0xddcc)
|
||||
REQUEST_IN = usb1.ENDPOINT_IN | usb1.TYPE_VENDOR | usb1.RECIPIENT_DEVICE
|
||||
REQUEST_OUT = usb1.ENDPOINT_OUT | usb1.TYPE_VENDOR | usb1.RECIPIENT_DEVICE
|
||||
|
||||
HW_TYPE_UNKNOWN = b'\x00'
|
||||
HW_TYPE_WHITE_PANDA = b'\x01'
|
||||
HW_TYPE_GREY_PANDA = b'\x02'
|
||||
HW_TYPE_BLACK_PANDA = b'\x03'
|
||||
HW_TYPE_PEDAL = b'\x04'
|
||||
HW_TYPE_UNO = b'\x05'
|
||||
HW_TYPE_DOS = b'\x06'
|
||||
HW_TYPE_RED_PANDA = b'\x07'
|
||||
HW_TYPE_RED_PANDA_V2 = b'\x08'
|
||||
HW_TYPE_TRES = b'\x09'
|
||||
HW_TYPE_CUATRO = b'\x0a'
|
||||
|
||||
CAN_PACKET_VERSION = 4
|
||||
HEALTH_PACKET_VERSION = 16
|
||||
CAN_HEALTH_PACKET_VERSION = 5
|
||||
HEALTH_STRUCT = struct.Struct("<IIIIIIIIBBBBBHBBBHfBBHBHHB")
|
||||
CAN_HEALTH_STRUCT = struct.Struct("<BIBBBBBBBBIIIIIIIHHBBBIIII")
|
||||
|
||||
F4_DEVICES = [HW_TYPE_WHITE_PANDA, HW_TYPE_GREY_PANDA, HW_TYPE_BLACK_PANDA, HW_TYPE_UNO, HW_TYPE_DOS]
|
||||
H7_DEVICES = [HW_TYPE_RED_PANDA, HW_TYPE_RED_PANDA_V2, HW_TYPE_TRES, HW_TYPE_CUATRO]
|
||||
|
||||
INTERNAL_DEVICES = (HW_TYPE_UNO, HW_TYPE_DOS, HW_TYPE_TRES, HW_TYPE_CUATRO)
|
||||
HAS_OBD = (HW_TYPE_BLACK_PANDA, HW_TYPE_UNO, HW_TYPE_DOS, HW_TYPE_RED_PANDA, HW_TYPE_RED_PANDA_V2, HW_TYPE_TRES, HW_TYPE_CUATRO)
|
||||
|
||||
MAX_FAN_RPMs = {
|
||||
HW_TYPE_UNO: 5100,
|
||||
HW_TYPE_DOS: 6500,
|
||||
HW_TYPE_TRES: 6600,
|
||||
HW_TYPE_CUATRO: 6600,
|
||||
}
|
||||
|
||||
HARNESS_STATUS_NC = 0
|
||||
HARNESS_STATUS_NORMAL = 1
|
||||
HARNESS_STATUS_FLIPPED = 2
|
||||
|
||||
def __init__(self, serial: str | None = None, claim: bool = True, disable_checks: bool = True, can_speed_kbps: int = 500, cli: bool = True):
|
||||
self._disable_checks = disable_checks
|
||||
|
||||
self._handle: BaseHandle
|
||||
self._handle_open = False
|
||||
self.can_rx_overflow_buffer = b''
|
||||
self._can_speed_kbps = can_speed_kbps
|
||||
|
||||
if cli and serial is None:
|
||||
self._connect_serial = self._cli_select_panda()
|
||||
else:
|
||||
self._connect_serial = serial
|
||||
|
||||
# connect and set mcu type
|
||||
self.connect(claim)
|
||||
|
||||
def _cli_select_panda(self):
|
||||
dfu_pandas = PandaDFU.list()
|
||||
if len(dfu_pandas) > 0:
|
||||
print("INFO: some attached pandas are in DFU mode.")
|
||||
|
||||
pandas = self.list()
|
||||
if len(pandas) == 0:
|
||||
print("INFO: panda not available")
|
||||
return None
|
||||
if len(pandas) == 1:
|
||||
print(f"INFO: connecting to panda {pandas[0]}")
|
||||
return pandas[0]
|
||||
while True:
|
||||
print("Multiple pandas available:")
|
||||
pandas.sort()
|
||||
for idx, serial in enumerate(pandas):
|
||||
print(f"{[idx]}: {serial}")
|
||||
try:
|
||||
choice = int(input("Choose serial [0]:") or "0")
|
||||
return pandas[choice]
|
||||
except (ValueError, IndexError):
|
||||
print("Enter a valid index.")
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, *args):
|
||||
self.close()
|
||||
|
||||
def close(self):
|
||||
if self._handle_open:
|
||||
self._handle.close()
|
||||
self._handle_open = False
|
||||
if self._context is not None:
|
||||
self._context.close()
|
||||
|
||||
def connect(self, claim=True, wait=False):
|
||||
self.close()
|
||||
|
||||
self._handle = None
|
||||
while self._handle is None:
|
||||
# try USB first, then SPI
|
||||
self._context, self._handle, serial, self.bootstub, bcd = self.usb_connect(self._connect_serial, claim=claim, no_error=wait)
|
||||
if self._handle is None:
|
||||
self._context, self._handle, serial, self.bootstub, bcd = self.spi_connect(self._connect_serial)
|
||||
if not wait:
|
||||
break
|
||||
|
||||
if self._handle is None:
|
||||
raise Exception("failed to connect to panda")
|
||||
|
||||
# Some fallback logic to determine panda and MCU type for old bootstubs,
|
||||
# since we now support multiple MCUs and need to know which fw to flash.
|
||||
# Three cases to consider:
|
||||
# A) oldest bootstubs don't have any way to distinguish
|
||||
# MCU or panda type
|
||||
# B) slightly newer (~2 weeks after first C3's built) bootstubs
|
||||
# have the panda type set in the USB bcdDevice
|
||||
# C) latest bootstubs also implement the endpoint for panda type
|
||||
self._bcd_hw_type = None
|
||||
ret = self._handle.controlRead(Panda.REQUEST_IN, 0xc1, 0, 0, 0x40)
|
||||
missing_hw_type_endpoint = self.bootstub and ret.startswith(b'\xff\x00\xc1\x3e\xde\xad\xd0\x0d')
|
||||
if missing_hw_type_endpoint and bcd is not None:
|
||||
self._bcd_hw_type = bcd
|
||||
|
||||
# For case A, we assume F4 MCU type, since all H7 pandas should be case B at worst
|
||||
self._assume_f4_mcu = (self._bcd_hw_type is None) and missing_hw_type_endpoint
|
||||
|
||||
self._serial = serial
|
||||
self._connect_serial = serial
|
||||
self._handle_open = True
|
||||
self._mcu_type = self.get_mcu_type()
|
||||
self.health_version, self.can_version, self.can_health_version = self.get_packets_versions()
|
||||
logger.debug("connected")
|
||||
|
||||
# disable openpilot's heartbeat checks
|
||||
if self._disable_checks:
|
||||
self.set_heartbeat_disabled()
|
||||
self.set_power_save(0)
|
||||
|
||||
# reset comms
|
||||
self.can_reset_communications()
|
||||
|
||||
# disable automatic CAN-FD switching
|
||||
#for bus in range(PANDA_BUS_CNT):
|
||||
# self.set_canfd_auto(bus, False)
|
||||
|
||||
# set CAN speed
|
||||
for bus in range(PANDA_BUS_CNT):
|
||||
self.set_can_speed_kbps(bus, self._can_speed_kbps)
|
||||
|
||||
@property
|
||||
def spi(self) -> bool:
|
||||
return isinstance(self._handle, PandaSpiHandle)
|
||||
|
||||
@classmethod
|
||||
def spi_connect(cls, serial, ignore_version=False):
|
||||
# get UID to confirm slave is present and up
|
||||
handle = None
|
||||
spi_serial = None
|
||||
bootstub = None
|
||||
spi_version = None
|
||||
try:
|
||||
handle = PandaSpiHandle()
|
||||
|
||||
# connect by protcol version
|
||||
try:
|
||||
dat = handle.get_protocol_version()
|
||||
spi_serial = binascii.hexlify(dat[:12]).decode()
|
||||
pid = dat[13]
|
||||
if pid not in (0xcc, 0xee):
|
||||
raise PandaSpiException("invalid bootstub status")
|
||||
bootstub = pid == 0xee
|
||||
spi_version = dat[14]
|
||||
except PandaSpiException:
|
||||
# fallback, we'll raise a protocol mismatch below
|
||||
dat = handle.controlRead(Panda.REQUEST_IN, 0xc3, 0, 0, 12, timeout=100)
|
||||
spi_serial = binascii.hexlify(dat).decode()
|
||||
bootstub = Panda.flasher_present(handle)
|
||||
spi_version = 0
|
||||
except PandaSpiException:
|
||||
pass
|
||||
|
||||
# no connection or wrong panda
|
||||
if None in (spi_serial, bootstub) or (serial is not None and (spi_serial != serial)):
|
||||
handle = None
|
||||
spi_serial = None
|
||||
bootstub = False
|
||||
|
||||
# ensure our protocol version matches the panda
|
||||
if handle is not None and not ignore_version:
|
||||
if spi_version != handle.PROTOCOL_VERSION:
|
||||
err = f"panda protocol mismatch: expected {handle.PROTOCOL_VERSION}, got {spi_version}. reflash panda"
|
||||
raise PandaProtocolMismatch(err)
|
||||
|
||||
return None, handle, spi_serial, bootstub, None
|
||||
|
||||
@classmethod
|
||||
def usb_connect(cls, serial, claim=True, no_error=False):
|
||||
handle, usb_serial, bootstub, bcd = None, None, None, None
|
||||
context = usb1.USBContext()
|
||||
context.open()
|
||||
try:
|
||||
for device in context.getDeviceList(skip_on_error=True):
|
||||
if device.getVendorID() in cls.USB_VIDS and device.getProductID() in cls.USB_PIDS:
|
||||
try:
|
||||
this_serial = device.getSerialNumber()
|
||||
except Exception:
|
||||
# Allow to ignore errors on reconnect. USB hubs need some time to initialize after panda reset
|
||||
if not no_error:
|
||||
logger.exception("failed to get serial number of panda")
|
||||
continue
|
||||
|
||||
if serial is None or this_serial == serial:
|
||||
logger.debug("opening device %s %s", this_serial, hex(device.getProductID()))
|
||||
|
||||
usb_serial = this_serial
|
||||
bootstub = (device.getProductID() & 0xF0) == 0xe0
|
||||
handle = device.open()
|
||||
if sys.platform not in ("win32", "cygwin", "msys", "darwin"):
|
||||
handle.setAutoDetachKernelDriver(True)
|
||||
if claim:
|
||||
handle.claimInterface(0)
|
||||
# handle.setInterfaceAltSetting(0, 0) # Issue in USB stack
|
||||
|
||||
# bcdDevice wasn't always set to the hw type, ignore if it's the old constant
|
||||
this_bcd = device.getbcdDevice()
|
||||
if this_bcd is not None and this_bcd != 0x2300:
|
||||
bcd = bytearray([this_bcd >> 8, ])
|
||||
|
||||
break
|
||||
except Exception:
|
||||
logger.exception("USB connect error")
|
||||
|
||||
usb_handle = None
|
||||
if handle is not None:
|
||||
usb_handle = PandaUsbHandle(handle)
|
||||
else:
|
||||
context.close()
|
||||
|
||||
return context, usb_handle, usb_serial, bootstub, bcd
|
||||
|
||||
def is_connected_spi(self):
|
||||
return isinstance(self._handle, PandaSpiHandle)
|
||||
|
||||
def is_connected_usb(self):
|
||||
return isinstance(self._handle, PandaUsbHandle)
|
||||
|
||||
@classmethod
|
||||
def list(cls):
|
||||
ret = cls.usb_list()
|
||||
ret += cls.spi_list()
|
||||
return list(set(ret))
|
||||
|
||||
@classmethod
|
||||
def usb_list(cls):
|
||||
ret = []
|
||||
try:
|
||||
with usb1.USBContext() as context:
|
||||
for device in context.getDeviceList(skip_on_error=True):
|
||||
if device.getVendorID() in cls.USB_VIDS and device.getProductID() in cls.USB_PIDS:
|
||||
try:
|
||||
serial = device.getSerialNumber()
|
||||
if len(serial) == 24:
|
||||
ret.append(serial)
|
||||
else:
|
||||
logger.warning(f"found device with panda descriptors but invalid serial: {serial}", RuntimeWarning)
|
||||
except Exception:
|
||||
logger.exception("error connecting to panda")
|
||||
except Exception:
|
||||
logger.exception("exception while listing pandas")
|
||||
return ret
|
||||
|
||||
@classmethod
|
||||
def spi_list(cls):
|
||||
_, _, serial, _, _ = cls.spi_connect(None, ignore_version=True)
|
||||
if serial is not None:
|
||||
return [serial, ]
|
||||
return []
|
||||
|
||||
def reset(self, enter_bootstub=False, enter_bootloader=False, reconnect=True):
|
||||
# no response is expected since it resets right away
|
||||
timeout = 5000 if isinstance(self._handle, PandaSpiHandle) else 15000
|
||||
try:
|
||||
if enter_bootloader:
|
||||
self._handle.controlWrite(Panda.REQUEST_IN, 0xd1, 0, 0, b'', timeout=timeout, expect_disconnect=True)
|
||||
else:
|
||||
if enter_bootstub:
|
||||
self._handle.controlWrite(Panda.REQUEST_IN, 0xd1, 1, 0, b'', timeout=timeout, expect_disconnect=True)
|
||||
else:
|
||||
self._handle.controlWrite(Panda.REQUEST_IN, 0xd8, 0, 0, b'', timeout=timeout, expect_disconnect=True)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
self.close()
|
||||
if not enter_bootloader and reconnect:
|
||||
self.reconnect()
|
||||
|
||||
@property
|
||||
def connected(self) -> bool:
|
||||
return self._handle_open
|
||||
|
||||
def reconnect(self):
|
||||
if self._handle_open:
|
||||
self.close()
|
||||
|
||||
success = False
|
||||
# wait up to 15 seconds
|
||||
for _ in range(15*10):
|
||||
try:
|
||||
self.connect(claim=False, wait=True)
|
||||
success = True
|
||||
break
|
||||
except Exception:
|
||||
pass
|
||||
time.sleep(0.1)
|
||||
if not success:
|
||||
raise Exception("reconnect failed")
|
||||
|
||||
@staticmethod
|
||||
def flasher_present(handle: BaseHandle) -> bool:
|
||||
fr = handle.controlRead(Panda.REQUEST_IN, 0xb0, 0, 0, 0xc)
|
||||
return fr[4:8] == b"\xde\xad\xd0\x0d"
|
||||
|
||||
@staticmethod
|
||||
def flash_static(handle, code, mcu_type):
|
||||
assert mcu_type is not None, "must set valid mcu_type to flash"
|
||||
|
||||
# confirm flasher is present
|
||||
assert Panda.flasher_present(handle)
|
||||
|
||||
# determine sectors to erase
|
||||
apps_sectors_cumsum = accumulate(mcu_type.config.sector_sizes[1:])
|
||||
last_sector = next((i + 1 for i, v in enumerate(apps_sectors_cumsum) if v > len(code)), -1)
|
||||
assert last_sector >= 1, "Binary too small? No sector to erase."
|
||||
assert last_sector < 7, "Binary too large! Risk of overwriting provisioning chunk."
|
||||
|
||||
# unlock flash
|
||||
logger.info("flash: unlocking")
|
||||
handle.controlWrite(Panda.REQUEST_IN, 0xb1, 0, 0, b'')
|
||||
|
||||
# erase sectors
|
||||
logger.info(f"flash: erasing sectors 1 - {last_sector}")
|
||||
for i in range(1, last_sector + 1):
|
||||
handle.controlWrite(Panda.REQUEST_IN, 0xb2, i, 0, b'')
|
||||
|
||||
# flash over EP2
|
||||
STEP = 0x10
|
||||
logger.info("flash: flashing")
|
||||
for i in range(0, len(code), STEP):
|
||||
handle.bulkWrite(2, code[i:i + STEP])
|
||||
|
||||
# reset
|
||||
logger.info("flash: resetting")
|
||||
try:
|
||||
handle.controlWrite(Panda.REQUEST_IN, 0xd8, 0, 0, b'', expect_disconnect=True)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def flash(self, fn=None, code=None, reconnect=True):
|
||||
if self.up_to_date(fn=fn):
|
||||
logger.info("flash: already up to date")
|
||||
return
|
||||
|
||||
if not fn:
|
||||
fn = os.path.join(FW_PATH, self._mcu_type.config.app_fn)
|
||||
assert os.path.isfile(fn)
|
||||
logger.debug("flash: main version is %s", self.get_version())
|
||||
if not self.bootstub:
|
||||
self.reset(enter_bootstub=True)
|
||||
assert(self.bootstub)
|
||||
|
||||
if code is None:
|
||||
with open(fn, "rb") as f:
|
||||
code = f.read()
|
||||
|
||||
# get version
|
||||
logger.debug("flash: bootstub version is %s", self.get_version())
|
||||
|
||||
# do flash
|
||||
Panda.flash_static(self._handle, code, mcu_type=self._mcu_type)
|
||||
|
||||
# reconnect
|
||||
if reconnect:
|
||||
self.reconnect()
|
||||
|
||||
def recover(self, timeout: int | None = 60, reset: bool = True) -> bool:
|
||||
dfu_serial = self.get_dfu_serial()
|
||||
|
||||
if reset:
|
||||
self.reset(enter_bootstub=True)
|
||||
self.reset(enter_bootloader=True)
|
||||
|
||||
if not self.wait_for_dfu(dfu_serial, timeout=timeout):
|
||||
return False
|
||||
|
||||
dfu = PandaDFU(dfu_serial)
|
||||
dfu.recover()
|
||||
|
||||
# reflash after recover
|
||||
self.connect(True, True)
|
||||
self.flash()
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def wait_for_dfu(dfu_serial: str | None, timeout: int | None = None) -> bool:
|
||||
t_start = time.monotonic()
|
||||
dfu_list = PandaDFU.list()
|
||||
while (dfu_serial is None and len(dfu_list) == 0) or (dfu_serial is not None and dfu_serial not in dfu_list):
|
||||
logger.debug("waiting for DFU...")
|
||||
time.sleep(0.1)
|
||||
if timeout is not None and (time.monotonic() - t_start) > timeout:
|
||||
return False
|
||||
dfu_list = PandaDFU.list()
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def wait_for_panda(serial: str | None, timeout: int) -> bool:
|
||||
t_start = time.monotonic()
|
||||
serials = Panda.list()
|
||||
while (serial is None and len(serials) == 0) or (serial is not None and serial not in serials):
|
||||
logger.debug("waiting for panda...")
|
||||
time.sleep(0.1)
|
||||
if timeout is not None and (time.monotonic() - t_start) > timeout:
|
||||
return False
|
||||
serials = Panda.list()
|
||||
return True
|
||||
|
||||
def up_to_date(self, fn=None) -> bool:
|
||||
current = self.get_signature()
|
||||
if fn is None:
|
||||
fn = os.path.join(FW_PATH, self.get_mcu_type().config.app_fn)
|
||||
expected = Panda.get_signature_from_firmware(fn)
|
||||
return (current == expected)
|
||||
|
||||
def call_control_api(self, msg):
|
||||
self._handle.controlWrite(Panda.REQUEST_OUT, msg, 0, 0, b'')
|
||||
|
||||
# ******************* health *******************
|
||||
|
||||
@ensure_health_packet_version
|
||||
def health(self):
|
||||
dat = self._handle.controlRead(Panda.REQUEST_IN, 0xd2, 0, 0, self.HEALTH_STRUCT.size)
|
||||
a = self.HEALTH_STRUCT.unpack(dat)
|
||||
return {
|
||||
"uptime": a[0],
|
||||
"voltage": a[1],
|
||||
"current": a[2],
|
||||
"safety_tx_blocked": a[3],
|
||||
"safety_rx_invalid": a[4],
|
||||
"tx_buffer_overflow": a[5],
|
||||
"rx_buffer_overflow": a[6],
|
||||
"faults": a[7],
|
||||
"ignition_line": a[8],
|
||||
"ignition_can": a[9],
|
||||
"controls_allowed": a[10],
|
||||
"car_harness_status": a[11],
|
||||
"safety_mode": a[12],
|
||||
"safety_param": a[13],
|
||||
"fault_status": a[14],
|
||||
"power_save_enabled": a[15],
|
||||
"heartbeat_lost": a[16],
|
||||
"alternative_experience": a[17],
|
||||
"interrupt_load": a[18],
|
||||
"fan_power": a[19],
|
||||
"safety_rx_checks_invalid": a[20],
|
||||
"spi_checksum_error_count": a[21],
|
||||
"fan_stall_count": a[22],
|
||||
"sbu1_voltage_mV": a[23],
|
||||
"sbu2_voltage_mV": a[24],
|
||||
"som_reset_triggered": a[25],
|
||||
}
|
||||
|
||||
@ensure_can_health_packet_version
|
||||
def can_health(self, can_number):
|
||||
LEC_ERROR_CODE = {
|
||||
0: "No error",
|
||||
1: "Stuff error",
|
||||
2: "Form error",
|
||||
3: "AckError",
|
||||
4: "Bit1Error",
|
||||
5: "Bit0Error",
|
||||
6: "CRCError",
|
||||
7: "NoChange",
|
||||
}
|
||||
dat = self._handle.controlRead(Panda.REQUEST_IN, 0xc2, int(can_number), 0, self.CAN_HEALTH_STRUCT.size)
|
||||
a = self.CAN_HEALTH_STRUCT.unpack(dat)
|
||||
return {
|
||||
"bus_off": a[0],
|
||||
"bus_off_cnt": a[1],
|
||||
"error_warning": a[2],
|
||||
"error_passive": a[3],
|
||||
"last_error": LEC_ERROR_CODE[a[4]],
|
||||
"last_stored_error": LEC_ERROR_CODE[a[5]],
|
||||
"last_data_error": LEC_ERROR_CODE[a[6]],
|
||||
"last_data_stored_error": LEC_ERROR_CODE[a[7]],
|
||||
"receive_error_cnt": a[8],
|
||||
"transmit_error_cnt": a[9],
|
||||
"total_error_cnt": a[10],
|
||||
"total_tx_lost_cnt": a[11],
|
||||
"total_rx_lost_cnt": a[12],
|
||||
"total_tx_cnt": a[13],
|
||||
"total_rx_cnt": a[14],
|
||||
"total_fwd_cnt": a[15],
|
||||
"total_tx_checksum_error_cnt": a[16],
|
||||
"can_speed": a[17],
|
||||
"can_data_speed": a[18],
|
||||
"canfd_enabled": a[19],
|
||||
"brs_enabled": a[20],
|
||||
"canfd_non_iso": a[21],
|
||||
"irq0_call_rate": a[22],
|
||||
"irq1_call_rate": a[23],
|
||||
"irq2_call_rate": a[24],
|
||||
"can_core_reset_count": a[25],
|
||||
}
|
||||
|
||||
# ******************* control *******************
|
||||
|
||||
def get_version(self):
|
||||
return self._handle.controlRead(Panda.REQUEST_IN, 0xd6, 0, 0, 0x40).decode('utf8')
|
||||
|
||||
@staticmethod
|
||||
def get_signature_from_firmware(fn) -> bytes:
|
||||
with open(fn, 'rb') as f:
|
||||
f.seek(-128, 2) # Seek from end of file
|
||||
return f.read(128)
|
||||
|
||||
def get_signature(self) -> bytes:
|
||||
part_1 = self._handle.controlRead(Panda.REQUEST_IN, 0xd3, 0, 0, 0x40)
|
||||
part_2 = self._handle.controlRead(Panda.REQUEST_IN, 0xd4, 0, 0, 0x40)
|
||||
return bytes(part_1 + part_2)
|
||||
|
||||
def get_type(self):
|
||||
ret = self._handle.controlRead(Panda.REQUEST_IN, 0xc1, 0, 0, 0x40)
|
||||
|
||||
# old bootstubs don't implement this endpoint, see comment in Panda.device
|
||||
if self._bcd_hw_type is not None and (ret is None or len(ret) != 1):
|
||||
ret = self._bcd_hw_type
|
||||
|
||||
return ret
|
||||
|
||||
# Returns tuple with health packet version and CAN packet/USB packet version
|
||||
def get_packets_versions(self):
|
||||
dat = self._handle.controlRead(Panda.REQUEST_IN, 0xdd, 0, 0, 3)
|
||||
if dat and len(dat) == 3:
|
||||
a = struct.unpack("BBB", dat)
|
||||
return (a[0], a[1], a[2])
|
||||
else:
|
||||
return (0, 0, 0)
|
||||
|
||||
def get_mcu_type(self) -> McuType:
|
||||
hw_type = self.get_type()
|
||||
if hw_type in Panda.F4_DEVICES:
|
||||
return McuType.F4
|
||||
elif hw_type in Panda.H7_DEVICES:
|
||||
return McuType.H7
|
||||
else:
|
||||
# have to assume F4, see comment in Panda.connect
|
||||
if self._assume_f4_mcu:
|
||||
return McuType.F4
|
||||
|
||||
raise ValueError(f"unknown HW type: {hw_type}")
|
||||
|
||||
def has_obd(self):
|
||||
return self.get_type() in Panda.HAS_OBD
|
||||
|
||||
def is_internal(self):
|
||||
return self.get_type() in Panda.INTERNAL_DEVICES
|
||||
|
||||
def get_serial(self):
|
||||
"""
|
||||
Returns the comma-issued dongle ID from our provisioning
|
||||
"""
|
||||
dat = self._handle.controlRead(Panda.REQUEST_IN, 0xd0, 0, 0, 0x20)
|
||||
hashsig, calc_hash = dat[0x1c:], hashlib.sha1(dat[0:0x1c]).digest()[0:4]
|
||||
assert(hashsig == calc_hash)
|
||||
return [dat[0:0x10].decode("utf8"), dat[0x10:0x10 + 10].decode("utf8")]
|
||||
|
||||
def get_usb_serial(self):
|
||||
"""
|
||||
Returns the serial number reported from the USB descriptor;
|
||||
matches the MCU UID
|
||||
"""
|
||||
return self._serial
|
||||
|
||||
def get_dfu_serial(self):
|
||||
return PandaDFU.st_serial_to_dfu_serial(self._serial, self._mcu_type)
|
||||
|
||||
def get_uid(self):
|
||||
"""
|
||||
Returns the UID from the MCU
|
||||
"""
|
||||
dat = self._handle.controlRead(Panda.REQUEST_IN, 0xc3, 0, 0, 12)
|
||||
return binascii.hexlify(dat).decode()
|
||||
|
||||
def get_secret(self):
|
||||
return self._handle.controlRead(Panda.REQUEST_IN, 0xd0, 1, 0, 0x10)
|
||||
|
||||
def get_interrupt_call_rate(self, irqnum):
|
||||
dat = self._handle.controlRead(Panda.REQUEST_IN, 0xc4, int(irqnum), 0, 4)
|
||||
return struct.unpack("I", dat)[0]
|
||||
|
||||
# ******************* configuration *******************
|
||||
|
||||
def set_alternative_experience(self, alternative_experience):
|
||||
self._handle.controlWrite(Panda.REQUEST_OUT, 0xdf, int(alternative_experience), 0, b'')
|
||||
|
||||
def set_power_save(self, power_save_enabled=0):
|
||||
self._handle.controlWrite(Panda.REQUEST_OUT, 0xe7, int(power_save_enabled), 0, b'')
|
||||
|
||||
def set_safety_mode(self, mode=CarParams.SafetyModel.silent, param=0):
|
||||
self._handle.controlWrite(Panda.REQUEST_OUT, 0xdc, mode, param, b'')
|
||||
|
||||
def set_obd(self, obd):
|
||||
self._handle.controlWrite(Panda.REQUEST_OUT, 0xdb, int(obd), 0, b'')
|
||||
|
||||
def set_can_loopback(self, enable):
|
||||
# set can loopback mode for all buses
|
||||
self._handle.controlWrite(Panda.REQUEST_OUT, 0xe5, int(enable), 0, b'')
|
||||
|
||||
def set_can_enable(self, bus_num, enable):
|
||||
# sets the can transceiver enable pin
|
||||
self._handle.controlWrite(Panda.REQUEST_OUT, 0xf4, int(bus_num), int(enable), b'')
|
||||
|
||||
def set_can_speed_kbps(self, bus, speed):
|
||||
self._handle.controlWrite(Panda.REQUEST_OUT, 0xde, bus, int(speed * 10), b'')
|
||||
|
||||
def set_can_data_speed_kbps(self, bus, speed):
|
||||
self._handle.controlWrite(Panda.REQUEST_OUT, 0xf9, bus, int(speed * 10), b'')
|
||||
|
||||
def set_canfd_non_iso(self, bus, non_iso):
|
||||
self._handle.controlWrite(Panda.REQUEST_OUT, 0xfc, bus, int(non_iso), b'')
|
||||
|
||||
def set_canfd_auto(self, bus, auto):
|
||||
self._handle.controlWrite(Panda.REQUEST_OUT, 0xe8, bus, int(auto), b'')
|
||||
|
||||
def set_uart_baud(self, uart, rate):
|
||||
self._handle.controlWrite(Panda.REQUEST_OUT, 0xe4, uart, int(rate / 300), b'')
|
||||
|
||||
def set_uart_parity(self, uart, parity):
|
||||
# parity, 0=off, 1=even, 2=odd
|
||||
self._handle.controlWrite(Panda.REQUEST_OUT, 0xe2, uart, parity, b'')
|
||||
|
||||
def set_uart_callback(self, uart, install):
|
||||
self._handle.controlWrite(Panda.REQUEST_OUT, 0xe3, uart, int(install), b'')
|
||||
|
||||
# ******************* can *******************
|
||||
|
||||
# The panda will NAK CAN writes when there is CAN congestion.
|
||||
# libusb will try to send it again, with a max timeout.
|
||||
# Timeout is in ms. If set to 0, the timeout is infinite.
|
||||
CAN_SEND_TIMEOUT_MS = 10
|
||||
|
||||
def can_reset_communications(self):
|
||||
self._handle.controlWrite(Panda.REQUEST_OUT, 0xc0, 0, 0, b'')
|
||||
|
||||
@ensure_can_packet_version
|
||||
def can_send_many(self, arr, *, fd=False, timeout=CAN_SEND_TIMEOUT_MS):
|
||||
snds = pack_can_buffer(arr, fd=fd)
|
||||
for tx in snds:
|
||||
while len(tx) > 0:
|
||||
bs = self._handle.bulkWrite(3, tx, timeout=timeout)
|
||||
tx = tx[bs:]
|
||||
|
||||
def can_send(self, addr, dat, bus, *, fd=False, timeout=CAN_SEND_TIMEOUT_MS):
|
||||
self.can_send_many([[addr, dat, bus]], fd=fd, timeout=timeout)
|
||||
|
||||
@ensure_can_packet_version
|
||||
def can_recv(self):
|
||||
dat = bytearray()
|
||||
while True:
|
||||
try:
|
||||
dat = self._handle.bulkRead(1, 16384) # Max receive batch size + 2 extra reserve frames
|
||||
break
|
||||
except (usb1.USBErrorIO, usb1.USBErrorOverflow):
|
||||
logger.error("CAN: BAD RECV, RETRYING")
|
||||
time.sleep(0.1)
|
||||
msgs, self.can_rx_overflow_buffer = unpack_can_buffer(self.can_rx_overflow_buffer + dat)
|
||||
return msgs
|
||||
|
||||
def can_clear(self, bus):
|
||||
"""Clears all messages from the specified internal CAN ringbuffer as
|
||||
though it were drained.
|
||||
|
||||
Args:
|
||||
bus (int): can bus number to clear a tx queue, or 0xFFFF to clear the
|
||||
global can rx queue.
|
||||
|
||||
"""
|
||||
self._handle.controlWrite(Panda.REQUEST_OUT, 0xf1, bus, 0, b'')
|
||||
|
||||
# ******************* serial *******************
|
||||
|
||||
def serial_read(self, port_number):
|
||||
ret = []
|
||||
while 1:
|
||||
lret = bytes(self._handle.controlRead(Panda.REQUEST_IN, 0xe0, port_number, 0, 0x40))
|
||||
if len(lret) == 0:
|
||||
break
|
||||
ret.append(lret)
|
||||
return b''.join(ret)
|
||||
|
||||
def serial_write(self, port_number, ln):
|
||||
ret = 0
|
||||
if isinstance(ln, str):
|
||||
ln = bytes(ln, 'utf-8')
|
||||
for i in range(0, len(ln), 0x20):
|
||||
ret += self._handle.bulkWrite(2, struct.pack("B", port_number) + ln[i:i + 0x20])
|
||||
return ret
|
||||
|
||||
def serial_clear(self, port_number):
|
||||
"""Clears all messages (tx and rx) from the specified internal uart
|
||||
ringbuffer as though it were drained.
|
||||
|
||||
Args:
|
||||
port_number (int): port number of the uart to clear.
|
||||
|
||||
"""
|
||||
self._handle.controlWrite(Panda.REQUEST_OUT, 0xf2, port_number, 0, b'')
|
||||
|
||||
def send_heartbeat(self, engaged=True):
|
||||
self._handle.controlWrite(Panda.REQUEST_OUT, 0xf3, engaged, 0, b'')
|
||||
|
||||
# disable heartbeat checks for use outside of openpilot
|
||||
# sending a heartbeat will reenable the checks
|
||||
def set_heartbeat_disabled(self):
|
||||
self._handle.controlWrite(Panda.REQUEST_OUT, 0xf8, 0, 0, b'')
|
||||
|
||||
# ****************** Timer *****************
|
||||
def get_microsecond_timer(self):
|
||||
dat = self._handle.controlRead(Panda.REQUEST_IN, 0xa8, 0, 0, 4)
|
||||
return struct.unpack("I", dat)[0]
|
||||
|
||||
# ******************* IR *******************
|
||||
def set_ir_power(self, percentage):
|
||||
self._handle.controlWrite(Panda.REQUEST_OUT, 0xb0, int(percentage), 0, b'')
|
||||
|
||||
# ******************* Fan ******************
|
||||
def set_fan_power(self, percentage):
|
||||
self._handle.controlWrite(Panda.REQUEST_OUT, 0xb1, int(percentage), 0, b'')
|
||||
|
||||
def get_fan_rpm(self):
|
||||
dat = self._handle.controlRead(Panda.REQUEST_IN, 0xb2, 0, 0, 2)
|
||||
a = struct.unpack("H", dat)
|
||||
return a[0]
|
||||
|
||||
# ****************** Siren *****************
|
||||
def set_siren(self, enabled):
|
||||
self._handle.controlWrite(Panda.REQUEST_OUT, 0xf6, int(enabled), 0, b'')
|
||||
|
||||
# ****************** Debug *****************
|
||||
def set_green_led(self, enabled):
|
||||
self._handle.controlWrite(Panda.REQUEST_OUT, 0xf7, int(enabled), 0, b'')
|
||||
|
||||
def set_clock_source_period(self, period):
|
||||
self._handle.controlWrite(Panda.REQUEST_OUT, 0xe6, period, 0, b'')
|
||||
|
||||
def force_relay_drive(self, intercept_relay_drive, ignition_relay_drive):
|
||||
self._handle.controlWrite(Panda.REQUEST_OUT, 0xc5, (int(intercept_relay_drive) | int(ignition_relay_drive) << 1), 0, b'')
|
||||
|
||||
def read_som_gpio(self) -> bool:
|
||||
r = self._handle.controlRead(Panda.REQUEST_IN, 0xc6, 0, 0, 1)
|
||||
return r[0] == 1
|
||||
61
panda/python/base.py
Normal file
61
panda/python/base.py
Normal file
@@ -0,0 +1,61 @@
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
from .constants import McuType
|
||||
|
||||
TIMEOUT = int(15 * 1e3) # default timeout, in milliseconds
|
||||
|
||||
class BaseHandle(ABC):
|
||||
"""
|
||||
A handle to talk to a panda.
|
||||
Borrows heavily from the libusb1 handle API.
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def close(self) -> None:
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
def controlWrite(self, request_type: int, request: int, value: int, index: int, data, timeout: int = TIMEOUT, expect_disconnect: bool = False):
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
def controlRead(self, request_type: int, request: int, value: int, index: int, length: int, timeout: int = TIMEOUT) -> bytes:
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
def bulkWrite(self, endpoint: int, data: bytes, timeout: int = TIMEOUT) -> int:
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
def bulkRead(self, endpoint: int, length: int, timeout: int = TIMEOUT) -> bytes:
|
||||
...
|
||||
|
||||
|
||||
class BaseSTBootloaderHandle(ABC):
|
||||
"""
|
||||
A handle to talk to a panda while it's in the STM32 bootloader.
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def get_mcu_type(self) -> McuType:
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
def close(self) -> None:
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
def clear_status(self) -> None:
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
def program(self, address: int, dat: bytes) -> None:
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
def erase_sector(self, sector: int) -> None:
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
def jump(self, address: int) -> None:
|
||||
...
|
||||
65
panda/python/constants.py
Normal file
65
panda/python/constants.py
Normal file
@@ -0,0 +1,65 @@
|
||||
import os
|
||||
import enum
|
||||
from typing import NamedTuple
|
||||
|
||||
BASEDIR = os.path.join(os.path.dirname(os.path.realpath(__file__)), "../")
|
||||
FW_PATH = os.path.join(BASEDIR, "board/obj/")
|
||||
|
||||
USBPACKET_MAX_SIZE = 0x40
|
||||
|
||||
class McuConfig(NamedTuple):
|
||||
mcu: str
|
||||
mcu_idcode: int
|
||||
sector_sizes: list[int]
|
||||
sector_count: int # total sector count, used for MCU identification in DFU mode
|
||||
uid_address: int
|
||||
block_size: int
|
||||
serial_number_address: int
|
||||
app_address: int
|
||||
app_fn: str
|
||||
bootstub_address: int
|
||||
bootstub_fn: str
|
||||
|
||||
def sector_address(self, i):
|
||||
# assume bootstub is in sector 0
|
||||
return self.bootstub_address + sum(self.sector_sizes[:i])
|
||||
|
||||
F4Config = McuConfig(
|
||||
"STM32F4",
|
||||
0x463,
|
||||
[0x4000 for _ in range(4)] + [0x10000] + [0x20000 for _ in range(11)],
|
||||
16,
|
||||
0x1FFF7A10,
|
||||
0x800,
|
||||
0x1FFF79C0,
|
||||
0x8004000,
|
||||
"panda.bin.signed",
|
||||
0x8000000,
|
||||
"bootstub.panda.bin",
|
||||
)
|
||||
|
||||
H7Config = McuConfig(
|
||||
"STM32H7",
|
||||
0x483,
|
||||
[0x20000 for _ in range(7)],
|
||||
8,
|
||||
0x1FF1E800,
|
||||
0x400,
|
||||
# there is an 8th sector, but we use that for the provisioning chunk, so don't program over that!
|
||||
0x080FFFC0,
|
||||
0x8020000,
|
||||
"panda_h7.bin.signed",
|
||||
0x8000000,
|
||||
"bootstub.panda_h7.bin",
|
||||
)
|
||||
|
||||
@enum.unique
|
||||
class McuType(enum.Enum):
|
||||
F4 = F4Config
|
||||
H7 = H7Config
|
||||
|
||||
@property
|
||||
def config(self):
|
||||
return self.value
|
||||
|
||||
MCU_TYPE_BY_IDCODE = {m.config.mcu_idcode: m for m in McuType}
|
||||
138
panda/python/dfu.py
Normal file
138
panda/python/dfu.py
Normal file
@@ -0,0 +1,138 @@
|
||||
import os
|
||||
import usb1
|
||||
import struct
|
||||
import binascii
|
||||
|
||||
from .base import BaseSTBootloaderHandle
|
||||
from .spi import STBootloaderSPIHandle, PandaSpiException
|
||||
from .usb import STBootloaderUSBHandle
|
||||
from .constants import FW_PATH, McuType
|
||||
|
||||
|
||||
class PandaDFU:
|
||||
def __init__(self, dfu_serial: str | None):
|
||||
# try USB, then SPI
|
||||
handle: BaseSTBootloaderHandle | None
|
||||
self._context, handle = PandaDFU.usb_connect(dfu_serial)
|
||||
if handle is None:
|
||||
self._context, handle = PandaDFU.spi_connect(dfu_serial)
|
||||
|
||||
if handle is None:
|
||||
raise Exception(f"failed to open DFU device {dfu_serial}")
|
||||
|
||||
self._handle: BaseSTBootloaderHandle = handle
|
||||
self._mcu_type: McuType = self._handle.get_mcu_type()
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, *args):
|
||||
self.close()
|
||||
|
||||
def close(self):
|
||||
if self._handle is not None:
|
||||
self._handle.close()
|
||||
self._handle = None
|
||||
if self._context is not None:
|
||||
self._context.close()
|
||||
|
||||
@staticmethod
|
||||
def usb_connect(dfu_serial: str | None):
|
||||
handle = None
|
||||
context = usb1.USBContext()
|
||||
context.open()
|
||||
for device in context.getDeviceList(skip_on_error=True):
|
||||
if device.getVendorID() == 0x0483 and device.getProductID() == 0xdf11:
|
||||
try:
|
||||
this_dfu_serial = device.open().getASCIIStringDescriptor(3)
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
if this_dfu_serial == dfu_serial or dfu_serial is None:
|
||||
handle = STBootloaderUSBHandle(device, device.open())
|
||||
break
|
||||
|
||||
return context, handle
|
||||
|
||||
@staticmethod
|
||||
def spi_connect(dfu_serial: str | None):
|
||||
handle = None
|
||||
this_dfu_serial = None
|
||||
|
||||
try:
|
||||
handle = STBootloaderSPIHandle()
|
||||
this_dfu_serial = PandaDFU.st_serial_to_dfu_serial(handle.get_uid(), handle.get_mcu_type())
|
||||
except PandaSpiException:
|
||||
handle = None
|
||||
|
||||
if dfu_serial is not None and dfu_serial != this_dfu_serial:
|
||||
handle = None
|
||||
|
||||
return None, handle
|
||||
|
||||
@staticmethod
|
||||
def usb_list() -> list[str]:
|
||||
dfu_serials = []
|
||||
try:
|
||||
with usb1.USBContext() as context:
|
||||
for device in context.getDeviceList(skip_on_error=True):
|
||||
if device.getVendorID() == 0x0483 and device.getProductID() == 0xdf11:
|
||||
try:
|
||||
dfu_serials.append(device.open().getASCIIStringDescriptor(3))
|
||||
except Exception:
|
||||
pass
|
||||
except Exception:
|
||||
pass
|
||||
return dfu_serials
|
||||
|
||||
@staticmethod
|
||||
def spi_list() -> list[str]:
|
||||
try:
|
||||
_, h = PandaDFU.spi_connect(None)
|
||||
if h is not None:
|
||||
dfu_serial = PandaDFU.st_serial_to_dfu_serial(h.get_uid(), h.get_mcu_type())
|
||||
return [dfu_serial, ]
|
||||
except PandaSpiException:
|
||||
pass
|
||||
return []
|
||||
|
||||
@staticmethod
|
||||
def st_serial_to_dfu_serial(st: str, mcu_type: McuType = McuType.F4):
|
||||
if st is None or st == "none":
|
||||
return None
|
||||
try:
|
||||
uid_base = struct.unpack("H" * 6, bytes.fromhex(st))
|
||||
if mcu_type == McuType.H7:
|
||||
return binascii.hexlify(struct.pack("!HHH", uid_base[1] + uid_base[5], uid_base[0] + uid_base[4], uid_base[3])).upper().decode("utf-8")
|
||||
else:
|
||||
return binascii.hexlify(struct.pack("!HHH", uid_base[1] + uid_base[5], uid_base[0] + uid_base[4] + 0xA, uid_base[3])).upper().decode("utf-8")
|
||||
except struct.error:
|
||||
return None
|
||||
|
||||
def get_mcu_type(self) -> McuType:
|
||||
return self._mcu_type
|
||||
|
||||
def reset(self):
|
||||
self._handle.jump(self._mcu_type.config.bootstub_address)
|
||||
|
||||
def program_bootstub(self, code_bootstub):
|
||||
self._handle.clear_status()
|
||||
|
||||
# erase all sectors
|
||||
for i in range(len(self._mcu_type.config.sector_sizes)):
|
||||
self._handle.erase_sector(i)
|
||||
|
||||
self._handle.program(self._mcu_type.config.bootstub_address, code_bootstub)
|
||||
|
||||
def recover(self):
|
||||
fn = os.path.join(FW_PATH, self._mcu_type.config.bootstub_fn)
|
||||
with open(fn, "rb") as f:
|
||||
code = f.read()
|
||||
self.program_bootstub(code)
|
||||
self.reset()
|
||||
|
||||
@staticmethod
|
||||
def list() -> list[str]:
|
||||
ret = PandaDFU.usb_list()
|
||||
ret += PandaDFU.spi_list()
|
||||
return list(set(ret))
|
||||
35
panda/python/serial.py
Normal file
35
panda/python/serial.py
Normal file
@@ -0,0 +1,35 @@
|
||||
# mimic a python serial port
|
||||
class PandaSerial:
|
||||
def __init__(self, panda, port, baud):
|
||||
self.panda = panda
|
||||
self.port = port
|
||||
self.panda.set_uart_parity(self.port, 0)
|
||||
self._baudrate = baud
|
||||
self.panda.set_uart_baud(self.port, baud)
|
||||
self.buf = b""
|
||||
|
||||
def read(self, l=1):
|
||||
tt = self.panda.serial_read(self.port)
|
||||
if len(tt) > 0:
|
||||
self.buf += tt
|
||||
ret = self.buf[0:l]
|
||||
self.buf = self.buf[l:]
|
||||
return ret
|
||||
|
||||
def write(self, dat):
|
||||
return self.panda.serial_write(self.port, dat)
|
||||
|
||||
def close(self):
|
||||
pass
|
||||
|
||||
def flush(self):
|
||||
pass
|
||||
|
||||
@property
|
||||
def baudrate(self):
|
||||
return self._baudrate
|
||||
|
||||
@baudrate.setter
|
||||
def baudrate(self, value):
|
||||
self.panda.set_uart_baud(self.port, value)
|
||||
self._baudrate = value
|
||||
94
panda/python/socketpanda.py
Normal file
94
panda/python/socketpanda.py
Normal file
@@ -0,0 +1,94 @@
|
||||
import socket
|
||||
import struct
|
||||
|
||||
# /**
|
||||
# * struct canfd_frame - CAN flexible data rate frame structure
|
||||
# * @can_id: CAN ID of the frame and CAN_*_FLAG flags, see canid_t definition
|
||||
# * @len: frame payload length in byte (0 .. CANFD_MAX_DLEN)
|
||||
# * @flags: additional flags for CAN FD
|
||||
# * @__res0: reserved / padding
|
||||
# * @__res1: reserved / padding
|
||||
# * @data: CAN FD frame payload (up to CANFD_MAX_DLEN byte)
|
||||
# */
|
||||
# struct canfd_frame {
|
||||
# canid_t can_id; /* 32 bit CAN_ID + EFF/RTR/ERR flags */
|
||||
# __u8 len; /* frame payload length in byte */
|
||||
# __u8 flags; /* additional flags for CAN FD */
|
||||
# __u8 __res0; /* reserved / padding */
|
||||
# __u8 __res1; /* reserved / padding */
|
||||
# __u8 data[CANFD_MAX_DLEN] __attribute__((aligned(8)));
|
||||
# };
|
||||
CAN_HEADER_FMT = "=IBB2x"
|
||||
CAN_HEADER_LEN = struct.calcsize(CAN_HEADER_FMT)
|
||||
CAN_MAX_DLEN = 8
|
||||
CANFD_MAX_DLEN = 64
|
||||
|
||||
CANFD_BRS = 0x01 # bit rate switch (second bitrate for payload data)
|
||||
CANFD_FDF = 0x04 # mark CAN FD for dual use of struct canfd_frame
|
||||
|
||||
# socket.SO_RXQ_OVFL is missing
|
||||
# https://github.com/torvalds/linux/blob/47ac09b91befbb6a235ab620c32af719f8208399/include/uapi/asm-generic/socket.h#L61
|
||||
SO_RXQ_OVFL = 40
|
||||
|
||||
def create_socketcan(interface:str, recv_buffer_size:int, fd:bool) -> socket.socket:
|
||||
# settings mostly from https://github.com/linux-can/can-utils/blob/master/candump.c
|
||||
socketcan = socket.socket(socket.AF_CAN, socket.SOCK_RAW, socket.CAN_RAW)
|
||||
if fd:
|
||||
socketcan.setsockopt(socket.SOL_CAN_RAW, socket.CAN_RAW_FD_FRAMES, 1)
|
||||
socketcan.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, recv_buffer_size)
|
||||
# TODO: why is it always 2x the requested size?
|
||||
assert socketcan.getsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF) == recv_buffer_size * 2
|
||||
# TODO: how to dectect and alert on buffer overflow?
|
||||
socketcan.setsockopt(socket.SOL_SOCKET, SO_RXQ_OVFL, 1)
|
||||
socketcan.bind((interface,))
|
||||
return socketcan
|
||||
|
||||
# Panda class substitute for socketcan device (to support using the uds/iso-tp/xcp/ccp library)
|
||||
class SocketPanda():
|
||||
def __init__(self, interface:str="can0", bus:int=0, fd:bool=False, recv_buffer_size:int=212992) -> None:
|
||||
self.interface = interface
|
||||
self.bus = bus
|
||||
self.fd = fd
|
||||
self.flags = CANFD_BRS | CANFD_FDF if fd else 0
|
||||
self.data_len = CANFD_MAX_DLEN if fd else CAN_MAX_DLEN
|
||||
self.recv_buffer_size = recv_buffer_size
|
||||
self.socket = create_socketcan(interface, recv_buffer_size, fd)
|
||||
|
||||
def __del__(self):
|
||||
self.socket.close()
|
||||
|
||||
def get_serial(self) -> tuple[int, int]:
|
||||
return (0, 0) # TODO: implemented in panda socketcan driver
|
||||
|
||||
def get_version(self) -> int:
|
||||
return 0 # TODO: implemented in panda socketcan driver
|
||||
|
||||
def can_clear(self, bus:int) -> None:
|
||||
# TODO: implemented in panda socketcan driver
|
||||
self.socket.close()
|
||||
self.socket = create_socketcan(self.interface, self.recv_buffer_size, self.fd)
|
||||
|
||||
def set_safety_mode(self, mode:int, param=0) -> None:
|
||||
pass # TODO: implemented in panda socketcan driver
|
||||
|
||||
def has_obd(self) -> bool:
|
||||
return False # TODO: implemented in panda socketcan driver
|
||||
|
||||
def can_send(self, addr, dat, bus=0, timeout=0) -> None:
|
||||
msg_len = len(dat)
|
||||
msg_dat = dat.ljust(self.data_len, b'\x00')
|
||||
can_frame = struct.pack(CAN_HEADER_FMT, addr, msg_len, self.flags) + msg_dat
|
||||
self.socket.sendto(can_frame, (self.interface,))
|
||||
|
||||
def can_recv(self) -> list[tuple[int, bytes, int]]:
|
||||
msgs = list()
|
||||
while True:
|
||||
try:
|
||||
dat, _ = self.socket.recvfrom(self.recv_buffer_size, socket.MSG_DONTWAIT)
|
||||
assert len(dat) == CAN_HEADER_LEN + self.data_len, f"ERROR: received {len(dat)} bytes"
|
||||
can_id, msg_len, _ = struct.unpack(CAN_HEADER_FMT, dat[:CAN_HEADER_LEN])
|
||||
msg_dat = dat[CAN_HEADER_LEN:CAN_HEADER_LEN+msg_len]
|
||||
msgs.append((can_id, msg_dat, self.bus))
|
||||
except BlockingIOError:
|
||||
break # buffered data exhausted
|
||||
return msgs
|
||||
445
panda/python/spi.py
Normal file
445
panda/python/spi.py
Normal file
@@ -0,0 +1,445 @@
|
||||
import binascii
|
||||
import ctypes
|
||||
import os
|
||||
import fcntl
|
||||
import math
|
||||
import time
|
||||
import struct
|
||||
import threading
|
||||
from contextlib import contextmanager
|
||||
from functools import reduce
|
||||
from collections.abc import Callable
|
||||
|
||||
from .base import BaseHandle, BaseSTBootloaderHandle, TIMEOUT
|
||||
from .constants import McuType, MCU_TYPE_BY_IDCODE, USBPACKET_MAX_SIZE
|
||||
from .utils import logger
|
||||
|
||||
try:
|
||||
import spidev
|
||||
except ImportError:
|
||||
spidev = None
|
||||
|
||||
# Constants
|
||||
SYNC = 0x5A
|
||||
HACK = 0x79
|
||||
DACK = 0x85
|
||||
NACK = 0x1F
|
||||
CHECKSUM_START = 0xAB
|
||||
|
||||
MIN_ACK_TIMEOUT_MS = 100
|
||||
MAX_XFER_RETRY_COUNT = 5
|
||||
|
||||
XFER_SIZE = 0x40*31
|
||||
|
||||
DEV_PATH = "/dev/spidev0.0"
|
||||
|
||||
|
||||
def crc8(data):
|
||||
crc = 0xFF # standard init value
|
||||
poly = 0xD5 # standard crc8: x8+x7+x6+x4+x2+1
|
||||
size = len(data)
|
||||
for i in range(size - 1, -1, -1):
|
||||
crc ^= data[i]
|
||||
for _ in range(8):
|
||||
if ((crc & 0x80) != 0):
|
||||
crc = ((crc << 1) ^ poly) & 0xFF
|
||||
else:
|
||||
crc <<= 1
|
||||
return crc
|
||||
|
||||
|
||||
class PandaSpiException(Exception):
|
||||
pass
|
||||
|
||||
class PandaProtocolMismatch(PandaSpiException):
|
||||
pass
|
||||
|
||||
class PandaSpiUnavailable(PandaSpiException):
|
||||
pass
|
||||
|
||||
class PandaSpiNackResponse(PandaSpiException):
|
||||
pass
|
||||
|
||||
class PandaSpiMissingAck(PandaSpiException):
|
||||
pass
|
||||
|
||||
class PandaSpiBadChecksum(PandaSpiException):
|
||||
pass
|
||||
|
||||
class PandaSpiTransferFailed(PandaSpiException):
|
||||
pass
|
||||
|
||||
|
||||
class PandaSpiTransfer(ctypes.Structure):
|
||||
_fields_ = [
|
||||
('rx_buf', ctypes.c_uint64),
|
||||
('tx_buf', ctypes.c_uint64),
|
||||
('tx_length', ctypes.c_uint32),
|
||||
('rx_length_max', ctypes.c_uint32),
|
||||
('timeout', ctypes.c_uint32),
|
||||
('endpoint', ctypes.c_uint8),
|
||||
('expect_disconnect', ctypes.c_uint8),
|
||||
]
|
||||
|
||||
|
||||
SPI_LOCK = threading.Lock()
|
||||
SPI_DEVICES = {}
|
||||
class SpiDevice:
|
||||
"""
|
||||
Provides locked, thread-safe access to a panda's SPI interface.
|
||||
"""
|
||||
|
||||
# 50MHz is the max of the 845. older rev comma three
|
||||
# may not support the full 50MHz
|
||||
MAX_SPEED = 50000000
|
||||
|
||||
def __init__(self, speed=MAX_SPEED):
|
||||
assert speed <= self.MAX_SPEED
|
||||
|
||||
if not os.path.exists(DEV_PATH):
|
||||
raise PandaSpiUnavailable(f"SPI device not found: {DEV_PATH}")
|
||||
if spidev is None:
|
||||
raise PandaSpiUnavailable("spidev is not installed")
|
||||
|
||||
with SPI_LOCK:
|
||||
if speed not in SPI_DEVICES:
|
||||
SPI_DEVICES[speed] = spidev.SpiDev() # pylint: disable=c-extension-no-member
|
||||
SPI_DEVICES[speed].open(0, 0)
|
||||
SPI_DEVICES[speed].max_speed_hz = speed
|
||||
self._spidev = SPI_DEVICES[speed]
|
||||
|
||||
@contextmanager
|
||||
def acquire(self):
|
||||
try:
|
||||
SPI_LOCK.acquire()
|
||||
fcntl.flock(self._spidev, fcntl.LOCK_EX)
|
||||
yield self._spidev
|
||||
finally:
|
||||
fcntl.flock(self._spidev, fcntl.LOCK_UN)
|
||||
SPI_LOCK.release()
|
||||
|
||||
def close(self):
|
||||
pass
|
||||
|
||||
|
||||
class PandaSpiHandle(BaseHandle):
|
||||
"""
|
||||
A class that mimics a libusb1 handle for panda SPI communications.
|
||||
"""
|
||||
|
||||
PROTOCOL_VERSION = 2
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.dev = SpiDevice()
|
||||
|
||||
self._transfer_raw: Callable[[SpiDevice, int, bytes, int, int, bool], bytes] = self._transfer_spidev
|
||||
|
||||
if "KERN" in os.environ:
|
||||
self._transfer_raw = self._transfer_kernel_driver
|
||||
|
||||
self.tx_buf = bytearray(1024)
|
||||
self.rx_buf = bytearray(1024)
|
||||
tx_buf_raw = ctypes.c_char.from_buffer(self.tx_buf)
|
||||
rx_buf_raw = ctypes.c_char.from_buffer(self.rx_buf)
|
||||
|
||||
self.ioctl_data = PandaSpiTransfer()
|
||||
self.ioctl_data.tx_buf = ctypes.addressof(tx_buf_raw)
|
||||
self.ioctl_data.rx_buf = ctypes.addressof(rx_buf_raw)
|
||||
self.fileno = self.dev._spidev.fileno()
|
||||
|
||||
# helpers
|
||||
def _calc_checksum(self, data: bytes) -> int:
|
||||
cksum = CHECKSUM_START
|
||||
for b in data:
|
||||
cksum ^= b
|
||||
return cksum
|
||||
|
||||
def _wait_for_ack(self, spi, ack_val: int, timeout: int, tx: int, length: int = 1) -> bytes:
|
||||
timeout_s = max(MIN_ACK_TIMEOUT_MS, timeout) * 1e-3
|
||||
|
||||
start = time.monotonic()
|
||||
while (timeout == 0) or ((time.monotonic() - start) < timeout_s):
|
||||
dat = spi.xfer2([tx, ] * length)
|
||||
if dat[0] == NACK:
|
||||
raise PandaSpiNackResponse
|
||||
elif dat[0] == ack_val:
|
||||
return bytes(dat)
|
||||
|
||||
raise PandaSpiMissingAck
|
||||
|
||||
def _transfer_spidev(self, spi, endpoint: int, data, timeout: int, max_rx_len: int = 1000, expect_disconnect: bool = False) -> bytes:
|
||||
max_rx_len = max(USBPACKET_MAX_SIZE, max_rx_len)
|
||||
|
||||
logger.debug("- send header")
|
||||
packet = struct.pack("<BBHH", SYNC, endpoint, len(data), max_rx_len)
|
||||
packet += bytes([self._calc_checksum(packet), ])
|
||||
spi.xfer2(packet)
|
||||
|
||||
logger.debug("- waiting for header ACK")
|
||||
self._wait_for_ack(spi, HACK, MIN_ACK_TIMEOUT_MS, 0x11)
|
||||
|
||||
logger.debug("- sending data")
|
||||
packet = bytes([*data, self._calc_checksum(data)])
|
||||
spi.xfer2(packet)
|
||||
|
||||
if expect_disconnect:
|
||||
logger.debug("- expecting disconnect, returning")
|
||||
return b""
|
||||
else:
|
||||
logger.debug("- waiting for data ACK")
|
||||
preread_len = USBPACKET_MAX_SIZE + 1 # read enough for a controlRead
|
||||
dat = self._wait_for_ack(spi, DACK, timeout, 0x13, length=3 + preread_len)
|
||||
|
||||
# get response length, then response
|
||||
response_len = struct.unpack("<H", dat[1:3])[0]
|
||||
if response_len > max_rx_len:
|
||||
raise PandaSpiException(f"response length greater than max ({max_rx_len} {response_len})")
|
||||
|
||||
# read rest
|
||||
remaining = (response_len + 1) - preread_len
|
||||
if remaining > 0:
|
||||
dat += bytes(spi.readbytes(remaining))
|
||||
|
||||
|
||||
dat = dat[:3 + response_len + 1]
|
||||
if self._calc_checksum(dat) != 0:
|
||||
raise PandaSpiBadChecksum
|
||||
|
||||
return dat[3:-1]
|
||||
|
||||
def _transfer_kernel_driver(self, spi, endpoint: int, data, timeout: int, max_rx_len: int = 1000, expect_disconnect: bool = False) -> bytes:
|
||||
import spidev2
|
||||
self.tx_buf[:len(data)] = data
|
||||
self.ioctl_data.endpoint = endpoint
|
||||
self.ioctl_data.tx_length = len(data)
|
||||
self.ioctl_data.rx_length_max = max_rx_len
|
||||
self.ioctl_data.expect_disconnect = int(expect_disconnect)
|
||||
|
||||
# TODO: use our own ioctl request
|
||||
try:
|
||||
ret = fcntl.ioctl(self.fileno, spidev2.SPI_IOC_RD_LSB_FIRST, self.ioctl_data)
|
||||
except OSError as e:
|
||||
raise PandaSpiException from e
|
||||
if ret < 0:
|
||||
raise PandaSpiException(f"ioctl returned {ret}")
|
||||
return bytes(self.rx_buf[:ret])
|
||||
|
||||
def _transfer(self, endpoint: int, data, timeout: int, max_rx_len: int = 1000, expect_disconnect: bool = False) -> bytes:
|
||||
logger.debug("starting transfer: endpoint=%d, max_rx_len=%d", endpoint, max_rx_len)
|
||||
logger.debug("==============================================")
|
||||
|
||||
n = 0
|
||||
start_time = time.monotonic()
|
||||
exc = PandaSpiException()
|
||||
while (timeout == 0) or (time.monotonic() - start_time) < timeout*1e-3:
|
||||
n += 1
|
||||
logger.debug("\ntry #%d", n)
|
||||
with self.dev.acquire() as spi:
|
||||
try:
|
||||
return self._transfer_raw(spi, endpoint, data, timeout, max_rx_len, expect_disconnect)
|
||||
except PandaSpiException as e:
|
||||
exc = e
|
||||
logger.debug("SPI transfer failed, retrying", exc_info=True)
|
||||
|
||||
raise exc
|
||||
|
||||
def get_protocol_version(self) -> bytes:
|
||||
vers_str = b"VERSION"
|
||||
def _get_version(spi) -> bytes:
|
||||
spi.writebytes(vers_str)
|
||||
|
||||
logger.debug("- waiting for echo")
|
||||
start = time.monotonic()
|
||||
while True:
|
||||
version_bytes = spi.readbytes(len(vers_str) + 2)
|
||||
if bytes(version_bytes).startswith(vers_str):
|
||||
break
|
||||
if (time.monotonic() - start) > 0.001:
|
||||
raise PandaSpiMissingAck
|
||||
|
||||
rlen = struct.unpack("<H", bytes(version_bytes[-2:]))[0]
|
||||
if rlen > 1000:
|
||||
raise PandaSpiException("response length greater than max")
|
||||
|
||||
# get response
|
||||
dat = spi.readbytes(rlen + 1)
|
||||
resp = dat[:-1]
|
||||
calculated_crc = crc8(bytes(version_bytes + resp))
|
||||
if calculated_crc != dat[-1]:
|
||||
raise PandaSpiBadChecksum
|
||||
return bytes(resp)
|
||||
|
||||
exc = PandaSpiException()
|
||||
with self.dev.acquire() as spi:
|
||||
for _ in range(10):
|
||||
try:
|
||||
return _get_version(spi)
|
||||
except PandaSpiException as e:
|
||||
exc = e
|
||||
logger.debug("SPI get protocol version failed, retrying", exc_info=True)
|
||||
raise exc
|
||||
|
||||
# libusb1 functions
|
||||
def close(self):
|
||||
self.dev.close()
|
||||
|
||||
def controlWrite(self, request_type: int, request: int, value: int, index: int, data, timeout: int = TIMEOUT, expect_disconnect: bool = False):
|
||||
return self._transfer(0, struct.pack("<BHHH", request, value, index, 0), timeout, expect_disconnect=expect_disconnect)
|
||||
|
||||
def controlRead(self, request_type: int, request: int, value: int, index: int, length: int, timeout: int = TIMEOUT):
|
||||
return self._transfer(0, struct.pack("<BHHH", request, value, index, length), timeout, max_rx_len=length)
|
||||
|
||||
def bulkWrite(self, endpoint: int, data: bytes, timeout: int = TIMEOUT) -> int:
|
||||
for x in range(math.ceil(len(data) / XFER_SIZE)):
|
||||
self._transfer(endpoint, data[XFER_SIZE*x:XFER_SIZE*(x+1)], timeout)
|
||||
return len(data)
|
||||
|
||||
def bulkRead(self, endpoint: int, length: int, timeout: int = TIMEOUT) -> bytes:
|
||||
ret = b""
|
||||
for _ in range(math.ceil(length / XFER_SIZE)):
|
||||
d = self._transfer(endpoint, [], timeout, max_rx_len=XFER_SIZE)
|
||||
ret += d
|
||||
if len(d) < XFER_SIZE:
|
||||
break
|
||||
return ret
|
||||
|
||||
|
||||
class STBootloaderSPIHandle(BaseSTBootloaderHandle):
|
||||
"""
|
||||
Implementation of the STM32 SPI bootloader protocol described in:
|
||||
https://www.st.com/resource/en/application_note/an4286-spi-protocol-used-in-the-stm32-bootloader-stmicroelectronics.pdf
|
||||
"""
|
||||
|
||||
SYNC = 0x5A
|
||||
ACK = 0x79
|
||||
NACK = 0x1F
|
||||
|
||||
def __init__(self):
|
||||
self.dev = SpiDevice(speed=1000000)
|
||||
|
||||
# say hello
|
||||
try:
|
||||
with self.dev.acquire() as spi:
|
||||
spi.xfer([self.SYNC, ])
|
||||
try:
|
||||
self._get_ack(spi, 0.1)
|
||||
except (PandaSpiNackResponse, PandaSpiMissingAck):
|
||||
# NACK ok here, will only ACK the first time
|
||||
pass
|
||||
|
||||
self._mcu_type = MCU_TYPE_BY_IDCODE[self.get_chip_id()]
|
||||
except PandaSpiException:
|
||||
raise PandaSpiException("failed to connect to panda") from None
|
||||
|
||||
def _get_ack(self, spi, timeout=1.0):
|
||||
data = 0x00
|
||||
start_time = time.monotonic()
|
||||
while data not in (self.ACK, self.NACK) and (time.monotonic() - start_time < timeout):
|
||||
data = spi.xfer([0x00, ])[0]
|
||||
time.sleep(0)
|
||||
spi.xfer([self.ACK, ])
|
||||
|
||||
if data == self.NACK:
|
||||
raise PandaSpiNackResponse
|
||||
elif data != self.ACK:
|
||||
raise PandaSpiMissingAck
|
||||
|
||||
def _cmd_no_retry(self, cmd: int, data: list[bytes] | None = None, read_bytes: int = 0, predata=None) -> bytes:
|
||||
ret = b""
|
||||
with self.dev.acquire() as spi:
|
||||
# sync + command
|
||||
spi.xfer([self.SYNC, ])
|
||||
spi.xfer([cmd, cmd ^ 0xFF])
|
||||
self._get_ack(spi, timeout=0.01)
|
||||
|
||||
# "predata" - for commands that send the first data without a checksum
|
||||
if predata is not None:
|
||||
spi.xfer(predata)
|
||||
self._get_ack(spi)
|
||||
|
||||
# send data
|
||||
if data is not None:
|
||||
for d in data:
|
||||
if predata is not None:
|
||||
spi.xfer(d + self._checksum(predata + d))
|
||||
else:
|
||||
spi.xfer(d + self._checksum(d))
|
||||
self._get_ack(spi, timeout=20)
|
||||
|
||||
# receive
|
||||
if read_bytes > 0:
|
||||
ret = spi.xfer([0x00, ]*(read_bytes + 1))[1:]
|
||||
if data is None or len(data) == 0:
|
||||
self._get_ack(spi)
|
||||
|
||||
return bytes(ret)
|
||||
|
||||
def _cmd(self, cmd: int, data: list[bytes] | None = None, read_bytes: int = 0, predata=None) -> bytes:
|
||||
exc = PandaSpiException()
|
||||
for n in range(MAX_XFER_RETRY_COUNT):
|
||||
try:
|
||||
return self._cmd_no_retry(cmd, data, read_bytes, predata)
|
||||
except PandaSpiException as e:
|
||||
exc = e
|
||||
logger.debug("SPI transfer failed, %d retries left", MAX_XFER_RETRY_COUNT - n - 1, exc_info=True)
|
||||
raise exc
|
||||
|
||||
def _checksum(self, data: bytes) -> bytes:
|
||||
if len(data) == 1:
|
||||
ret = data[0] ^ 0xFF
|
||||
else:
|
||||
ret = reduce(lambda a, b: a ^ b, data)
|
||||
return bytes([ret, ])
|
||||
|
||||
# *** Bootloader commands ***
|
||||
|
||||
def read(self, address: int, length: int):
|
||||
data = [struct.pack('>I', address), struct.pack('B', length - 1)]
|
||||
return self._cmd(0x11, data=data, read_bytes=length)
|
||||
|
||||
def get_bootloader_id(self):
|
||||
return self.read(0x1FF1E7FE, 1)
|
||||
|
||||
def get_chip_id(self) -> int:
|
||||
r = self._cmd(0x02, read_bytes=3)
|
||||
if r[0] != 1: # response length - 1
|
||||
raise PandaSpiException("incorrect response length")
|
||||
return ((r[1] << 8) + r[2])
|
||||
|
||||
def go_cmd(self, address: int) -> None:
|
||||
self._cmd(0x21, data=[struct.pack('>I', address), ])
|
||||
|
||||
# *** helpers ***
|
||||
|
||||
def get_uid(self):
|
||||
dat = self.read(McuType.H7.config.uid_address, 12)
|
||||
return binascii.hexlify(dat).decode()
|
||||
|
||||
def erase_sector(self, sector: int):
|
||||
p = struct.pack('>H', 0) # number of sectors to erase
|
||||
d = struct.pack('>H', sector)
|
||||
self._cmd(0x44, data=[d, ], predata=p)
|
||||
|
||||
# *** PandaDFU API ***
|
||||
|
||||
def get_mcu_type(self):
|
||||
return self._mcu_type
|
||||
|
||||
def clear_status(self):
|
||||
pass
|
||||
|
||||
def close(self):
|
||||
self.dev.close()
|
||||
|
||||
def program(self, address, dat):
|
||||
bs = 256 # max block size for writing to flash over SPI
|
||||
dat += b"\xFF" * ((bs - len(dat)) % bs)
|
||||
for i in range(len(dat) // bs):
|
||||
block = dat[i * bs:(i + 1) * bs]
|
||||
self._cmd(0x31, data=[
|
||||
struct.pack('>I', address + i*bs),
|
||||
bytes([len(block) - 1]) + block,
|
||||
])
|
||||
|
||||
def jump(self, address):
|
||||
self.go_cmd(self._mcu_type.config.bootstub_address)
|
||||
98
panda/python/usb.py
Normal file
98
panda/python/usb.py
Normal file
@@ -0,0 +1,98 @@
|
||||
import struct
|
||||
|
||||
from .base import BaseHandle, BaseSTBootloaderHandle, TIMEOUT
|
||||
from .constants import McuType
|
||||
|
||||
class PandaUsbHandle(BaseHandle):
|
||||
def __init__(self, libusb_handle):
|
||||
self._libusb_handle = libusb_handle
|
||||
|
||||
def close(self):
|
||||
self._libusb_handle.close()
|
||||
|
||||
def controlWrite(self, request_type: int, request: int, value: int, index: int, data, timeout: int = TIMEOUT, expect_disconnect: bool = False):
|
||||
return self._libusb_handle.controlWrite(request_type, request, value, index, data, timeout)
|
||||
|
||||
def controlRead(self, request_type: int, request: int, value: int, index: int, length: int, timeout: int = TIMEOUT):
|
||||
return self._libusb_handle.controlRead(request_type, request, value, index, length, timeout)
|
||||
|
||||
def bulkWrite(self, endpoint: int, data: bytes, timeout: int = TIMEOUT) -> int:
|
||||
return self._libusb_handle.bulkWrite(endpoint, data, timeout) # type: ignore
|
||||
|
||||
def bulkRead(self, endpoint: int, length: int, timeout: int = TIMEOUT) -> bytes:
|
||||
return self._libusb_handle.bulkRead(endpoint, length, timeout) # type: ignore
|
||||
|
||||
|
||||
|
||||
class STBootloaderUSBHandle(BaseSTBootloaderHandle):
|
||||
DFU_DNLOAD = 1
|
||||
DFU_UPLOAD = 2
|
||||
DFU_GETSTATUS = 3
|
||||
DFU_CLRSTATUS = 4
|
||||
DFU_ABORT = 6
|
||||
|
||||
def __init__(self, libusb_device, libusb_handle):
|
||||
self._libusb_handle = libusb_handle
|
||||
|
||||
# example from F4: lsusb -v | grep Flash
|
||||
# iInterface 4 @Internal Flash /0x08000000/04*016Kg,01*064Kg,011*128Kg
|
||||
for i in range(20):
|
||||
desc = libusb_handle.getStringDescriptor(i, 0)
|
||||
if desc is not None and desc.startswith("@Internal Flash"):
|
||||
sector_count = sum([int(s.split('*')[0]) for s in desc.split('/')[-1].split(',')])
|
||||
break
|
||||
mcu_by_sector_count = {m.config.sector_count: m for m in McuType}
|
||||
assert sector_count in mcu_by_sector_count, f"Unkown MCU: {sector_count=}"
|
||||
self._mcu_type = mcu_by_sector_count[sector_count]
|
||||
|
||||
def _status(self) -> None:
|
||||
while 1:
|
||||
dat = self._libusb_handle.controlRead(0x21, self.DFU_GETSTATUS, 0, 0, 6)
|
||||
if dat[1] == 0:
|
||||
break
|
||||
|
||||
def _erase_page_address(self, address: int) -> None:
|
||||
self._libusb_handle.controlWrite(0x21, self.DFU_DNLOAD, 0, 0, b"\x41" + struct.pack("I", address))
|
||||
self._status()
|
||||
|
||||
def get_mcu_type(self):
|
||||
return self._mcu_type
|
||||
|
||||
def erase_sector(self, sector: int):
|
||||
self._erase_page_address(self._mcu_type.config.sector_address(sector))
|
||||
|
||||
def clear_status(self):
|
||||
# Clear status
|
||||
stat = self._libusb_handle.controlRead(0x21, self.DFU_GETSTATUS, 0, 0, 6)
|
||||
if stat[4] == 0xa:
|
||||
self._libusb_handle.controlRead(0x21, self.DFU_CLRSTATUS, 0, 0, 0)
|
||||
elif stat[4] == 0x9:
|
||||
self._libusb_handle.controlWrite(0x21, self.DFU_ABORT, 0, 0, b"")
|
||||
self._status()
|
||||
stat = str(self._libusb_handle.controlRead(0x21, self.DFU_GETSTATUS, 0, 0, 6))
|
||||
|
||||
def close(self):
|
||||
self._libusb_handle.close()
|
||||
|
||||
def program(self, address, dat):
|
||||
# Set Address Pointer
|
||||
self._libusb_handle.controlWrite(0x21, self.DFU_DNLOAD, 0, 0, b"\x21" + struct.pack("I", address))
|
||||
self._status()
|
||||
|
||||
# Program
|
||||
bs = min(len(dat), self._mcu_type.config.block_size)
|
||||
dat += b"\xFF" * ((bs - len(dat)) % bs)
|
||||
for i in range(len(dat) // bs):
|
||||
ldat = dat[i * bs:(i + 1) * bs]
|
||||
print("programming %d with length %d" % (i, len(ldat)))
|
||||
self._libusb_handle.controlWrite(0x21, self.DFU_DNLOAD, 2 + i, 0, ldat)
|
||||
self._status()
|
||||
|
||||
def jump(self, address):
|
||||
self._libusb_handle.controlWrite(0x21, self.DFU_DNLOAD, 0, 0, b"\x21" + struct.pack("I", address))
|
||||
self._status()
|
||||
try:
|
||||
self._libusb_handle.controlWrite(0x21, self.DFU_DNLOAD, 2, 0, b"")
|
||||
_ = str(self._libusb_handle.controlRead(0x21, self.DFU_GETSTATUS, 0, 0, 6))
|
||||
except Exception:
|
||||
pass
|
||||
11
panda/python/utils.py
Normal file
11
panda/python/utils.py
Normal file
@@ -0,0 +1,11 @@
|
||||
import os
|
||||
import logging
|
||||
|
||||
# set up logging
|
||||
LOGLEVEL = os.environ.get('LOGLEVEL', 'INFO').upper()
|
||||
logger = logging.getLogger('panda')
|
||||
logger.setLevel(LOGLEVEL)
|
||||
|
||||
handler = logging.StreamHandler()
|
||||
handler.setFormatter(logging.Formatter('%(message)s'))
|
||||
logger.addHandler(handler)
|
||||
Reference in New Issue
Block a user