Merge branch commaai/openpilot:master into branch sunnypilot/sunnypilot:master-new

This commit is contained in:
discountchubbs
2025-05-25 09:09:02 -07:00
committed by Jason Wen
207 changed files with 4167 additions and 1948 deletions
+1 -1
View File
@@ -29,7 +29,7 @@ cabana_lib = cabana_env.Library("cabana_lib", ['mainwin.cc', 'streams/socketcans
'streams/routes.cc', 'dbc/dbc.cc', 'dbc/dbcfile.cc', 'dbc/dbcmanager.cc',
'utils/export.cc', 'utils/util.cc',
'chart/chartswidget.cc', 'chart/chart.cc', 'chart/signalselector.cc', 'chart/tiplabel.cc', 'chart/sparkline.cc',
'commands.cc', 'messageswidget.cc', 'streamselector.cc', 'settings.cc', 'detailwidget.cc', 'tools/findsimilarbits.cc', 'tools/findsignal.cc'], LIBS=cabana_libs, FRAMEWORKS=base_frameworks)
'commands.cc', 'messageswidget.cc', 'streamselector.cc', 'settings.cc', 'detailwidget.cc', 'tools/findsimilarbits.cc', 'tools/findsignal.cc', 'tools/routeinfo.cc'], LIBS=cabana_libs, FRAMEWORKS=base_frameworks)
cabana_env.Program('cabana', ['cabana.cc', cabana_lib, assets], LIBS=cabana_libs, FRAMEWORKS=base_frameworks)
if GetOption('extras'):
+40
View File
@@ -0,0 +1,40 @@
#include "tools/cabana/tools/routeinfo.h"
#include <QHeaderView>
#include <QScrollBar>
#include <QTableWidget>
#include <QVBoxLayout>
#include "tools/cabana/streams/replaystream.h"
RouteInfoDlg::RouteInfoDlg(QWidget *parent) : QDialog(parent) {
auto *replay = qobject_cast<ReplayStream *>(can)->getReplay();
setWindowTitle(tr("Route: %1").arg(QString::fromStdString(replay->route().name())));
auto *table = new QTableWidget(replay->route().segments().size(), 7, this);
table->setToolTip(tr("Click on a row to seek to the corresponding segment."));
table->setEditTriggers(QAbstractItemView::NoEditTriggers);
table->setSelectionBehavior(QAbstractItemView::SelectRows);
table->setSelectionMode(QAbstractItemView::SingleSelection);
table->setHorizontalHeaderLabels({"", "rlog", "fcam", "ecam", "dcam", "qlog", "qcam"});
table->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents);
table->verticalHeader()->setVisible(false);
table->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
int row = 0;
for (const auto &[seg_num, seg] : replay->route().segments()) {
table->setItem(row, 0, new QTableWidgetItem(QString::number(seg_num)));
table->setItem(row, 1, new QTableWidgetItem(seg.rlog.empty() ? "--" : "Yes"));
table->setItem(row, 2, new QTableWidgetItem(seg.road_cam.empty() ? "--" : "Yes"));
table->setItem(row, 3, new QTableWidgetItem(seg.wide_road_cam.empty() ? "--" : "Yes"));
table->setItem(row, 4, new QTableWidgetItem(seg.driver_cam.empty() ? "--" : "Yes"));
table->setItem(row, 5, new QTableWidgetItem(seg.qlog.empty() ? "--" : "Yes"));
table->setItem(row, 6, new QTableWidgetItem(seg.qcamera.empty() ? "--" : "Yes"));
++row;
}
table->setMinimumWidth(table->horizontalHeader()->length() + table->verticalScrollBar()->sizeHint().width());
table->setMinimumHeight(table->rowHeight(0) * std::min(table->rowCount(), 13) + table->horizontalHeader()->height() + table->frameWidth() * 2);
connect(table, &QTableWidget::itemClicked, [](QTableWidgetItem *item) { can->seekTo(item->row() * 60.0); });
QVBoxLayout *layout = new QVBoxLayout(this);
layout->addWidget(table);
}
+8
View File
@@ -0,0 +1,8 @@
#pragma once
#include <QDialog>
class RouteInfoDlg : public QDialog {
Q_OBJECT
public:
RouteInfoDlg(QWidget *parent = nullptr);
};
+13 -2
View File
@@ -11,6 +11,8 @@
#include <QVBoxLayout>
#include <QtConcurrent>
#include "tools/cabana/tools/routeinfo.h"
const int MIN_VIDEO_HEIGHT = 100;
const int THUMBNAIL_MARGIN = 3;
@@ -100,9 +102,12 @@ void VideoWidget::createPlaybackController() {
if (!can->liveStreaming()) {
toolbar->addAction(utils::icon("repeat"), tr("Loop playback"), this, &VideoWidget::loopPlaybackClicked);
createSpeedDropdown(toolbar);
toolbar->addSeparator();
toolbar->addAction(utils::icon("info-circle"), tr("View route details"), this, &VideoWidget::showRouteInfo);
} else {
createSpeedDropdown(toolbar);
}
createSpeedDropdown(toolbar);
}
void VideoWidget::createSpeedDropdown(QToolBar *toolbar) {
@@ -230,6 +235,12 @@ void VideoWidget::showThumbnail(double seconds) {
slider->update();
}
void VideoWidget::showRouteInfo() {
RouteInfoDlg *route_info = new RouteInfoDlg(this);
route_info->setAttribute(Qt::WA_DeleteOnClose);
route_info->show();
}
bool VideoWidget::eventFilter(QObject *obj, QEvent *event) {
if (event->type() == QEvent::MouseMove) {
auto [min_sec, max_sec] = can->timeRange().value_or(std::make_pair(can->minSeconds(), can->maxSeconds()));
+1
View File
@@ -71,6 +71,7 @@ protected:
void createSpeedDropdown(QToolBar *toolbar);
void loopPlaybackClicked();
void vipcAvailableStreamsUpdated(std::set<VisionStreamType> streams);
void showRouteInfo();
StreamCameraView *cam_widget;
QAction *time_display_action = nullptr;
+89 -18
View File
@@ -5,6 +5,7 @@ import logging
import os
import platform
import shutil
import sys
import time
from argparse import ArgumentParser, ArgumentTypeError
from collections.abc import Sequence
@@ -15,8 +16,10 @@ from typing import Literal
from cereal.messaging import SubMaster
from openpilot.common.basedir import BASEDIR
from openpilot.common.params import Params, UnknownKeyName
from openpilot.common.prefix import OpenpilotPrefix
from openpilot.tools.lib.route import Route
from openpilot.tools.lib.logreader import LogReader
DEFAULT_OUTPUT = 'output.mp4'
DEMO_START = 90
@@ -28,6 +31,7 @@ RESOLUTION = '2160x1080'
SECONDS_TO_WARM = 2
PROC_WAIT_SECONDS = 30
OPENPILOT_FONT = str(Path(BASEDIR, 'selfdrive/assets/fonts/Inter-Regular.ttf').resolve())
REPLAY = str(Path(BASEDIR, 'tools/replay/replay').resolve())
UI = str(Path(BASEDIR, 'selfdrive/ui/ui').resolve())
@@ -60,18 +64,23 @@ def escape_ffmpeg_text(value: str):
return value
def get_meta_text(route: Route):
metadata = route.get_metadata()
origin_parts = metadata['git_remote'].split('/')
def get_logreader(route: Route):
return LogReader(route.qlog_paths()[0] if len(route.qlog_paths()) else route.name.canonical_name)
def get_meta_text(lr: LogReader, route: Route):
init_data = lr.first('initData')
car_params = lr.first('carParams')
origin_parts = init_data.gitRemote.split('/')
origin = origin_parts[3] if len(origin_parts) > 3 else 'unknown'
return ', '.join([
f"openpilot v{metadata['version']}",
f"route: {metadata['fullname']}",
f"car: {metadata['platform']}",
f"openpilot v{init_data.version}",
f"route: {route.name.canonical_name}",
f"car: {car_params.carFingerprint}",
f"origin: {origin}",
f"branch: {metadata['git_branch']}",
f"commit: {metadata['git_commit'][:7]}",
f"modified: {str(metadata['git_dirty']).lower()}",
f"branch: {init_data.gitBranch}",
f"commit: {init_data.gitCommit[:7]}",
f"modified: {str(init_data.dirty).lower()}",
])
@@ -112,6 +121,22 @@ def parse_args(parser: ArgumentParser):
return args
def populate_car_params(lr: LogReader):
init_data = lr.first('initData')
assert init_data is not None
params = Params()
entries = init_data.params.entries
for cp in entries:
key, value = cp.key, cp.value
try:
params.put(key, value)
except UnknownKeyName:
# forks of openpilot may have other Params keys configured. ignore these
logger.warning(f"unknown Params key '{key}', skipping")
logger.debug('persisted CarParams')
def start_proc(args: list[str], env: dict[str, str]):
return Popen(args, env=env, stdout=PIPE, stderr=PIPE)
@@ -155,8 +180,19 @@ def wait_for_frames(procs: list[Popen]):
check_for_failure(proc)
def clip(data_dir: str | None, quality: Literal['low', 'high'], prefix: str, route: Route, out: str, start: int, end: int, target_mb: int, title: str | None):
def clip(
data_dir: str | None,
quality: Literal['low', 'high'],
prefix: str,
route: Route,
out: str,
start: int,
end: int,
target_mb: int,
title: str | None,
):
logger.info(f'clipping route {route.name.canonical_name}, start={start} end={end} quality={quality} target_filesize={target_mb}MB')
lr = get_logreader(route)
begin_at = max(start - SECONDS_TO_WARM, 0)
duration = end - start
@@ -165,20 +201,40 @@ def clip(data_dir: str | None, quality: Literal['low', 'high'], prefix: str, rou
# TODO: evaluate creating fn that inspects /tmp/.X11-unix and creates unused display to avoid possibility of collision
display = f':{randint(99, 999)}'
meta_text = get_meta_text(route)
box_style = 'box=1:boxcolor=black@0.33:boxborderw=7'
meta_text = get_meta_text(lr, route)
overlays = [
f"drawtext=text='{escape_ffmpeg_text(meta_text)}':fontfile=Inter.tff:fontcolor=white:fontsize=18:box=1:boxcolor=black@0.33:boxborderw=7:x=(w-text_w)/2:y=5.5:enable='between(t,1,5)'"
# metadata overlay
f"drawtext=text='{escape_ffmpeg_text(meta_text)}':fontfile={OPENPILOT_FONT}:fontcolor=white:fontsize=15:{box_style}:x=(w-text_w)/2:y=5.5:enable='between(t,1,5)'",
# route time overlay
f"drawtext=text='%{{eif\\:floor(({start}+t)/60)\\:d\\:2}}\\:%{{eif\\:mod({start}+t\\,60)\\:d\\:2}}':fontfile={OPENPILOT_FONT}:fontcolor=white:fontsize=24:{box_style}:x=w-text_w-38:y=38"
]
if title:
overlays.append(f"drawtext=text='{escape_ffmpeg_text(title)}':fontfile=Inter.tff:fontcolor=white:fontsize=32:box=1:boxcolor=black@0.33:boxborderw=10:x=(w-text_w)/2:y=53")
overlays.append(f"drawtext=text='{escape_ffmpeg_text(title)}':fontfile={OPENPILOT_FONT}:fontcolor=white:fontsize=32:{box_style}:x=(w-text_w)/2:y=53")
ffmpeg_cmd = [
'ffmpeg', '-y', '-video_size', RESOLUTION, '-framerate', str(FRAMERATE), '-f', 'x11grab', '-draw_mouse', '0',
'-i', display, '-c:v', 'libx264', '-maxrate', f'{bit_rate_kbps}k', '-bufsize', f'{bit_rate_kbps*2}k', '-crf', '23',
'-filter:v', ','.join(overlays), '-preset', 'ultrafast', '-pix_fmt', 'yuv420p', '-movflags', '+faststart', '-f', 'mp4', '-t', str(duration), out
'ffmpeg', '-y',
'-video_size', RESOLUTION,
'-framerate', str(FRAMERATE),
'-f', 'x11grab',
'-rtbufsize', '100M',
'-draw_mouse', '0',
'-i', display,
'-c:v', 'libx264',
'-maxrate', f'{bit_rate_kbps}k',
'-bufsize', f'{bit_rate_kbps*2}k',
'-crf', '23',
'-filter:v', ','.join(overlays),
'-preset', 'ultrafast',
'-tune', 'zerolatency',
'-pix_fmt', 'yuv420p',
'-movflags', '+faststart',
'-f', 'mp4',
'-t', str(duration),
out,
]
replay_cmd = [REPLAY, '-c', '1', '-s', str(begin_at), '--prefix', prefix]
replay_cmd = [REPLAY, '--ecam', '-c', '1', '-s', str(begin_at), '--prefix', prefix]
if data_dir:
replay_cmd.extend(['--data_dir', data_dir])
if quality == 'low':
@@ -189,6 +245,8 @@ def clip(data_dir: str | None, quality: Literal['low', 'high'], prefix: str, rou
xvfb_cmd = ['Xvfb', display, '-terminate', '-screen', '0', f'{RESOLUTION}x{PIXEL_DEPTH}']
with OpenpilotPrefix(prefix, shared_download_cache=True):
populate_car_params(lr)
env = os.environ.copy()
env['DISPLAY'] = display
@@ -234,14 +292,27 @@ def main():
p.add_argument('-s', '--start', help='start clipping at <start> seconds', type=int)
p.add_argument('-t', '--title', help='overlay this title on the video (e.g. "Chill driving across the Golden Gate Bridge")', type=validate_title)
args = parse_args(p)
exit_code = 1
try:
clip(args.data_dir, args.quality, args.prefix, args.route, args.output, args.start, args.end, args.file_size, args.title)
clip(
data_dir=args.data_dir,
quality=args.quality,
prefix=args.prefix,
route=args.route,
out=args.output,
start=args.start,
end=args.end,
target_mb=args.file_size,
title=args.title,
)
exit_code = 0
except KeyboardInterrupt as e:
logger.exception('interrupted by user', exc_info=e)
except Exception as e:
logger.exception('encountered error', exc_info=e)
finally:
atexit._run_exitfuncs()
sys.exit(exit_code)
if __name__ == '__main__':
-1
View File
@@ -33,7 +33,6 @@ function install_ubuntu_common_requirements() {
git \
git-lfs \
ffmpeg \
fonts-inter \
libavformat-dev \
libavcodec-dev \
libavdevice-dev \
+1 -1
View File
@@ -10,7 +10,7 @@ from openpilot.common.params import Params
from openpilot.common.swaglog import cloudlog
LongCtrlState = car.CarControl.Actuators.LongControlState
MAX_LAT_ACCEL = 2.5
MAX_LAT_ACCEL = 3.0
def joystickd_thread():
+1 -2
View File
@@ -87,8 +87,8 @@ class GithubUtils:
def comment_on_pr(self, comment, pr_branch, commenter="", overwrite=False):
pr_number = self.get_pr_number(pr_branch)
data = f'{{"body": "{comment}"}}'
github_path = f'issues/{pr_number}/comments'
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:
@@ -96,7 +96,6 @@ class GithubUtils:
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
+27 -10
View File
@@ -1,12 +1,13 @@
import os
import re
import requests
from functools import cache
from urllib.parse import urlparse
from collections import defaultdict
from itertools import chain
from openpilot.tools.lib.auth_config import get_token
from openpilot.tools.lib.api import CommaApi
from openpilot.tools.lib.api import APIError, CommaApi
from openpilot.tools.lib.helpers import RE
QLOG_FILENAMES = ['qlog', 'qlog.bz2', 'qlog.zst']
@@ -19,15 +20,22 @@ ECAMERA_FILENAMES = ['ecamera.hevc']
class Route:
def __init__(self, name, data_dir=None):
self._metadata = None
self._name = RouteName(name)
self.files = None
self.metadata = None
if data_dir is not None:
self._segments = self._get_segments_local(data_dir)
else:
self._segments = self._get_segments_remote()
self.max_seg_number = self._segments[-1].name.segment_num
@property
def metadata(self):
if not self._metadata:
api = CommaApi(get_token())
self._metadata = api.get('v1/route/' + self.name.canonical_name)
return self._metadata
@property
def name(self):
return self._name
@@ -60,12 +68,6 @@ class Route:
qcamera_path_by_seg_num = {s.name.segment_num: s.qcamera_path for s in self._segments}
return [qcamera_path_by_seg_num.get(i, None) for i in range(self.max_seg_number + 1)]
def get_metadata(self):
if not self.metadata:
api = CommaApi(get_token())
self.metadata = api.get('v1/route/' + self.name.canonical_name)
return self.metadata
# TODO: refactor this, it's super repetitive
def _get_segments_remote(self):
api = CommaApi(get_token())
@@ -85,6 +87,7 @@ class Route:
url if fn in DCAMERA_FILENAMES else segments[segment_name].dcamera_path,
url if fn in ECAMERA_FILENAMES else segments[segment_name].ecamera_path,
url if fn in QCAMERA_FILENAMES else segments[segment_name].qcamera_path,
self.metadata['url'],
)
else:
segments[segment_name] = Segment(
@@ -95,6 +98,7 @@ class Route:
url if fn in DCAMERA_FILENAMES else None,
url if fn in ECAMERA_FILENAMES else None,
url if fn in QCAMERA_FILENAMES else None,
self.metadata['url'],
)
return sorted(segments.values(), key=lambda seg: seg.name.segment_num)
@@ -160,7 +164,7 @@ class Route:
except StopIteration:
qcamera_path = None
segments.append(Segment(segment, log_path, qlog_path, camera_path, dcamera_path, ecamera_path, qcamera_path))
segments.append(Segment(segment, log_path, qlog_path, camera_path, dcamera_path, ecamera_path, qcamera_path, self.metadata['url']))
if len(segments) == 0:
raise ValueError(f'Could not find segments for route {self.name.canonical_name} in data directory {data_dir}')
@@ -168,8 +172,10 @@ class Route:
class Segment:
def __init__(self, name, log_path, qlog_path, camera_path, dcamera_path, ecamera_path, qcamera_path):
def __init__(self, name, log_path, qlog_path, camera_path, dcamera_path, ecamera_path, qcamera_path, url):
self._events = None
self._name = SegmentName(name)
self.url = f'{url}/{self._name.segment_num}'
self.log_path = log_path
self.qlog_path = qlog_path
self.camera_path = camera_path
@@ -181,6 +187,17 @@ class Segment:
def name(self):
return self._name
@property
def events(self):
if not self._events:
try:
resp = requests.get(f'{self.url}/events.json')
resp.raise_for_status()
self._events = resp.json()
except Exception as e:
raise APIError(f'error getting events for segment {self._name}') from e
return self._events
class RouteName:
def __init__(self, name_str: str):
+8 -1
View File
@@ -254,7 +254,7 @@ function op_setup() {
function op_auth() {
op_before_cmd
op_run_command tools/lib/auth.py
op_run_command tools/lib/auth.py "$@"
}
function op_activate_venv() {
@@ -293,6 +293,11 @@ function op_check() {
unset VERBOSE
}
function op_esim() {
op_before_cmd
op_run_command system/hardware/esim.py "$@"
}
function op_build() {
CDIR=$(pwd)
op_before_cmd
@@ -392,6 +397,7 @@ function op_default() {
echo -e "${BOLD}${UNDERLINE}Commands [System]:${NC}"
echo -e " ${BOLD}auth${NC} Authenticate yourself for API use"
echo -e " ${BOLD}check${NC} Check the development environment (git, os, python) to start using openpilot"
echo -e " ${BOLD}esim${NC} Manage eSIM profiles on your comma device"
echo -e " ${BOLD}venv${NC} Activate the python virtual environment"
echo -e " ${BOLD}setup${NC} Install openpilot dependencies"
echo -e " ${BOLD}build${NC} Run the openpilot build system in the current working directory"
@@ -448,6 +454,7 @@ function _op() {
auth ) shift 1; op_auth "$@" ;;
venv ) shift 1; op_venv "$@" ;;
check ) shift 1; op_check "$@" ;;
esim ) shift 1; op_esim "$@" ;;
setup ) shift 1; op_setup "$@" ;;
build ) shift 1; op_build "$@" ;;
juggle ) shift 1; op_juggle "$@" ;;
+1 -1
View File
@@ -18,6 +18,6 @@ if __name__ == "__main__":
params.put_bool("SshEnabled", True)
params.put("GithubSshKeys", keys.text)
params.put("GithubUsername", username)
print("Setup ssh keys successfully")
print("Set up ssh keys successfully")
else:
print("Error getting public keys from github")