python lib: add SPI bootloader support (#1224)

* talk to spi bootloader

* flashing bootstub

* reset

* get mcu type

* little more

* pull out low level panda comms

* lint

* program app

---------

Co-authored-by: Comma Device <device@comma.ai>
This commit is contained in:
Adeeb Shihadeh
2023-01-28 00:32:07 -08:00
committed by GitHub
parent 76d0459182
commit 6ae5212d6a
4 changed files with 193 additions and 33 deletions

View File

@@ -4,6 +4,7 @@ import math
import time
import struct
import logging
import threading
from contextlib import contextmanager
from functools import reduce
from typing import List
@@ -47,26 +48,42 @@ class PandaSpiTransferFailed(PandaSpiException):
pass
@contextmanager
def flocked(fd):
try:
fcntl.flock(fd, fcntl.LOCK_EX)
yield
finally:
fcntl.flock(fd, fcntl.LOCK_UN)
SPI_LOCK = threading.Lock()
# This mimics the handle given by libusb1 for easy interoperability
class SpiHandle:
def __init__(self):
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.spi = spidev.SpiDev() # pylint: disable=c-extension-no-member
self.spi.open(0, 0)
self._spidev = spidev.SpiDev() # pylint: disable=c-extension-no-member
self._spidev.open(0, 0)
self._spidev.max_speed_hz = speed
self.spi.max_speed_hz = 30000000
@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 SpiHandle:
"""
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:
@@ -75,10 +92,10 @@ class SpiHandle:
cksum ^= b
return cksum
def _wait_for_ack(self, ack_val: int) -> None:
def _wait_for_ack(self, spi, ack_val: int) -> None:
start = time.monotonic()
while (time.monotonic() - start) < ACK_TIMEOUT_SECONDS:
dat = self.spi.xfer2(b"\x12")[0]
dat = spi.xfer2(b"\x12")[0]
if dat == NACK:
raise PandaSpiNackResponse
elif dat == ack_val:
@@ -86,7 +103,7 @@ class SpiHandle:
raise PandaSpiMissingAck
def _transfer(self, endpoint: int, data, max_rx_len: int = 1000) -> bytes:
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("==============================================")
@@ -97,25 +114,25 @@ class SpiHandle:
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 header ACK")
self._wait_for_ack(HACK)
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 data ACK")
self._wait_for_ack(DACK)
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 PandaSpiBadChecksum
@@ -127,28 +144,28 @@ class SpiHandle:
# 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):
with flocked(self.spi):
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):
with flocked(self.spi):
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:
with flocked(self.spi):
with self.dev.acquire() as spi:
for x in range(math.ceil(len(data) / USB_MAX_SIZE)):
self._transfer(endpoint, data[USB_MAX_SIZE*x:USB_MAX_SIZE*(x+1)])
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] = []
with flocked(self.spi):
with self.dev.acquire() as spi:
for _ in range(math.ceil(length / USB_MAX_SIZE)):
d = self._transfer(endpoint, [], max_rx_len=USB_MAX_SIZE)
d = self._transfer(spi, endpoint, [], max_rx_len=USB_MAX_SIZE)
ret += d
if len(d) < USB_MAX_SIZE:
break