long manuevers
This commit is contained in:
@@ -156,13 +156,41 @@ export function LongitudinalManeuvers() {
|
||||
<p><strong>Popup Text:</strong> ${state.data.uiText1 || ""} ${state.data.uiText2 ? `| ${state.data.uiText2}` : ""}</p>
|
||||
</div>
|
||||
|
||||
<div class="longManeuverStatusGrid">
|
||||
${statusLine("openpilot Long", state.data.support?.openpilotLongitudinalControl ? "Yes" : "No")}
|
||||
${statusLine("Full Stop + Go", state.data.support?.fullStopAndGo ? "Yes" : "No")}
|
||||
${statusLine("Auto Resume", state.data.support?.autoResumeFromStop ? "Yes" : "No")}
|
||||
${statusLine("Resume Assist", state.data.support?.requiresResumeAssist ? "Yes" : "No")}
|
||||
${statusLine("Expected Zero", state.data.support?.expectedToReachZero ? "Yes" : "No")}
|
||||
${statusLine("Min Enable", Number.isFinite(Number(state.data.support?.minEnableSpeed)) ? `${Number(state.data.support.minEnableSpeed).toFixed(2)} m/s` : "n/a")}
|
||||
${statusLine("Stop Accel", Number.isFinite(Number(state.data.support?.stopAccel)) ? `${Number(state.data.support.stopAccel).toFixed(2)} m/s²` : "n/a")}
|
||||
</div>
|
||||
|
||||
${(state.data.caveats || []).length ? html`
|
||||
<div class="longManeuverInstructions">
|
||||
<h3>Platform Caveats</h3>
|
||||
<ul>
|
||||
${(state.data.caveats || []).map((line) => html`<li>${line}</li>`)}
|
||||
</ul>
|
||||
</div>
|
||||
` : ""}
|
||||
|
||||
${(state.data.skippedManeuvers || []).length ? html`
|
||||
<div class="longManeuverInstructions">
|
||||
<h3>Expected Skips</h3>
|
||||
<ul>
|
||||
${(state.data.skippedManeuvers || []).map((line) => html`<li>${line}</li>`)}
|
||||
</ul>
|
||||
</div>
|
||||
` : ""}
|
||||
|
||||
<div class="longManeuverInstructions">
|
||||
<h3>Quick Guide</h3>
|
||||
<ol>
|
||||
<li>Find a large, empty, straight road or lot with no traffic.</li>
|
||||
<li>Press <strong>Start / Arm</strong> here, then engage openpilot with SET.</li>
|
||||
<li>Keep full supervision and be ready to disengage at all times.</li>
|
||||
<li>For GM pedal-long cars, the suite runs both pedal-only and pedal+paddle phases automatically.</li>
|
||||
<li>Review the platform caveats before starting. Low-speed maneuvers can be skipped automatically on cars that do not fully stop.</li>
|
||||
<li>When the status says complete, collect logs and generate your HTML report.</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
@@ -40,6 +40,7 @@ from openpilot.system.hardware import HARDWARE, PC
|
||||
from openpilot.system.hardware.hw import Paths
|
||||
from openpilot.system.loggerd.deleter import PRESERVE_ATTR_NAME, PRESERVE_ATTR_VALUE, PRESERVE_COUNT
|
||||
from openpilot.system.version import get_build_metadata
|
||||
from openpilot.tools.longitudinal_maneuvers.capabilities import get_longitudinal_maneuver_support
|
||||
from panda import Panda
|
||||
|
||||
from openpilot.starpilot.assets.theme_manager import HOLIDAY_THEME_PATH, THEME_COMPONENT_PARAMS
|
||||
@@ -2824,6 +2825,9 @@ def _default_longitudinal_maneuver_status():
|
||||
"uiText1": "Long Maneuvers",
|
||||
"uiText2": "",
|
||||
"updatedAtSec": 0.0,
|
||||
"support": {},
|
||||
"caveats": [],
|
||||
"skippedManeuvers": [],
|
||||
"history": [],
|
||||
}
|
||||
|
||||
@@ -2868,15 +2872,32 @@ def _append_longitudinal_maneuver_history(status, line):
|
||||
status["history"] = history[-120:]
|
||||
return status
|
||||
|
||||
def _get_longitudinal_maneuver_support_snapshot():
|
||||
cp_bytes = _safe_params_get_live_raw("CarParamsPersistent")
|
||||
if not cp_bytes:
|
||||
return None
|
||||
|
||||
try:
|
||||
with car.CarParams.from_bytes(cp_bytes) as cp:
|
||||
return get_longitudinal_maneuver_support(cp).to_dict()
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def _serialize_longitudinal_maneuver_status(status):
|
||||
updated_at = _safe_float(status.get("updatedAtSec"), 0.0)
|
||||
age_seconds = max(0.0, time.time() - updated_at) if updated_at > 0 else None
|
||||
mode_enabled = params.get_bool("LongitudinalManeuverMode")
|
||||
paddle_mode = params.get("LongitudinalManeuverPaddleMode", encoding="utf-8") or str(status.get("paddleMode") or "auto")
|
||||
support = _get_longitudinal_maneuver_support_snapshot() or status.get("support") or {}
|
||||
caveats = support.get("caveats", status.get("caveats") or [])
|
||||
skipped_maneuvers = support.get("skippedManeuvers", status.get("skippedManeuvers") or [])
|
||||
return {
|
||||
**status,
|
||||
"modeEnabled": mode_enabled,
|
||||
"paddleMode": paddle_mode,
|
||||
"support": support,
|
||||
"caveats": list(caveats),
|
||||
"skippedManeuvers": list(skipped_maneuvers),
|
||||
"isOnroad": params.get_bool("IsOnroad"),
|
||||
"isEngaged": params.get_bool("IsEngaged"),
|
||||
"updatedAgeSec": age_seconds,
|
||||
|
||||
@@ -18,9 +18,9 @@ Test your vehicle's longitudinal control tuning with this tool. The tool will te
|
||||
|
||||

|
||||
|
||||
5. Ensure the road ahead is clear, as openpilot will not brake for any obstructions in this mode. Once you are ready, press "Set" on your steering wheel to start the tests. The tests will run for about 4 minutes. If you need to pause the tests, press "Cancel" on your steering wheel. You can resume the tests by pressing "Resume" on your steering wheel.
|
||||
5. Ensure the road ahead is clear, as openpilot will not brake for any obstructions in this mode. Once you are ready, press "Set" on your steering wheel to start the tests. The tests will run for about 4 minutes. If you need to pause the tests, press "Cancel" on your steering wheel. You can resume the tests by pressing "Resume" on your steering wheel.
|
||||
|
||||
**Note:** For GM cars, it is recommended to hold down the resume button for all low-speed tests (starting, stopping and creep) to avoid the car entering standstill.
|
||||
**Note:** The suite now reads vehicle stop/go capability from `CarParams`. Cars that can reach a stop but need resume assistance will allow standstill during zero-speed tests, and cars that are not expected to reach zero will skip stop/start/creep maneuvers automatically.
|
||||
|
||||

|
||||
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import asdict, dataclass
|
||||
from typing import Any
|
||||
|
||||
|
||||
LOW_SPEED_MANEUVER_DESCRIPTIONS = (
|
||||
"come to stop",
|
||||
"start from stop",
|
||||
"creep: alternate between +1m/s^2 and -1m/s^2",
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class LongitudinalManeuverSupport:
|
||||
openpilotLongitudinalControl: bool
|
||||
fullStopAndGo: bool
|
||||
autoResumeFromStop: bool
|
||||
requiresResumeAssist: bool
|
||||
expectedToReachZero: bool
|
||||
minEnableSpeed: float
|
||||
stopAccel: float
|
||||
caveats: tuple[str, ...]
|
||||
skippedManeuvers: tuple[str, ...]
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return asdict(self)
|
||||
|
||||
|
||||
def get_longitudinal_maneuver_support(CP: Any) -> LongitudinalManeuverSupport:
|
||||
openpilot_longitudinal = bool(getattr(CP, "openpilotLongitudinalControl", False))
|
||||
auto_resume_supported = bool(getattr(CP, "autoResumeSng", False))
|
||||
|
||||
min_enable_speed = float(getattr(CP, "minEnableSpeed", 0.0) or 0.0)
|
||||
stop_accel = float(getattr(CP, "stopAccel", 0.0) or 0.0)
|
||||
|
||||
# CarParams does not expose a dedicated "won't fully brake to zero" flag,
|
||||
# so low-speed engagement support is the closest reliable proxy.
|
||||
full_stop_and_go = min_enable_speed <= 0.0
|
||||
auto_resume_from_stop = full_stop_and_go and auto_resume_supported
|
||||
expected_to_reach_zero = full_stop_and_go
|
||||
requires_resume_assist = expected_to_reach_zero and not auto_resume_from_stop
|
||||
|
||||
caveats: list[str] = []
|
||||
if not openpilot_longitudinal:
|
||||
caveats.append("openpilot longitudinal is disabled, so the maneuver suite cannot drive longitudinal tests on this platform.")
|
||||
|
||||
if not expected_to_reach_zero:
|
||||
caveats.append("This car is not expected to reach a true standstill in the suite. Stop, start, and creep maneuvers will be skipped.")
|
||||
elif requires_resume_assist:
|
||||
caveats.append("This car can reach a stop, but restart-from-stop needs resume assistance. Zero-speed maneuvers will allow cruise standstill instead of treating it as setup failure.")
|
||||
|
||||
skipped_maneuvers = LOW_SPEED_MANEUVER_DESCRIPTIONS if not expected_to_reach_zero else ()
|
||||
|
||||
return LongitudinalManeuverSupport(
|
||||
openpilotLongitudinalControl=openpilot_longitudinal,
|
||||
fullStopAndGo=full_stop_and_go,
|
||||
autoResumeFromStop=auto_resume_from_stop,
|
||||
requiresResumeAssist=requires_resume_assist,
|
||||
expectedToReachZero=expected_to_reach_zero,
|
||||
minEnableSpeed=min_enable_speed,
|
||||
stopAccel=stop_accel,
|
||||
caveats=tuple(caveats),
|
||||
skippedManeuvers=tuple(skipped_maneuvers),
|
||||
)
|
||||
|
||||
|
||||
def get_maneuver_skip_reason(description: str, support: LongitudinalManeuverSupport) -> str | None:
|
||||
if not support.openpilotLongitudinalControl:
|
||||
return "openpilot longitudinal is disabled"
|
||||
|
||||
if description in LOW_SPEED_MANEUVER_DESCRIPTIONS and not support.expectedToReachZero:
|
||||
return "vehicle is not expected to reach a true standstill"
|
||||
|
||||
return None
|
||||
@@ -12,6 +12,7 @@ import matplotlib.pyplot as plt
|
||||
from tabulate import tabulate
|
||||
|
||||
from openpilot.tools.lib.logreader import LogReader
|
||||
from openpilot.tools.longitudinal_maneuvers.capabilities import get_longitudinal_maneuver_support
|
||||
from openpilot.system.hardware.hw import Paths
|
||||
|
||||
|
||||
@@ -19,11 +20,42 @@ def format_car_params(CP):
|
||||
return pprint.pformat({k: v for k, v in CP.to_dict().items() if not k.endswith('DEPRECATED')}, indent=2)
|
||||
|
||||
|
||||
def format_support_section(support):
|
||||
rows = [
|
||||
("openpilot longitudinal", "yes" if support.openpilotLongitudinalControl else "no"),
|
||||
("full stop and go", "yes" if support.fullStopAndGo else "no"),
|
||||
("auto resume from stop", "yes" if support.autoResumeFromStop else "no"),
|
||||
("resume assist expected", "yes" if support.requiresResumeAssist else "no"),
|
||||
("expected to reach zero", "yes" if support.expectedToReachZero else "no"),
|
||||
("min enable speed", f"{support.minEnableSpeed:.2f} m/s"),
|
||||
("stop accel", f"{support.stopAccel:.2f} m/s^2"),
|
||||
]
|
||||
|
||||
builder = ["<details><summary><h3 style='display: inline-block;'>Platform Caveats</h3></summary>\n"]
|
||||
builder.append(tabulate(rows, headers=("capability", "value"), tablefmt="html") + "\n")
|
||||
|
||||
if support.caveats:
|
||||
builder.append("<h4>Caveats</h4><ul>\n")
|
||||
for caveat in support.caveats:
|
||||
builder.append(f"<li>{caveat}</li>\n")
|
||||
builder.append("</ul>\n")
|
||||
|
||||
if support.skippedManeuvers:
|
||||
builder.append("<h4>Expected skipped maneuvers</h4><ul>\n")
|
||||
for description in support.skippedManeuvers:
|
||||
builder.append(f"<li>{description}</li>\n")
|
||||
builder.append("</ul>\n")
|
||||
|
||||
builder.append("</details>\n")
|
||||
return ''.join(builder)
|
||||
|
||||
|
||||
def report(platform, route, _description, CP, ID, maneuvers):
|
||||
output_path = Path(__file__).resolve().parent / "longitudinal_reports"
|
||||
output_fn = output_path / f"{platform}_{route.replace('/', '_')}.html"
|
||||
output_path.mkdir(exist_ok=True)
|
||||
target_cross_times = defaultdict(list)
|
||||
support = get_longitudinal_maneuver_support(CP)
|
||||
|
||||
builder = [
|
||||
"<style>summary { cursor: pointer; }\n td, th { padding: 8px; } </style>\n",
|
||||
@@ -35,6 +67,7 @@ def report(platform, route, _description, CP, ID, maneuvers):
|
||||
if _description is not None:
|
||||
builder.append(f"<h3>Description: {_description}</h3>\n")
|
||||
builder.append(f"<details><summary><h3 style='display: inline-block;'>CarParams</h3></summary><pre>{format_car_params(CP)}</pre></details>\n")
|
||||
builder.append(format_support_section(support))
|
||||
builder.append('{ summary }') # to be replaced below
|
||||
for description, runs in maneuvers:
|
||||
print(f'plotting maneuver: {description}, runs: {len(runs)}')
|
||||
@@ -56,7 +89,7 @@ def report(platform, route, _description, CP, ID, maneuvers):
|
||||
|
||||
# maneuver validity
|
||||
longActive = [m.longActive for m in carControl]
|
||||
maneuver_valid = all(longActive) and (not any(cs.cruiseState.standstill for cs in carState) or CP.autoResumeSng)
|
||||
maneuver_valid = all(longActive)
|
||||
|
||||
_open = 'open' if maneuver_valid else ''
|
||||
title = f'Run #{int(run)+1}' + (' <span style="color: red">(invalid maneuver!)</span>' if not maneuver_valid else '')
|
||||
|
||||
@@ -7,6 +7,10 @@ from openpilot.common.constants import CV
|
||||
from openpilot.common.realtime import DT_MDL
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.common.swaglog import cloudlog
|
||||
from openpilot.tools.longitudinal_maneuvers.capabilities import (
|
||||
get_longitudinal_maneuver_support,
|
||||
get_maneuver_skip_reason,
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -33,9 +37,11 @@ class Maneuver:
|
||||
_repeated: int = 0
|
||||
|
||||
def get_accel(self, v_ego: float, long_active: bool, standstill: bool, cruise_standstill: bool) -> float:
|
||||
ready = abs(v_ego - self.initial_speed) < 0.3 and long_active and not cruise_standstill
|
||||
ready = abs(v_ego - self.initial_speed) < 0.3 and long_active
|
||||
if self.initial_speed < 0.01:
|
||||
ready = ready and standstill
|
||||
else:
|
||||
ready = ready and not cruise_standstill
|
||||
self._ready_cnt = (self._ready_cnt + 1) if ready else 0
|
||||
|
||||
if self._ready_cnt > (3. / DT_MDL):
|
||||
@@ -76,65 +82,75 @@ class Maneuver:
|
||||
return self._active
|
||||
|
||||
|
||||
MANEUVERS = [
|
||||
Maneuver(
|
||||
"come to stop",
|
||||
[Action([-0.5], [12])],
|
||||
repeat=2,
|
||||
initial_speed=5.,
|
||||
),
|
||||
Maneuver(
|
||||
"start from stop",
|
||||
[Action([1.5], [6])],
|
||||
repeat=2,
|
||||
initial_speed=0.,
|
||||
),
|
||||
Maneuver(
|
||||
"creep: alternate between +1m/s^2 and -1m/s^2",
|
||||
[
|
||||
Action([1], [3]), Action([-1], [3]),
|
||||
Action([1], [3]), Action([-1], [3]),
|
||||
Action([1], [3]), Action([-1], [3]),
|
||||
],
|
||||
repeat=2,
|
||||
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 build_maneuvers():
|
||||
return [
|
||||
Maneuver(
|
||||
"come to stop",
|
||||
[Action([-0.5], [12])],
|
||||
repeat=2,
|
||||
initial_speed=5.,
|
||||
),
|
||||
Maneuver(
|
||||
"start from stop",
|
||||
[Action([1.5], [6])],
|
||||
repeat=2,
|
||||
initial_speed=0.,
|
||||
),
|
||||
Maneuver(
|
||||
"creep: alternate between +1m/s^2 and -1m/s^2",
|
||||
[
|
||||
Action([1], [3]), Action([-1], [3]),
|
||||
Action([1], [3]), Action([-1], [3]),
|
||||
Action([1], [3]), Action([-1], [3]),
|
||||
],
|
||||
repeat=2,
|
||||
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)
|
||||
support = get_longitudinal_maneuver_support(CP)
|
||||
|
||||
supported_maneuvers = []
|
||||
for maneuver in build_maneuvers():
|
||||
skip_reason = get_maneuver_skip_reason(maneuver.description, support)
|
||||
if skip_reason is not None:
|
||||
cloudlog.warning(f"Skipping longitudinal maneuver '{maneuver.description}': {skip_reason}")
|
||||
continue
|
||||
supported_maneuvers.append(maneuver)
|
||||
|
||||
sm = messaging.SubMaster(['carState', 'carControl', 'controlsState', 'selfdriveState', 'modelV2'], poll='modelV2')
|
||||
pm = messaging.PubMaster(['longitudinalPlan', 'driverAssistance', 'alertDebug'])
|
||||
|
||||
maneuvers = iter(MANEUVERS)
|
||||
maneuvers = iter(supported_maneuvers)
|
||||
maneuver = None
|
||||
|
||||
while True:
|
||||
|
||||
Reference in New Issue
Block a user