longitudinal profile runner (#1197)
* long profiles * start with creep * lil cleanup * corolla updates * cleanup * 2s * plot is a little nicer * strict mode * cleanup * unused * fix that --------- Co-authored-by: Comma Device <device@comma.ai>
This commit is contained in:
parent
cbad7f0066
commit
f2fa755ab7
|
@ -10,6 +10,7 @@
|
||||||
.sconsign.dblite
|
.sconsign.dblite
|
||||||
.hypothesis
|
.hypothesis
|
||||||
*.egg-info/
|
*.egg-info/
|
||||||
|
*.html
|
||||||
uv.lock
|
uv.lock
|
||||||
|
|
||||||
opendbc/can/*.so
|
opendbc/can/*.so
|
||||||
|
|
|
@ -0,0 +1,156 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
import io
|
||||||
|
import time
|
||||||
|
import base64
|
||||||
|
import argparse
|
||||||
|
import numpy as np
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
from collections import defaultdict
|
||||||
|
from dataclasses import dataclass, asdict
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from opendbc.car.structs import CarControl
|
||||||
|
from opendbc.car.panda_runner import PandaRunner
|
||||||
|
|
||||||
|
DT = 0.01 # step time (s)
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Action:
|
||||||
|
accel: float # m/s^2
|
||||||
|
duration: float # seconds
|
||||||
|
longControlState: CarControl.Actuators.LongControlState = CarControl.Actuators.LongControlState.pid
|
||||||
|
|
||||||
|
def get_msgs(self):
|
||||||
|
return [
|
||||||
|
(t, CarControl(
|
||||||
|
enabled=True,
|
||||||
|
longActive=True,
|
||||||
|
actuators=CarControl.Actuators(
|
||||||
|
accel=self.accel,
|
||||||
|
longControlState=self.longControlState,
|
||||||
|
),
|
||||||
|
))
|
||||||
|
for t in np.linspace(0, self.duration, int(self.duration/DT))
|
||||||
|
]
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Maneuver:
|
||||||
|
description: str
|
||||||
|
actions: list[Action]
|
||||||
|
|
||||||
|
def get_msgs(self):
|
||||||
|
t0 = 0
|
||||||
|
for action in self.actions:
|
||||||
|
for lt, msg in action.get_msgs():
|
||||||
|
yield lt + t0, msg
|
||||||
|
t0 += lt
|
||||||
|
|
||||||
|
MANEUVERS = [
|
||||||
|
Maneuver(
|
||||||
|
"creeping: alternate between +1m/ss and -1m/ss",
|
||||||
|
[
|
||||||
|
Action(1, 2), Action(-1, 2),
|
||||||
|
Action(1, 2), Action(-1, 2),
|
||||||
|
Action(1, 2), Action(-1, 2),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
def main(args):
|
||||||
|
with PandaRunner() as p:
|
||||||
|
print("\n\n")
|
||||||
|
|
||||||
|
logs = {}
|
||||||
|
for i, m in enumerate(MANEUVERS):
|
||||||
|
print(f"Running {i+1}/{len(MANEUVERS)} '{m.description}'")
|
||||||
|
|
||||||
|
print("- setting up")
|
||||||
|
good_cnt = 0
|
||||||
|
for _ in range(int(30./DT)):
|
||||||
|
cs = p.read(strict=False)
|
||||||
|
|
||||||
|
cc = CarControl(
|
||||||
|
enabled=True,
|
||||||
|
longActive=True,
|
||||||
|
actuators=CarControl.Actuators(accel=-1.5, longControlState=CarControl.Actuators.LongControlState.stopping),
|
||||||
|
)
|
||||||
|
p.write(cc)
|
||||||
|
|
||||||
|
good_cnt = (good_cnt+1) if cs.vEgo < 0.1 and cs.cruiseState.enabled and not cs.cruiseState.standstill else 0
|
||||||
|
if good_cnt > (2./DT):
|
||||||
|
break
|
||||||
|
time.sleep(DT)
|
||||||
|
else:
|
||||||
|
print("ERROR: failed to setup")
|
||||||
|
continue
|
||||||
|
|
||||||
|
print("- executing maneuver")
|
||||||
|
logs[m.description] = defaultdict(list)
|
||||||
|
for t, cc in m.get_msgs():
|
||||||
|
cs = p.read()
|
||||||
|
p.write(cc)
|
||||||
|
|
||||||
|
logs[m.description]["t"].append(t)
|
||||||
|
to_log = {"carControl": cc, "carState": cs, "carControl.actuators": cc.actuators,
|
||||||
|
"carControl.cruiseControl": cc.cruiseControl, "carState.cruiseState": cs.cruiseState}
|
||||||
|
for k, v in to_log.items():
|
||||||
|
for k2, v2 in asdict(v).items():
|
||||||
|
logs[m.description][f"{k}.{k2}"].append(v2)
|
||||||
|
|
||||||
|
time.sleep(DT)
|
||||||
|
|
||||||
|
# ***** write out report *****
|
||||||
|
|
||||||
|
output_path = Path(__file__).resolve().parent / "longitudinal_reports"
|
||||||
|
output_fn = args.output or output_path / f"{p.CI.CP.carFingerprint}_{time.strftime('%Y%m%d-%H_%M_%S')}.html"
|
||||||
|
output_path.mkdir(exist_ok=True)
|
||||||
|
with open(output_fn, "w") as f:
|
||||||
|
f.write("<h1>Longitudinal maneuver report</h1>\n")
|
||||||
|
f.write(f"<h3>{p.CI.CP.carFingerprint}</h3>\n")
|
||||||
|
if args.desc:
|
||||||
|
f.write(f"<h3>{args.desc}</h3>")
|
||||||
|
for m in MANEUVERS:
|
||||||
|
f.write("<div style='border-top: 1px solid #000; margin: 20px 0;'></div>\n")
|
||||||
|
f.write(f"<h2>{m.description}</h2>\n")
|
||||||
|
|
||||||
|
log = logs[m.description]
|
||||||
|
|
||||||
|
plt.rcParams['font.size'] = 40
|
||||||
|
fig = plt.figure(figsize=(30, 20))
|
||||||
|
ax = fig.subplots(3, 1, sharex=True, gridspec_kw={'hspace': 0, 'height_ratios': [5, 1, 1]})
|
||||||
|
|
||||||
|
ax[0].grid(linewidth=4)
|
||||||
|
ax[0].plot(log["t"], log["carState.aEgo"], label='aEgo', linewidth=6)
|
||||||
|
ax[0].plot(log["t"], log["carControl.actuators.accel"], label='accel command', linewidth=6)
|
||||||
|
ax[0].set_ylabel('Acceleration (m/s^2)')
|
||||||
|
ax[0].set_ylim(-4.5, 4.5)
|
||||||
|
ax[0].legend()
|
||||||
|
|
||||||
|
ax[1].plot(log["t"], log["carControl.enabled"], label='enabled', linewidth=6)
|
||||||
|
ax[2].plot(log["t"], log["carState.gasPressed"], label='gasPressed', linewidth=6)
|
||||||
|
for i in (1, 2):
|
||||||
|
ax[i].set_yticks([0, 1], minor=False)
|
||||||
|
ax[i].set_ylim(-1, 2)
|
||||||
|
ax[i].legend()
|
||||||
|
|
||||||
|
ax[-1].set_xlabel("Time (s)")
|
||||||
|
fig.tight_layout()
|
||||||
|
|
||||||
|
buffer = io.BytesIO()
|
||||||
|
fig.savefig(buffer, format='png')
|
||||||
|
buffer.seek(0)
|
||||||
|
f.write(f"<img src='data:image/png;base64,{base64.b64encode(buffer.getvalue()).decode()}' style='width:100%; max-width:800px;'>\n")
|
||||||
|
|
||||||
|
print(f"\nReport written to {output_fn}\n")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
parser = argparse.ArgumentParser(description="A tool for longitudinal control testing.",
|
||||||
|
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
|
||||||
|
parser.add_argument('--desc', help="Extra description to include in report.")
|
||||||
|
parser.add_argument('--output', help="Write out report to this file.", default=None)
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
assert args.output is None or args.output.endswith(".html"), "Output filename must end with '.html'"
|
||||||
|
|
||||||
|
main(args)
|
|
@ -1,35 +1,57 @@
|
||||||
from contextlib import contextmanager
|
import time
|
||||||
|
from contextlib import AbstractContextManager
|
||||||
|
|
||||||
from panda import Panda
|
from panda import Panda
|
||||||
from opendbc.car.car_helpers import get_car
|
from opendbc.car.car_helpers import get_car
|
||||||
from opendbc.car.can_definitions import CanData
|
from opendbc.car.can_definitions import CanData
|
||||||
|
from opendbc.car.structs import CarControl
|
||||||
|
|
||||||
@contextmanager
|
class PandaRunner(AbstractContextManager):
|
||||||
def PandaRunner():
|
def __enter__(self):
|
||||||
p = Panda()
|
self.p = Panda()
|
||||||
|
self.p.reset()
|
||||||
|
|
||||||
def _can_recv(wait_for_one: bool = False) -> list[list[CanData]]:
|
# setup + fingerprinting
|
||||||
recv = p.can_recv()
|
self.p.set_safety_mode(Panda.SAFETY_ELM327, 1)
|
||||||
|
self.CI = get_car(self._can_recv, self.p.can_send_many, self.p.set_obd, True)
|
||||||
|
print("fingerprinted", self.CI.CP.carName)
|
||||||
|
assert self.CI.CP.carFingerprint != "mock", "Unable to identify car. Check connections and ensure car is supported."
|
||||||
|
|
||||||
|
self.p.set_safety_mode(Panda.SAFETY_ELM327, 1)
|
||||||
|
self.CI.init(self.CI.CP, self._can_recv, self.p.can_send_many)
|
||||||
|
self.p.set_safety_mode(Panda.SAFETY_TOYOTA, self.CI.CP.safetyConfigs[0].safetyParam)
|
||||||
|
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, exc_type, exc_value, traceback):
|
||||||
|
self.p.set_safety_mode(Panda.SAFETY_NOOUTPUT)
|
||||||
|
self.p.reset() # avoid siren
|
||||||
|
return super().__exit__(exc_type, exc_value, traceback)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def panda(self) -> Panda:
|
||||||
|
return self.p
|
||||||
|
|
||||||
|
def _can_recv(self, wait_for_one: bool = False) -> list[list[CanData]]:
|
||||||
|
recv = self.p.can_recv()
|
||||||
while len(recv) == 0 and wait_for_one:
|
while len(recv) == 0 and wait_for_one:
|
||||||
recv = p.can_recv()
|
recv = self.p.can_recv()
|
||||||
return [[CanData(addr, dat, bus) for addr, dat, bus in recv], ]
|
return [[CanData(addr, dat, bus) for addr, dat, bus in recv], ]
|
||||||
|
|
||||||
try:
|
def read(self, strict: bool = True):
|
||||||
# setup + fingerprinting
|
cs = self.CI.update([int(time.monotonic()*1e9), self._can_recv()[0]])
|
||||||
p.set_safety_mode(Panda.SAFETY_ELM327, 1)
|
if strict:
|
||||||
CI = get_car(_can_recv, p.can_send_many, p.set_obd, True)
|
assert cs.canValid, "CAN went invalid, check connections"
|
||||||
print("fingerprinted", CI.CP.carName)
|
return cs
|
||||||
assert CI.CP.carFingerprint != "mock", "Unable to identify car. Check connections and ensure car is supported."
|
|
||||||
|
|
||||||
p.set_safety_mode(Panda.SAFETY_ELM327, 1)
|
|
||||||
CI.init(CI.CP, _can_recv, p.can_send_many)
|
|
||||||
p.set_safety_mode(Panda.SAFETY_TOYOTA, CI.CP.safetyConfigs[0].safetyParam)
|
|
||||||
|
|
||||||
yield p, CI
|
|
||||||
finally:
|
|
||||||
p.set_safety_mode(Panda.SAFETY_NOOUTPUT)
|
|
||||||
|
|
||||||
|
def write(self, cc: CarControl) -> None:
|
||||||
|
if cc.enabled and not self.p.health()['controls_allowed']:
|
||||||
|
# prevent the car from faulting. print a warning?
|
||||||
|
cc = CarControl(enabled=False)
|
||||||
|
_, can_sends = self.CI.apply(cc)
|
||||||
|
self.p.can_send_many(can_sends, timeout=25)
|
||||||
|
self.p.send_heartbeat()
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
with PandaRunner() as (p, CI):
|
with PandaRunner() as p:
|
||||||
print(p.can_recv())
|
print(p.read())
|
||||||
|
|
|
@ -35,6 +35,7 @@ docs = [
|
||||||
]
|
]
|
||||||
examples = [
|
examples = [
|
||||||
"inputs",
|
"inputs",
|
||||||
|
"matplotlib",
|
||||||
]
|
]
|
||||||
|
|
||||||
[tool.pytest.ini_options]
|
[tool.pytest.ini_options]
|
||||||
|
|
Loading…
Reference in New Issue