IQ.Pilot Prebuilt Release @ 27f668a
This commit is contained in:
3
iqpilot/selfdrive/pandad/.gitignore
vendored
Normal file
3
iqpilot/selfdrive/pandad/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
pandad
|
||||
pandad_api_impl.cpp
|
||||
tests/test_pandad_usbprotocol
|
||||
3
iqpilot/selfdrive/pandad/__init__.py
Normal file
3
iqpilot/selfdrive/pandad/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from iqpilot.selfdrive.pandad.pandad_api_impl import can_list_to_can_capnp, can_capnp_to_list
|
||||
assert can_list_to_can_capnp
|
||||
assert can_capnp_to_list
|
||||
BIN
iqpilot/selfdrive/pandad/pandad
Executable file
BIN
iqpilot/selfdrive/pandad/pandad
Executable file
Binary file not shown.
199
iqpilot/selfdrive/pandad/pandad.py
Executable file
199
iqpilot/selfdrive/pandad/pandad.py
Executable file
@@ -0,0 +1,199 @@
|
||||
#!/usr/bin/env python3
|
||||
# simple pandad wrapper that updates the panda first
|
||||
import os
|
||||
import usb1
|
||||
import time
|
||||
import signal
|
||||
import subprocess
|
||||
|
||||
from panda import Panda, PandaDFU, PandaProtocolMismatch, FW_PATH
|
||||
from iqpilot.common.basedir import BASEDIR
|
||||
from iqpilot.common.params import Params
|
||||
from iqpilot.system.hardware import HARDWARE
|
||||
from iqpilot.common.swaglog import cloudlog
|
||||
|
||||
|
||||
def get_expected_signature(panda: Panda) -> bytes:
|
||||
try:
|
||||
fn = os.path.join(FW_PATH, panda.get_mcu_type().config.app_fn)
|
||||
return Panda.get_signature_from_firmware(fn)
|
||||
except Exception:
|
||||
cloudlog.exception("Error computing expected signature")
|
||||
return b""
|
||||
|
||||
def flash_panda(panda_serial: str) -> Panda:
|
||||
try:
|
||||
panda = Panda(panda_serial)
|
||||
except PandaProtocolMismatch:
|
||||
cloudlog.warning("detected protocol mismatch, reflashing panda")
|
||||
HARDWARE.recover_internal_panda()
|
||||
raise
|
||||
|
||||
# skip flashing if the detected panda is not supported
|
||||
supported_panda = check_panda_support(panda)
|
||||
if not supported_panda:
|
||||
cloudlog.warning(f"Panda {panda_serial} is not supported (hw_type: {panda.get_type()}), skipping flash...")
|
||||
return panda
|
||||
|
||||
fw_signature = get_expected_signature(panda)
|
||||
internal_panda = panda.is_internal()
|
||||
|
||||
panda_version = "bootstub" if panda.bootstub else panda.get_version()
|
||||
panda_signature = b"" if panda.bootstub else panda.get_signature()
|
||||
cloudlog.warning(f"Panda {panda_serial} connected, version: {panda_version}, signature {panda_signature.hex()[:16]}, expected {fw_signature.hex()[:16]}")
|
||||
|
||||
if panda.bootstub or panda_signature != fw_signature:
|
||||
cloudlog.info("Panda firmware out of date, update required")
|
||||
panda.flash()
|
||||
cloudlog.info("Done flashing")
|
||||
|
||||
if panda.bootstub:
|
||||
bootstub_version = panda.get_version()
|
||||
cloudlog.info(f"Flashed firmware not booting, flashing development bootloader. {bootstub_version=}, {internal_panda=}")
|
||||
if internal_panda:
|
||||
HARDWARE.recover_internal_panda()
|
||||
panda.recover(reset=(not internal_panda))
|
||||
cloudlog.info("Done flashing bootstub")
|
||||
|
||||
if panda.bootstub:
|
||||
cloudlog.info("Panda still not booting, exiting")
|
||||
raise AssertionError
|
||||
|
||||
panda_signature = panda.get_signature()
|
||||
if panda_signature != fw_signature:
|
||||
cloudlog.info("Version mismatch after flashing, exiting")
|
||||
raise AssertionError
|
||||
|
||||
return panda
|
||||
|
||||
|
||||
def check_panda_support(panda) -> bool:
|
||||
hw_type = panda.get_type()
|
||||
if hw_type in Panda.SUPPORTED_DEVICES:
|
||||
return True
|
||||
if hw_type == Panda.HW_TYPE_UNKNOWN and (os.environ.get('LITE') == '1' or os.path.exists('/tmp/lite_hw')):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def main() -> None:
|
||||
os.environ["IQPILOT_PANDA_FW_PATH"] = FW_PATH
|
||||
|
||||
# signal pandad to close the relay and exit
|
||||
def signal_handler(signum, frame):
|
||||
cloudlog.info(f"Caught signal {signum}, exiting")
|
||||
nonlocal do_exit
|
||||
do_exit = True
|
||||
if process is not None:
|
||||
process.send_signal(signal.SIGINT)
|
||||
|
||||
process = None
|
||||
do_exit = False
|
||||
signal.signal(signal.SIGINT, signal_handler)
|
||||
|
||||
count = 0
|
||||
first_run = True
|
||||
params = Params()
|
||||
no_internal_panda_count = 0
|
||||
|
||||
while not do_exit:
|
||||
try:
|
||||
count += 1
|
||||
cloudlog.event("pandad.flash_and_connect", count=count)
|
||||
params.remove("PandaSignatures")
|
||||
|
||||
# Handle missing internal panda
|
||||
if time.monotonic() < 60.:
|
||||
no_internal_panda_count = 0
|
||||
if no_internal_panda_count > 0:
|
||||
if no_internal_panda_count == 3:
|
||||
cloudlog.info("No pandas found, putting internal panda into DFU")
|
||||
HARDWARE.recover_internal_panda()
|
||||
else:
|
||||
cloudlog.info("No pandas found, resetting internal panda")
|
||||
HARDWARE.reset_internal_panda()
|
||||
time.sleep(3) # wait to come back up
|
||||
|
||||
# Flash all Pandas in DFU mode
|
||||
dfu_serials = PandaDFU.list()
|
||||
if len(dfu_serials) > 0:
|
||||
for serial in dfu_serials:
|
||||
cloudlog.info(f"Panda in DFU mode found, flashing recovery {serial}")
|
||||
PandaDFU(serial).recover()
|
||||
time.sleep(1)
|
||||
|
||||
panda_serials = Panda.list()
|
||||
if len(panda_serials) == 0:
|
||||
no_internal_panda_count += 1
|
||||
continue
|
||||
|
||||
cloudlog.info(f"{len(panda_serials)} panda(s) found, connecting - {panda_serials}")
|
||||
|
||||
# Flash pandas
|
||||
pandas: list[Panda] = []
|
||||
for serial in panda_serials:
|
||||
pandas.append(flash_panda(serial))
|
||||
|
||||
# Ensure internal panda is present if expected
|
||||
internal_pandas = [panda for panda in pandas if panda.is_internal()]
|
||||
if HARDWARE.has_internal_panda() and len(internal_pandas) == 0:
|
||||
cloudlog.error("Internal panda is missing, trying again")
|
||||
no_internal_panda_count += 1
|
||||
continue
|
||||
no_internal_panda_count = 0
|
||||
|
||||
# sort pandas to have deterministic order
|
||||
# * the internal one is always first
|
||||
# * then sort by hardware type
|
||||
# * as a last resort, sort by serial number
|
||||
pandas.sort(key=lambda x: (not x.is_internal(), x.get_type(), x.get_usb_serial()))
|
||||
panda_serials = [p.get_usb_serial() for p in pandas]
|
||||
|
||||
# log panda fw versions
|
||||
params.put("PandaSignatures", b','.join(p.get_signature() for p in pandas))
|
||||
|
||||
for panda in pandas:
|
||||
# skip health check if the detected panda is not supported
|
||||
supported_panda = check_panda_support(panda)
|
||||
if not supported_panda:
|
||||
cloudlog.warning(f"Panda {panda.get_usb_serial()} is not supported (hw_type: {panda.get_type()}), skipping health check...")
|
||||
continue
|
||||
|
||||
# check health for lost heartbeat
|
||||
health = panda.health()
|
||||
if health["heartbeat_lost"]:
|
||||
params.put_bool("PandaHeartbeatLost", True)
|
||||
cloudlog.event("heartbeat lost", deviceState=health, serial=panda.get_usb_serial())
|
||||
if health["som_reset_triggered"]:
|
||||
params.put_bool("PandaSomResetTriggered", True)
|
||||
cloudlog.event("panda.som_reset_triggered", health=health, serial=panda.get_usb_serial())
|
||||
|
||||
if first_run:
|
||||
# reset panda to ensure we're in a good state
|
||||
cloudlog.info(f"Resetting panda {panda.get_usb_serial()}")
|
||||
panda.reset(reconnect=True)
|
||||
|
||||
for p in pandas:
|
||||
p.close()
|
||||
# TODO: wrap all panda exceptions in a base panda exception
|
||||
except (usb1.USBErrorNoDevice, usb1.USBErrorPipe):
|
||||
# a panda was disconnected while setting everything up. let's try again
|
||||
cloudlog.exception("Panda USB exception while setting up")
|
||||
continue
|
||||
except PandaProtocolMismatch:
|
||||
cloudlog.exception("pandad.protocol_mismatch")
|
||||
continue
|
||||
except Exception:
|
||||
cloudlog.exception("pandad.uncaught_exception")
|
||||
continue
|
||||
|
||||
first_run = False
|
||||
|
||||
# run pandad with all connected serials as arguments
|
||||
os.environ['MANAGER_DAEMON'] = 'pandad'
|
||||
process = subprocess.Popen(["./pandad", *panda_serials], cwd=os.path.join(BASEDIR, "iqpilot/selfdrive/pandad"))
|
||||
process.wait()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
88
iqpilot/selfdrive/pandad/pandad_api_impl.py
Normal file
88
iqpilot/selfdrive/pandad/pandad_api_impl.py
Normal file
@@ -0,0 +1,88 @@
|
||||
import time
|
||||
from iqpilot.cereal import log
|
||||
|
||||
NO_TRAVERSAL_LIMIT = 2**64 - 1
|
||||
|
||||
# Cache schema fields for faster access (avoids string lookup on each field access)
|
||||
_cached_reader_fields = None # (address_field, dat_field, src_field) for reading
|
||||
_cached_writer_fields = None # (address_field, dat_field, src_field) for writing
|
||||
|
||||
|
||||
def _get_reader_fields(schema):
|
||||
"""Get cached schema field objects for reading."""
|
||||
global _cached_reader_fields
|
||||
if _cached_reader_fields is None:
|
||||
fields = schema.fields
|
||||
_cached_reader_fields = (fields['address'], fields['dat'], fields['src'])
|
||||
return _cached_reader_fields
|
||||
|
||||
|
||||
def _get_writer_fields(schema):
|
||||
"""Get cached schema field objects for writing."""
|
||||
global _cached_writer_fields
|
||||
if _cached_writer_fields is None:
|
||||
fields = schema.fields
|
||||
_cached_writer_fields = (fields['address'], fields['dat'], fields['src'])
|
||||
return _cached_writer_fields
|
||||
|
||||
|
||||
def can_list_to_can_capnp(can_msgs, msgtype='can', valid=True):
|
||||
"""Convert list of CAN messages to Cap'n Proto serialized bytes.
|
||||
|
||||
Args:
|
||||
can_msgs: List of tuples [(address, data_bytes, src), ...]
|
||||
msgtype: 'can' or 'sendcan'
|
||||
valid: Whether the event is valid
|
||||
|
||||
Returns:
|
||||
Cap'n Proto serialized bytes
|
||||
"""
|
||||
global _cached_writer_fields
|
||||
|
||||
dat = log.Event.new_message(valid=valid, logMonoTime=int(time.monotonic() * 1e9))
|
||||
can_data = dat.init(msgtype, len(can_msgs))
|
||||
|
||||
# Cache schema fields on first call
|
||||
if _cached_writer_fields is None and len(can_msgs) > 0:
|
||||
_cached_writer_fields = _get_writer_fields(can_data[0].schema)
|
||||
|
||||
if _cached_writer_fields is not None:
|
||||
addr_f, dat_f, src_f = _cached_writer_fields
|
||||
for i, msg in enumerate(can_msgs):
|
||||
f = can_data[i]
|
||||
f._set_by_field(addr_f, msg[0])
|
||||
f._set_by_field(dat_f, msg[1])
|
||||
f._set_by_field(src_f, msg[2])
|
||||
|
||||
return dat.to_bytes()
|
||||
|
||||
|
||||
def can_capnp_to_list(strings, msgtype='can'):
|
||||
"""Convert Cap'n Proto serialized bytes to list of CAN messages.
|
||||
|
||||
Args:
|
||||
strings: Tuple/list of serialized Cap'n Proto bytes
|
||||
msgtype: 'can' or 'sendcan'
|
||||
|
||||
Returns:
|
||||
List of tuples [(nanos, [(address, data, src), ...]), ...]
|
||||
"""
|
||||
global _cached_reader_fields
|
||||
result = []
|
||||
|
||||
for s in strings:
|
||||
with log.Event.from_bytes(s, traversal_limit_in_words=NO_TRAVERSAL_LIMIT) as event:
|
||||
frames = getattr(event, msgtype)
|
||||
|
||||
# Cache schema fields on first frame for faster access
|
||||
if _cached_reader_fields is None and len(frames) > 0:
|
||||
_cached_reader_fields = _get_reader_fields(frames[0].schema)
|
||||
|
||||
if _cached_reader_fields is not None:
|
||||
addr_f, dat_f, src_f = _cached_reader_fields
|
||||
frame_list = [(f._get_by_field(addr_f), f._get_by_field(dat_f), f._get_by_field(src_f)) for f in frames]
|
||||
else:
|
||||
frame_list = []
|
||||
|
||||
result.append((event.logMonoTime, frame_list))
|
||||
return result
|
||||
0
iqpilot/selfdrive/pandad/tests/__init__.py
Normal file
0
iqpilot/selfdrive/pandad/tests/__init__.py
Normal file
BIN
iqpilot/selfdrive/pandad/tests/bootstub.panda.bin
Executable file
BIN
iqpilot/selfdrive/pandad/tests/bootstub.panda.bin
Executable file
Binary file not shown.
BIN
iqpilot/selfdrive/pandad/tests/bootstub.panda_h7.bin
Executable file
BIN
iqpilot/selfdrive/pandad/tests/bootstub.panda_h7.bin
Executable file
Binary file not shown.
BIN
iqpilot/selfdrive/pandad/tests/bootstub.panda_h7_spiv0.bin
Executable file
BIN
iqpilot/selfdrive/pandad/tests/bootstub.panda_h7_spiv0.bin
Executable file
Binary file not shown.
114
iqpilot/selfdrive/pandad/tests/test_pandad.py
Normal file
114
iqpilot/selfdrive/pandad/tests/test_pandad.py
Normal file
@@ -0,0 +1,114 @@
|
||||
import os
|
||||
import pytest
|
||||
import time
|
||||
|
||||
import iqpilot.cereal.messaging as messaging
|
||||
from iqpilot.cereal import log
|
||||
from iqpilot.common.gpio import gpio_set, gpio_init
|
||||
from panda import Panda, PandaDFU
|
||||
from iqpilot.system.manager.process_config import managed_processes
|
||||
from iqpilot.system.hardware import HARDWARE
|
||||
from iqpilot.system.hardware.tici.pins import GPIO
|
||||
|
||||
HERE = os.path.dirname(os.path.realpath(__file__))
|
||||
|
||||
|
||||
@pytest.mark.tici
|
||||
class TestPandad:
|
||||
|
||||
def setup_method(self):
|
||||
# ensure panda is up
|
||||
if len(Panda.list()) == 0:
|
||||
self._run_test(60)
|
||||
|
||||
def teardown_method(self):
|
||||
managed_processes['pandad'].stop()
|
||||
|
||||
def _run_test(self, timeout=30) -> float:
|
||||
st = time.monotonic()
|
||||
sm = messaging.SubMaster(['pandaStates'])
|
||||
|
||||
managed_processes['pandad'].start()
|
||||
while (time.monotonic() - st) < timeout:
|
||||
sm.update(100)
|
||||
if len(sm['pandaStates']) and sm['pandaStates'][0].pandaType != log.PandaState.PandaType.unknown:
|
||||
break
|
||||
dt = time.monotonic() - st
|
||||
managed_processes['pandad'].stop()
|
||||
|
||||
if len(sm['pandaStates']) == 0 or sm['pandaStates'][0].pandaType == log.PandaState.PandaType.unknown:
|
||||
raise Exception("pandad failed to start")
|
||||
|
||||
return dt
|
||||
|
||||
def _go_to_dfu(self):
|
||||
HARDWARE.recover_internal_panda()
|
||||
assert Panda.wait_for_dfu(None, 10)
|
||||
|
||||
def _assert_no_panda(self):
|
||||
assert not Panda.wait_for_dfu(None, 3)
|
||||
assert not Panda.wait_for_panda(None, 3)
|
||||
|
||||
def _flash_bootstub(self, fn):
|
||||
self._go_to_dfu()
|
||||
pd = PandaDFU(None)
|
||||
if fn is None:
|
||||
fn = os.path.join(HERE, pd.get_mcu_type().config.bootstub_fn)
|
||||
with open(fn, "rb") as f:
|
||||
pd.program_bootstub(f.read())
|
||||
pd.reset()
|
||||
HARDWARE.reset_internal_panda()
|
||||
|
||||
def test_in_dfu(self):
|
||||
HARDWARE.recover_internal_panda()
|
||||
self._run_test(60)
|
||||
|
||||
def test_in_bootstub(self):
|
||||
with Panda() as p:
|
||||
p.reset(enter_bootstub=True)
|
||||
assert p.bootstub
|
||||
self._run_test()
|
||||
|
||||
def test_internal_panda_reset(self):
|
||||
gpio_init(GPIO.STM_RST_N, True)
|
||||
gpio_set(GPIO.STM_RST_N, 1)
|
||||
time.sleep(0.5)
|
||||
assert all(not Panda(s).is_internal() for s in Panda.list())
|
||||
self._run_test()
|
||||
|
||||
assert any(Panda(s).is_internal() for s in Panda.list())
|
||||
|
||||
def test_best_case_startup_time(self):
|
||||
# run once so we're up to date
|
||||
self._run_test(60)
|
||||
|
||||
ts = []
|
||||
for _ in range(10):
|
||||
# should be nearly instant this time
|
||||
dt = self._run_test(5)
|
||||
ts.append(dt)
|
||||
|
||||
# 5s for USB (due to enumeration)
|
||||
# - 0.2s pandad -> pandad
|
||||
# - plus some buffer
|
||||
print("startup times", ts, sum(ts) / len(ts))
|
||||
assert 0.1 < (sum(ts)/len(ts)) < 0.7
|
||||
|
||||
def test_old_spi_protocol(self):
|
||||
# flash firmware with old SPI protocol
|
||||
self._flash_bootstub(os.path.join(HERE, "bootstub.panda_h7_spiv0.bin"))
|
||||
self._run_test(45)
|
||||
|
||||
def test_release_to_devel_bootstub(self):
|
||||
self._flash_bootstub(None)
|
||||
self._run_test(45)
|
||||
|
||||
def test_recover_from_bad_bootstub(self):
|
||||
self._go_to_dfu()
|
||||
with PandaDFU(None) as pd:
|
||||
pd.program_bootstub(b"\x00"*1024)
|
||||
pd.reset()
|
||||
HARDWARE.reset_internal_panda()
|
||||
self._assert_no_panda()
|
||||
|
||||
self._run_test(60)
|
||||
113
iqpilot/selfdrive/pandad/tests/test_pandad_loopback.py
Normal file
113
iqpilot/selfdrive/pandad/tests/test_pandad_loopback.py
Normal file
@@ -0,0 +1,113 @@
|
||||
import os
|
||||
import copy
|
||||
import random
|
||||
import time
|
||||
import pytest
|
||||
from collections import defaultdict
|
||||
from pprint import pprint
|
||||
|
||||
import iqpilot.cereal.messaging as messaging
|
||||
from iqpilot.cereal import car, log
|
||||
from iqdbc.car.can_definitions import CanData
|
||||
from iqpilot.common.utils import retry
|
||||
from iqpilot.common.params import Params
|
||||
from iqpilot.common.timeout import Timeout
|
||||
from iqpilot.selfdrive.pandad import can_list_to_can_capnp
|
||||
from iqpilot.system.hardware import TICI
|
||||
from iqpilot.selfdrive.test.helpers import with_processes
|
||||
|
||||
|
||||
@retry(attempts=3)
|
||||
def setup_pandad(num_pandas):
|
||||
params = Params()
|
||||
params.clear_all()
|
||||
params.put_bool("IsOnroad", False)
|
||||
|
||||
sm = messaging.SubMaster(['pandaStates'])
|
||||
with Timeout(90, "pandad didn't start"):
|
||||
while sm.recv_frame['pandaStates'] < 1 or len(sm['pandaStates']) == 0 or \
|
||||
any(ps.pandaType == log.PandaState.PandaType.unknown for ps in sm['pandaStates']):
|
||||
sm.update(1000)
|
||||
|
||||
found_pandas = len(sm['pandaStates'])
|
||||
assert num_pandas == found_pandas, "connected pandas ({found_pandas}) doesn't match expected panda count ({num_pandas}). \
|
||||
connect another panda for multipanda tests."
|
||||
|
||||
# pandad safety setting relies on these params
|
||||
cp = car.CarParams.new_message()
|
||||
|
||||
safety_config = car.CarParams.SafetyConfig.new_message()
|
||||
safety_config.safetyModel = car.CarParams.SafetyModel.allOutput
|
||||
cp.safetyConfigs = [safety_config]*num_pandas
|
||||
|
||||
params.put_bool("IsOnroad", True)
|
||||
params.put_bool("FirmwareQueryDone", True)
|
||||
params.put_bool("ControlsReady", True)
|
||||
params.put("CarParams", cp.to_bytes())
|
||||
|
||||
with Timeout(90, "pandad didn't set safety mode"):
|
||||
while any(ps.safetyModel != car.CarParams.SafetyModel.allOutput for ps in sm['pandaStates']):
|
||||
sm.update(1000)
|
||||
|
||||
def send_random_can_messages(sendcan, count, num_pandas=1):
|
||||
sent_msgs = defaultdict(set)
|
||||
for _ in range(count):
|
||||
to_send = []
|
||||
for __ in range(random.randrange(20)):
|
||||
bus = random.choice([b for b in range(3*num_pandas) if b % 4 != 3])
|
||||
addr = random.randrange(1, 1<<29)
|
||||
dat = bytes(random.getrandbits(8) for _ in range(random.randrange(1, 9)))
|
||||
if (addr, dat) in sent_msgs[bus]:
|
||||
continue
|
||||
sent_msgs[bus].add((addr, dat))
|
||||
to_send.append(CanData(addr, dat, bus))
|
||||
sendcan.send(can_list_to_can_capnp(to_send, msgtype='sendcan'))
|
||||
return sent_msgs
|
||||
|
||||
|
||||
@pytest.mark.tici
|
||||
class TestBoarddLoopback:
|
||||
@classmethod
|
||||
def setup_class(cls):
|
||||
os.environ['STARTED'] = '1'
|
||||
os.environ['BOARDD_LOOPBACK'] = '1'
|
||||
|
||||
@with_processes(['pandad'])
|
||||
def test_loopback(self):
|
||||
num_pandas = 2 if TICI and "SINGLE_PANDA" not in os.environ else 1
|
||||
setup_pandad(num_pandas)
|
||||
|
||||
sendcan = messaging.pub_sock('sendcan')
|
||||
can = messaging.sub_sock('can', conflate=False, timeout=100)
|
||||
sm = messaging.SubMaster(['pandaStates'])
|
||||
time.sleep(1)
|
||||
|
||||
n = 200
|
||||
for i in range(n):
|
||||
print(f"pandad loopback {i}/{n}")
|
||||
|
||||
sent_msgs = send_random_can_messages(sendcan, random.randrange(20, 100), num_pandas)
|
||||
|
||||
sent_loopback = copy.deepcopy(sent_msgs)
|
||||
sent_loopback.update({k+128: copy.deepcopy(v) for k, v in sent_msgs.items()})
|
||||
sent_total = {k: len(v) for k, v in sent_loopback.items()}
|
||||
for _ in range(100 * 5):
|
||||
sm.update(0)
|
||||
recvd = messaging.drain_sock(can, wait_for_one=True)
|
||||
for msg in recvd:
|
||||
for m in msg.can:
|
||||
key = (m.address, m.dat)
|
||||
assert key in sent_loopback[m.src], f"got unexpected msg: {m.src=} {m.address=} {m.dat=}"
|
||||
sent_loopback[m.src].discard(key)
|
||||
|
||||
if all(len(v) == 0 for v in sent_loopback.values()):
|
||||
break
|
||||
|
||||
# if a set isn't empty, messages got dropped
|
||||
pprint(sent_msgs)
|
||||
pprint(sent_loopback)
|
||||
print({k: len(x) for k, x in sent_loopback.items()})
|
||||
print(sum([len(x) for x in sent_loopback.values()]))
|
||||
pprint(sm['pandaStates']) # may drop messages due to RX buffer overflow
|
||||
for bus in sent_loopback.keys():
|
||||
assert not len(sent_loopback[bus]), f"loop {i}: bus {bus} missing {len(sent_loopback[bus])} out of {sent_total[bus]} messages"
|
||||
102
iqpilot/selfdrive/pandad/tests/test_pandad_spi.py
Normal file
102
iqpilot/selfdrive/pandad/tests/test_pandad_spi.py
Normal file
@@ -0,0 +1,102 @@
|
||||
import os
|
||||
import time
|
||||
import numpy as np
|
||||
import pytest
|
||||
import random
|
||||
|
||||
import iqpilot.cereal.messaging as messaging
|
||||
from iqpilot.cereal.services import SERVICE_LIST
|
||||
from iqpilot.selfdrive.test.helpers import with_processes
|
||||
from iqpilot.selfdrive.pandad.tests.test_pandad_loopback import setup_pandad, send_random_can_messages
|
||||
|
||||
JUNGLE_SPAM = "JUNGLE_SPAM" in os.environ
|
||||
|
||||
@pytest.mark.tici
|
||||
class TestBoarddSpi:
|
||||
@classmethod
|
||||
def setup_class(cls):
|
||||
os.environ['STARTED'] = '1'
|
||||
os.environ['SPI_ERR_PROB'] = '0.001'
|
||||
if not JUNGLE_SPAM:
|
||||
os.environ['BOARDD_LOOPBACK'] = '1'
|
||||
|
||||
@with_processes(['pandad'])
|
||||
def test_spi_corruption(self, subtests):
|
||||
setup_pandad(1)
|
||||
|
||||
sendcan = messaging.pub_sock('sendcan')
|
||||
socks = {s: messaging.sub_sock(s, conflate=False, timeout=100) for s in ('can', 'pandaStates', 'peripheralState')}
|
||||
time.sleep(2)
|
||||
for s in socks.values():
|
||||
messaging.drain_sock_raw(s)
|
||||
|
||||
total_recv_count = 0
|
||||
total_sent_count = 0
|
||||
sent_msgs = {bus: list() for bus in range(3)}
|
||||
|
||||
st = time.monotonic()
|
||||
ts = {s: list() for s in socks.keys()}
|
||||
for _ in range(int(os.getenv("TEST_TIME", "20"))):
|
||||
# send some CAN messages
|
||||
if not JUNGLE_SPAM:
|
||||
sent = send_random_can_messages(sendcan, random.randrange(2, 20))
|
||||
for k, v in sent.items():
|
||||
sent_msgs[k].extend(list(v))
|
||||
total_sent_count += len(v)
|
||||
|
||||
for service, sock in socks.items():
|
||||
for m in messaging.drain_sock(sock):
|
||||
ts[service].append(m.logMonoTime)
|
||||
|
||||
# sanity check for corruption
|
||||
assert m.valid or (service == "can")
|
||||
if service == "can":
|
||||
for msg in m.can:
|
||||
if JUNGLE_SPAM:
|
||||
# PandaJungle.set_generated_can(True)
|
||||
i = msg.address - 0x200
|
||||
assert msg.address >= 0x200
|
||||
assert msg.src == (i%3)
|
||||
assert msg.dat == b"\xff"*(i%8)
|
||||
total_recv_count += 1
|
||||
continue
|
||||
|
||||
if msg.src > 4:
|
||||
continue
|
||||
key = (msg.address, msg.dat)
|
||||
assert key in sent_msgs[msg.src], f"got unexpected msg: {msg.src=} {msg.address=} {msg.dat=}"
|
||||
# TODO: enable this
|
||||
#sent_msgs[msg.src].remove(key)
|
||||
total_recv_count += 1
|
||||
elif service == "pandaStates":
|
||||
assert len(m.pandaStates) == 1
|
||||
ps = m.pandaStates[0]
|
||||
assert ps.uptime < 1000
|
||||
assert ps.pandaType == "tres"
|
||||
assert ps.ignitionLine
|
||||
assert not ps.ignitionCan
|
||||
assert 4000 < ps.voltage < 14000
|
||||
elif service == "peripheralState":
|
||||
ps = m.peripheralState
|
||||
assert ps.pandaType == "tres"
|
||||
assert 4000 < ps.voltage < 14000
|
||||
assert 50 < ps.current < 1000
|
||||
assert ps.fanSpeedRpm < 10000
|
||||
|
||||
time.sleep(0.5)
|
||||
et = time.monotonic() - st
|
||||
|
||||
print("\n======== timing report ========")
|
||||
for service, times in ts.items():
|
||||
dts = np.diff(times)/1e6
|
||||
print(service.ljust(17), f"{np.mean(dts):7.2f} {np.min(dts):7.2f} {np.max(dts):7.2f}")
|
||||
with subtests.test(msg="timing check", service=service):
|
||||
edt = 1e3 / SERVICE_LIST[service].frequency
|
||||
assert edt*0.9 < np.mean(dts) < edt*1.1
|
||||
assert np.max(dts) < edt*8
|
||||
assert np.min(dts) < edt
|
||||
assert len(dts) >= ((et-0.5)*SERVICE_LIST[service].frequency*0.8)
|
||||
|
||||
with subtests.test(msg="CAN traffic"):
|
||||
print(f"Sent {total_sent_count} CAN messages, got {total_recv_count} back. {total_recv_count/(total_sent_count+1e-4):.2%} received")
|
||||
assert total_recv_count > 20
|
||||
Reference in New Issue
Block a user