mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-10-28 00:53:45 +08:00
Vendor dependencies and resources for standalone builds
Keep official openpilot ancestry and dependency licenses. Store resources directly in Git, remove LFS setup requirements, and leave the downloadable big model outside the source tree.
This commit is contained in:
@@ -0,0 +1,800 @@
|
||||
# python library to interface with panda
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import time
|
||||
import usb1
|
||||
import struct
|
||||
import hashlib
|
||||
import binascii
|
||||
import ctypes
|
||||
from functools import wraps, partial
|
||||
from itertools import accumulate
|
||||
|
||||
import opendbc
|
||||
from opendbc.car.structs import CarParams
|
||||
|
||||
from .base import BaseHandle
|
||||
from .constants import BASEDIR, FW_PATH, McuType, compute_version_hash
|
||||
from .dfu import PandaDFU
|
||||
from .spi import PandaSpiHandle, PandaSpiException, PandaProtocolMismatch
|
||||
from .usb import PandaUsbHandle
|
||||
from .utils import logger
|
||||
|
||||
# load libusb from pip package
|
||||
try:
|
||||
import libusb_package
|
||||
usb1._libusb1.loadLibrary(ctypes.CDLL(str(libusb_package.get_library_path())))
|
||||
except ImportError:
|
||||
# TODO: remove this on next AGNOS update
|
||||
pass
|
||||
|
||||
__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_CAN_CNT = 3
|
||||
|
||||
|
||||
def calculate_checksum(data):
|
||||
res = 0
|
||||
for b in data:
|
||||
res ^= b
|
||||
return res
|
||||
|
||||
def _parse_c_struct(path, name):
|
||||
type_to_format = {"uint8_t": "B", "uint16_t": "H", "uint32_t": "I", "float": "f"}
|
||||
with open(path) as f:
|
||||
lines = [l.strip() for l in f.read().split(f"struct __attribute__((packed)) {name} {{", 1)[1].split("};", 1)[0].splitlines() if l.strip()]
|
||||
fields = [re.fullmatch(rf"({'|'.join(type_to_format)})\s+\w+;", l) for l in lines]
|
||||
if not all(fields):
|
||||
raise ValueError(f"unsupported {name} layout in {path}")
|
||||
return struct.Struct("<" + "".join(type_to_format[m[1]] for m in fields))
|
||||
|
||||
def pack_can_buffer(arr, chunk=False, fd=False):
|
||||
snds = [bytearray(), ]
|
||||
for address, dat, bus in arr:
|
||||
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].extend(header)
|
||||
snds[-1].extend(dat)
|
||||
if chunk and len(snds[-1]) > 256:
|
||||
snds.append(bytearray())
|
||||
|
||||
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_health_packet_version = partial(ensure_version, "health", "HEALTH_PACKET_VERSION", "health_version")
|
||||
|
||||
|
||||
|
||||
class Panda:
|
||||
|
||||
|
||||
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
|
||||
|
||||
# from https://github.com/commaai/openpilot/blob/103b4df18cbc38f4129555ab8b15824d1a672bdf/cereal/log.capnp#L648
|
||||
HW_TYPE_UNKNOWN = b'\x00'
|
||||
HW_TYPE_RED_PANDA = b'\x07'
|
||||
HW_TYPE_TRES = b'\x09'
|
||||
HW_TYPE_CUATRO = b'\x0a'
|
||||
HW_TYPE_BODY = b'\xb1'
|
||||
|
||||
CAN_PACKET_VERSION = compute_version_hash(os.path.join(opendbc.INCLUDE_PATH, "opendbc/safety/can.h"))
|
||||
HEALTH_PACKET_VERSION = compute_version_hash(os.path.join(BASEDIR, "board/health.h"))
|
||||
HEALTH_STRUCT = _parse_c_struct(os.path.join(BASEDIR, "board/health.h"), "health_t")
|
||||
CAN_HEALTH_STRUCT = struct.Struct("<BIBBBBBBBBIIIIIIIHHBBBIIII")
|
||||
|
||||
HEALTH_FLAG_IGNITION_LINE = 1 << 0
|
||||
HEALTH_FLAG_IGNITION_CAN = 1 << 1
|
||||
HEALTH_FLAG_CONTROLS_ALLOWED = 1 << 2
|
||||
HEALTH_FLAG_POWER_SAVE_ENABLED = 1 << 3
|
||||
HEALTH_FLAG_HEARTBEAT_LOST = 1 << 4
|
||||
HEALTH_FLAG_SAFETY_RX_CHECKS_INVALID = 1 << 5
|
||||
HEALTH_FLAG_SOM_RESET_TRIGGERED = 1 << 6
|
||||
HEALTH_FLAG_NMI_RESET = 1 << 7
|
||||
HEALTH_FLAG_HARDFAULT_RESET = 1 << 8
|
||||
|
||||
H7_DEVICES = [HW_TYPE_RED_PANDA, HW_TYPE_TRES, HW_TYPE_CUATRO, HW_TYPE_BODY]
|
||||
SUPPORTED_DEVICES = H7_DEVICES
|
||||
|
||||
INTERNAL_DEVICES = (HW_TYPE_TRES, HW_TYPE_CUATRO)
|
||||
|
||||
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
|
||||
|
||||
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 = self.usb_connect(self._connect_serial, claim=claim, no_error=wait)
|
||||
if self._handle is None:
|
||||
self._context, self._handle, serial, self.bootstub = self.spi_connect(self._connect_serial)
|
||||
if not wait:
|
||||
break
|
||||
|
||||
if self._handle is None:
|
||||
raise Exception("failed to connect to panda")
|
||||
|
||||
self._serial = serial
|
||||
self._connect_serial = serial
|
||||
self._handle_open = True
|
||||
self.health_version, self.can_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_CAN_CNT):
|
||||
self.set_canfd_auto(bus, False)
|
||||
|
||||
# set CAN speed
|
||||
for bus in range(PANDA_CAN_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):
|
||||
try:
|
||||
handle = PandaSpiHandle()
|
||||
dat = handle.get_protocol_version()
|
||||
except PandaSpiException:
|
||||
return None, None, None, False
|
||||
|
||||
spi_serial = binascii.hexlify(dat[:12]).decode()
|
||||
pid = dat[13]
|
||||
if pid not in (0xcc, 0xee):
|
||||
raise PandaProtocolMismatch(f"invalid bootstub status ({pid=}). reflash panda")
|
||||
bootstub = pid == 0xee
|
||||
spi_version = dat[14]
|
||||
|
||||
# did we get the right panda?
|
||||
if serial is not None and spi_serial != serial:
|
||||
return None, None, None, False
|
||||
|
||||
# ensure our protocol version matches the panda
|
||||
if (not ignore_version) and spi_version != handle.PROTOCOL_VERSION:
|
||||
raise PandaProtocolMismatch(f"panda protocol mismatch: expected {handle.PROTOCOL_VERSION}, got {spi_version}. reflash panda")
|
||||
|
||||
# got a device and all good
|
||||
return None, handle, spi_serial, bootstub
|
||||
|
||||
@classmethod
|
||||
def usb_connect(cls, serial, claim=True, no_error=False):
|
||||
handle, usb_serial, bootstub = 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 or sys.platform == "darwin":
|
||||
handle.claimInterface(0)
|
||||
# handle.setInterfaceAltSetting(0, 0) # Issue in USB stack
|
||||
|
||||
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
|
||||
|
||||
def is_connected_spi(self):
|
||||
return isinstance(self._handle, PandaSpiHandle)
|
||||
|
||||
def is_connected_usb(self):
|
||||
return isinstance(self._handle, PandaUsbHandle)
|
||||
|
||||
@classmethod
|
||||
def list(cls, usb_only: bool = False):
|
||||
ret = cls.usb_list()
|
||||
if not usb_only:
|
||||
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):
|
||||
if enter_bootstub or enter_bootloader:
|
||||
assert (hw_type := self.get_type()) in self.SUPPORTED_DEVICES, f"Unknown HW: {hw_type}"
|
||||
|
||||
# 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 = 0x200
|
||||
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):
|
||||
assert (hw_type := self.get_type()) in self.SUPPORTED_DEVICES, f"Unknown HW: {hw_type}"
|
||||
|
||||
if self.up_to_date(fn=fn):
|
||||
logger.info("flash: already up to date")
|
||||
return
|
||||
|
||||
if not fn:
|
||||
fn = os.path.join(FW_PATH, McuType.H7.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=McuType.H7)
|
||||
|
||||
# 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
|
||||
|
||||
@classmethod
|
||||
def wait_for_panda(cls, serial: str | None, timeout: int) -> bool:
|
||||
t_start = time.monotonic()
|
||||
serials = cls.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 = cls.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, McuType.H7.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)
|
||||
flags = a[8]
|
||||
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": bool(flags & self.HEALTH_FLAG_IGNITION_LINE),
|
||||
"ignition_can": bool(flags & self.HEALTH_FLAG_IGNITION_CAN),
|
||||
"controls_allowed": bool(flags & self.HEALTH_FLAG_CONTROLS_ALLOWED),
|
||||
"car_harness_status": a[9],
|
||||
"safety_mode": a[10],
|
||||
"safety_param": a[11],
|
||||
"fault_status": a[12],
|
||||
"power_save_enabled": bool(flags & self.HEALTH_FLAG_POWER_SAVE_ENABLED),
|
||||
"heartbeat_lost": bool(flags & self.HEALTH_FLAG_HEARTBEAT_LOST),
|
||||
"alternative_experience": a[13],
|
||||
"interrupt_load": a[14] / 255.0,
|
||||
"fan_power": a[15],
|
||||
"safety_rx_checks_invalid": bool(flags & self.HEALTH_FLAG_SAFETY_RX_CHECKS_INVALID),
|
||||
"spi_error_count": a[16],
|
||||
"sbu1_voltage_mV": a[17],
|
||||
"sbu2_voltage_mV": a[18],
|
||||
"som_reset_triggered": bool(flags & self.HEALTH_FLAG_SOM_RESET_TRIGGERED),
|
||||
"nmi_reset": bool(flags & self.HEALTH_FLAG_NMI_RESET),
|
||||
"hardfault_reset": bool(flags & self.HEALTH_FLAG_HARDFAULT_RESET),
|
||||
"sound_output_level": a[19],
|
||||
"temperature": a[20] - 40.0,
|
||||
}
|
||||
|
||||
@ensure_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):
|
||||
return self._handle.controlRead(Panda.REQUEST_IN, 0xc1, 0, 0, 0x40)
|
||||
|
||||
def get_packets_versions(self):
|
||||
dat = self._handle.controlRead(Panda.REQUEST_IN, 0xdd, 0, 0, 8)
|
||||
if dat and len(dat) == 8:
|
||||
return struct.unpack("<II", dat)
|
||||
return (0, 0)
|
||||
|
||||
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, McuType.H7)
|
||||
|
||||
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 enter_stop_mode(self):
|
||||
self._handle.controlWrite(Panda.REQUEST_OUT, 0xb5, 0, 0, b'', expect_disconnect=True)
|
||||
|
||||
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'')
|
||||
|
||||
# ******************* 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, chunk=(not self.spi), 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'')
|
||||
|
||||
def debug_read(self, maxlen=1024):
|
||||
"""Read up to maxlen bytes of internal debug logs."""
|
||||
ret = bytearray()
|
||||
while len(ret) < maxlen:
|
||||
dat = self._handle.controlRead(Panda.REQUEST_IN, 0xb6, 0, 0, min(0x40, maxlen - len(ret)))
|
||||
if not dat:
|
||||
break
|
||||
ret.extend(dat)
|
||||
return bytes(ret)
|
||||
|
||||
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 *****************
|
||||
|
||||
# arr: timer period
|
||||
# ccrN: channel N pulse length
|
||||
def set_clock_source_timer_params(self, arr, ccr1, ccr2, ccr3):
|
||||
param1 = ((ccr1 & 0xFF) << 8) | (ccr2 & 0xFF)
|
||||
param2 = ((ccr3 & 0xFF) << 8) | (arr & 0xFF)
|
||||
self._handle.controlWrite(Panda.REQUEST_OUT, 0xe6, param1, param2, 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
|
||||
@@ -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:
|
||||
...
|
||||
@@ -0,0 +1,57 @@
|
||||
import os
|
||||
import enum
|
||||
import hashlib
|
||||
from typing import NamedTuple
|
||||
|
||||
BASEDIR = os.path.join(os.path.dirname(os.path.realpath(__file__)), "../")
|
||||
FW_PATH = os.path.join(BASEDIR, "board/obj/")
|
||||
|
||||
def compute_version_hash(filepath):
|
||||
with open(filepath, "rb") as f:
|
||||
# Normalize line endings on Windows
|
||||
return int.from_bytes(hashlib.sha256(f.read().replace(b'\r', b'')).digest()[:4], 'little')
|
||||
|
||||
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])
|
||||
|
||||
|
||||
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):
|
||||
H7 = H7Config
|
||||
|
||||
@property
|
||||
def config(self):
|
||||
return self.value
|
||||
|
||||
MCU_TYPE_BY_IDCODE = {m.config.mcu_idcode: m for m in McuType}
|
||||
@@ -0,0 +1,137 @@
|
||||
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.H7):
|
||||
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")
|
||||
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 bootstub + app sectors
|
||||
for i in (0, 1):
|
||||
self._handle.erase_sector(i)
|
||||
|
||||
# write bootstub
|
||||
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))
|
||||
@@ -0,0 +1,96 @@
|
||||
# Python interface for the preflashed Jungle fixtures used by HITL tests.
|
||||
import struct
|
||||
from functools import wraps
|
||||
|
||||
from panda import Panda
|
||||
|
||||
|
||||
def ensure_jungle_health_packet_version(fn):
|
||||
@wraps(fn)
|
||||
def wrapper(self, *args, **kwargs):
|
||||
if self.health_version != self.HEALTH_PACKET_VERSION:
|
||||
raise RuntimeError(f"Jungle firmware ({self.health_version}) doesn't match the \
|
||||
library's health packet version ({self.HEALTH_PACKET_VERSION}).")
|
||||
return fn(self, *args, **kwargs)
|
||||
return wrapper
|
||||
|
||||
|
||||
class PandaJungle(Panda):
|
||||
USB_PIDS = (0xddef, 0xddcf)
|
||||
|
||||
HW_TYPE_UNKNOWN = b'\x00'
|
||||
HW_TYPE_V2 = b'\x02'
|
||||
|
||||
H7_DEVICES = [HW_TYPE_V2, ]
|
||||
SUPPORTED_DEVICES = H7_DEVICES
|
||||
|
||||
# Frozen protocol hash of the health packet in the installed Jungle firmware.
|
||||
HEALTH_PACKET_VERSION = 0xDD322770
|
||||
HEALTH_STRUCT = struct.Struct("<IffffffHHHHHHHHHHHH")
|
||||
|
||||
HARNESS_ORIENTATION_NONE = 0
|
||||
HARNESS_ORIENTATION_1 = 1
|
||||
HARNESS_ORIENTATION_2 = 2
|
||||
|
||||
@classmethod
|
||||
def spi_connect(cls, serial, ignore_version=False):
|
||||
return None, None, None, None
|
||||
|
||||
def flash(self, fn=None, code=None, reconnect=True):
|
||||
raise NotImplementedError("Jungle firmware flashing is no longer supported")
|
||||
|
||||
def recover(self, timeout: int | None = 60, reset: bool = True) -> bool:
|
||||
raise NotImplementedError("Jungle firmware recovery is no longer supported")
|
||||
|
||||
def up_to_date(self, fn=None) -> bool:
|
||||
raise NotImplementedError("Jungle firmware is no longer distributed")
|
||||
|
||||
# ******************* health *******************
|
||||
|
||||
@ensure_jungle_health_packet_version
|
||||
def health(self):
|
||||
dat = self._handle.controlRead(PandaJungle.REQUEST_IN, 0xd2, 0, 0, self.HEALTH_STRUCT.size)
|
||||
a = self.HEALTH_STRUCT.unpack(dat)
|
||||
return {
|
||||
"uptime": a[0],
|
||||
"ch1_power": a[1],
|
||||
"ch2_power": a[2],
|
||||
"ch3_power": a[3],
|
||||
"ch4_power": a[4],
|
||||
"ch5_power": a[5],
|
||||
"ch6_power": a[6],
|
||||
"ch1_sbu1_voltage": a[7] / 1000.0,
|
||||
"ch1_sbu2_voltage": a[8] / 1000.0,
|
||||
"ch2_sbu1_voltage": a[9] / 1000.0,
|
||||
"ch2_sbu2_voltage": a[10] / 1000.0,
|
||||
"ch3_sbu1_voltage": a[11] / 1000.0,
|
||||
"ch3_sbu2_voltage": a[12] / 1000.0,
|
||||
"ch4_sbu1_voltage": a[13] / 1000.0,
|
||||
"ch4_sbu2_voltage": a[14] / 1000.0,
|
||||
"ch5_sbu1_voltage": a[15] / 1000.0,
|
||||
"ch5_sbu2_voltage": a[16] / 1000.0,
|
||||
"ch6_sbu1_voltage": a[17] / 1000.0,
|
||||
"ch6_sbu2_voltage": a[18] / 1000.0,
|
||||
}
|
||||
|
||||
# ******************* control *******************
|
||||
|
||||
def get_packets_versions(self):
|
||||
dat = self._handle.controlRead(PandaJungle.REQUEST_IN, 0xdd, 0, 0, 8)
|
||||
if dat and len(dat) == 8:
|
||||
return struct.unpack("<II", dat)
|
||||
return (0, 0)
|
||||
|
||||
# ******************* jungle stuff *******************
|
||||
|
||||
def set_panda_power(self, enabled):
|
||||
self._handle.controlWrite(PandaJungle.REQUEST_OUT, 0xa0, int(enabled), 0, b'')
|
||||
|
||||
def set_harness_orientation(self, mode):
|
||||
self._handle.controlWrite(PandaJungle.REQUEST_OUT, 0xa1, int(mode), 0, b'')
|
||||
|
||||
def set_ignition(self, enabled):
|
||||
self._handle.controlWrite(PandaJungle.REQUEST_OUT, 0xa2, int(enabled), 0, b'')
|
||||
|
||||
def set_can_silent(self, silent):
|
||||
self._handle.controlWrite(PandaJungle.REQUEST_OUT, 0xf5, int(silent), 0, b'')
|
||||
@@ -0,0 +1,123 @@
|
||||
import socket
|
||||
import struct
|
||||
import time
|
||||
|
||||
# /**
|
||||
# * 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
|
||||
|
||||
CAN_CONFIRM_FLAG = 0x800
|
||||
CAN_EFF_FLAG = 0x80000000
|
||||
|
||||
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
|
||||
|
||||
import typing
|
||||
@typing.no_type_check # mypy struggles with macOS here...
|
||||
def create_socketcan(interface:str, recv_buffer_size:int) -> 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)
|
||||
socketcan.setblocking(False)
|
||||
socketcan.setsockopt(socket.SOL_CAN_RAW, socket.CAN_RAW_FD_FRAMES, 1)
|
||||
socketcan.setsockopt(socket.SOL_CAN_RAW, socket.CAN_RAW_RECV_OWN_MSGS, 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", recv_buffer_size:int=212992) -> None:
|
||||
self.interface = interface
|
||||
self.recv_buffer_size = recv_buffer_size
|
||||
self.socket = create_socketcan(interface, recv_buffer_size)
|
||||
|
||||
def __del__(self):
|
||||
self.socket.close()
|
||||
|
||||
def get_serial(self) -> tuple[int, int]:
|
||||
return (0, 0)
|
||||
|
||||
def get_version(self) -> int:
|
||||
return 0
|
||||
|
||||
def can_clear(self, bus:int) -> None:
|
||||
self.socket.close()
|
||||
self.socket = create_socketcan(self.interface, self.recv_buffer_size)
|
||||
|
||||
def set_safety_mode(self, mode:int, param=0) -> None:
|
||||
pass
|
||||
|
||||
def can_send_many(self, arr, *, fd=False, timeout=0) -> None:
|
||||
for msg in arr:
|
||||
self.can_send(*msg, fd=fd, timeout=timeout)
|
||||
|
||||
def can_send(self, addr, dat, bus, *, fd=False, timeout=0) -> None:
|
||||
# Even if the CANFD_FDF flag is not set, the data still must be 8 bytes for classic CAN frames.
|
||||
data_len = CANFD_MAX_DLEN if fd else CAN_MAX_DLEN
|
||||
msg_len = len(dat)
|
||||
msg_dat = dat.ljust(data_len, b'\x00')
|
||||
|
||||
# Set extended ID flag
|
||||
if addr > 0x7ff:
|
||||
addr |= CAN_EFF_FLAG
|
||||
|
||||
# Set FD flags
|
||||
flags = CANFD_BRS | CANFD_FDF if fd else 0
|
||||
|
||||
can_frame = struct.pack(CAN_HEADER_FMT, addr, msg_len, flags) + msg_dat
|
||||
|
||||
# Try to send until timeout. sendto might block if the TX buffer is full.
|
||||
# TX buffer size can also be adjusted through `ip link set can0 txqueuelen <size>` if needed
|
||||
start_t = time.monotonic()
|
||||
while (time.monotonic() - start_t < (timeout / 1000)) or (timeout == 0):
|
||||
try:
|
||||
self.socket.sendto(can_frame, (self.interface,))
|
||||
break
|
||||
except (BlockingIOError, OSError):
|
||||
continue
|
||||
else:
|
||||
raise TimeoutError
|
||||
|
||||
|
||||
def can_recv(self) -> list[tuple[int, bytes, int]]:
|
||||
msgs = list()
|
||||
while True:
|
||||
try:
|
||||
dat, _, msg_flags, _ = self.socket.recvmsg(self.recv_buffer_size)
|
||||
assert len(dat) >= CAN_HEADER_LEN, f"ERROR: received {len(dat)} bytes"
|
||||
|
||||
can_id, msg_len, _ = struct.unpack(CAN_HEADER_FMT, dat[:CAN_HEADER_LEN])
|
||||
assert len(dat) >= CAN_HEADER_LEN + msg_len, f"ERROR: received {len(dat)} bytes, expected at least {CAN_HEADER_LEN + msg_len} bytes"
|
||||
|
||||
msg_dat = dat[CAN_HEADER_LEN:CAN_HEADER_LEN+msg_len]
|
||||
bus = 128 if (msg_flags & CAN_CONFIRM_FLAG) else 0
|
||||
msgs.append((can_id, msg_dat, bus))
|
||||
except BlockingIOError:
|
||||
break # buffered data exhausted
|
||||
return msgs
|
||||
@@ -0,0 +1,471 @@
|
||||
import binascii
|
||||
import ctypes
|
||||
import os
|
||||
import math
|
||||
import time
|
||||
import struct
|
||||
import threading
|
||||
from contextlib import contextmanager
|
||||
from functools import reduce
|
||||
|
||||
from .base import BaseHandle, BaseSTBootloaderHandle, TIMEOUT
|
||||
from .constants import McuType, MCU_TYPE_BY_IDCODE, USBPACKET_MAX_SIZE
|
||||
from .utils import logger
|
||||
|
||||
# Constants
|
||||
SYNC = 0x5A
|
||||
HACK = 0x79
|
||||
DACK = 0x85
|
||||
NACK = 0x1F
|
||||
CHECKSUM_START = 0xAB
|
||||
|
||||
MIN_ACK_TIMEOUT_MS = 100
|
||||
MAX_XFER_RETRY_COUNT = 5
|
||||
|
||||
SPI_BUF_SIZE = 4096 # from panda/board/drivers/spi.h
|
||||
XFER_SIZE = SPI_BUF_SIZE - 0x40 # give some room for SPI protocol overhead
|
||||
|
||||
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
|
||||
|
||||
|
||||
SPI_LOCK = threading.Lock()
|
||||
SPI_DEVICES = {}
|
||||
class SpiDevice:
|
||||
"""Provides locked, thread-safe access to a panda's SPI interface.
|
||||
|
||||
xfer2 returns a view valid until the next transfer; xfer returns an owned copy.
|
||||
"""
|
||||
|
||||
MAX_SPEED = 50000000 # max of the SDM845
|
||||
|
||||
# Linux asm-generic ioctl ABI (including aarch64 and x86_64), linux/spi/spidev.h.
|
||||
SPI_IOC_RD_BITS_PER_WORD = 0x80016B03
|
||||
SPI_IOC_WR_MAX_SPEED_HZ = 0x40046B04
|
||||
SPI_IOC_MESSAGE_1 = 0x40206B00
|
||||
SPI_IOC_TRANSFER = struct.Struct('=QQIIHBBBBBB')
|
||||
MAX_TRANSFER_SIZE = 4096
|
||||
|
||||
def __init__(self, speed=MAX_SPEED):
|
||||
try:
|
||||
import fcntl
|
||||
except ImportError as e:
|
||||
raise PandaSpiUnavailable("SPI requires Linux") from e
|
||||
|
||||
assert speed <= self.MAX_SPEED
|
||||
if not os.path.exists(DEV_PATH):
|
||||
raise PandaSpiUnavailable(f"SPI device not found: {DEV_PATH}")
|
||||
|
||||
self._ioctl = fcntl.ioctl
|
||||
self._flock = fcntl.flock
|
||||
self._lock_ex, self._lock_un = fcntl.LOCK_EX, fcntl.LOCK_UN
|
||||
with SPI_LOCK:
|
||||
if speed not in SPI_DEVICES:
|
||||
file = open(DEV_PATH, 'r+b', buffering=0)
|
||||
try:
|
||||
self._ioctl(file.fileno(), self.SPI_IOC_WR_MAX_SPEED_HZ, struct.pack('=I', speed))
|
||||
bits = self._ioctl(file.fileno(), self.SPI_IOC_RD_BITS_PER_WORD, b'\x00')[0]
|
||||
except BaseException:
|
||||
file.close()
|
||||
raise
|
||||
SPI_DEVICES[speed] = (file, bits)
|
||||
self._file, bits = SPI_DEVICES[speed]
|
||||
|
||||
self._fd = self._file.fileno()
|
||||
self._buffer = bytearray(self.MAX_TRANSFER_SIZE)
|
||||
self._view = memoryview(self._buffer)
|
||||
address = ctypes.addressof(ctypes.c_char.from_buffer(self._buffer))
|
||||
# The kernel supports using the same buffer for transmit and receive.
|
||||
self._transfer = bytearray(self.SPI_IOC_TRANSFER.pack(address, address, 0, speed, 0, bits, 0, 0, 0, 0, 0))
|
||||
self._transfer_words = memoryview(self._transfer).cast('I')
|
||||
|
||||
@contextmanager
|
||||
def acquire(self):
|
||||
with SPI_LOCK:
|
||||
self._flock(self._fd, self._lock_ex)
|
||||
try:
|
||||
yield self
|
||||
finally:
|
||||
self._flock(self._fd, self._lock_un)
|
||||
|
||||
def fileno(self) -> int:
|
||||
return self._fd
|
||||
|
||||
def close(self):
|
||||
pass # Connections are shared and cached by speed.
|
||||
|
||||
@classmethod
|
||||
def _check_length(cls, length: int):
|
||||
if not 0 < length <= cls.MAX_TRANSFER_SIZE:
|
||||
raise ValueError(f"SPI transfer length must be between 1 and {cls.MAX_TRANSFER_SIZE}")
|
||||
|
||||
def xfer2(self, data) -> memoryview:
|
||||
length = len(data)
|
||||
if not 0 < length <= self.MAX_TRANSFER_SIZE:
|
||||
raise ValueError(f"SPI transfer length must be between 1 and {self.MAX_TRANSFER_SIZE}")
|
||||
self._view[:length] = bytes(data)
|
||||
self._transfer_words[4] = length # len field at byte offset 16
|
||||
if self._ioctl(self._fd, self.SPI_IOC_MESSAGE_1, self._transfer) != length:
|
||||
raise OSError("Short SPI transfer")
|
||||
return self._view[:length]
|
||||
|
||||
# Both operations use one SPI_IOC_MESSAGE(1) for the single-block calls in panda.
|
||||
def xfer(self, data) -> bytes:
|
||||
return bytes(self.xfer2(data))
|
||||
|
||||
def readbytes(self, length: int) -> bytes:
|
||||
self._check_length(length)
|
||||
data = os.read(self.fileno(), length)
|
||||
if len(data) != length:
|
||||
raise OSError("Short SPI read")
|
||||
return data
|
||||
|
||||
def writebytes(self, data):
|
||||
data = bytes(data)
|
||||
self._check_length(len(data))
|
||||
if os.write(self.fileno(), data) != len(data):
|
||||
raise OSError("Short SPI write")
|
||||
|
||||
class PandaSpiHandle(BaseHandle):
|
||||
"""
|
||||
A class that mimics a libusb1 handle for panda SPI communications.
|
||||
"""
|
||||
|
||||
PROTOCOL_VERSION = 2
|
||||
HEADER = struct.Struct("<BBHH")
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.dev = SpiDevice()
|
||||
self.no_retry = "NO_RETRY" in os.environ
|
||||
|
||||
# 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] == ack_val:
|
||||
return bytes(dat)
|
||||
elif dat[0] == NACK:
|
||||
raise PandaSpiNackResponse
|
||||
|
||||
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 = self.HEADER.pack(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(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_spidev(spi, endpoint, data, timeout, max_rx_len, expect_disconnect)
|
||||
except PandaSpiException as e:
|
||||
exc = e
|
||||
logger.debug("SPI transfer failed, retrying", exc_info=True)
|
||||
if self.no_retry:
|
||||
break
|
||||
|
||||
# ensure slave is in a consistent state and ready for the next transfer
|
||||
# (e.g. slave TX buffer isn't stuck full)
|
||||
nack_cnt = 0
|
||||
attempts = 5
|
||||
while (nack_cnt <= 3) and (attempts > 0):
|
||||
attempts -= 1
|
||||
try:
|
||||
self._wait_for_ack(spi, NACK, MIN_ACK_TIMEOUT_MS, 0x11, length=XFER_SIZE//2)
|
||||
nack_cnt += 1
|
||||
except PandaSpiException:
|
||||
nack_cnt = 0
|
||||
|
||||
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:
|
||||
mv = memoryview(data)
|
||||
for x in range(math.ceil(len(data) / XFER_SIZE)):
|
||||
self._transfer(endpoint, mv[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
|
||||
|
||||
NOTE: the bootloader's state machine is fragile and immediately gets into a bad state when
|
||||
sending any junk, e.g. when using the panda SPI protocol.
|
||||
"""
|
||||
|
||||
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)
|
||||
@@ -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
|
||||
@@ -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