alerts;
InfoLabel *thumbnail_label;
};
diff --git a/tools/camerastream/compressed_vipc.py b/tools/camerastream/compressed_vipc.py
index f531a289f9..cbea060920 100755
--- a/tools/camerastream/compressed_vipc.py
+++ b/tools/camerastream/compressed_vipc.py
@@ -120,7 +120,7 @@ class CompressedVipc:
self.vipc_server = VisionIpcServer("camerad")
for vst in vision_streams:
ed = sm[ENCODE_SOCKETS[vst]]
- self.vipc_server.create_buffers(vst, 4, False, ed.width, ed.height)
+ self.vipc_server.create_buffers(vst, 4, ed.width, ed.height)
self.vipc_server.start_listener()
self.procs = []
diff --git a/tools/car_porting/test_car_model.py b/tools/car_porting/test_car_model.py
index 6f274aaf69..dd248f562e 100755
--- a/tools/car_porting/test_car_model.py
+++ b/tools/car_porting/test_car_model.py
@@ -5,14 +5,14 @@ import unittest # noqa: TID251
from opendbc.car.tests.routes import CarTestRoute
from openpilot.selfdrive.car.tests.test_models import TestCarModel
-from openpilot.tools.lib.route import SegmentName
+from openpilot.tools.lib.route import SegmentRange
-def create_test_models_suite(routes: list[CarTestRoute], ci=False) -> unittest.TestSuite:
+def create_test_models_suite(routes: list[CarTestRoute]) -> unittest.TestSuite:
test_suite = unittest.TestSuite()
for test_route in routes:
# create new test case and discover tests
- test_case_args = {"platform": test_route.car_model, "test_route": test_route, "test_route_on_bucket": ci}
+ test_case_args = {"platform": test_route.car_model, "test_route": test_route}
CarModelTestCase = type("CarModelTestCase", (TestCarModel,), test_case_args)
test_suite.addTest(unittest.TestLoader().loadTestsFromTestCase(CarModelTestCase))
return test_suite
@@ -23,16 +23,14 @@ if __name__ == "__main__":
"Uses selfdrive/car/tests/test_models.py")
parser.add_argument("route_or_segment_name", help="Specify route to run tests on")
parser.add_argument("--car", help="Specify car model for test route")
- parser.add_argument("--ci", action="store_true", help="Attempt to get logs using openpilotci, need to specify car")
args = parser.parse_args()
if len(sys.argv) == 1:
parser.print_help()
sys.exit()
- route_or_segment_name = SegmentName(args.route_or_segment_name.strip(), allow_route_name=True)
- segment_num = route_or_segment_name.segment_num if route_or_segment_name.segment_num != -1 else None
+ sr = SegmentRange(args.route_or_segment_name)
- test_route = CarTestRoute(route_or_segment_name.route_name.canonical_name, args.car, segment=segment_num)
- test_suite = create_test_models_suite([test_route], ci=args.ci)
+ test_routes = [CarTestRoute(sr.route_name, args.car, segment=seg_idx) for seg_idx in sr.seg_idxs]
+ test_suite = create_test_models_suite(test_routes)
unittest.TextTestRunner().run(test_suite)
diff --git a/tools/install_python_dependencies.sh b/tools/install_python_dependencies.sh
index 8621ee0b00..7657457744 100755
--- a/tools/install_python_dependencies.sh
+++ b/tools/install_python_dependencies.sh
@@ -5,8 +5,8 @@ set -e
export PIP_DEFAULT_TIMEOUT=200
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )"
-ROOT=$DIR/../
-cd $ROOT
+ROOT="$DIR"/../
+cd "$ROOT"
# updating uv on macOS results in 403 sometimes
function update_uv() {
@@ -24,9 +24,9 @@ function update_uv() {
if ! command -v "uv" > /dev/null 2>&1; then
echo "installing uv..."
curl -LsSf https://astral.sh/uv/install.sh | sh
- UV_BIN='$HOME/.cargo/env'
+ UV_BIN="$HOME/.cargo/env"
ADD_PATH_CMD=". \"$UV_BIN\""
- eval $ADD_PATH_CMD
+ eval "$ADD_PATH_CMD"
fi
echo "updating uv..."
@@ -36,9 +36,9 @@ echo "installing python packages..."
uv sync --frozen --all-extras
source .venv/bin/activate
-echo "PYTHONPATH=${PWD}" > $ROOT/.env
+echo "PYTHONPATH=${PWD}" > "$ROOT"/.env
if [[ "$(uname)" == 'Darwin' ]]; then
- echo "# msgq doesn't work on mac" >> $ROOT/.env
- echo "export ZMQ=1" >> $ROOT/.env
- echo "export OBJC_DISABLE_INITIALIZE_FORK_SAFETY=YES" >> $ROOT/.env
+ echo "# msgq doesn't work on mac" >> "$ROOT"/.env
+ echo "export ZMQ=1" >> "$ROOT"/.env
+ echo "export OBJC_DISABLE_INITIALIZE_FORK_SAFETY=YES" >> "$ROOT"/.env
fi
diff --git a/tools/lib/github_utils.py b/tools/lib/github_utils.py
new file mode 100644
index 0000000000..4dc22b9524
--- /dev/null
+++ b/tools/lib/github_utils.py
@@ -0,0 +1,114 @@
+import base64
+import requests
+from http import HTTPMethod
+
+class GithubUtils:
+ def __init__(self, api_token, data_token, owner='commaai', api_repo='openpilot', data_repo='ci-artifacts'):
+ self.OWNER = owner
+ self.API_REPO = api_repo
+ self.DATA_REPO = data_repo
+ self.API_TOKEN = api_token
+ self.DATA_TOKEN = data_token
+
+ @property
+ def API_ROUTE(self):
+ return f"https://api.github.com/repos/{self.OWNER}/{self.API_REPO}"
+
+ @property
+ def DATA_ROUTE(self):
+ return f"https://api.github.com/repos/{self.OWNER}/{self.DATA_REPO}"
+
+ def api_call(self, path, data="", method=HTTPMethod.GET, accept="", data_call=False, raise_on_failure=True):
+ token = self.DATA_TOKEN if data_call else self.API_TOKEN
+ if token:
+ headers = {"Authorization": f"Bearer {self.DATA_TOKEN if data_call else self.API_TOKEN}", \
+ "Accept": f"application/vnd.github{accept}+json"}
+ else:
+ headers = {}
+ path = f'{self.DATA_ROUTE if data_call else self.API_ROUTE}/{path}'
+ r = requests.request(method, path, headers=headers, data=data)
+ if not r.ok and raise_on_failure:
+ raise Exception(f"Call to {path} failed with {r.status_code}")
+ else:
+ return r
+
+ def upload_file(self, bucket, path, file_name):
+ with open(path, "rb") as f:
+ encoded = base64.b64encode(f.read()).decode()
+
+ # check if file already exists
+ sha = self.get_file_sha(bucket, file_name)
+ sha = f'"sha":"{sha}",' if sha else ''
+
+ data = f'{{"message":"uploading {file_name}", \
+ "branch":"{bucket}", \
+ "committer":{{"name":"Vehicle Researcher", "email": "user@comma.ai"}}, \
+ {sha} \
+ "content":"{encoded}"}}'
+ github_path = f"contents/{file_name}"
+ self.api_call(github_path, data=data, method=HTTPMethod.PUT, data_call=True)
+
+ def upload_files(self, bucket, files):
+ self.create_bucket(bucket)
+ for file_name,path in files:
+ self.upload_file(bucket, path, file_name)
+
+ def create_bucket(self, bucket):
+ if self.get_bucket_sha(bucket):
+ return
+ master_sha = self.get_bucket_sha('master')
+ github_path = "git/refs"
+ data = f'{{"ref":"refs/heads/{bucket}", "sha":"{master_sha}"}}'
+ self.api_call(github_path, data=data, method=HTTPMethod.POST, data_call=True)
+
+ def get_bucket_sha(self, bucket):
+ github_path = f"git/refs/heads/{bucket}"
+ r = self.api_call(github_path, data_call=True, raise_on_failure=False)
+ return r.json()['object']['sha'] if r.ok else None
+
+ def get_file_url(self, bucket, file_name):
+ github_path = f"contents/{file_name}?ref={bucket}"
+ r = self.api_call(github_path, data_call=True)
+ return r.json()['download_url']
+
+ def get_file_sha(self, bucket, file_name):
+ github_path = f"contents/{file_name}?ref={bucket}"
+ r = self.api_call(github_path, data_call=True, raise_on_failure=False)
+ return r.json()['sha'] if r.ok else None
+
+ def get_pr_number(self, pr_branch):
+ github_path = f"commits/{pr_branch}/pulls"
+ r = self.api_call(github_path)
+ return r.json()[0]['number']
+
+ def get_bucket_link(self, bucket):
+ return f'https://raw.githubusercontent.com/{self.OWNER}/{self.DATA_REPO}/refs/heads/{bucket}'
+
+ def comment_on_pr(self, comment, pr_branch, commenter="", overwrite=False):
+ pr_number = self.get_pr_number(pr_branch)
+ data = f'{{"body": "{comment}"}}'
+ if overwrite:
+ github_path = f'issues/{pr_number}/comments'
+ r = self.api_call(github_path)
+ comments = [x['id'] for x in r.json() if x['user']['login'] == commenter]
+ if comments:
+ github_path = f'issues/comments/{comments[0]}'
+ self.api_call(github_path, data=data, method=HTTPMethod.PATCH)
+ return
+
+ github_path=f'issues/{pr_number}/comments'
+ self.api_call(github_path, data=data, method=HTTPMethod.POST)
+
+ # upload files to github and comment them on the pr
+ def comment_images_on_pr(self, title, commenter, pr_branch, bucket, images):
+ self.upload_files(bucket, images)
+ table = [f'{title}
']
+ for i,f in enumerate(images):
+ if not (i % 2):
+ table.append('')
+ table.append(f' | ')
+ if (i % 2):
+ table.append('
')
+ table.append('
')
+ table = ''.join(table)
+ self.comment_on_pr(table, commenter, pr_branch)
diff --git a/tools/lib/logreader.py b/tools/lib/logreader.py
index cf8748929c..5f7cdd3043 100755
--- a/tools/lib/logreader.py
+++ b/tools/lib/logreader.py
@@ -49,7 +49,7 @@ class _LogFileReader:
_, ext = os.path.splitext(urllib.parse.urlparse(fn).path)
if ext not in ('', '.bz2', '.zst'):
# old rlogs weren't compressed
- raise Exception(f"unknown extension {ext}")
+ raise ValueError(f"unknown extension {ext}")
with FileReader(fn) as f:
dat = f.read()
@@ -99,6 +99,10 @@ Source = Callable[[SegmentRange, ReadMode], list[LogPath]]
InternalUnavailableException = Exception("Internal source not available")
+class LogsUnavailable(Exception):
+ pass
+
+
@cache
def default_valid_file(fn: LogPath) -> bool:
return fn is not None and file_exists(fn)
@@ -128,7 +132,7 @@ def apply_strategy(mode: ReadMode, rlog_paths: list[LogPath], qlog_paths: list[L
return auto_strategy(rlog_paths, qlog_paths, False, valid_file)
elif mode == ReadMode.AUTO_INTERACTIVE:
return auto_strategy(rlog_paths, qlog_paths, True, valid_file)
- raise Exception(f"invalid mode: {mode}")
+ raise ValueError(f"invalid mode: {mode}")
def comma_api_source(sr: SegmentRange, mode: ReadMode) -> list[LogPath]:
@@ -224,8 +228,8 @@ def auto_source(sr: SegmentRange, mode=ReadMode.RLOG, sources: list[Source] = No
except Exception as e:
exceptions[source.__name__] = e
- raise Exception("auto_source could not find any valid source, exceptions for sources:\n - " +
- "\n - ".join([f"{k}: {repr(v)}" for k, v in exceptions.items()]))
+ raise LogsUnavailable("auto_source could not find any valid source, exceptions for sources:\n - " +
+ "\n - ".join([f"{k}: {repr(v)}" for k, v in exceptions.items()]))
def parse_indirect(identifier: str) -> str:
diff --git a/tools/longitudinal_maneuvers/README.md b/tools/longitudinal_maneuvers/README.md
index 66ec697912..643af7fd82 100644
--- a/tools/longitudinal_maneuvers/README.md
+++ b/tools/longitudinal_maneuvers/README.md
@@ -18,7 +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.

diff --git a/tools/longitudinal_maneuvers/generate_report.py b/tools/longitudinal_maneuvers/generate_report.py
index eb8c13271a..f038469e94 100755
--- a/tools/longitudinal_maneuvers/generate_report.py
+++ b/tools/longitudinal_maneuvers/generate_report.py
@@ -8,6 +8,7 @@ import pprint
from collections import defaultdict
from pathlib import Path
import matplotlib.pyplot as plt
+from tabulate import tabulate
from openpilot.tools.lib.logreader import LogReader
from openpilot.system.hardware.hw import Paths
@@ -17,115 +18,129 @@ 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 report(platform, route, _description, CP, maneuvers):
+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)
+
+ builder = [
+ "\n",
+ "Longitudinal maneuver report
\n",
+ f"{platform}
\n",
+ f"{route}
\n",
+ f"{ID.gitCommit}, {ID.gitBranch}, {ID.gitRemote}
\n",
+ ]
+ if _description is not None:
+ builder.append(f"Description: {_description}
\n")
+ builder.append(f"CarParams
{format_car_params(CP)} \n")
+ builder.append('{ summary }') # to be replaced below
+ for description, runs in maneuvers:
+ print(f'plotting maneuver: {description}, runs: {len(runs)}')
+ builder.append("\n")
+ builder.append(f"{description}
\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_livePose, livePose = zip(*[(m.logMonoTime, m.livePose) for m in msgs if m.which() == 'livePose'], strict=True)
+ t_longitudinalPlan, longitudinalPlan = zip(*[(m.logMonoTime, m.longitudinalPlan) for m in msgs if m.which() == 'longitudinalPlan'], strict=True)
+
+ # make time relative seconds
+ t_carControl = [(t - t_carControl[0]) / 1e9 for t in t_carControl]
+ t_carOutput = [(t - t_carOutput[0]) / 1e9 for t in t_carOutput]
+ t_carState = [(t - t_carState[0]) / 1e9 for t in t_carState]
+ t_livePose = [(t - t_livePose[0]) / 1e9 for t in t_livePose]
+ t_longitudinalPlan = [(t - t_longitudinalPlan[0]) / 1e9 for t in t_longitudinalPlan]
+
+ # 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)
+
+ _open = 'open' if maneuver_valid else ''
+ title = f'Run #{int(run)+1}' + (' (invalid maneuver!)' if not maneuver_valid else '')
+
+ builder.append(f"{title}
\n")
+
+ # get first acceleration target and first intersection
+ aTarget = longitudinalPlan[0].aTarget
+ target_cross_time = None
+ builder.append(f'Initial aTarget: {round(aTarget, 2)} m/s^2')
+
+ # Localizer is noisy, require two consecutive 20Hz frames above threshold
+ prev_crossed = False
+ for t, lp in zip(t_livePose, livePose, strict=True):
+ crossed = (0 < aTarget < lp.accelerationDevice.x) or (0 > aTarget > lp.accelerationDevice.x)
+ if crossed and prev_crossed:
+ builder.append(f', crossed in {t:.3f}s')
+ target_cross_time = t
+ if maneuver_valid:
+ target_cross_times[description].append(t)
+ break
+ prev_crossed = crossed
+ else:
+ builder.append(', not crossed')
+ builder.append('
')
+
+ pitches = [math.degrees(m.orientationNED[1]) for m in carControl]
+ builder.append(f'Average pitch: {sum(pitches) / len(pitches):0.2f} degrees
')
+
+ plt.rcParams['font.size'] = 40
+ fig = plt.figure(figsize=(30, 26))
+ ax = fig.subplots(4, 1, sharex=True, gridspec_kw={'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)
+ ax[0].plot(t_livePose, [m.accelerationDevice.x for m in livePose], label='livePose.accelerationDevice.x', linewidth=6)
+ # TODO localizer accel
+ ax[0].set_ylabel('Acceleration (m/s^2)')
+ #ax[0].set_ylim(-6.5, 6.5)
+ ax[0].legend(prop={'size': 30})
+
+ if target_cross_time is not None:
+ ax[0].plot(target_cross_time, aTarget, marker='o', markersize=50, markeredgewidth=7, markeredgecolor='black', markerfacecolor='None')
+
+ 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, [m.gasPressed for m in carState], label='gasPressed', linewidth=6)
+ ax[3].plot(t_carState, [m.brakePressed for m in carState], 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='webp')
+ buffer.seek(0)
+ builder.append(f"
\n")
+ builder.append(" \n")
+
+ summary = ["Summary
\n"]
+ cols = ['maneuver', 'crossed', 'runs', 'mean', 'min', 'max']
+ table = []
+ for description, runs in maneuvers:
+ times = target_cross_times[description]
+ l = [description, len(times), len(runs)]
+ if len(times):
+ l.extend([round(sum(times) / len(times), 2), round(min(times), 2), round(max(times), 2)])
+ table.append(l)
+ summary.append(tabulate(table, headers=cols, tablefmt='html', numalign='left') + '\n')
+
+ sum_idx = builder.index('{ summary }')
+ builder[sum_idx:sum_idx + 1] = summary
+
with open(output_fn, "w") as f:
- f.write("Longitudinal maneuver report
\n")
- f.write(f"{platform}
\n")
- f.write(f"{route}
\n")
- if _description is not None:
- f.write(f"Description: {_description}
\n")
- f.write(f"CarParams
{format_car_params(CP)} \n")
- for description, runs in maneuvers:
- print(f'plotting maneuver: {description}, runs: {len(runs)}')
- f.write("\n")
- f.write(f"{description}
\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_livePose, livePose = zip(*[(m.logMonoTime, m.livePose) for m in msgs if m.which() == 'livePose'], strict=True)
- t_longitudinalPlan, longitudinalPlan = zip(*[(m.logMonoTime, m.longitudinalPlan) for m in msgs if m.which() == 'longitudinalPlan'], strict=True)
-
- # make time relative seconds
- t_carControl = [(t - t_carControl[0]) / 1e9 for t in t_carControl]
- t_carOutput = [(t - t_carOutput[0]) / 1e9 for t in t_carOutput]
- t_carState = [(t - t_carState[0]) / 1e9 for t in t_carState]
- t_livePose = [(t - t_livePose[0]) / 1e9 for t in t_livePose]
- t_longitudinalPlan = [(t - t_longitudinalPlan[0]) / 1e9 for t in t_longitudinalPlan]
-
- # maneuver validity
- longActive = [m.longActive for m in carControl]
- maneuver_valid = all(longActive) and not any(cs.cruiseState.standstill for cs in carState)
-
- _open = 'open' if maneuver_valid else ''
- title = f'Run #{int(run)+1}' + (' (invalid maneuver!)' if not maneuver_valid else '')
-
- f.write(f"{title}
\n")
-
- # get first acceleration target and first intersection
- aTarget = longitudinalPlan[0].aTarget
- target_cross_time = None
- f.write(f'Initial aTarget: {aTarget} m/s^2')
-
- # Localizer is noisy, require two consecutive 20Hz frames above threshold
- prev_crossed = False
- for t, lp in zip(t_livePose, livePose, strict=True):
- crossed = (0 < aTarget < lp.accelerationDevice.x) or (0 > aTarget > lp.accelerationDevice.x)
- if crossed and prev_crossed:
- f.write(f', crossed in {t:.3f}s')
- target_cross_time = t
- if maneuver_valid:
- target_cross_times[description].append(t)
- break
- prev_crossed = crossed
- else:
- f.write(', not crossed')
- f.write('
')
-
- pitches = [math.degrees(m.orientationNED[1]) for m in carControl]
- f.write(f'Average pitch: {sum(pitches) / len(pitches):0.2f} degrees
')
-
- plt.rcParams['font.size'] = 40
- fig = plt.figure(figsize=(30, 26))
- ax = fig.subplots(4, 1, sharex=True, gridspec_kw={'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)
- ax[0].plot(t_livePose, [m.accelerationDevice.x for m in livePose], label='livePose.accelerationDevice.x', linewidth=6)
- # TODO localizer accel
- ax[0].set_ylabel('Acceleration (m/s^2)')
- #ax[0].set_ylim(-6.5, 6.5)
- ax[0].legend(prop={'size': 30})
-
- if target_cross_time is not None:
- ax[0].plot(target_cross_time, aTarget, marker='o', markersize=50, markeredgewidth=7, markeredgecolor='black', markerfacecolor='None')
-
- 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, [m.gasPressed for m in carState], label='gasPressed', linewidth=6)
- ax[3].plot(t_carState, [m.brakePressed for m in carState], 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"
\n")
- f.write(" \n")
-
- f.write("Summary
\n")
- for description, runs in maneuvers:
- times = target_cross_times[description]
- f.write(f"{description}
\n")
- f.write(f"Target crossed {len(times)} out of {len(runs)} runs
\n")
- if len(times):
- f.write(f"Mean time to cross: {sum(times) / len(times):.3f}s, min: {min(times):.3f}s, max: {max(times):.3f}s
\n")
+ f.write(''.join(builder))
print(f"\nReport written to {output_fn}\n")
@@ -144,6 +159,7 @@ if __name__ == '__main__':
lr = LogReader([os.path.join(Paths.log_root(), seg, 'rlog') for seg in segs])
CP = lr.first('carParams')
+ ID = lr.first('initData')
platform = CP.carFingerprint
print('processing report for', platform)
@@ -165,4 +181,4 @@ if __name__ == '__main__':
if active_prev:
maneuvers[-1][1][-1].append(msg)
- report(platform, args.route, args.description, CP, maneuvers)
+ report(platform, args.route, args.description, CP, ID, maneuvers)
diff --git a/tools/longitudinal_maneuvers/maneuversd.py b/tools/longitudinal_maneuvers/maneuversd.py
index 07582c3e6c..b10ffa1ac0 100755
--- a/tools/longitudinal_maneuvers/maneuversd.py
+++ b/tools/longitudinal_maneuvers/maneuversd.py
@@ -81,7 +81,7 @@ MANEUVERS = [
Maneuver(
"start from stop",
[Action(1.5, 5)],
- repeat=3,
+ repeat=2,
initial_speed=0.,
),
Maneuver(
@@ -168,6 +168,8 @@ def main():
longitudinalPlan.allowThrottle = True
longitudinalPlan.hasLead = True
+ longitudinalPlan.speeds = [0.2] # triggers carControl.cruiseControl.resume in controlsd
+
pm.send('longitudinalPlan', plan_send)
assistance_send = messaging.new_message('driverAssistance')
diff --git a/tools/op.sh b/tools/op.sh
index 4c792edefe..62d390c9b5 100755
--- a/tools/op.sh
+++ b/tools/op.sh
@@ -148,15 +148,20 @@ function op_check_python() {
INSTALLED_PYTHON_VERSION=$(python3 --version 2> /dev/null || true)
if [[ -z $INSTALLED_PYTHON_VERSION ]]; then
- echo -e " ↳ [${RED}✗${NC}] python3 not found on your system. You need python version at least $(echo $REQUIRED_PYTHON_VERSION | tr -d -c '[0-9.]') to continue!"
+ echo -e " ↳ [${RED}✗${NC}] python3 not found on your system. You need python version satisfying $(echo $REQUIRED_PYTHON_VERSION | cut -d '=' -f2-) to continue!"
loge "ERROR_PYTHON_NOT_FOUND"
return 1
- elif [[ $(echo $INSTALLED_PYTHON_VERSION | grep -o '[0-9]\+\.[0-9]\+' | tr -d -c '[0-9]') -ge $(echo $REQUIRED_PYTHON_VERSION | tr -d -c '[0-9]') ]]; then
- echo -e " ↳ [${GREEN}✔${NC}] $INSTALLED_PYTHON_VERSION detected."
else
- echo -e " ↳ [${RED}✗${NC}] You need python version at least $(echo $REQUIRED_PYTHON_VERSION | tr -d -c '[0-9.]') to continue!"
- loge "ERROR_PYTHON_VERSION" "$INSTALLED_PYTHON_VERSION"
- return 1
+ LB=$(echo $REQUIRED_PYTHON_VERSION | tr -d -c '[0-9,]' | cut -d ',' -f1)
+ UB=$(echo $REQUIRED_PYTHON_VERSION | tr -d -c '[0-9,]' | cut -d ',' -f2)
+ VERSION=$(echo $INSTALLED_PYTHON_VERSION | grep -o '[0-9]\+\.[0-9]\+' | tr -d -c '[0-9]')
+ if [[ $VERSION -ge LB && $VERSION -le UB ]]; then
+ echo -e " ↳ [${GREEN}✔${NC}] $INSTALLED_PYTHON_VERSION detected."
+ else
+ echo -e " ↳ [${RED}✗${NC}] You need a python version satisfying $(echo $REQUIRED_PYTHON_VERSION | cut -d '=' -f2-) to continue!"
+ loge "ERROR_PYTHON_VERSION" "$INSTALLED_PYTHON_VERSION"
+ return 1
+ fi
fi
}
diff --git a/tools/plotjuggler/layouts/longitudinal.xml b/tools/plotjuggler/layouts/longitudinal.xml
index 33a24f76d6..460de97025 100644
--- a/tools/plotjuggler/layouts/longitudinal.xml
+++ b/tools/plotjuggler/layouts/longitudinal.xml
@@ -11,6 +11,7 @@
+
diff --git a/tools/replay/README.md b/tools/replay/README.md
index 65a1e7a0b3..5e91aae943 100644
--- a/tools/replay/README.md
+++ b/tools/replay/README.md
@@ -37,7 +37,7 @@ tools/replay/replay --data_dir="/path_to/route"
# a2a0ccea32023010|2023-07-27--13-01-19--0
# a2a0ccea32023010|2023-07-27--13-01-19--1
# You can replay it like this:
-tools/replay/replay "a2a0ccea32023010|2023-07-27--13-01-19" --data-dir="/path_to_routes"
+tools/replay/replay "a2a0ccea32023010|2023-07-27--13-01-19" --data_dir="/path_to_routes"
```
## Send Messages via ZMQ
diff --git a/tools/replay/SConscript b/tools/replay/SConscript
index 1f966d4372..179af69d42 100644
--- a/tools/replay/SConscript
+++ b/tools/replay/SConscript
@@ -9,7 +9,8 @@ if arch == "Darwin":
else:
base_libs.append('OpenCL')
-replay_lib_src = ["replay.cc", "consoleui.cc", "camera.cc", "filereader.cc", "logreader.cc", "framereader.cc", "route.cc", "util.cc"]
+replay_lib_src = ["replay.cc", "consoleui.cc", "camera.cc", "filereader.cc", "logreader.cc", "framereader.cc",
+ "route.cc", "util.cc", "timeline.cc"]
replay_lib = qt_env.Library("qt_replay", replay_lib_src, LIBS=base_libs, FRAMEWORKS=base_frameworks)
Export('replay_lib')
replay_libs = [replay_lib, 'avutil', 'avcodec', 'avformat', 'bz2', 'zstd', 'curl', 'yuv', 'ncurses'] + base_libs
diff --git a/tools/replay/camera.cc b/tools/replay/camera.cc
index 914c18e15f..73243ed20d 100644
--- a/tools/replay/camera.cc
+++ b/tools/replay/camera.cc
@@ -51,7 +51,7 @@ void CameraServer::startVipcServer() {
if (cam.width > 0 && cam.height > 0) {
rInfo("camera[%d] frame size %dx%d", cam.type, cam.width, cam.height);
auto [nv12_width, nv12_height, nv12_buffer_size] = get_nv12_info(cam.width, cam.height);
- vipc_server_->create_buffers_with_sizes(cam.stream_type, BUFFER_COUNT, false, cam.width, cam.height,
+ vipc_server_->create_buffers_with_sizes(cam.stream_type, BUFFER_COUNT, cam.width, cam.height,
nv12_buffer_size, nv12_width, nv12_width * nv12_height);
if (!cam.thread.joinable()) {
cam.thread = std::thread(&CameraServer::cameraThread, this, std::ref(cam));
diff --git a/tools/replay/consoleui.cc b/tools/replay/consoleui.cc
index 21ca0ad74b..b5415ac808 100644
--- a/tools/replay/consoleui.cc
+++ b/tools/replay/consoleui.cc
@@ -1,5 +1,6 @@
#include "tools/replay/consoleui.h"
+#include
#include
#include
#include
@@ -7,6 +8,7 @@
#include
+#include "common/ratekeeper.h"
#include "common/util.h"
#include "common/version.h"
@@ -57,7 +59,7 @@ void add_str(WINDOW *w, const char *str, Color color = Color::Default, bool bold
} // namespace
-ConsoleUI::ConsoleUI(Replay *replay, QObject *parent) : replay(replay), sm({"carState", "liveParameters"}), QObject(parent) {
+ConsoleUI::ConsoleUI(Replay *replay) : replay(replay), sm({"carState", "liveParameters"}) {
// Initialize curses
initscr();
clear();
@@ -80,24 +82,16 @@ ConsoleUI::ConsoleUI(Replay *replay, QObject *parent) : replay(replay), sm({"car
initWindows();
- qRegisterMetaType("uint64_t");
- qRegisterMetaType("ReplyMsgType");
installMessageHandler([this](ReplyMsgType type, const std::string msg) {
- emit logMessageSignal(type, QString::fromStdString(msg));
+ std::scoped_lock lock(mutex);
+ logs.emplace_back(type, msg);
});
installDownloadProgressHandler([this](uint64_t cur, uint64_t total, bool success) {
- emit updateProgressBarSignal(cur, total, success);
+ std::scoped_lock lock(mutex);
+ progress_cur = cur;
+ progress_total = total;
+ download_success = success;
});
-
- QObject::connect(replay, &Replay::streamStarted, this, &ConsoleUI::updateSummary);
- QObject::connect(¬ifier, SIGNAL(activated(int)), SLOT(readyRead()));
- QObject::connect(this, &ConsoleUI::updateProgressBarSignal, this, &ConsoleUI::updateProgressBar);
- QObject::connect(this, &ConsoleUI::logMessageSignal, this, &ConsoleUI::logMessage);
-
- sm_timer.callOnTimeout(this, &ConsoleUI::updateStatus);
- sm_timer.start(100);
- getch_timer.start(1000, this);
- readyRead();
}
ConsoleUI::~ConsoleUI() {
@@ -136,9 +130,7 @@ void ConsoleUI::initWindows() {
}
}
-void ConsoleUI::timerEvent(QTimerEvent *ev) {
- if (ev->timerId() != getch_timer.timerId()) return;
-
+void ConsoleUI::updateSize() {
if (is_term_resized(max_height, max_width)) {
for (auto win : w) {
if (win) delwin(win);
@@ -149,7 +141,6 @@ void ConsoleUI::timerEvent(QTimerEvent *ev) {
initWindows();
rWarning("resize term %dx%d", max_height, max_width);
}
- updateTimeline();
}
void ConsoleUI::updateStatus() {
@@ -162,23 +153,18 @@ void ConsoleUI::updateStatus() {
add_str(win, unit.c_str());
};
static const std::pair status_text[] = {
- {"loading...", Color::Red},
{"playing", Color::Green},
{"paused...", Color::Yellow},
};
sm.update(0);
- if (status != Status::Paused) {
- auto events = replay->events();
- uint64_t current_mono_time = replay->routeStartNanos() + replay->currentSeconds() * 1e9;
- bool playing = !events->empty() && events->back().mono_time > current_mono_time;
- status = playing ? Status::Playing : Status::Waiting;
- }
auto [status_str, status_color] = status_text[status];
write_item(0, 0, "STATUS: ", status_str, " ", false, status_color);
+ auto cur_ts = replay->routeDateTime() + (int)replay->currentSeconds();
+ char *time_string = ctime(&cur_ts);
std::string current_segment = " - " + std::to_string((int)(replay->currentSeconds() / 60));
- write_item(0, 25, "TIME: ", replay->currentDateTime().toString("ddd MMMM dd hh:mm:ss").toStdString(), current_segment, true);
+ write_item(0, 25, "TIME: ", time_string, current_segment, true);
auto p = sm["liveParameters"].getLiveParameters();
write_item(1, 0, "STIFFNESS: ", util::string_format("%.2f %%", p.getStiffnessFactor() * 100), " ");
@@ -218,7 +204,7 @@ void ConsoleUI::displayTimelineDesc() {
}
}
-void ConsoleUI::logMessage(ReplyMsgType type, const QString &msg) {
+void ConsoleUI::logMessage(ReplyMsgType type, const std::string &msg) {
if (auto win = w[Win::Log]) {
Color color = Color::Default;
if (type == ReplyMsgType::Debug) {
@@ -228,26 +214,26 @@ void ConsoleUI::logMessage(ReplyMsgType type, const QString &msg) {
} else if (type == ReplyMsgType::Critical) {
color = Color::Red;
}
- add_str(win, qPrintable(msg + "\n"), color);
+ add_str(win, (msg + "\n").c_str(), color);
wrefresh(win);
}
}
-void ConsoleUI::updateProgressBar(uint64_t cur, uint64_t total, bool success) {
+void ConsoleUI::updateProgressBar() {
werase(w[Win::DownloadBar]);
- if (success && cur < total) {
+ if (download_success && progress_cur < progress_total) {
const int width = 35;
- const float progress = cur / (double)total;
+ const float progress = progress_cur / (double)progress_total;
const int pos = width * progress;
wprintw(w[Win::DownloadBar], "Downloading [%s>%s] %d%% %s", std::string(pos, '=').c_str(),
- std::string(width - pos, ' ').c_str(), int(progress * 100.0), formattedDataSize(total).c_str());
+ std::string(width - pos, ' ').c_str(), int(progress * 100.0), formattedDataSize(progress_total).c_str());
}
wrefresh(w[Win::DownloadBar]);
}
void ConsoleUI::updateSummary() {
const auto &route = replay->route();
- mvwprintw(w[Win::Stats], 0, 0, "Route: %s, %lu segments", qPrintable(route->name()), route->segments().size());
+ mvwprintw(w[Win::Stats], 0, 0, "Route: %s, %lu segments", route->name().c_str(), route->segments().size());
mvwprintw(w[Win::Stats], 1, 0, "Car Fingerprint: %s", replay->carFingerprint().c_str());
wrefresh(w[Win::Stats]);
}
@@ -263,18 +249,18 @@ void ConsoleUI::updateTimeline() {
wattroff(win, COLOR_PAIR(Color::Disengaged));
const int total_sec = replay->maxSeconds() - replay->minSeconds();
- for (auto [begin, end, type] : replay->getTimeline()) {
- int start_pos = ((begin - replay->minSeconds()) / total_sec) * width;
- int end_pos = ((end - replay->minSeconds()) / total_sec) * width;
- if (type == TimelineType::Engaged) {
+ for (const auto &entry : *replay->getTimeline()) {
+ int start_pos = ((entry.start_time - replay->minSeconds()) / total_sec) * width;
+ int end_pos = ((entry.end_time - replay->minSeconds()) / total_sec) * width;
+ if (entry.type == TimelineType::Engaged) {
mvwchgat(win, 1, start_pos, end_pos - start_pos + 1, A_COLOR, Color::Engaged, NULL);
mvwchgat(win, 2, start_pos, end_pos - start_pos + 1, A_COLOR, Color::Engaged, NULL);
- } else if (type == TimelineType::UserFlag) {
+ } else if (entry.type == TimelineType::UserFlag) {
mvwchgat(win, 3, start_pos, end_pos - start_pos + 1, ACS_S3, Color::Cyan, NULL);
} else {
auto color_id = Color::Green;
- if (type != TimelineType::AlertInfo) {
- color_id = type == TimelineType::AlertWarning ? Color::Yellow : Color::Red;
+ if (entry.type != TimelineType::AlertInfo) {
+ color_id = entry.type == TimelineType::AlertWarning ? Color::Yellow : Color::Red;
}
mvwchgat(win, 3, start_pos, end_pos - start_pos + 1, ACS_S3, color_id, NULL);
}
@@ -288,16 +274,9 @@ void ConsoleUI::updateTimeline() {
wrefresh(win);
}
-void ConsoleUI::readyRead() {
- int c;
- while ((c = getch()) != ERR) {
- handleKey(c);
- }
-}
-
void ConsoleUI::pauseReplay(bool pause) {
replay->pause(pause);
- status = pause ? Status::Paused : Status::Waiting;
+ status = pause ? Status::Paused : Status::Playing;
}
void ConsoleUI::handleKey(char c) {
@@ -305,7 +284,6 @@ void ConsoleUI::handleKey(char c) {
// pause the replay and blocking getchar()
pauseReplay(true);
updateStatus();
- getch_timer.stop();
curs_set(true);
nodelay(stdscr, false);
@@ -330,7 +308,6 @@ void ConsoleUI::handleKey(char c) {
nodelay(stdscr, true);
curs_set(false);
refresh();
- getch_timer.start(1000, this);
} else if (c == '+' || c == '=') {
auto it = std::upper_bound(speed_array.begin(), speed_array.end(), replay->getSpeed());
@@ -367,7 +344,37 @@ void ConsoleUI::handleKey(char c) {
replay->seekTo(-10, true);
} else if (c == ' ') {
pauseReplay(!replay->isPaused());
- } else if (c == 'q' || c == 'Q') {
- qApp->exit();
}
}
+
+int ConsoleUI::exec() {
+ RateKeeper rk("Replay", 20);
+ while (true) {
+ int c = getch();
+ if (c == 'q' || c == 'Q') {
+ break;
+ }
+ handleKey(c);
+
+ if (rk.frame() % 25) {
+ updateSize();
+ updateSummary();
+ }
+
+ updateTimeline();
+ updateStatus();
+
+ {
+ std::scoped_lock lock(mutex);
+ updateProgressBar();
+ for (auto &[type, msg] : logs) {
+ logMessage(type, msg);
+ }
+ logs.clear();
+ }
+
+ qApp->processEvents();
+ rk.keepTime();
+ }
+ return 0;
+}
diff --git a/tools/replay/consoleui.h b/tools/replay/consoleui.h
index 6ed44bc623..3d4abeb458 100644
--- a/tools/replay/consoleui.h
+++ b/tools/replay/consoleui.h
@@ -1,21 +1,17 @@
#pragma once
#include
-#include
-#include
-#include
-#include
-#include
+#include
+#include
#include "tools/replay/replay.h"
#include
-class ConsoleUI : public QObject {
- Q_OBJECT
-
+class ConsoleUI {
public:
- ConsoleUI(Replay *replay, QObject *parent = 0);
+ ConsoleUI(Replay *replay);
~ConsoleUI();
+ int exec();
inline static const std::array speed_array = {0.2f, 0.5f, 1.0f, 2.0f, 3.0f};
private:
@@ -27,25 +23,21 @@ private:
void updateSummary();
void updateStatus();
void pauseReplay(bool pause);
+ void updateSize();
+ void updateProgressBar();
+ void logMessage(ReplyMsgType type, const std::string &msg);
- enum Status { Waiting, Playing, Paused };
+ enum Status { Playing, Paused };
enum Win { Title, Stats, Log, LogBorder, DownloadBar, Timeline, TimelineDesc, Help, CarState, Max};
std::array w{};
SubMaster sm;
Replay *replay;
- QBasicTimer getch_timer;
- QTimer sm_timer;
- QSocketNotifier notifier{0, QSocketNotifier::Read, this};
int max_width, max_height;
- Status status = Status::Waiting;
+ Status status = Status::Playing;
-signals:
- void updateProgressBarSignal(uint64_t cur, uint64_t total, bool success);
- void logMessageSignal(ReplyMsgType type, const QString &msg);
-
-private slots:
- void readyRead();
- void timerEvent(QTimerEvent *ev);
- void updateProgressBar(uint64_t cur, uint64_t total, bool success);
- void logMessage(ReplyMsgType type, const QString &msg);
+ std::mutex mutex;
+ std::vector> logs;
+ uint64_t progress_cur = 0;
+ uint64_t progress_total = 0;
+ bool download_success = false;
};
diff --git a/tools/replay/lib/rp_helpers.py b/tools/replay/lib/rp_helpers.py
index 95eef9d233..aa20ab8e32 100644
--- a/tools/replay/lib/rp_helpers.py
+++ b/tools/replay/lib/rp_helpers.py
@@ -8,7 +8,8 @@ rerunColorPalette = [(96, "red", (255, 0, 0)),
(230, "vibrantpink", (255, 36, 170)),
(240, "orange", (255, 146, 0)),
(255, "white", (255, 255, 255)),
- (110, "carColor", (255,0,127))]
+ (110, "carColor", (255,0,127)),
+ (0, "background", (0, 0, 0))]
class UIParams:
@@ -68,7 +69,7 @@ def plot_lead(rs, lid_overlay):
lid_overlay[px_left:px_right, py] = rerunColorPalette[0][0]
-def maybe_update_radar_points(lt, lid_overlay):
+def update_radar_points(lt, lid_overlay):
ar_pts = []
if lt is not None:
ar_pts = {}
diff --git a/tools/replay/main.cc b/tools/replay/main.cc
index a0c072438d..b880e99e23 100644
--- a/tools/replay/main.cc
+++ b/tools/replay/main.cc
@@ -1,9 +1,124 @@
+#include
+
#include
-#include
+#include
+#include