mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-07-19 16:22:08 +08:00
openpilot v0.9.1 release
date: 2023-02-17T21:02:50 master commit: 89f68bf0cbf53a81b0553d3816fdbe522f941fa1
This commit is contained in:
committed by
Adeeb Shihadeh
parent
75c97aaf3f
commit
bcd4bb4821
+237
-173
@@ -5,20 +5,19 @@ import time
|
||||
import usb1
|
||||
import struct
|
||||
import hashlib
|
||||
import binascii
|
||||
import datetime
|
||||
import traceback
|
||||
import warnings
|
||||
import logging
|
||||
from functools import wraps
|
||||
from typing import Optional
|
||||
from itertools import accumulate
|
||||
|
||||
from .dfu import PandaDFU, MCU_TYPE_F2, MCU_TYPE_F4, MCU_TYPE_H7 # pylint: disable=import-error
|
||||
from .flash_release import flash_release # noqa pylint: disable=import-error
|
||||
from .update import ensure_st_up_to_date # noqa pylint: disable=import-error
|
||||
from .serial import PandaSerial # noqa pylint: disable=import-error
|
||||
from .isotp import isotp_send, isotp_recv # pylint: disable=import-error
|
||||
from .config import DEFAULT_FW_FN, DEFAULT_H7_FW_FN, SECTOR_SIZES_FX, SECTOR_SIZES_H7 # noqa pylint: disable=import-error
|
||||
from .constants import McuType
|
||||
from .dfu import PandaDFU
|
||||
from .isotp import isotp_send, isotp_recv
|
||||
from .spi import PandaSpiHandle, PandaSpiException
|
||||
from .usb import PandaUsbHandle
|
||||
|
||||
__version__ = '0.0.10'
|
||||
|
||||
@@ -27,82 +26,70 @@ LOGLEVEL = os.environ.get('LOGLEVEL', 'INFO').upper()
|
||||
logging.basicConfig(level=LOGLEVEL, format='%(message)s')
|
||||
|
||||
|
||||
BASEDIR = os.path.join(os.path.dirname(os.path.realpath(__file__)), "../")
|
||||
|
||||
DEBUG = os.getenv("PANDADEBUG") is not None
|
||||
|
||||
CANPACKET_HEAD_SIZE = 0x5
|
||||
USBPACKET_MAX_SIZE = 0x40
|
||||
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)}
|
||||
|
||||
def calculate_checksum(data):
|
||||
res = 0
|
||||
for b in data:
|
||||
res ^= b
|
||||
return res
|
||||
|
||||
def pack_can_buffer(arr):
|
||||
snds = [b'']
|
||||
idx = 0
|
||||
for address, _, dat, bus in arr:
|
||||
assert len(dat) in LEN_TO_DLC
|
||||
if DEBUG:
|
||||
print(f" W 0x{address:x}: 0x{dat.hex()}")
|
||||
#logging.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(5)
|
||||
header = bytearray(CANPACKET_HEAD_SIZE)
|
||||
word_4b = address << 3 | extended << 2
|
||||
header[0] = (data_len_code << 4) | (bus << 1)
|
||||
header[1] = word_4b & 0xFF
|
||||
header[2] = (word_4b >> 8) & 0xFF
|
||||
header[3] = (word_4b >> 16) & 0xFF
|
||||
header[4] = (word_4b >> 24) & 0xFF
|
||||
snds[idx] += header + dat
|
||||
if len(snds[idx]) > 256: # Limit chunks to 256 bytes
|
||||
snds.append(b'')
|
||||
idx += 1
|
||||
header[5] = calculate_checksum(header[:5] + dat)
|
||||
|
||||
snds[-1] += header + dat
|
||||
if len(snds[-1]) > 256: # Limit chunks to 256 bytes
|
||||
snds.append(b'')
|
||||
|
||||
# Apply counter to each 64 byte packet
|
||||
for idx in range(len(snds)):
|
||||
tx = b''
|
||||
counter = 0
|
||||
for i in range (0, len(snds[idx]), 63):
|
||||
tx += bytes([counter]) + snds[idx][i:i+63]
|
||||
counter += 1
|
||||
snds[idx] = tx
|
||||
return snds
|
||||
|
||||
def unpack_can_buffer(dat):
|
||||
ret = []
|
||||
counter = 0
|
||||
tail = bytearray()
|
||||
for i in range(0, len(dat), 64):
|
||||
if counter != dat[i]:
|
||||
print("CAN: LOST RECV PACKET COUNTER")
|
||||
|
||||
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
|
||||
counter+=1
|
||||
chunk = tail + dat[i+1:i+64]
|
||||
tail = bytearray()
|
||||
pos = 0
|
||||
while pos<len(chunk):
|
||||
data_len = DLC_TO_LEN[(chunk[pos]>>4)]
|
||||
pckt_len = CANPACKET_HEAD_SIZE + data_len
|
||||
if pckt_len <= len(chunk[pos:]):
|
||||
header = chunk[pos:pos+CANPACKET_HEAD_SIZE]
|
||||
if len(header) < 5:
|
||||
print("CAN: MALFORMED USB RECV PACKET")
|
||||
break
|
||||
bus = (header[0] >> 1) & 0x7
|
||||
address = (header[4] << 24 | header[3] << 16 | header[2] << 8 | header[1]) >> 3
|
||||
returned = (header[1] >> 1) & 0x1
|
||||
rejected = header[1] & 0x1
|
||||
data = chunk[pos + CANPACKET_HEAD_SIZE:pos + CANPACKET_HEAD_SIZE + data_len]
|
||||
if returned:
|
||||
bus += 128
|
||||
if rejected:
|
||||
bus += 192
|
||||
if DEBUG:
|
||||
print(f" R 0x{address:x}: 0x{data.hex()}")
|
||||
ret.append((address, 0, data, bus))
|
||||
pos += pckt_len
|
||||
else:
|
||||
tail = chunk[pos:]
|
||||
break
|
||||
return ret
|
||||
|
||||
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, 0, data, bus))
|
||||
|
||||
return (ret, dat)
|
||||
|
||||
def ensure_health_packet_version(fn):
|
||||
@wraps(fn)
|
||||
@@ -174,6 +161,7 @@ class Panda:
|
||||
SERIAL_ESP = 1
|
||||
SERIAL_LIN1 = 2
|
||||
SERIAL_LIN2 = 3
|
||||
SERIAL_SOM_DEBUG = 4
|
||||
|
||||
GMLAN_CAN2 = 1
|
||||
GMLAN_CAN3 = 2
|
||||
@@ -192,11 +180,11 @@ class Panda:
|
||||
HW_TYPE_RED_PANDA_V2 = b'\x08'
|
||||
HW_TYPE_TRES = b'\x09'
|
||||
|
||||
CAN_PACKET_VERSION = 2
|
||||
CAN_PACKET_VERSION = 4
|
||||
HEALTH_PACKET_VERSION = 11
|
||||
CAN_HEALTH_PACKET_VERSION = 3
|
||||
CAN_HEALTH_PACKET_VERSION = 4
|
||||
HEALTH_STRUCT = struct.Struct("<IIIIIIIIIBBBBBBHBBBHfBB")
|
||||
CAN_HEALTH_STRUCT = struct.Struct("<BIBBBBBBBBIIIIIIHHBBB")
|
||||
CAN_HEALTH_STRUCT = struct.Struct("<BIBBBBBBBBIIIIIIIHHBBB")
|
||||
|
||||
F2_DEVICES = (HW_TYPE_PEDAL, )
|
||||
F4_DEVICES = (HW_TYPE_WHITE_PANDA, HW_TYPE_GREY_PANDA, HW_TYPE_BLACK_PANDA, HW_TYPE_UNO, HW_TYPE_DOS)
|
||||
@@ -205,9 +193,6 @@ class Panda:
|
||||
INTERNAL_DEVICES = (HW_TYPE_UNO, HW_TYPE_DOS)
|
||||
HAS_OBD = (HW_TYPE_BLACK_PANDA, HW_TYPE_UNO, HW_TYPE_DOS, HW_TYPE_RED_PANDA, HW_TYPE_RED_PANDA_V2, HW_TYPE_TRES)
|
||||
|
||||
CLOCK_SOURCE_MODE_DISABLED = 0
|
||||
CLOCK_SOURCE_MODE_FREE_RUNNING = 1
|
||||
|
||||
# first byte is for EPS scaling factor
|
||||
FLAG_TOYOTA_ALT_BRAKE = (1 << 8)
|
||||
FLAG_TOYOTA_STOCK_LONGITUDINAL = (2 << 8)
|
||||
@@ -223,6 +208,7 @@ class Panda:
|
||||
FLAG_HYUNDAI_CAMERA_SCC = 8
|
||||
FLAG_HYUNDAI_CANFD_HDA2 = 16
|
||||
FLAG_HYUNDAI_CANFD_ALT_BUTTONS = 32
|
||||
FLAG_HYUNDAI_ALT_LIMITS = 64
|
||||
|
||||
FLAG_TESLA_POWERTRAIN = 1
|
||||
FLAG_TESLA_LONG_CONTROL = 2
|
||||
@@ -237,17 +223,25 @@ class Panda:
|
||||
FLAG_GM_HW_CAM = 1
|
||||
FLAG_GM_HW_CAM_LONG = 2
|
||||
|
||||
def __init__(self, serial: Optional[str] = None, claim: bool = True, spi: bool = False, disable_checks: bool = True):
|
||||
def __init__(self, serial: Optional[str] = None, claim: bool = True, disable_checks: bool = True):
|
||||
self._serial = serial
|
||||
self._disable_checks = disable_checks
|
||||
|
||||
self._handle = None
|
||||
self._bcd_device = None
|
||||
self.can_rx_overflow_buffer = b''
|
||||
|
||||
# connect and set mcu type
|
||||
self._spi = spi
|
||||
self.connect(claim)
|
||||
|
||||
# reset comms
|
||||
self.can_reset_communications()
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, *args):
|
||||
self.close()
|
||||
|
||||
def close(self):
|
||||
self._handle.close()
|
||||
self._handle = None
|
||||
@@ -257,29 +251,64 @@ class Panda:
|
||||
self.close()
|
||||
self._handle = None
|
||||
|
||||
if self._spi:
|
||||
# TODO: move this back. need to wait until next AGNOS build
|
||||
from .spi import SpiHandle # noqa pylint: disable=import-error
|
||||
self._handle = SpiHandle()
|
||||
# try USB first, then SPI
|
||||
self._handle, serial, self.bootstub, bcd = self.usb_connect(self._serial, claim=claim, wait=wait)
|
||||
if self._handle is None:
|
||||
self._handle, serial, self.bootstub, bcd = self.spi_connect(self._serial)
|
||||
|
||||
# TODO implement
|
||||
self._serial = "SPIDEV"
|
||||
self.bootstub = False
|
||||
if self._handle is None:
|
||||
raise Exception("failed to connect to panda")
|
||||
|
||||
else:
|
||||
self.usb_connect(claim=claim, wait=wait)
|
||||
# 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
|
||||
|
||||
assert self._handle is not None
|
||||
# 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._mcu_type = self.get_mcu_type()
|
||||
self.health_version, self.can_version, self.can_health_version = self.get_packets_versions()
|
||||
print("connected")
|
||||
logging.debug("connected")
|
||||
|
||||
# disable openpilot's heartbeat checks
|
||||
if self._disable_checks:
|
||||
self.set_heartbeat_disabled()
|
||||
self.set_power_save(0)
|
||||
|
||||
def usb_connect(self, claim=True, wait=False):
|
||||
@staticmethod
|
||||
def spi_connect(serial):
|
||||
# get UID to confirm slave is present and up
|
||||
handle = None
|
||||
spi_serial = None
|
||||
try:
|
||||
handle = PandaSpiHandle()
|
||||
dat = handle.controlRead(Panda.REQUEST_IN, 0xc3, 0, 0, 12)
|
||||
spi_serial = binascii.hexlify(dat).decode()
|
||||
except PandaSpiException:
|
||||
pass
|
||||
|
||||
# no connection or wrong panda
|
||||
if spi_serial is None or (serial is not None and (spi_serial != serial)):
|
||||
handle = None
|
||||
spi_serial = None
|
||||
|
||||
# TODO: detect bootstub
|
||||
return handle, spi_serial, False, None
|
||||
|
||||
@staticmethod
|
||||
def usb_connect(serial, claim=True, wait=False):
|
||||
handle, usb_serial, bootstub, bcd = None, None, None, None
|
||||
context = usb1.USBContext()
|
||||
while 1:
|
||||
try:
|
||||
@@ -289,30 +318,69 @@ class Panda:
|
||||
this_serial = device.getSerialNumber()
|
||||
except Exception:
|
||||
continue
|
||||
if self._serial is None or this_serial == self._serial:
|
||||
self._serial = this_serial
|
||||
print("opening device", self._serial, hex(device.getProductID()))
|
||||
self.bootstub = device.getProductID() == 0xddee
|
||||
self._handle = device.open()
|
||||
|
||||
if serial is None or this_serial == serial:
|
||||
logging.debug("opening device %s %s", this_serial, hex(device.getProductID()))
|
||||
|
||||
usb_serial = this_serial
|
||||
bootstub = device.getProductID() == 0xddee
|
||||
handle = device.open()
|
||||
if sys.platform not in ("win32", "cygwin", "msys", "darwin"):
|
||||
self._handle.setAutoDetachKernelDriver(True)
|
||||
handle.setAutoDetachKernelDriver(True)
|
||||
if claim:
|
||||
self._handle.claimInterface(0)
|
||||
# self._handle.setInterfaceAltSetting(0, 0) # Issue in USB stack
|
||||
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
|
||||
bcd = device.getbcdDevice()
|
||||
if bcd is not None and bcd != 0x2300:
|
||||
self._bcd_device = bytearray([bcd >> 8, ])
|
||||
this_bcd = device.getbcdDevice()
|
||||
if this_bcd is not None and this_bcd != 0x2300:
|
||||
bcd = bytearray([this_bcd >> 8, ])
|
||||
|
||||
break
|
||||
except Exception as e:
|
||||
print("exception", e)
|
||||
traceback.print_exc()
|
||||
if not wait or self._handle is not None:
|
||||
except Exception:
|
||||
logging.exception("USB connect error")
|
||||
if not wait or handle is not None:
|
||||
break
|
||||
context = usb1.USBContext() # New context needed so new devices show up
|
||||
|
||||
usb_handle = None
|
||||
if handle is not None:
|
||||
usb_handle = PandaUsbHandle(handle)
|
||||
|
||||
return usb_handle, usb_serial, bootstub, bcd
|
||||
|
||||
@staticmethod
|
||||
def list():
|
||||
ret = Panda.usb_list()
|
||||
ret += Panda.spi_list()
|
||||
return list(set(ret))
|
||||
|
||||
@staticmethod
|
||||
def usb_list():
|
||||
context = usb1.USBContext()
|
||||
ret = []
|
||||
try:
|
||||
for device in context.getDeviceList(skip_on_error=True):
|
||||
if device.getVendorID() == 0xbbaa and device.getProductID() in (0xddcc, 0xddee):
|
||||
try:
|
||||
serial = device.getSerialNumber()
|
||||
if len(serial) == 24:
|
||||
ret.append(serial)
|
||||
else:
|
||||
warnings.warn(f"found device with panda descriptors but invalid serial: {serial}", RuntimeWarning)
|
||||
except Exception:
|
||||
continue
|
||||
except Exception:
|
||||
pass
|
||||
return ret
|
||||
|
||||
@staticmethod
|
||||
def spi_list():
|
||||
_, serial, _, _ = Panda.spi_connect(None)
|
||||
if serial is not None:
|
||||
return [serial, ]
|
||||
return []
|
||||
|
||||
def reset(self, enter_bootstub=False, enter_bootloader=False, reconnect=True):
|
||||
try:
|
||||
if enter_bootloader:
|
||||
@@ -340,7 +408,7 @@ class Panda:
|
||||
success = True
|
||||
break
|
||||
except Exception:
|
||||
print("reconnecting is taking %d seconds..." % (i + 1))
|
||||
logging.debug("reconnecting is taking %d seconds...", i + 1)
|
||||
try:
|
||||
dfu = PandaDFU(PandaDFU.st_serial_to_dfu_serial(self._serial, self._mcu_type))
|
||||
dfu.recover()
|
||||
@@ -350,8 +418,6 @@ class Panda:
|
||||
if not success:
|
||||
raise Exception("reconnect failed")
|
||||
|
||||
|
||||
|
||||
@staticmethod
|
||||
def flash_static(handle, code, mcu_type):
|
||||
assert mcu_type is not None, "must set valid mcu_type to flash"
|
||||
@@ -361,28 +427,28 @@ class Panda:
|
||||
assert fr[4:8] == b"\xde\xad\xd0\x0d"
|
||||
|
||||
# determine sectors to erase
|
||||
apps_sectors_cumsum = accumulate(SECTOR_SIZES_H7[1:] if mcu_type == MCU_TYPE_H7 else SECTOR_SIZES_FX[1:])
|
||||
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
|
||||
print("flash: unlocking")
|
||||
logging.warning("flash: unlocking")
|
||||
handle.controlWrite(Panda.REQUEST_IN, 0xb1, 0, 0, b'')
|
||||
|
||||
# erase sectors
|
||||
print(f"flash: erasing sectors 1 - {last_sector}")
|
||||
logging.warning(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
|
||||
print("flash: flashing")
|
||||
logging.warning("flash: flashing")
|
||||
for i in range(0, len(code), STEP):
|
||||
handle.bulkWrite(2, code[i:i + STEP])
|
||||
|
||||
# reset
|
||||
print("flash: resetting")
|
||||
logging.warning("flash: resetting")
|
||||
try:
|
||||
handle.controlWrite(Panda.REQUEST_IN, 0xd8, 0, 0, b'')
|
||||
except Exception:
|
||||
@@ -390,9 +456,9 @@ class Panda:
|
||||
|
||||
def flash(self, fn=None, code=None, reconnect=True):
|
||||
if not fn:
|
||||
fn = DEFAULT_H7_FW_FN if self._mcu_type == MCU_TYPE_H7 else DEFAULT_FW_FN
|
||||
fn = self._mcu_type.config.app_path
|
||||
assert os.path.isfile(fn)
|
||||
print("flash: main version is " + self.get_version())
|
||||
logging.debug("flash: main version is %s", self.get_version())
|
||||
if not self.bootstub:
|
||||
self.reset(enter_bootstub=True)
|
||||
assert(self.bootstub)
|
||||
@@ -402,7 +468,7 @@ class Panda:
|
||||
code = f.read()
|
||||
|
||||
# get version
|
||||
print("flash: bootstub version is " + self.get_version())
|
||||
logging.debug("flash: bootstub version is %s", self.get_version())
|
||||
|
||||
# do flash
|
||||
Panda.flash_static(self._handle, code, mcu_type=self._mcu_type)
|
||||
@@ -433,31 +499,12 @@ class Panda:
|
||||
def wait_for_dfu(dfu_serial: str, timeout: Optional[int] = None) -> bool:
|
||||
t_start = time.monotonic()
|
||||
while dfu_serial not in PandaDFU.list():
|
||||
print("waiting for DFU...")
|
||||
logging.debug("waiting for DFU...")
|
||||
time.sleep(0.1)
|
||||
if timeout is not None and (time.monotonic() - t_start) > timeout:
|
||||
return False
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def list():
|
||||
context = usb1.USBContext()
|
||||
ret = []
|
||||
try:
|
||||
for device in context.getDeviceList(skip_on_error=True):
|
||||
if device.getVendorID() == 0xbbaa and device.getProductID() in (0xddcc, 0xddee):
|
||||
try:
|
||||
serial = device.getSerialNumber()
|
||||
if len(serial) == 24:
|
||||
ret.append(serial)
|
||||
else:
|
||||
warnings.warn(f"found device with panda descriptors but invalid serial: {serial}", RuntimeWarning)
|
||||
except Exception:
|
||||
continue
|
||||
except Exception:
|
||||
pass
|
||||
return ret
|
||||
|
||||
def call_control_api(self, msg):
|
||||
self._handle.controlWrite(Panda.REQUEST_OUT, msg, 0, 0, b'')
|
||||
|
||||
@@ -524,11 +571,12 @@ class Panda:
|
||||
"total_tx_cnt": a[13],
|
||||
"total_rx_cnt": a[14],
|
||||
"total_fwd_cnt": a[15],
|
||||
"can_speed": a[16],
|
||||
"can_data_speed": a[17],
|
||||
"canfd_enabled": a[18],
|
||||
"brs_enabled": a[19],
|
||||
"canfd_non_iso": a[20],
|
||||
"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],
|
||||
}
|
||||
|
||||
# ******************* control *******************
|
||||
@@ -536,8 +584,8 @@ class Panda:
|
||||
def enter_bootloader(self):
|
||||
try:
|
||||
self._handle.controlWrite(Panda.REQUEST_OUT, 0xd1, 0, 0, b'')
|
||||
except Exception as e:
|
||||
print(e)
|
||||
except Exception:
|
||||
logging.exception("exception while entering bootloader")
|
||||
|
||||
def get_version(self):
|
||||
return self._handle.controlRead(Panda.REQUEST_IN, 0xd6, 0, 0, 0x40).decode('utf8')
|
||||
@@ -556,10 +604,9 @@ class Panda:
|
||||
def get_type(self):
|
||||
ret = self._handle.controlRead(Panda.REQUEST_IN, 0xc1, 0, 0, 0x40)
|
||||
|
||||
# bootstub doesn't implement this call, so fallback to bcdDevice
|
||||
invalid_type = self.bootstub and (ret is None or len(ret) != 1)
|
||||
if invalid_type and self._bcd_device is not None:
|
||||
ret = self._bcd_device
|
||||
# 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
|
||||
|
||||
@@ -572,15 +619,20 @@ class Panda:
|
||||
else:
|
||||
return (0, 0, 0)
|
||||
|
||||
def get_mcu_type(self):
|
||||
def get_mcu_type(self) -> McuType:
|
||||
hw_type = self.get_type()
|
||||
if hw_type in Panda.F2_DEVICES:
|
||||
return MCU_TYPE_F2
|
||||
return McuType.F2
|
||||
elif hw_type in Panda.F4_DEVICES:
|
||||
return MCU_TYPE_F4
|
||||
return McuType.F4
|
||||
elif hw_type in Panda.H7_DEVICES:
|
||||
return MCU_TYPE_H7
|
||||
return None
|
||||
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
|
||||
@@ -589,22 +641,33 @@ class Panda:
|
||||
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_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)
|
||||
|
||||
# ******************* configuration *******************
|
||||
|
||||
def set_usb_power(self, on):
|
||||
self._handle.controlWrite(Panda.REQUEST_OUT, 0xe6, int(on), 0, b'')
|
||||
|
||||
def set_power_save(self, power_save_enabled=0):
|
||||
self._handle.controlWrite(Panda.REQUEST_OUT, 0xe7, int(power_save_enabled), 0, b'')
|
||||
|
||||
@@ -666,6 +729,9 @@ class Panda:
|
||||
# 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, timeout=CAN_SEND_TIMEOUT_MS):
|
||||
snds = pack_can_buffer(arr)
|
||||
@@ -677,10 +743,10 @@ class Panda:
|
||||
tx = tx[bs:]
|
||||
if len(tx) == 0:
|
||||
break
|
||||
print("CAN: PARTIAL SEND MANY, RETRYING")
|
||||
logging.error("CAN: PARTIAL SEND MANY, RETRYING")
|
||||
break
|
||||
except (usb1.USBErrorIO, usb1.USBErrorOverflow):
|
||||
print("CAN: BAD SEND MANY, RETRYING")
|
||||
logging.error("CAN: BAD SEND MANY, RETRYING")
|
||||
|
||||
def can_send(self, addr, dat, bus, timeout=CAN_SEND_TIMEOUT_MS):
|
||||
self.can_send_many([[addr, None, dat, bus]], timeout=timeout)
|
||||
@@ -693,9 +759,10 @@ class Panda:
|
||||
dat = self._handle.bulkRead(1, 16384) # Max receive batch size + 2 extra reserve frames
|
||||
break
|
||||
except (usb1.USBErrorIO, usb1.USBErrorOverflow):
|
||||
print("CAN: BAD RECV, RETRYING")
|
||||
logging.error("CAN: BAD RECV, RETRYING")
|
||||
time.sleep(0.1)
|
||||
return unpack_can_buffer(dat)
|
||||
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
|
||||
@@ -729,6 +796,8 @@ class Panda:
|
||||
|
||||
def serial_write(self, port_number, ln):
|
||||
ret = 0
|
||||
if type(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
|
||||
@@ -748,19 +817,15 @@ class Panda:
|
||||
# pulse low for wakeup
|
||||
def kline_wakeup(self, k=True, l=True):
|
||||
assert k or l, "must specify k-line, l-line, or both"
|
||||
if DEBUG:
|
||||
print("kline wakeup...")
|
||||
logging.debug("kline wakeup...")
|
||||
self._handle.controlWrite(Panda.REQUEST_OUT, 0xf0, 2 if k and l else int(l), 0, b'')
|
||||
if DEBUG:
|
||||
print("kline wakeup done")
|
||||
logging.debug("kline wakeup done")
|
||||
|
||||
def kline_5baud(self, addr, k=True, l=True):
|
||||
assert k or l, "must specify k-line, l-line, or both"
|
||||
if DEBUG:
|
||||
print("kline 5 baud...")
|
||||
logging.debug("kline 5 baud...")
|
||||
self._handle.controlWrite(Panda.REQUEST_OUT, 0xf4, 2 if k and l else int(l), addr, b'')
|
||||
if DEBUG:
|
||||
print("kline 5 baud done")
|
||||
logging.debug("kline 5 baud done")
|
||||
|
||||
def kline_drain(self, bus=2):
|
||||
# drain buffer
|
||||
@@ -769,8 +834,7 @@ class Panda:
|
||||
ret = self._handle.controlRead(Panda.REQUEST_IN, 0xe0, bus, 0, 0x40)
|
||||
if len(ret) == 0:
|
||||
break
|
||||
elif DEBUG:
|
||||
print(f"kline drain: 0x{ret.hex()}")
|
||||
logging.debug(f"kline drain: 0x{ret.hex()}")
|
||||
bret += ret
|
||||
return bytes(bret)
|
||||
|
||||
@@ -778,8 +842,8 @@ class Panda:
|
||||
echo = bytearray()
|
||||
while len(echo) != cnt:
|
||||
ret = self._handle.controlRead(Panda.REQUEST_OUT, 0xe0, bus, 0, cnt - len(echo))
|
||||
if DEBUG and len(ret) > 0:
|
||||
print(f"kline recv: 0x{ret.hex()}")
|
||||
if len(ret) > 0:
|
||||
logging.debug(f"kline recv: 0x{ret.hex()}")
|
||||
echo += ret
|
||||
return bytes(echo)
|
||||
|
||||
@@ -789,14 +853,13 @@ class Panda:
|
||||
x += bytes([sum(x) % 0x100])
|
||||
for i in range(0, len(x), 0xf):
|
||||
ts = x[i:i + 0xf]
|
||||
if DEBUG:
|
||||
print(f"kline send: 0x{ts.hex()}")
|
||||
logging.debug(f"kline send: 0x{ts.hex()}")
|
||||
self._handle.bulkWrite(2, bytes([bus]) + ts)
|
||||
echo = self.kline_ll_recv(len(ts), bus=bus)
|
||||
if echo != ts:
|
||||
print(f"**** ECHO ERROR {i} ****")
|
||||
print(f"0x{echo.hex()}")
|
||||
print(f"0x{ts.hex()}")
|
||||
logging.error(f"**** ECHO ERROR {i} ****")
|
||||
logging.error(f"0x{echo.hex()}")
|
||||
logging.error(f"0x{ts.hex()}")
|
||||
assert echo == ts
|
||||
|
||||
def kline_recv(self, bus=2, header_len=4):
|
||||
@@ -829,6 +892,11 @@ class Panda:
|
||||
a = struct.unpack("HBBBBBB", dat)
|
||||
return datetime.datetime(a[0], a[1], a[2], a[4], a[5], a[6])
|
||||
|
||||
# ****************** 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'')
|
||||
@@ -846,10 +914,6 @@ class Panda:
|
||||
def set_phone_power(self, enabled):
|
||||
self._handle.controlWrite(Panda.REQUEST_OUT, 0xb3, int(enabled), 0, b'')
|
||||
|
||||
# ************** Clock Source **************
|
||||
def set_clock_source_mode(self, mode):
|
||||
self._handle.controlWrite(Panda.REQUEST_OUT, 0xf5, int(mode), 0, b'')
|
||||
|
||||
# ****************** Siren *****************
|
||||
def set_siren(self, enabled):
|
||||
self._handle.controlWrite(Panda.REQUEST_OUT, 0xf6, int(enabled), 0, b'')
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import List
|
||||
|
||||
|
||||
# This mimics the handle given by libusb1 for easy interoperability
|
||||
class BaseHandle(ABC):
|
||||
@abstractmethod
|
||||
def close(self) -> None:
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
def controlWrite(self, request_type: int, request: int, value: int, index: int, data, timeout: int = 0) -> int:
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
def controlRead(self, request_type: int, request: int, value: int, index: int, length: int, timeout: int = 0) -> bytes:
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
def bulkWrite(self, endpoint: int, data: List[int], timeout: int = 0) -> int:
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
def bulkRead(self, endpoint: int, length: int, timeout: int = 0) -> bytes:
|
||||
...
|
||||
+6
-6
@@ -100,9 +100,9 @@ class CcpClient():
|
||||
msgs = self._panda.can_recv() or []
|
||||
if len(msgs) >= 256:
|
||||
print("CAN RX buffer overflow!!!", file=sys.stderr)
|
||||
for rx_addr, _, rx_data, rx_bus in msgs:
|
||||
for rx_addr, _, rx_data_bytearray, rx_bus in msgs:
|
||||
if rx_bus == self.can_bus and rx_addr == self.rx_addr:
|
||||
rx_data = bytes(rx_data) # convert bytearray to bytes
|
||||
rx_data = bytes(rx_data_bytearray)
|
||||
if self.debug:
|
||||
print(f"CAN-RX: {hex(rx_addr)} - 0x{bytes.hex(rx_data)}")
|
||||
assert len(rx_data) == 8, f"message length not 8: {len(rx_data)}"
|
||||
@@ -183,7 +183,7 @@ class CcpClient():
|
||||
resp = self._recv_dto(0.025)
|
||||
# mta_addr_ext = resp[0]
|
||||
mta_addr = struct.unpack(f"{self.byte_order.value}I", resp[1:5])[0]
|
||||
return mta_addr
|
||||
return mta_addr # type: ignore
|
||||
|
||||
def download_6_bytes(self, data: bytes) -> int:
|
||||
if len(data) != 6:
|
||||
@@ -192,7 +192,7 @@ class CcpClient():
|
||||
resp = self._recv_dto(0.025)
|
||||
# mta_addr_ext = resp[0]
|
||||
mta_addr = struct.unpack(f"{self.byte_order.value}I", resp[1:5])[0]
|
||||
return mta_addr
|
||||
return mta_addr # type: ignore
|
||||
|
||||
def upload(self, size: int) -> bytes:
|
||||
if size > 5:
|
||||
@@ -296,7 +296,7 @@ class CcpClient():
|
||||
resp = self._recv_dto(0.1)
|
||||
# mta_addr_ext = resp[0]
|
||||
mta_addr = struct.unpack(f"{self.byte_order.value}I", resp[1:5])[0]
|
||||
return mta_addr
|
||||
return mta_addr # type: ignore
|
||||
|
||||
def program_6_bytes(self, data: bytes) -> int:
|
||||
if len(data) != 6:
|
||||
@@ -305,7 +305,7 @@ class CcpClient():
|
||||
resp = self._recv_dto(0.1)
|
||||
# mta_addr_ext = resp[0]
|
||||
mta_addr = struct.unpack(f"{self.byte_order.value}I", resp[1:5])[0]
|
||||
return mta_addr
|
||||
return mta_addr # type: ignore
|
||||
|
||||
def move_memory_block(self, size: int) -> None:
|
||||
self._send_cro(COMMAND_CODE.MOVE, struct.pack(f"{self.byte_order.value}I", size))
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
import os
|
||||
|
||||
BASEDIR = os.path.join(os.path.dirname(os.path.realpath(__file__)), "../")
|
||||
|
||||
BOOTSTUB_ADDRESS = 0x8000000
|
||||
|
||||
BLOCK_SIZE_FX = 0x800
|
||||
APP_ADDRESS_FX = 0x8004000
|
||||
SECTOR_SIZES_FX = [0x4000 for _ in range(4)] + [0x10000] + [0x20000 for _ in range(11)]
|
||||
DEVICE_SERIAL_NUMBER_ADDR_FX = 0x1FFF79C0
|
||||
DEFAULT_FW_FN = os.path.join(BASEDIR, "board", "obj", "panda.bin.signed")
|
||||
DEFAULT_BOOTSTUB_FN = os.path.join(BASEDIR, "board", "obj", "bootstub.panda.bin")
|
||||
|
||||
BLOCK_SIZE_H7 = 0x400
|
||||
APP_ADDRESS_H7 = 0x8020000
|
||||
SECTOR_SIZES_H7 = [0x20000 for _ in range(7)] # there is an 8th sector, but we use that for the provisioning chunk, so don't program over that!
|
||||
DEVICE_SERIAL_NUMBER_ADDR_H7 = 0x080FFFC0
|
||||
DEFAULT_H7_FW_FN = os.path.join(BASEDIR, "board", "obj", "panda_h7.bin.signed")
|
||||
DEFAULT_H7_BOOTSTUB_FN = os.path.join(BASEDIR, "board", "obj", "bootstub.panda_h7.bin")
|
||||
@@ -0,0 +1,52 @@
|
||||
import os
|
||||
import enum
|
||||
from typing import List, NamedTuple
|
||||
|
||||
BASEDIR = os.path.join(os.path.dirname(os.path.realpath(__file__)), "../")
|
||||
|
||||
|
||||
class McuConfig(NamedTuple):
|
||||
mcu: str
|
||||
mcu_idcode: int
|
||||
block_size: int
|
||||
sector_sizes: List[int]
|
||||
serial_number_address: int
|
||||
app_address: int
|
||||
app_path: str
|
||||
bootstub_address: int
|
||||
bootstub_path: str
|
||||
|
||||
Fx = (
|
||||
0x800,
|
||||
[0x4000 for _ in range(4)] + [0x10000] + [0x20000 for _ in range(11)],
|
||||
0x1FFF79C0,
|
||||
0x8004000,
|
||||
os.path.join(BASEDIR, "board", "obj", "panda.bin.signed"),
|
||||
0x8000000,
|
||||
os.path.join(BASEDIR, "board", "obj", "bootstub.panda.bin"),
|
||||
)
|
||||
F2Config = McuConfig("STM32F2", 0x411, *Fx)
|
||||
F4Config = McuConfig("STM32F4", 0x463, *Fx)
|
||||
|
||||
H7Config = McuConfig(
|
||||
"STM32H7",
|
||||
0x483,
|
||||
0x400,
|
||||
# there is an 8th sector, but we use that for the provisioning chunk, so don't program over that!
|
||||
[0x20000 for _ in range(7)],
|
||||
0x080FFFC0,
|
||||
0x8020000,
|
||||
os.path.join(BASEDIR, "board", "obj", "panda_h7.bin.signed"),
|
||||
0x8000000,
|
||||
os.path.join(BASEDIR, "board", "obj", "bootstub.panda_h7.bin"),
|
||||
)
|
||||
|
||||
@enum.unique
|
||||
class McuType(enum.Enum):
|
||||
F2 = F2Config
|
||||
F4 = F4Config
|
||||
H7 = H7Config
|
||||
|
||||
@property
|
||||
def config(self):
|
||||
return self.value
|
||||
+19
-26
@@ -1,12 +1,9 @@
|
||||
import usb1
|
||||
import struct
|
||||
import binascii
|
||||
from .config import BOOTSTUB_ADDRESS, APP_ADDRESS_H7, APP_ADDRESS_FX, BLOCK_SIZE_H7, BLOCK_SIZE_FX, DEFAULT_H7_BOOTSTUB_FN, DEFAULT_BOOTSTUB_FN
|
||||
|
||||
from .constants import McuType
|
||||
|
||||
MCU_TYPE_F2 = 0
|
||||
MCU_TYPE_F4 = 1
|
||||
MCU_TYPE_H7 = 2
|
||||
|
||||
# *** DFU mode ***
|
||||
DFU_DNLOAD = 1
|
||||
@@ -15,8 +12,9 @@ DFU_GETSTATUS = 3
|
||||
DFU_CLRSTATUS = 4
|
||||
DFU_ABORT = 6
|
||||
|
||||
class PandaDFU(object):
|
||||
class PandaDFU:
|
||||
def __init__(self, dfu_serial):
|
||||
self._handle = None
|
||||
context = usb1.USBContext()
|
||||
for device in context.getDeviceList(skip_on_error=True):
|
||||
if device.getVendorID() == 0x0483 and device.getProductID() == 0xdf11:
|
||||
@@ -25,10 +23,12 @@ class PandaDFU(object):
|
||||
except Exception:
|
||||
continue
|
||||
if this_dfu_serial == dfu_serial or dfu_serial is None:
|
||||
self._mcu_type = self.get_mcu_type(device)
|
||||
self._handle = device.open()
|
||||
return
|
||||
raise Exception("failed to open " + dfu_serial if dfu_serial is not None else "DFU device")
|
||||
self._mcu_type = self.get_mcu_type(device)
|
||||
break
|
||||
|
||||
if self._handle is None:
|
||||
raise Exception(f"failed to open DFU device {dfu_serial}")
|
||||
|
||||
@staticmethod
|
||||
def list():
|
||||
@@ -46,18 +46,19 @@ class PandaDFU(object):
|
||||
return dfu_serials
|
||||
|
||||
@staticmethod
|
||||
def st_serial_to_dfu_serial(st, mcu_type=MCU_TYPE_F4):
|
||||
def st_serial_to_dfu_serial(st, mcu_type=McuType.F4):
|
||||
if st is None or st == "none":
|
||||
return None
|
||||
uid_base = struct.unpack("H" * 6, bytes.fromhex(st))
|
||||
if mcu_type == MCU_TYPE_H7:
|
||||
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")
|
||||
|
||||
# TODO: Find a way to detect F4 vs F2
|
||||
def get_mcu_type(self, dev):
|
||||
return MCU_TYPE_H7 if dev.getbcdDevice() == 512 else MCU_TYPE_F4
|
||||
def get_mcu_type(self, dev) -> McuType:
|
||||
# TODO: Find a way to detect F4 vs F2
|
||||
# TODO: also check F4 BCD, don't assume in else
|
||||
return McuType.H7 if dev.getbcdDevice() == 512 else McuType.F4
|
||||
|
||||
def status(self):
|
||||
while 1:
|
||||
@@ -97,26 +98,18 @@ class PandaDFU(object):
|
||||
|
||||
def program_bootstub(self, code_bootstub):
|
||||
self.clear_status()
|
||||
self.erase(BOOTSTUB_ADDRESS)
|
||||
if self._mcu_type == MCU_TYPE_H7:
|
||||
self.erase(APP_ADDRESS_H7)
|
||||
self.program(BOOTSTUB_ADDRESS, code_bootstub, BLOCK_SIZE_H7)
|
||||
else:
|
||||
self.erase(APP_ADDRESS_FX)
|
||||
self.program(BOOTSTUB_ADDRESS, code_bootstub, BLOCK_SIZE_FX)
|
||||
self.erase(self._mcu_type.config.bootstub_address)
|
||||
self.erase(self._mcu_type.config.app_address)
|
||||
self.program(self._mcu_type.config.bootstub_address, code_bootstub, self._mcu_type.config.block_size)
|
||||
self.reset()
|
||||
|
||||
def recover(self):
|
||||
fn = DEFAULT_H7_BOOTSTUB_FN if self._mcu_type == MCU_TYPE_H7 else DEFAULT_BOOTSTUB_FN
|
||||
|
||||
with open(fn, "rb") as f:
|
||||
with open(self._mcu_type.config.bootstub_path, "rb") as f:
|
||||
code = f.read()
|
||||
|
||||
self.program_bootstub(code)
|
||||
|
||||
def reset(self):
|
||||
# **** Reset ****
|
||||
self._handle.controlWrite(0x21, DFU_DNLOAD, 0, 0, b"\x21" + struct.pack("I", BOOTSTUB_ADDRESS))
|
||||
self._handle.controlWrite(0x21, DFU_DNLOAD, 0, 0, b"\x21" + struct.pack("I", self._mcu_type.config.bootstub_address))
|
||||
self.status()
|
||||
try:
|
||||
self._handle.controlWrite(0x21, DFU_DNLOAD, 2, 0, b"")
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import sys
|
||||
import time
|
||||
import requests
|
||||
import json
|
||||
import io
|
||||
|
||||
def flash_release(path=None, st_serial=None):
|
||||
from panda import Panda, PandaDFU
|
||||
from zipfile import ZipFile
|
||||
|
||||
def status(x):
|
||||
print("\033[1;32;40m" + x + "\033[00m")
|
||||
|
||||
if st_serial is not None:
|
||||
# look for Panda
|
||||
panda_list = Panda.list()
|
||||
if len(panda_list) == 0:
|
||||
raise Exception("panda not found, make sure it's connected and your user can access it")
|
||||
elif len(panda_list) > 1:
|
||||
raise Exception("Please only connect one panda")
|
||||
st_serial = panda_list[0]
|
||||
print("Using panda with serial %s" % st_serial)
|
||||
|
||||
if path is None:
|
||||
print("Fetching latest firmware from github.com/commaai/panda-artifacts")
|
||||
r = requests.get("https://raw.githubusercontent.com/commaai/panda-artifacts/master/latest.json")
|
||||
url = json.loads(r.text)['url']
|
||||
r = requests.get(url)
|
||||
print("Fetching firmware from %s" % url)
|
||||
path = io.BytesIO(r.content)
|
||||
|
||||
zf = ZipFile(path)
|
||||
zf.printdir()
|
||||
|
||||
version = zf.read("version").decode().strip()
|
||||
status("0. Preparing to flash " + str(version))
|
||||
|
||||
code_bootstub = zf.read("bootstub.panda.bin")
|
||||
code_panda = zf.read("panda.bin")
|
||||
|
||||
# enter DFU mode
|
||||
status("1. Entering DFU mode")
|
||||
panda = Panda(st_serial)
|
||||
panda.reset(enter_bootstub=True)
|
||||
panda.reset(enter_bootloader=True)
|
||||
time.sleep(1)
|
||||
|
||||
# program bootstub
|
||||
status("2. Programming bootstub")
|
||||
dfu = PandaDFU(PandaDFU.st_serial_to_dfu_serial(st_serial))
|
||||
dfu.program_bootstub(code_bootstub)
|
||||
time.sleep(1)
|
||||
|
||||
# flash main code
|
||||
status("3. Flashing main code")
|
||||
panda = Panda(st_serial)
|
||||
panda.flash(code=code_panda)
|
||||
panda.close()
|
||||
|
||||
# check for connection
|
||||
status("4. Verifying version")
|
||||
panda = Panda(st_serial)
|
||||
my_version = panda.get_version()
|
||||
print("dongle id: %s" % panda.get_serial()[0])
|
||||
print(my_version, "should be", version)
|
||||
assert(str(version) == str(my_version))
|
||||
|
||||
# done!
|
||||
status("6. Success!")
|
||||
|
||||
if __name__ == "__main__":
|
||||
flash_release(*sys.argv[1:])
|
||||
+113
-44
@@ -1,10 +1,21 @@
|
||||
import os
|
||||
import fcntl
|
||||
import math
|
||||
import time
|
||||
import struct
|
||||
import spidev
|
||||
import logging
|
||||
import threading
|
||||
from contextlib import contextmanager
|
||||
from functools import reduce
|
||||
from typing import List
|
||||
|
||||
from .base import BaseHandle
|
||||
|
||||
try:
|
||||
import spidev
|
||||
except ImportError:
|
||||
spidev = None
|
||||
|
||||
# Constants
|
||||
SYNC = 0x5A
|
||||
HACK = 0x79
|
||||
@@ -12,17 +23,69 @@ DACK = 0x85
|
||||
NACK = 0x1F
|
||||
CHECKSUM_START = 0xAB
|
||||
|
||||
MAX_RETRY_COUNT = 5
|
||||
ACK_TIMEOUT_SECONDS = 0.1
|
||||
MAX_XFER_RETRY_COUNT = 5
|
||||
|
||||
USB_MAX_SIZE = 0x40
|
||||
|
||||
# This mimics the handle given by libusb1 for easy interoperability
|
||||
class SpiHandle:
|
||||
def __init__(self):
|
||||
self.spi = spidev.SpiDev() # pylint: disable=c-extension-no-member
|
||||
self.spi.open(0, 0)
|
||||
DEV_PATH = "/dev/spidev0.0"
|
||||
|
||||
self.spi.max_speed_hz = 30000000
|
||||
|
||||
class PandaSpiException(Exception):
|
||||
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()
|
||||
|
||||
class SpiDevice:
|
||||
"""
|
||||
Provides locked, thread-safe access to a panda's SPI interface.
|
||||
"""
|
||||
def __init__(self, speed=30000000):
|
||||
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")
|
||||
|
||||
self._spidev = spidev.SpiDev() # pylint: disable=c-extension-no-member
|
||||
self._spidev.open(0, 0)
|
||||
self._spidev.max_speed_hz = 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):
|
||||
self._spidev.close()
|
||||
|
||||
|
||||
class PandaSpiHandle(BaseHandle):
|
||||
"""
|
||||
A class that mimics a libusb1 handle for panda SPI communications.
|
||||
"""
|
||||
def __init__(self):
|
||||
self.dev = SpiDevice()
|
||||
|
||||
# helpers
|
||||
def _calc_checksum(self, data: List[int]) -> int:
|
||||
@@ -31,75 +94,81 @@ class SpiHandle:
|
||||
cksum ^= b
|
||||
return cksum
|
||||
|
||||
def _transfer(self, endpoint: int, data, max_rx_len: int = 1000) -> bytes:
|
||||
def _wait_for_ack(self, spi, ack_val: int) -> None:
|
||||
start = time.monotonic()
|
||||
while (time.monotonic() - start) < ACK_TIMEOUT_SECONDS:
|
||||
dat = spi.xfer2(b"\x12")[0]
|
||||
if dat == NACK:
|
||||
raise PandaSpiNackResponse
|
||||
elif dat == ack_val:
|
||||
return
|
||||
|
||||
raise PandaSpiMissingAck
|
||||
|
||||
def _transfer(self, spi, endpoint: int, data, max_rx_len: int = 1000) -> bytes:
|
||||
logging.debug("starting transfer: endpoint=%d, max_rx_len=%d", endpoint, max_rx_len)
|
||||
logging.debug("==============================================")
|
||||
|
||||
for n in range(MAX_RETRY_COUNT):
|
||||
exc = PandaSpiException()
|
||||
for n in range(MAX_XFER_RETRY_COUNT):
|
||||
logging.debug("\ntry #%d", n+1)
|
||||
try:
|
||||
logging.debug("- send header")
|
||||
packet = struct.pack("<BBHH", SYNC, endpoint, len(data), max_rx_len)
|
||||
packet += bytes([reduce(lambda x, y: x^y, packet) ^ CHECKSUM_START])
|
||||
self.spi.xfer2(packet)
|
||||
spi.xfer2(packet)
|
||||
|
||||
logging.debug("- waiting for ACK")
|
||||
# TODO: add timeout?
|
||||
dat = b"\x00"
|
||||
while dat[0] not in [HACK, NACK]:
|
||||
dat = self.spi.xfer2(b"\x12")
|
||||
|
||||
if dat[0] == NACK:
|
||||
raise Exception("Got NACK response for header")
|
||||
logging.debug("- waiting for header ACK")
|
||||
self._wait_for_ack(spi, HACK)
|
||||
|
||||
# send data
|
||||
logging.debug("- sending data")
|
||||
packet = bytes([*data, self._calc_checksum(data)])
|
||||
self.spi.xfer2(packet)
|
||||
spi.xfer2(packet)
|
||||
|
||||
logging.debug("- waiting for ACK")
|
||||
dat = b"\x00"
|
||||
while dat[0] not in [DACK, NACK]:
|
||||
dat = self.spi.xfer2(b"\xab")
|
||||
|
||||
if dat[0] == NACK:
|
||||
raise Exception("Got NACK response for data")
|
||||
logging.debug("- waiting for data ACK")
|
||||
self._wait_for_ack(spi, DACK)
|
||||
|
||||
# get response length, then response
|
||||
response_len_bytes = bytes(self.spi.xfer2(b"\x00" * 2))
|
||||
response_len_bytes = bytes(spi.xfer2(b"\x00" * 2))
|
||||
response_len = struct.unpack("<H", response_len_bytes)[0]
|
||||
|
||||
logging.debug("- receiving response")
|
||||
dat = bytes(self.spi.xfer2(b"\x00" * (response_len + 1)))
|
||||
dat = bytes(spi.xfer2(b"\x00" * (response_len + 1)))
|
||||
if self._calc_checksum([DACK, *response_len_bytes, *dat]) != 0:
|
||||
raise Exception("SPI got bad checksum")
|
||||
raise PandaSpiBadChecksum
|
||||
|
||||
return dat[:-1]
|
||||
except Exception:
|
||||
logging.exception("SPI transfer failed, %d retries left", n)
|
||||
raise Exception(f"SPI transaction failed {MAX_RETRY_COUNT} times")
|
||||
except PandaSpiException as e:
|
||||
exc = e
|
||||
logging.debug("SPI transfer failed, %d retries left", n, exc_info=True)
|
||||
raise exc
|
||||
|
||||
# libusb1 functions
|
||||
def close(self):
|
||||
self.spi.close()
|
||||
self.dev.close()
|
||||
|
||||
def controlWrite(self, request_type: int, request: int, value: int, index: int, data, timeout: int = 0):
|
||||
return self._transfer(0, struct.pack("<BHHH", request, value, index, 0))
|
||||
with self.dev.acquire() as spi:
|
||||
return self._transfer(spi, 0, struct.pack("<BHHH", request, value, index, 0))
|
||||
|
||||
def controlRead(self, request_type: int, request: int, value: int, index: int, length: int, timeout: int = 0):
|
||||
return self._transfer(0, struct.pack("<BHHH", request, value, index, length))
|
||||
with self.dev.acquire() as spi:
|
||||
return self._transfer(spi, 0, struct.pack("<BHHH", request, value, index, length))
|
||||
|
||||
# TODO: implement these properly
|
||||
def bulkWrite(self, endpoint: int, data: List[int], timeout: int = 0) -> int:
|
||||
for x in range(math.ceil(len(data) / USB_MAX_SIZE)):
|
||||
self._transfer(endpoint, data[USB_MAX_SIZE*x:USB_MAX_SIZE*(x+1)])
|
||||
return len(data)
|
||||
with self.dev.acquire() as spi:
|
||||
for x in range(math.ceil(len(data) / USB_MAX_SIZE)):
|
||||
self._transfer(spi, endpoint, data[USB_MAX_SIZE*x:USB_MAX_SIZE*(x+1)])
|
||||
return len(data)
|
||||
|
||||
def bulkRead(self, endpoint: int, length: int, timeout: int = 0) -> bytes:
|
||||
ret: List[int] = []
|
||||
for _ in range(math.ceil(length / USB_MAX_SIZE)):
|
||||
d = self._transfer(endpoint, [], max_rx_len=USB_MAX_SIZE)
|
||||
ret += d
|
||||
if len(d) < USB_MAX_SIZE:
|
||||
break
|
||||
with self.dev.acquire() as spi:
|
||||
for _ in range(math.ceil(length / USB_MAX_SIZE)):
|
||||
d = self._transfer(spi, endpoint, [], max_rx_len=USB_MAX_SIZE)
|
||||
ret += d
|
||||
if len(d) < USB_MAX_SIZE:
|
||||
break
|
||||
return bytes(ret)
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
import time
|
||||
import struct
|
||||
from functools import reduce
|
||||
|
||||
from .constants import McuType
|
||||
from .spi import SpiDevice
|
||||
|
||||
SYNC = 0x5A
|
||||
ACK = 0x79
|
||||
NACK = 0x1F
|
||||
|
||||
# https://www.st.com/resource/en/application_note/an4286-spi-protocol-used-in-the-stm32-bootloader-stmicroelectronics.pdf
|
||||
class PandaSpiDFU:
|
||||
def __init__(self, dfu_serial):
|
||||
self.dev = SpiDevice(speed=1000000)
|
||||
|
||||
# say hello
|
||||
with self.dev.acquire() as spi:
|
||||
try:
|
||||
spi.xfer([SYNC, ])
|
||||
self._get_ack(spi)
|
||||
except Exception:
|
||||
raise Exception("failed to connect to panda") # pylint: disable=W0707
|
||||
|
||||
self._mcu_type = self.get_mcu_type()
|
||||
|
||||
def _get_ack(self, spi, timeout=1.0):
|
||||
data = 0x00
|
||||
start_time = time.monotonic()
|
||||
while data not in (ACK, NACK) and (time.monotonic() - start_time < timeout):
|
||||
data = spi.xfer([0x00, ])[0]
|
||||
time.sleep(0.001)
|
||||
spi.xfer([ACK, ])
|
||||
|
||||
if data == NACK:
|
||||
raise Exception("Got NACK response")
|
||||
elif data != ACK:
|
||||
raise Exception("Missing ACK")
|
||||
|
||||
def _cmd(self, cmd, data=None, read_bytes=0) -> bytes:
|
||||
ret = b""
|
||||
with self.dev.acquire() as spi:
|
||||
# sync
|
||||
spi.xfer([SYNC, ])
|
||||
|
||||
# send command
|
||||
spi.xfer([cmd, cmd ^ 0xFF])
|
||||
self._get_ack(spi)
|
||||
|
||||
# send data
|
||||
if data is not None:
|
||||
for d in data:
|
||||
spi.xfer(self.add_checksum(d))
|
||||
self._get_ack(spi, timeout=20)
|
||||
|
||||
# receive
|
||||
if read_bytes > 0:
|
||||
# send busy byte
|
||||
ret = spi.xfer([0x00, ]*(read_bytes + 1))[1:]
|
||||
self._get_ack(spi)
|
||||
|
||||
return ret
|
||||
|
||||
def add_checksum(self, data):
|
||||
return data + bytes([reduce(lambda a, b: a ^ b, data)])
|
||||
|
||||
# ***** ST Bootloader functions *****
|
||||
|
||||
def get_bootloader_version(self) -> int:
|
||||
return self._cmd(0x01, read_bytes=1)[0]
|
||||
|
||||
def get_id(self) -> int:
|
||||
ret = self._cmd(0x02, read_bytes=3)
|
||||
assert ret[0] == 1
|
||||
return ((ret[1] << 8) + ret[2])
|
||||
|
||||
def go_cmd(self, address: int) -> None:
|
||||
self._cmd(0x21, data=[struct.pack('>I', address), ])
|
||||
|
||||
def erase(self, address: int) -> None:
|
||||
d = struct.pack('>H', address)
|
||||
self._cmd(0x44, data=[d, ])
|
||||
|
||||
# ***** panda api *****
|
||||
|
||||
def get_mcu_type(self) -> McuType:
|
||||
mcu_by_id = {mcu.config.mcu_idcode: mcu for mcu in McuType}
|
||||
return mcu_by_id[self.get_id()]
|
||||
|
||||
def global_erase(self):
|
||||
self.erase(0xFFFF)
|
||||
|
||||
def program_file(self, address, fn):
|
||||
with open(fn, 'rb') as f:
|
||||
code = f.read()
|
||||
|
||||
i = 0
|
||||
while i < len(code):
|
||||
#print(i, len(code))
|
||||
block = code[i:i+256]
|
||||
if len(block) < 256:
|
||||
block += b'\xFF' * (256 - len(block))
|
||||
|
||||
self._cmd(0x31, data=[
|
||||
struct.pack('>I', address + i),
|
||||
bytes([len(block) - 1]) + block,
|
||||
])
|
||||
#print(f"Written {len(block)} bytes to {hex(address + i)}")
|
||||
i += 256
|
||||
|
||||
def program_bootstub(self):
|
||||
self.program_file(self._mcu_type.config.bootstub_address, self._mcu_type.config.bootstub_path)
|
||||
|
||||
def program_app(self):
|
||||
self.program_file(self._mcu_type.config.app_address, self._mcu_type.config.app_path)
|
||||
|
||||
def reset(self):
|
||||
self.go_cmd(self._mcu_type.config.bootstub_address)
|
||||
@@ -1,44 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
import os
|
||||
import time
|
||||
|
||||
def ensure_st_up_to_date():
|
||||
from panda import Panda, PandaDFU, BASEDIR
|
||||
|
||||
with open(os.path.join(BASEDIR, "VERSION")) as f:
|
||||
repo_version = f.read()
|
||||
|
||||
repo_version += "-EON" if os.path.isfile('/EON') else "-DEV"
|
||||
|
||||
panda = None
|
||||
panda_dfu = None
|
||||
|
||||
while 1:
|
||||
# break on normal mode Panda
|
||||
panda_list = Panda.list()
|
||||
if len(panda_list) > 0:
|
||||
panda = Panda(panda_list[0])
|
||||
break
|
||||
|
||||
# flash on DFU mode Panda
|
||||
panda_dfu = PandaDFU.list()
|
||||
if len(panda_dfu) > 0:
|
||||
panda_dfu = PandaDFU(panda_dfu[0])
|
||||
panda_dfu.recover()
|
||||
|
||||
print("waiting for board...")
|
||||
time.sleep(1)
|
||||
|
||||
if panda.bootstub or not panda.get_version().startswith(repo_version):
|
||||
panda.flash()
|
||||
|
||||
if panda.bootstub:
|
||||
panda.recover()
|
||||
|
||||
assert(not panda.bootstub)
|
||||
version = str(panda.get_version())
|
||||
print("%s should be %s" % (version, repo_version))
|
||||
assert(version.startswith(repo_version))
|
||||
|
||||
if __name__ == "__main__":
|
||||
ensure_st_up_to_date()
|
||||
@@ -0,0 +1,23 @@
|
||||
from typing import List
|
||||
|
||||
from .base import BaseHandle
|
||||
|
||||
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 = 0):
|
||||
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 = 0):
|
||||
return self._libusb_handle.controlRead(request_type, request, value, index, length, timeout)
|
||||
|
||||
def bulkWrite(self, endpoint: int, data: List[int], timeout: int = 0) -> int:
|
||||
return self._libusb_handle.bulkWrite(endpoint, data, timeout) # type: ignore
|
||||
|
||||
def bulkRead(self, endpoint: int, length: int, timeout: int = 0) -> bytes:
|
||||
return self._libusb_handle.bulkRead(endpoint, length, timeout) # type: ignore
|
||||
|
||||
Reference in New Issue
Block a user