add longitudinal maneuvers (#33527)
* add longitudinal profiles * stash * unfortunately even longitudinalPlan causes circle * add to process config * reach target speed smoothly * stash * works * clean up * debug alert * rename * fix * better text * toggle via exp button * try coming to a stop better, smoother target reaching * closer to target * revert controlsd migration * add description to alert * generate report from local logs * hide bad maneuvers * pdflike * Revert "pdflike" This reverts commitpull/33549/head6d4af1bf9b
. * try this * use alert manager * fix that check * wat * Revert "wat" This reverts commit93d0d27ab8
. * some clean up * rm * cleanup * move * fix test * more fix * clean up * fix that
parent
9211b701ce
commit
82f8db87f4
10 changed files with 320 additions and 3 deletions
@ -0,0 +1,115 @@ |
||||
#!/usr/bin/env python3 |
||||
import argparse |
||||
import base64 |
||||
import io |
||||
import os |
||||
import time |
||||
from pathlib import Path |
||||
import matplotlib.pyplot as plt |
||||
|
||||
from openpilot.tools.lib.logreader import LogReader |
||||
from openpilot.system.hardware.hw import Paths |
||||
|
||||
|
||||
def report(platform, maneuvers): |
||||
output_path = Path(__file__).resolve().parent / "longitudinal_reports" |
||||
output_fn = output_path / f"{platform}_{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>{platform}</h3>\n") |
||||
for description, runs in maneuvers: |
||||
print('plotting maneuver:', description, 'runs:', len(runs)) |
||||
f.write("<div style='border-top: 1px solid #000; margin: 20px 0;'></div>\n") |
||||
f.write(f"<h2>{description}</h2>\n") |
||||
for run, msgs in enumerate(runs): |
||||
t_carControl, carControl = zip(*[(m.logMonoTime, m.carControl) for m in msgs if m.which() == 'carControl'], strict=True) |
||||
t_carOutput, carOutput = zip(*[(m.logMonoTime, m.carOutput) for m in msgs if m.which() == 'carOutput'], strict=True) |
||||
t_carState, carState = zip(*[(m.logMonoTime, m.carState) for m in msgs if m.which() == 'carState'], strict=True) |
||||
t_longitudinalPlan, longitudinalPlan = zip(*[(m.logMonoTime, m.longitudinalPlan) for m in msgs if m.which() == 'longitudinalPlan'], strict=True) |
||||
|
||||
longActive = [m.longActive for m in carControl] |
||||
gasPressed = [m.gasPressed for m in carState] |
||||
brakePressed = [m.brakePressed for m in carState] |
||||
|
||||
maneuver_valid = all(longActive) and not (any(gasPressed) or any(brakePressed)) |
||||
|
||||
_open = 'open' if maneuver_valid else '' |
||||
title = f'Run #{int(run)+1}' + (' <span style="color: red">(invalid maneuver!)</span>' if not maneuver_valid else '') |
||||
|
||||
f.write(f"<details {_open}><summary><h3 style='display: inline-block;'>{title}</h3></summary>\n") |
||||
|
||||
plt.rcParams['font.size'] = 40 |
||||
fig = plt.figure(figsize=(30, 25)) |
||||
ax = fig.subplots(4, 1, sharex=True, gridspec_kw={'hspace': 0, 'height_ratios': [5, 3, 1, 1]}) |
||||
|
||||
ax[0].grid(linewidth=4) |
||||
ax[0].plot(t_carControl, [m.actuators.accel for m in carControl], label='carControl.actuators.accel', linewidth=6) |
||||
ax[0].plot(t_carOutput, [m.actuatorsOutput.accel for m in carOutput], label='carOutput.actuatorsOutput.accel', linewidth=6) |
||||
ax[0].plot(t_longitudinalPlan, [m.aTarget for m in longitudinalPlan], label='longitudinalPlan.aTarget', linewidth=6) |
||||
ax[0].plot(t_carState, [m.aEgo for m in carState], label='carState.aEgo', linewidth=6) |
||||
# TODO localizer accel |
||||
ax[0].set_ylabel('Acceleration (m/s^2)') |
||||
#ax[0].set_ylim(-6.5, 6.5) |
||||
ax[0].legend() |
||||
|
||||
ax[1].grid(linewidth=4) |
||||
ax[1].plot(t_carState, [m.vEgo for m in carState], 'g', label='vEgo', linewidth=6) |
||||
ax[1].set_ylabel('Velocity (m/s)') |
||||
ax[1].legend() |
||||
|
||||
ax[2].plot(t_carControl, longActive, label='longActive', linewidth=6) |
||||
ax[3].plot(t_carState, gasPressed, label='gasPressed', linewidth=6) |
||||
ax[3].plot(t_carState, brakePressed, label='brakePressed', linewidth=6) |
||||
for i in (2, 3): |
||||
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") |
||||
f.write("</details>\n") |
||||
|
||||
print(f"\nReport written to {output_fn}\n") |
||||
|
||||
|
||||
if __name__ == '__main__': |
||||
parser = argparse.ArgumentParser(description='Generate longitudinal maneuver report from route') |
||||
parser.add_argument('route', type=str, help='Route name (e.g. 00000000--5f742174be)') |
||||
|
||||
args = parser.parse_args() |
||||
|
||||
if '/' in args.route or '|' in args.route: |
||||
lr = LogReader(args.route) |
||||
else: |
||||
segs = [seg for seg in os.listdir(Paths.log_root()) if args.route in seg] |
||||
lr = LogReader([os.path.join(Paths.log_root(), seg, 'rlog') for seg in segs]) |
||||
|
||||
CP = lr.first('carParams') |
||||
platform = CP.carFingerprint |
||||
print('processing report for', platform) |
||||
|
||||
maneuvers: list[tuple[str, list[list]]] = [] |
||||
active_prev = False |
||||
description_prev = None |
||||
|
||||
for msg in lr: |
||||
if msg.which() == 'alertDebug': |
||||
active = 'Maneuver Active' in msg.alertDebug.alertText1 |
||||
if active and not active_prev: |
||||
if msg.alertDebug.alertText2 == description_prev: |
||||
maneuvers[-1][1].append([]) |
||||
else: |
||||
maneuvers.append((msg.alertDebug.alertText2, [[]])) |
||||
description_prev = maneuvers[-1][0] |
||||
active_prev = active |
||||
|
||||
if active_prev: |
||||
maneuvers[-1][1][-1].append(msg) |
||||
|
||||
report(platform, maneuvers) |
@ -0,0 +1,165 @@ |
||||
#!/usr/bin/env python3 |
||||
from dataclasses import dataclass |
||||
|
||||
from cereal import messaging, car |
||||
from opendbc.car.common.conversions import Conversions as CV |
||||
from openpilot.common.realtime import DT_MDL |
||||
from openpilot.common.params import Params |
||||
from openpilot.common.swaglog import cloudlog |
||||
|
||||
|
||||
@dataclass |
||||
class Action: |
||||
accel: float # m/s^2 |
||||
duration: float # seconds |
||||
|
||||
|
||||
@dataclass |
||||
class Maneuver: |
||||
description: str |
||||
actions: list[Action] |
||||
repeat: int = 0 |
||||
initial_speed: float = 0. # m/s |
||||
|
||||
_active: bool = False |
||||
_finished: bool = False |
||||
_action_index: int = 0 |
||||
_action_frames: int = 0 |
||||
_ready_cnt: int = 0 |
||||
_repeated: int = 0 |
||||
|
||||
def get_accel(self, v_ego: float, enabled: bool, standstill: bool) -> float: |
||||
ready = abs(v_ego - self.initial_speed) < 0.3 and enabled and not standstill |
||||
self._ready_cnt = (self._ready_cnt + 1) if ready else 0 |
||||
|
||||
if self._ready_cnt > (3. / DT_MDL): |
||||
self._active = True |
||||
|
||||
if not self._active: |
||||
return min(max(self.initial_speed - v_ego, -2.), 2.) |
||||
|
||||
action = self.actions[self._action_index] |
||||
|
||||
self._action_frames += 1 |
||||
|
||||
# reached duration of action |
||||
if self._action_frames > (action.duration / DT_MDL): |
||||
# next action |
||||
if self._action_index < len(self.actions) - 1: |
||||
self._action_index += 1 |
||||
self._action_frames = 0 |
||||
# repeat maneuver |
||||
elif self._repeated < self.repeat: |
||||
self._repeated += 1 |
||||
self._action_index = 0 |
||||
self._action_frames = 0 |
||||
self._active = False |
||||
# finish maneuver |
||||
else: |
||||
self._finished = True |
||||
|
||||
return action.accel |
||||
|
||||
@property |
||||
def finished(self): |
||||
return self._finished |
||||
|
||||
@property |
||||
def active(self): |
||||
return self._active |
||||
|
||||
|
||||
MANEUVERS = [ |
||||
Maneuver( |
||||
"creep: alternate between +1m/s^2 and -1m/s^2", |
||||
[ |
||||
Action(1, 2), Action(-1, 2), |
||||
Action(1, 2), Action(-1, 2), |
||||
Action(1, 2), Action(-1, 2), |
||||
], |
||||
repeat=1, |
||||
initial_speed=0., |
||||
), |
||||
Maneuver( |
||||
"brake step response: -1m/s^2 from 20mph", |
||||
[Action(-1, 3)], |
||||
repeat=2, |
||||
initial_speed=20. * CV.MPH_TO_MS, |
||||
), |
||||
Maneuver( |
||||
"brake step response: -4m/s^2 from 20mph", |
||||
[Action(-4, 3)], |
||||
repeat=2, |
||||
initial_speed=20. * CV.MPH_TO_MS, |
||||
), |
||||
Maneuver( |
||||
"gas step response: +1m/s^2 from 20mph", |
||||
[Action(1, 3)], |
||||
repeat=2, |
||||
initial_speed=20. * CV.MPH_TO_MS, |
||||
), |
||||
Maneuver( |
||||
"gas step response: +4m/s^2 from 20mph", |
||||
[Action(4, 3)], |
||||
repeat=2, |
||||
initial_speed=20. * CV.MPH_TO_MS, |
||||
), |
||||
] |
||||
|
||||
|
||||
def main(): |
||||
params = Params() |
||||
cloudlog.info("joystickd is waiting for CarParams") |
||||
CP = messaging.log_from_bytes(params.get("CarParams", block=True), car.CarParams) |
||||
|
||||
sm = messaging.SubMaster(['carState', 'controlsState', 'selfdriveState', 'modelV2'], poll='modelV2') |
||||
pm = messaging.PubMaster(['longitudinalPlan', 'driverAssistance', 'alertDebug']) |
||||
|
||||
maneuvers = iter(MANEUVERS) |
||||
maneuver = None |
||||
|
||||
while True: |
||||
sm.update() |
||||
|
||||
if maneuver is None: |
||||
maneuver = next(maneuvers, None) |
||||
|
||||
alert_msg = messaging.new_message('alertDebug') |
||||
alert_msg.valid = True |
||||
|
||||
plan_send = messaging.new_message('longitudinalPlan') |
||||
plan_send.valid = sm.all_checks() |
||||
|
||||
longitudinalPlan = plan_send.longitudinalPlan |
||||
accel = 0 |
||||
cs = sm['carState'] |
||||
v_ego = max(cs.vEgo, 0) |
||||
|
||||
if maneuver is not None: |
||||
accel = maneuver.get_accel(v_ego, cs.cruiseState.enabled, cs.cruiseState.standstill) |
||||
|
||||
if maneuver.active: |
||||
alert_msg.alertDebug.alertText1 = f'Maneuver Active: {accel:0.2f} m/s^2' |
||||
else: |
||||
alert_msg.alertDebug.alertText1 = f'Reaching Speed: {maneuver.initial_speed * CV.MS_TO_MPH:0.2f} mph' |
||||
alert_msg.alertDebug.alertText2 = f'{maneuver.description}' |
||||
else: |
||||
alert_msg.alertDebug.alertText1 = 'Maneuvers Finished' |
||||
|
||||
pm.send('alertDebug', alert_msg) |
||||
|
||||
longitudinalPlan.aTarget = accel |
||||
longitudinalPlan.shouldStop = v_ego < CP.vEgoStopping and accel < 1e-2 |
||||
|
||||
longitudinalPlan.allowBrake = True |
||||
longitudinalPlan.allowThrottle = True |
||||
longitudinalPlan.hasLead = True |
||||
|
||||
pm.send('longitudinalPlan', plan_send) |
||||
|
||||
assistance_send = messaging.new_message('driverAssistance') |
||||
assistance_send.valid = True |
||||
pm.send('driverAssistance', assistance_send) |
||||
|
||||
if maneuver is not None and maneuver.finished: |
||||
maneuver = None |
Loading…
Reference in new issue