Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

longitudinal profile runner #1197

Merged
merged 11 commits into from
Aug 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
.sconsign.dblite
.hypothesis
*.egg-info/
*.html
uv.lock

opendbc/can/*.so
Expand Down
156 changes: 156 additions & 0 deletions examples/longitudinal-profiles.py
Original file line number Diff line number Diff line change
@@ -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)
68 changes: 45 additions & 23 deletions opendbc/car/panda_runner.py
Original file line number Diff line number Diff line change
@@ -1,35 +1,57 @@
from contextlib import contextmanager
import time
from contextlib import AbstractContextManager

from panda import Panda
from opendbc.car.car_helpers import get_car
from opendbc.car.can_definitions import CanData
from opendbc.car.structs import CarControl

@contextmanager
def PandaRunner():
p = Panda()
class PandaRunner(AbstractContextManager):
def __enter__(self):
self.p = Panda()
self.p.reset()

def _can_recv(wait_for_one: bool = False) -> list[list[CanData]]:
recv = p.can_recv()
while len(recv) == 0 and wait_for_one:
recv = p.can_recv()
return [[CanData(addr, dat, bus) for addr, dat, bus in recv], ]

try:
# setup + fingerprinting
p.set_safety_mode(Panda.SAFETY_ELM327, 1)
CI = get_car(_can_recv, p.can_send_many, p.set_obd, True)
print("fingerprinted", CI.CP.carName)
assert 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 = 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

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)
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:
recv = self.p.can_recv()
return [[CanData(addr, dat, bus) for addr, dat, bus in recv], ]

yield p, CI
finally:
p.set_safety_mode(Panda.SAFETY_NOOUTPUT)
def read(self, strict: bool = True):
cs = self.CI.update([int(time.monotonic()*1e9), self._can_recv()[0]])
if strict:
assert cs.canValid, "CAN went invalid, check connections"
return cs

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__":
with PandaRunner() as (p, CI):
print(p.can_recv())
with PandaRunner() as p:
print(p.read())
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ docs = [
]
examples = [
"inputs",
"matplotlib",
]

[tool.pytest.ini_options]
Expand Down
Loading