mirror of
https://github.com/dragonpilot/dragonpilot.git
synced 2026-08-21 08:03:42 +08:00
openpilot v0.9.9 release (#35334)
* openpilot v0.9.9 release date: 2025-06-05T19:54:08 master commit: 8aadf02b2fd91f4e1285e18c2c7feb32d93b66f5 * AGNOS 12.4 (#35558) agnos12.4 --------- Co-authored-by: Vehicle Researcher <user@comma.ai> Co-authored-by: Maxime Desroches <desroches.maxime@gmail.com>
This commit is contained in:
@@ -1,8 +0,0 @@
|
||||
#!/usr/bin/env expect
|
||||
spawn adb shell
|
||||
expect "#"
|
||||
send "cd data/openpilot\r"
|
||||
send "export TERM=xterm-256color\r"
|
||||
send "su comma\r"
|
||||
send "clear\r"
|
||||
interact
|
||||
Executable
+17
@@ -0,0 +1,17 @@
|
||||
#!/usr/bin/env python3
|
||||
import sys
|
||||
from openpilot.tools.lib.logreader import LogReader
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) != 2:
|
||||
print("Usage: python auto_source.py <log_path>")
|
||||
sys.exit(1)
|
||||
|
||||
log_path = sys.argv[1]
|
||||
lr = LogReader(log_path, sort_by_time=True)
|
||||
print("\n".join(lr.logreader_identifiers))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -12,6 +12,8 @@ Options:
|
||||
-h, --help Displays help on commandline options.
|
||||
--help-all Displays help including Qt specific options.
|
||||
--demo use a demo route instead of providing your own
|
||||
--auto Auto load the route from the best available source (no video):
|
||||
internal, openpilotci, comma_api, car_segments, testing_closet
|
||||
--qcam load qcamera
|
||||
--ecam load wide road camera
|
||||
--msgq read can messages from msgq
|
||||
|
||||
@@ -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'):
|
||||
|
||||
@@ -23,6 +23,7 @@ int main(int argc, char *argv[]) {
|
||||
cmd_parser.addHelpOption();
|
||||
cmd_parser.addPositionalArgument("route", "the drive to replay. find your drives at connect.comma.ai");
|
||||
cmd_parser.addOption({"demo", "use a demo route instead of providing your own"});
|
||||
cmd_parser.addOption({"auto", "Auto load the route from the best available source (no video): internal, openpilotci, comma_api, car_segments, testing_closet"});
|
||||
cmd_parser.addOption({"qcam", "load qcamera"});
|
||||
cmd_parser.addOption({"ecam", "load wide road camera"});
|
||||
cmd_parser.addOption({"dcam", "load driver camera"});
|
||||
@@ -69,7 +70,8 @@ int main(int argc, char *argv[]) {
|
||||
}
|
||||
if (!route.isEmpty()) {
|
||||
auto replay_stream = std::make_unique<ReplayStream>(&app);
|
||||
if (!replay_stream->loadRoute(route, cmd_parser.value("data_dir"), replay_flags)) {
|
||||
bool auto_source = cmd_parser.isSet("auto");
|
||||
if (!replay_stream->loadRoute(route, cmd_parser.value("data_dir"), replay_flags, auto_source)) {
|
||||
return 0;
|
||||
}
|
||||
stream = replay_stream.release();
|
||||
|
||||
+26
-15
@@ -180,25 +180,36 @@ bool cabana::Signal::operator==(const cabana::Signal &other) const {
|
||||
// helper functions
|
||||
|
||||
double get_raw_value(const uint8_t *data, size_t data_size, const cabana::Signal &sig) {
|
||||
int64_t val = 0;
|
||||
const int msb_byte = sig.msb / 8;
|
||||
if (msb_byte >= (int)data_size) return 0;
|
||||
|
||||
int i = sig.msb / 8;
|
||||
int bits = sig.size;
|
||||
while (i >= 0 && i < data_size && bits > 0) {
|
||||
int lsb = (int)(sig.lsb / 8) == i ? sig.lsb : i * 8;
|
||||
int msb = (int)(sig.msb / 8) == i ? sig.msb : (i + 1) * 8 - 1;
|
||||
int size = msb - lsb + 1;
|
||||
const int lsb_byte = sig.lsb / 8;
|
||||
uint64_t val = 0;
|
||||
|
||||
uint64_t d = (data[i] >> (lsb - (i * 8))) & ((1ULL << size) - 1);
|
||||
val |= d << (bits - size);
|
||||
|
||||
bits -= size;
|
||||
i = sig.is_little_endian ? i - 1 : i + 1;
|
||||
// Fast path: signal fits in a single byte
|
||||
if (msb_byte == lsb_byte) {
|
||||
val = (data[msb_byte] >> (sig.lsb & 7)) & ((1ULL << sig.size) - 1);
|
||||
} else {
|
||||
// Multi-byte case: signal spans across multiple bytes
|
||||
int bits = sig.size;
|
||||
int i = msb_byte;
|
||||
const int step = sig.is_little_endian ? -1 : 1;
|
||||
while (i >= 0 && i < (int)data_size && bits > 0) {
|
||||
const int msb = (i == msb_byte) ? sig.msb & 7 : 7;
|
||||
const int lsb = (i == lsb_byte) ? sig.lsb & 7 : 0;
|
||||
const int nbits = msb - lsb + 1;
|
||||
val = (val << nbits) | ((data[i] >> lsb) & ((1ULL << nbits) - 1));
|
||||
bits -= nbits;
|
||||
i += step;
|
||||
}
|
||||
}
|
||||
if (sig.is_signed) {
|
||||
val -= ((val >> (sig.size - 1)) & 0x1) ? (1ULL << sig.size) : 0;
|
||||
|
||||
// Sign extension (if needed)
|
||||
if (sig.is_signed && (val & (1ULL << (sig.size - 1)))) {
|
||||
val |= ~((1ULL << sig.size) - 1);
|
||||
}
|
||||
return val * sig.factor + sig.offset;
|
||||
|
||||
return static_cast<int64_t>(val) * sig.factor + sig.offset;
|
||||
}
|
||||
|
||||
void updateMsbLsb(cabana::Signal &s) {
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include <QPushButton>
|
||||
|
||||
#include "common/timing.h"
|
||||
#include "common/util.h"
|
||||
#include "tools/cabana/streams/routes.h"
|
||||
|
||||
ReplayStream::ReplayStream(QObject *parent) : AbstractStream(parent) {
|
||||
@@ -45,9 +46,9 @@ void ReplayStream::mergeSegments() {
|
||||
}
|
||||
}
|
||||
|
||||
bool ReplayStream::loadRoute(const QString &route, const QString &data_dir, uint32_t replay_flags) {
|
||||
bool ReplayStream::loadRoute(const QString &route, const QString &data_dir, uint32_t replay_flags, bool auto_source) {
|
||||
replay.reset(new Replay(route.toStdString(), {"can", "roadEncodeIdx", "driverEncodeIdx", "wideRoadEncodeIdx", "carParams"},
|
||||
{}, nullptr, replay_flags, data_dir.toStdString()));
|
||||
{}, nullptr, replay_flags, data_dir.toStdString(), auto_source));
|
||||
replay->setSegmentCacheLimit(settings.max_cached_minutes);
|
||||
replay->installEventFilter([this](const Event *event) { return eventFilter(event); });
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ class ReplayStream : public AbstractStream {
|
||||
public:
|
||||
ReplayStream(QObject *parent);
|
||||
void start() override { replay->start(); }
|
||||
bool loadRoute(const QString &route, const QString &data_dir, uint32_t replay_flags = REPLAY_FLAG_NONE);
|
||||
bool loadRoute(const QString &route, const QString &data_dir, uint32_t replay_flags = REPLAY_FLAG_NONE, bool auto_source = false);
|
||||
bool eventFilter(const Event *event);
|
||||
void seekTo(double ts) override { replay->seekTo(std::max(double(0), ts), false); }
|
||||
bool liveStreaming() const override { return false; }
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
#pragma once
|
||||
#include <QDialog>
|
||||
|
||||
class RouteInfoDlg : public QDialog {
|
||||
Q_OBJECT
|
||||
public:
|
||||
RouteInfoDlg(QWidget *parent = nullptr);
|
||||
};
|
||||
@@ -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()));
|
||||
|
||||
@@ -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;
|
||||
|
||||
Executable
+320
@@ -0,0 +1,320 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import atexit
|
||||
import logging
|
||||
import os
|
||||
import platform
|
||||
import shutil
|
||||
import sys
|
||||
import time
|
||||
from argparse import ArgumentParser, ArgumentTypeError
|
||||
from collections.abc import Sequence
|
||||
from pathlib import Path
|
||||
from random import randint
|
||||
from subprocess import Popen, PIPE
|
||||
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
|
||||
DEMO_END = 105
|
||||
DEMO_ROUTE = 'a2a0ccea32023010/2023-07-27--13-01-19'
|
||||
FRAMERATE = 20
|
||||
PIXEL_DEPTH = '24'
|
||||
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())
|
||||
|
||||
logger = logging.getLogger('clip.py')
|
||||
|
||||
|
||||
def check_for_failure(proc: Popen):
|
||||
exit_code = proc.poll()
|
||||
if exit_code is not None and exit_code != 0:
|
||||
cmd = str(proc.args)
|
||||
if isinstance(proc.args, str):
|
||||
cmd = proc.args
|
||||
elif isinstance(proc.args, Sequence):
|
||||
cmd = str(proc.args[0])
|
||||
msg = f'{cmd} failed, exit code {exit_code}'
|
||||
logger.error(msg)
|
||||
stdout, stderr = proc.communicate()
|
||||
if stdout:
|
||||
logger.error(stdout.decode())
|
||||
if stderr:
|
||||
logger.error(stderr.decode())
|
||||
raise ChildProcessError(msg)
|
||||
|
||||
|
||||
def escape_ffmpeg_text(value: str):
|
||||
special_chars = {',': '\\,', ':': '\\:', '=': '\\=', '[': '\\[', ']': '\\]'}
|
||||
value = value.replace('\\', '\\\\\\\\\\\\\\\\')
|
||||
for char, escaped in special_chars.items():
|
||||
value = value.replace(char, escaped)
|
||||
return value
|
||||
|
||||
|
||||
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{init_data.version}",
|
||||
f"route: {route.name.canonical_name}",
|
||||
f"car: {car_params.carFingerprint}",
|
||||
f"origin: {origin}",
|
||||
f"branch: {init_data.gitBranch}",
|
||||
f"commit: {init_data.gitCommit[:7]}",
|
||||
f"modified: {str(init_data.dirty).lower()}",
|
||||
])
|
||||
|
||||
|
||||
def parse_args(parser: ArgumentParser):
|
||||
args = parser.parse_args()
|
||||
if args.demo:
|
||||
args.route = DEMO_ROUTE
|
||||
if args.start is None or args.end is None:
|
||||
args.start = DEMO_START
|
||||
args.end = DEMO_END
|
||||
elif args.route.count('/') == 1:
|
||||
if args.start is None or args.end is None:
|
||||
parser.error('must provide both start and end if timing is not in the route ID')
|
||||
elif args.route.count('/') == 3:
|
||||
if args.start is not None or args.end is not None:
|
||||
parser.error('don\'t provide timing when including it in the route ID')
|
||||
parts = args.route.split('/')
|
||||
args.route = '/'.join(parts[:2])
|
||||
args.start = int(parts[2])
|
||||
args.end = int(parts[3])
|
||||
if args.end <= args.start:
|
||||
parser.error(f'end ({args.end}) must be greater than start ({args.start})')
|
||||
if args.start < SECONDS_TO_WARM:
|
||||
parser.error(f'start must be greater than {SECONDS_TO_WARM}s to allow the UI time to warm up')
|
||||
|
||||
try:
|
||||
args.route = Route(args.route, data_dir=args.data_dir)
|
||||
except Exception as e:
|
||||
parser.error(f'failed to get route: {e}')
|
||||
|
||||
# FIXME: length isn't exactly max segment seconds, simplify to replay exiting at end of data
|
||||
length = round(args.route.max_seg_number * 60)
|
||||
if args.start >= length:
|
||||
parser.error(f'start ({args.start}s) cannot be after end of route ({length}s)')
|
||||
if args.end > length:
|
||||
parser.error(f'end ({args.end}s) cannot be after end of route ({length}s)')
|
||||
|
||||
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)
|
||||
|
||||
|
||||
def validate_env(parser: ArgumentParser):
|
||||
if platform.system() not in ['Linux']:
|
||||
parser.exit(1, f'clip.py: error: {platform.system()} is not a supported operating system\n')
|
||||
for proc in ['Xvfb', 'ffmpeg']:
|
||||
if shutil.which(proc) is None:
|
||||
parser.exit(1, f'clip.py: error: missing {proc} command, is it installed?\n')
|
||||
for proc in [REPLAY, UI]:
|
||||
if shutil.which(proc) is None:
|
||||
parser.exit(1, f'clip.py: error: missing {proc} command, did you build openpilot yet?\n')
|
||||
|
||||
|
||||
def validate_output_file(output_file: str):
|
||||
if not output_file.endswith('.mp4'):
|
||||
raise ArgumentTypeError('output must be an mp4')
|
||||
return output_file
|
||||
|
||||
|
||||
def validate_route(route: str):
|
||||
if route.count('/') not in (1, 3):
|
||||
raise ArgumentTypeError(f'route must include or exclude timing, example: {DEMO_ROUTE}')
|
||||
return route
|
||||
|
||||
|
||||
def validate_title(title: str):
|
||||
if len(title) > 80:
|
||||
raise ArgumentTypeError('title must be no longer than 80 chars')
|
||||
return title
|
||||
|
||||
|
||||
def wait_for_frames(procs: list[Popen]):
|
||||
sm = SubMaster(['uiDebug'])
|
||||
no_frames_drawn = True
|
||||
while no_frames_drawn:
|
||||
sm.update()
|
||||
no_frames_drawn = sm['uiDebug'].drawTimeMillis == 0.
|
||||
for proc in procs:
|
||||
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,
|
||||
):
|
||||
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
|
||||
bit_rate_kbps = int(round(target_mb * 8 * 1024 * 1024 / duration / 1000))
|
||||
|
||||
# TODO: evaluate creating fn that inspects /tmp/.X11-unix and creates unused display to avoid possibility of collision
|
||||
display = f':{randint(99, 999)}'
|
||||
|
||||
box_style = 'box=1:boxcolor=black@0.33:boxborderw=7'
|
||||
meta_text = get_meta_text(lr, route)
|
||||
overlays = [
|
||||
# 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={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',
|
||||
'-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, '--ecam', '-c', '1', '-s', str(begin_at), '--prefix', prefix]
|
||||
if data_dir:
|
||||
replay_cmd.extend(['--data_dir', data_dir])
|
||||
if quality == 'low':
|
||||
replay_cmd.append('--qcam')
|
||||
replay_cmd.append(route.name.canonical_name)
|
||||
|
||||
ui_cmd = [UI, '-platform', 'xcb']
|
||||
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
|
||||
|
||||
xvfb_proc = start_proc(xvfb_cmd, env)
|
||||
atexit.register(lambda: xvfb_proc.terminate())
|
||||
ui_proc = start_proc(ui_cmd, env)
|
||||
atexit.register(lambda: ui_proc.terminate())
|
||||
replay_proc = start_proc(replay_cmd, env)
|
||||
atexit.register(lambda: replay_proc.terminate())
|
||||
procs = [replay_proc, ui_proc, xvfb_proc]
|
||||
|
||||
logger.info('waiting for replay to begin (loading segments, may take a while)...')
|
||||
wait_for_frames(procs)
|
||||
|
||||
logger.debug(f'letting UI warm up ({SECONDS_TO_WARM}s)...')
|
||||
time.sleep(SECONDS_TO_WARM)
|
||||
for proc in procs:
|
||||
check_for_failure(proc)
|
||||
|
||||
ffmpeg_proc = start_proc(ffmpeg_cmd, env)
|
||||
procs.append(ffmpeg_proc)
|
||||
atexit.register(lambda: ffmpeg_proc.terminate())
|
||||
|
||||
logger.info(f'recording in progress ({duration}s)...')
|
||||
ffmpeg_proc.wait(duration + PROC_WAIT_SECONDS)
|
||||
for proc in procs:
|
||||
check_for_failure(proc)
|
||||
logger.info(f'recording complete: {Path(out).resolve()}')
|
||||
|
||||
|
||||
def main():
|
||||
p = ArgumentParser(prog='clip.py', description='clip your openpilot route.', epilog='comma.ai')
|
||||
validate_env(p)
|
||||
route_group = p.add_mutually_exclusive_group(required=True)
|
||||
route_group.add_argument('route', nargs='?', type=validate_route, help=f'The route (e.g. {DEMO_ROUTE} or {DEMO_ROUTE}/{DEMO_START}/{DEMO_END})')
|
||||
route_group.add_argument('--demo', help='use the demo route', action='store_true')
|
||||
p.add_argument('-d', '--data-dir', help='local directory where route data is stored')
|
||||
p.add_argument('-e', '--end', help='stop clipping at <end> seconds', type=int)
|
||||
p.add_argument('-f', '--file-size', help='target file size (Discord/GitHub support max 10MB, default is 9MB)', type=float, default=9.)
|
||||
p.add_argument('-o', '--output', help='output clip to (.mp4)', type=validate_output_file, default=DEFAULT_OUTPUT)
|
||||
p.add_argument('-p', '--prefix', help='openpilot prefix', default=f'clip_{randint(100, 99999)}')
|
||||
p.add_argument('-q', '--quality', help='quality of camera (low = qcam, high = hevc)', choices=['low', 'high'], default='high')
|
||||
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(
|
||||
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__':
|
||||
logging.basicConfig(level=logging.INFO, format='%(asctime)s %(name)s %(levelname)s\t%(message)s')
|
||||
main()
|
||||
@@ -63,7 +63,8 @@ function install_ubuntu_common_requirements() {
|
||||
libqt5svg5-dev \
|
||||
libqt5serialbus5-dev \
|
||||
libqt5x11extras5-dev \
|
||||
libqt5opengl5-dev
|
||||
libqt5opengl5-dev \
|
||||
xvfb
|
||||
}
|
||||
|
||||
# Install Ubuntu 24.04 LTS packages
|
||||
|
||||
@@ -28,7 +28,7 @@ class Keyboard:
|
||||
key = self.kb.getch().lower()
|
||||
self.cancel = False
|
||||
if key == 'r':
|
||||
self.axes_values = {ax: 0. for ax in self.axes_values}
|
||||
self.axes_values = dict.fromkeys(self.axes_values, 0.)
|
||||
elif key == 'c':
|
||||
self.cancel = True
|
||||
elif key in self.axes_map:
|
||||
@@ -65,7 +65,7 @@ class Joystick:
|
||||
try:
|
||||
joystick_event = get_gamepad()[0]
|
||||
except (OSError, UnpluggedError):
|
||||
self.axes_values = {ax: 0. for ax in self.axes_values}
|
||||
self.axes_values = dict.fromkeys(self.axes_values, 0.)
|
||||
return False
|
||||
|
||||
event = (joystick_event.code, joystick_event.state)
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -209,7 +209,7 @@ class BaseFrameReader:
|
||||
def close(self):
|
||||
pass
|
||||
|
||||
def get(self, num, count=1, pix_fmt="yuv420p"):
|
||||
def get(self, num, count=1, pix_fmt="rgb24"):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
@@ -497,7 +497,7 @@ class GOPFrameReader(BaseFrameReader):
|
||||
|
||||
return self.frame_cache[(num, pix_fmt)]
|
||||
|
||||
def get(self, num, count=1, pix_fmt="yuv420p"):
|
||||
def get(self, num, count=1, pix_fmt="rgb24"):
|
||||
assert self.frame_count is not None
|
||||
|
||||
if num + count > self.frame_count:
|
||||
@@ -523,37 +523,15 @@ class StreamFrameReader(StreamGOPReader, GOPFrameReader):
|
||||
GOPFrameReader.__init__(self, readahead, readbehind)
|
||||
|
||||
|
||||
def GOPFrameIterator(gop_reader, pix_fmt):
|
||||
def GOPFrameIterator(gop_reader, pix_fmt='rgb24'):
|
||||
dec = VideoStreamDecompressor(gop_reader.fn, gop_reader.vid_fmt, gop_reader.w, gop_reader.h, pix_fmt)
|
||||
yield from dec.read()
|
||||
|
||||
|
||||
def FrameIterator(fn, pix_fmt, **kwargs):
|
||||
def FrameIterator(fn, pix_fmt='rgb24', **kwargs):
|
||||
fr = FrameReader(fn, **kwargs)
|
||||
if isinstance(fr, GOPReader):
|
||||
yield from GOPFrameIterator(fr, pix_fmt)
|
||||
else:
|
||||
for i in range(fr.frame_count):
|
||||
yield fr.get(i, pix_fmt=pix_fmt)[0]
|
||||
|
||||
|
||||
class NumpyFrameReader:
|
||||
def __init__(self, name, w, h, cache_size):
|
||||
self.name = name
|
||||
self.pos = -1
|
||||
self.frames = None
|
||||
self.w = w
|
||||
self.h = h
|
||||
self.cache_size = cache_size
|
||||
|
||||
def close(self):
|
||||
pass
|
||||
|
||||
def get(self, num, count=1, pix_fmt="nv12"):
|
||||
num -= 1
|
||||
q = num // self.cache_size
|
||||
if q != self.pos:
|
||||
del self.frames
|
||||
self.pos = q
|
||||
self.frames = np.load(f'{self.name}_{self.pos}.npy')
|
||||
return [self.frames[num % self.cache_size]]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -300,11 +300,11 @@ class LogReader:
|
||||
def _run_on_segment(self, func, i):
|
||||
return func(self._get_lr(i))
|
||||
|
||||
def run_across_segments(self, num_processes, func, desc=None):
|
||||
def run_across_segments(self, num_processes, func, disable_tqdm=False, desc=None):
|
||||
with multiprocessing.Pool(num_processes) as pool:
|
||||
ret = []
|
||||
num_segs = len(self.logreader_identifiers)
|
||||
for p in tqdm.tqdm(pool.imap(partial(self._run_on_segment, func), range(num_segs)), total=num_segs, desc=desc):
|
||||
for p in tqdm.tqdm(pool.imap(partial(self._run_on_segment, func), range(num_segs)), total=num_segs, disable=disable_tqdm, desc=desc):
|
||||
ret.extend(p)
|
||||
return ret
|
||||
|
||||
|
||||
+27
-3
@@ -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,6 +20,7 @@ ECAMERA_FILENAMES = ['ecamera.hevc']
|
||||
|
||||
class Route:
|
||||
def __init__(self, name, data_dir=None):
|
||||
self._metadata = None
|
||||
self._name = RouteName(name)
|
||||
self.files = None
|
||||
if data_dir is not None:
|
||||
@@ -27,6 +29,13 @@ class Route:
|
||||
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
|
||||
@@ -78,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(
|
||||
@@ -88,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)
|
||||
@@ -153,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}')
|
||||
@@ -161,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
|
||||
@@ -174,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):
|
||||
|
||||
@@ -16,8 +16,7 @@ CHUNK_SIZE = 1000 * K
|
||||
logging.getLogger("urllib3").setLevel(logging.WARNING)
|
||||
|
||||
def hash_256(link: str) -> str:
|
||||
hsh = str(sha256((link.split("?")[0]).encode('utf-8')).hexdigest())
|
||||
return hsh
|
||||
return sha256((link.split("?")[0]).encode('utf-8')).hexdigest()
|
||||
|
||||
|
||||
class URLFileException(Exception):
|
||||
|
||||
@@ -28,6 +28,8 @@ if [[ $(command -v brew) == "" ]]; then
|
||||
echo 'eval "$(/opt/homebrew/bin/brew shellenv)"' >> $RC_FILE
|
||||
eval "$(/opt/homebrew/bin/brew shellenv)"
|
||||
fi
|
||||
else
|
||||
brew up
|
||||
fi
|
||||
|
||||
brew bundle --file=- <<-EOS
|
||||
|
||||
+25
-7
@@ -52,6 +52,7 @@ function op_run_command() {
|
||||
# be default, assume openpilot dir is in current directory
|
||||
OPENPILOT_ROOT=$(pwd)
|
||||
function op_get_openpilot_dir() {
|
||||
# First try traversing up the directory tree
|
||||
while [[ "$OPENPILOT_ROOT" != '/' ]];
|
||||
do
|
||||
if find "$OPENPILOT_ROOT/launch_openpilot.sh" -maxdepth 1 -mindepth 1 &> /dev/null; then
|
||||
@@ -59,6 +60,14 @@ function op_get_openpilot_dir() {
|
||||
fi
|
||||
OPENPILOT_ROOT="$(readlink -f "$OPENPILOT_ROOT/"..)"
|
||||
done
|
||||
|
||||
# Fallback to hardcoded directories if not found
|
||||
for dir in "$HOME/openpilot" "/data/openpilot"; do
|
||||
if [[ -f "$dir/launch_openpilot.sh" ]]; then
|
||||
OPENPILOT_ROOT="$dir"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
function op_install_post_commit() {
|
||||
@@ -245,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() {
|
||||
@@ -275,7 +284,7 @@ function op_venv() {
|
||||
|
||||
function op_adb() {
|
||||
op_before_cmd
|
||||
op_run_command tools/adb_shell.sh
|
||||
op_run_command tools/scripts/adb_ssh.sh
|
||||
}
|
||||
|
||||
function op_check() {
|
||||
@@ -284,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
|
||||
@@ -328,6 +342,11 @@ function op_sim() {
|
||||
op_run_command exec tools/sim/launch_openpilot.sh
|
||||
}
|
||||
|
||||
function op_clip() {
|
||||
op_before_cmd
|
||||
op_run_command tools/clip/run.py $@
|
||||
}
|
||||
|
||||
function op_switch() {
|
||||
REMOTE="origin"
|
||||
if [ "$#" -gt 1 ]; then
|
||||
@@ -373,16 +392,12 @@ function op_default() {
|
||||
echo " op is only a wrapper for existing scripts, tools, and commands."
|
||||
echo " op will always show you what it will run on your system."
|
||||
echo ""
|
||||
echo " op will try to find your openpilot directory in the following order:"
|
||||
echo " 1: use the directory specified with the --dir option"
|
||||
echo " 2: use the current working directory"
|
||||
echo " 3: go up the file tree non-recursively"
|
||||
echo ""
|
||||
echo -e "${BOLD}${UNDERLINE}Usage:${NC} op [OPTIONS] <COMMAND>"
|
||||
echo ""
|
||||
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"
|
||||
@@ -395,6 +410,7 @@ function op_default() {
|
||||
echo -e " ${BOLD}juggle${NC} Run PlotJuggler"
|
||||
echo -e " ${BOLD}replay${NC} Run Replay"
|
||||
echo -e " ${BOLD}cabana${NC} Run Cabana"
|
||||
echo -e " ${BOLD}clip${NC} Run clip (linux only)"
|
||||
echo -e " ${BOLD}adb${NC} Run adb shell"
|
||||
echo ""
|
||||
echo -e "${BOLD}${UNDERLINE}Commands [Testing]:${NC}"
|
||||
@@ -438,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 "$@" ;;
|
||||
@@ -445,6 +462,7 @@ function _op() {
|
||||
lint ) shift 1; op_lint "$@" ;;
|
||||
test ) shift 1; op_test "$@" ;;
|
||||
replay ) shift 1; op_replay "$@" ;;
|
||||
clip ) shift 1; op_clip "$@" ;;
|
||||
sim ) shift 1; op_sim "$@" ;;
|
||||
install ) shift 1; op_install "$@" ;;
|
||||
switch ) shift 1; op_switch "$@" ;;
|
||||
|
||||
@@ -61,13 +61,15 @@ def start_juggler(fn=None, dbc=None, layout=None, route_or_segment_name=None, pl
|
||||
env["BASEDIR"] = BASEDIR
|
||||
env["PATH"] = f"{INSTALL_DIR}:{os.getenv('PATH', '')}"
|
||||
if dbc:
|
||||
if os.path.exists(dbc):
|
||||
dbc = os.path.abspath(dbc)
|
||||
env["DBC_NAME"] = dbc
|
||||
|
||||
extra_args = ""
|
||||
if fn is not None:
|
||||
extra_args += f" -d {fn}"
|
||||
extra_args += f" -d {os.path.abspath(fn)}"
|
||||
if layout is not None:
|
||||
extra_args += f" -l {layout}"
|
||||
extra_args += f" -l {os.path.abspath(layout)}"
|
||||
if route_or_segment_name is not None:
|
||||
extra_args += f" --window_title \"{route_or_segment_name}{f' ({platform})' if platform is not None else ''}\""
|
||||
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
<?xml version='1.0' encoding='UTF-8'?>
|
||||
<root>
|
||||
<tabbed_widget parent="main_window" name="Main Window">
|
||||
<Tab tab_name="tab1" containers="1">
|
||||
<Container>
|
||||
<DockSplitter orientation="-" sizes="0.24977;0.250689;0.24977;0.24977" count="4">
|
||||
<DockArea name="...">
|
||||
<plot flip_y="false" flip_x="false" style="Lines" mode="TimeSeries">
|
||||
<range left="0.000000" right="1678.753571" bottom="-0.025000" top="1.025000"/>
|
||||
<limitY/>
|
||||
<curve name="/gpsLocationExternal/hasFix" color="#1f77b4"/>
|
||||
</plot>
|
||||
</DockArea>
|
||||
<DockArea name="...">
|
||||
<plot flip_y="false" flip_x="false" style="Lines" mode="TimeSeries">
|
||||
<range left="0.000000" right="1678.753571" bottom="-0.425000" top="17.425000"/>
|
||||
<limitY/>
|
||||
<curve name="/gpsLocationExternal/satelliteCount" color="#d62728"/>
|
||||
</plot>
|
||||
</DockArea>
|
||||
<DockArea name="...">
|
||||
<plot flip_y="false" flip_x="false" style="Lines" mode="TimeSeries">
|
||||
<range left="0.000000" right="1678.753571" bottom="0.000000" top="3.000000"/>
|
||||
<limitY max="3" min="0"/>
|
||||
<curve name="/gpsLocationExternal/horizontalAccuracy" color="#1ac938"/>
|
||||
</plot>
|
||||
</DockArea>
|
||||
<DockArea name="...">
|
||||
<plot flip_y="false" flip_x="false" style="Lines" mode="TimeSeries">
|
||||
<range left="0.000000" right="1678.753571" bottom="-17.262000" top="766.374004"/>
|
||||
<limitY/>
|
||||
<curve name="/gpsLocationExternal/horizontalAccuracy" color="#1ac938"/>
|
||||
</plot>
|
||||
</DockArea>
|
||||
</DockSplitter>
|
||||
</Container>
|
||||
</Tab>
|
||||
<currentTabIndex index="0"/>
|
||||
</tabbed_widget>
|
||||
<use_relative_time_offset enabled="1"/>
|
||||
<!-- - - - - - - - - - - - - - - -->
|
||||
<!-- - - - - - - - - - - - - - - -->
|
||||
<Plugins>
|
||||
<plugin ID="DataLoad CSV">
|
||||
<default time_axis="" delimiter="0"/>
|
||||
</plugin>
|
||||
<plugin ID="DataLoad Rlog"/>
|
||||
<plugin ID="DataLoad ULog"/>
|
||||
<plugin ID="Cereal Subscriber"/>
|
||||
<plugin ID="UDP Server"/>
|
||||
<plugin ID="WebSocket Server"/>
|
||||
<plugin ID="ZMQ Subscriber"/>
|
||||
<plugin ID="Fast Fourier Transform"/>
|
||||
<plugin ID="Quaternion to RPY"/>
|
||||
<plugin ID="CSV Exporter"/>
|
||||
</Plugins>
|
||||
<!-- - - - - - - - - - - - - - - -->
|
||||
<customMathEquations/>
|
||||
<snippets/>
|
||||
<!-- - - - - - - - - - - - - - - -->
|
||||
</root>
|
||||
|
||||
@@ -150,44 +150,44 @@
|
||||
<plot flip_x="false" style="Lines" flip_y="false" mode="TimeSeries">
|
||||
<range top="3.586831" right="269.943117" left="0.134774" bottom="-2.354077"/>
|
||||
<limitY/>
|
||||
<curve color="#d62728" name="/controlsState/lateralControlState/torqueState/desiredLateralAccel"/>
|
||||
<curve color="#1f77b4" name="/controlsState/lateralControlState/torqueState/actualLateralAccel"/>
|
||||
<curve color="#ff7f0e" name="/controlsState/lateralControlState/torqueState/desiredLateralAccel"/>
|
||||
<curve color="#1ac938" name="/controlsState/lateralControlState/torqueState/actualLateralAccel"/>
|
||||
</plot>
|
||||
</DockArea>
|
||||
<DockArea name="desired vs actual lateral acceleration (desired shifted by +0.1s)">
|
||||
<plot flip_x="false" style="Lines" flip_y="false" mode="TimeSeries">
|
||||
<range top="3.586831" right="269.943117" left="0.134774" bottom="-2.354077"/>
|
||||
<limitY/>
|
||||
<curve color="#1ac938" name="/controlsState/lateralControlState/torqueState/desiredLateralAccel">
|
||||
<curve color="#ff7f0e" name="/controlsState/lateralControlState/torqueState/desiredLateralAccel">
|
||||
<transform alias="/controlsState/lateralControlState/torqueState/desiredLateralAccel[Scale/Offset]" name="Scale/Offset">
|
||||
<options value_scale="1.0" time_offset="0.1" value_offset="0"/>
|
||||
</transform>
|
||||
</curve>
|
||||
<curve color="#ff7f0e" name="/controlsState/lateralControlState/torqueState/actualLateralAccel"/>
|
||||
<curve color="#1ac938" name="/controlsState/lateralControlState/torqueState/actualLateralAccel"/>
|
||||
</plot>
|
||||
</DockArea>
|
||||
<DockArea name="desired vs actual lateral acceleration (desired shifted by +0.2s)">
|
||||
<plot flip_x="false" style="Lines" flip_y="false" mode="TimeSeries">
|
||||
<range top="3.586831" right="269.943117" left="0.134774" bottom="-2.354077"/>
|
||||
<limitY/>
|
||||
<curve color="#1ac938" name="/controlsState/lateralControlState/torqueState/desiredLateralAccel">
|
||||
<curve color="#ff7f0e" name="/controlsState/lateralControlState/torqueState/desiredLateralAccel">
|
||||
<transform alias="/controlsState/lateralControlState/torqueState/desiredLateralAccel[Scale/Offset]" name="Scale/Offset">
|
||||
<options value_scale="1.0" time_offset="0.2" value_offset="0"/>
|
||||
</transform>
|
||||
</curve>
|
||||
<curve color="#ff7f0e" name="/controlsState/lateralControlState/torqueState/actualLateralAccel"/>
|
||||
<curve color="#1ac938" name="/controlsState/lateralControlState/torqueState/actualLateralAccel"/>
|
||||
</plot>
|
||||
</DockArea>
|
||||
<DockArea name="desired vs actual lateral acceleration (desired shifted by +0.3s)">
|
||||
<plot flip_x="false" style="Lines" flip_y="false" mode="TimeSeries">
|
||||
<range top="3.586831" right="269.943117" left="0.134774" bottom="-2.354077"/>
|
||||
<limitY/>
|
||||
<curve color="#1ac938" name="/controlsState/lateralControlState/torqueState/desiredLateralAccel">
|
||||
<curve color="#ff7f0e" name="/controlsState/lateralControlState/torqueState/desiredLateralAccel">
|
||||
<transform alias="/controlsState/lateralControlState/torqueState/desiredLateralAccel[Scale/Offset]" name="Scale/Offset">
|
||||
<options value_scale="1.0" time_offset="0.3" value_offset="0"/>
|
||||
</transform>
|
||||
</curve>
|
||||
<curve color="#ff7f0e" name="/controlsState/lateralControlState/torqueState/actualLateralAccel"/>
|
||||
<curve color="#1ac938" name="/controlsState/lateralControlState/torqueState/actualLateralAccel"/>
|
||||
</plot>
|
||||
</DockArea>
|
||||
</DockSplitter>
|
||||
|
||||
@@ -64,6 +64,8 @@ Options:
|
||||
-s, --start <seconds> start from <seconds>
|
||||
-x <speed> playback <speed>. between 0.2 - 3
|
||||
--demo use a demo route instead of providing your own
|
||||
--auto Auto load the route from the best available source (no video):
|
||||
internal, openpilotci, comma_api, car_segments, testing_closet
|
||||
--data_dir <data_dir> local directory with routes
|
||||
--prefix <prefix> set OPENPILOT_PREFIX
|
||||
--dcam load driver camera
|
||||
@@ -81,7 +83,7 @@ Arguments:
|
||||
connect.comma.ai
|
||||
```
|
||||
|
||||
## Visualize the Replay in the Openpilot UI
|
||||
## Visualize the Replay in the openpilot UI
|
||||
To visualize the replay within the openpilot UI, run the following commands:
|
||||
|
||||
```bash
|
||||
|
||||
@@ -12,7 +12,7 @@ public:
|
||||
ConsoleUI(Replay *replay);
|
||||
~ConsoleUI();
|
||||
int exec();
|
||||
inline static const std::array speed_array = {0.2f, 0.5f, 1.0f, 2.0f, 3.0f};
|
||||
inline static const std::array speed_array = {0.2f, 0.5f, 1.0f, 2.0f, 4.0f, 8.0f};
|
||||
|
||||
private:
|
||||
void initWindows();
|
||||
|
||||
@@ -19,6 +19,8 @@ Options:
|
||||
-s, --start Start from <seconds>
|
||||
-x, --playback Playback <speed>
|
||||
--demo Use a demo route instead of providing your own
|
||||
--auto Auto load the route from the best available source (no video):
|
||||
internal, openpilotci, comma_api, car_segments, testing_closet
|
||||
-d, --data_dir Local directory with routes
|
||||
-p, --prefix Set OPENPILOT_PREFIX
|
||||
--dcam Load driver camera
|
||||
@@ -39,6 +41,7 @@ struct ReplayConfig {
|
||||
std::string data_dir;
|
||||
std::string prefix;
|
||||
uint32_t flags = REPLAY_FLAG_NONE;
|
||||
bool auto_source = false;
|
||||
int start_seconds = 0;
|
||||
int cache_segments = -1;
|
||||
float playback_speed = -1;
|
||||
@@ -52,6 +55,7 @@ bool parseArgs(int argc, char *argv[], ReplayConfig &config) {
|
||||
{"start", required_argument, nullptr, 's'},
|
||||
{"playback", required_argument, nullptr, 'x'},
|
||||
{"demo", no_argument, nullptr, 0},
|
||||
{"auto", no_argument, nullptr, 0},
|
||||
{"data_dir", required_argument, nullptr, 'd'},
|
||||
{"prefix", required_argument, nullptr, 'p'},
|
||||
{"dcam", no_argument, nullptr, 0},
|
||||
@@ -94,11 +98,9 @@ bool parseArgs(int argc, char *argv[], ReplayConfig &config) {
|
||||
case 'p': config.prefix = optarg; break;
|
||||
case 0: {
|
||||
std::string name = cli_options[option_index].name;
|
||||
if (name == "demo") {
|
||||
config.route = DEMO_ROUTE;
|
||||
} else {
|
||||
config.flags |= flag_map.at(name);
|
||||
}
|
||||
if (name == "demo") config.route = DEMO_ROUTE;
|
||||
else if (name == "auto") config.auto_source = true;
|
||||
else config.flags |= flag_map.at(name);
|
||||
break;
|
||||
}
|
||||
case 'h': std::cout << helpText; return false;
|
||||
@@ -136,7 +138,7 @@ int main(int argc, char *argv[]) {
|
||||
op_prefix = std::make_unique<OpenpilotPrefix>(config.prefix);
|
||||
}
|
||||
|
||||
Replay replay(config.route, config.allow, config.block, nullptr, config.flags, config.data_dir);
|
||||
Replay replay(config.route, config.allow, config.block, nullptr, config.flags, config.data_dir, config.auto_source);
|
||||
if (config.cache_segments > 0) {
|
||||
replay.setSegmentCacheLimit(config.cache_segments);
|
||||
}
|
||||
|
||||
@@ -15,8 +15,8 @@ void notifyEvent(Callback &callback, Args &&...args) {
|
||||
}
|
||||
|
||||
Replay::Replay(const std::string &route, std::vector<std::string> allow, std::vector<std::string> block,
|
||||
SubMaster *sm, uint32_t flags, const std::string &data_dir)
|
||||
: sm_(sm), flags_(flags), seg_mgr_(std::make_unique<SegmentManager>(route, flags, data_dir)) {
|
||||
SubMaster *sm, uint32_t flags, const std::string &data_dir, bool auto_source)
|
||||
: sm_(sm), flags_(flags), seg_mgr_(std::make_unique<SegmentManager>(route, flags, data_dir, auto_source)) {
|
||||
std::signal(SIGUSR1, interrupt_sleep_handler);
|
||||
|
||||
if (!(flags_ & REPLAY_FLAG_ALL_SERVICES)) {
|
||||
|
||||
@@ -29,7 +29,7 @@ enum REPLAY_FLAGS {
|
||||
class Replay {
|
||||
public:
|
||||
Replay(const std::string &route, std::vector<std::string> allow, std::vector<std::string> block, SubMaster *sm = nullptr,
|
||||
uint32_t flags = REPLAY_FLAG_NONE, const std::string &data_dir = "");
|
||||
uint32_t flags = REPLAY_FLAG_NONE, const std::string &data_dir = "", bool auto_source = false);
|
||||
~Replay();
|
||||
bool load();
|
||||
RouteLoadError lastRouteError() const { return route().lastError(); }
|
||||
|
||||
+50
-16
@@ -10,9 +10,8 @@
|
||||
#include "tools/replay/replay.h"
|
||||
#include "tools/replay/util.h"
|
||||
|
||||
Route::Route(const std::string &route, const std::string &data_dir) : data_dir_(data_dir) {
|
||||
route_ = parseRoute(route);
|
||||
}
|
||||
Route::Route(const std::string &route, const std::string &data_dir, bool auto_source)
|
||||
: route_string_(route), data_dir_(data_dir), auto_source_(auto_source) {}
|
||||
|
||||
RouteIdentifier Route::parseRoute(const std::string &str) {
|
||||
RouteIdentifier identifier = {};
|
||||
@@ -44,27 +43,62 @@ RouteIdentifier Route::parseRoute(const std::string &str) {
|
||||
}
|
||||
|
||||
bool Route::load() {
|
||||
err_ = RouteLoadError::None;
|
||||
route_ = parseRoute(route_string_);
|
||||
if (route_.str.empty() || (data_dir_.empty() && route_.dongle_id.empty())) {
|
||||
rInfo("invalid route format");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Parse the timestamp from the route identifier (only applicable for old route formats).
|
||||
struct tm tm_time = {0};
|
||||
strptime(route_.timestamp.c_str(), "%Y-%m-%d--%H-%M-%S", &tm_time);
|
||||
date_time_ = mktime(&tm_time);
|
||||
if (strptime(route_.timestamp.c_str(), "%Y-%m-%d--%H-%M-%S", &tm_time)) {
|
||||
date_time_ = mktime(&tm_time);
|
||||
}
|
||||
|
||||
bool ret = data_dir_.empty() ? loadFromServer() : loadFromLocal();
|
||||
if (ret) {
|
||||
if (route_.begin_segment == -1) route_.begin_segment = segments_.rbegin()->first;
|
||||
if (route_.end_segment == -1) route_.end_segment = segments_.rbegin()->first;
|
||||
for (auto it = segments_.begin(); it != segments_.end(); /**/) {
|
||||
if (it->first < route_.begin_segment || it->first > route_.end_segment) {
|
||||
it = segments_.erase(it);
|
||||
} else {
|
||||
++it;
|
||||
if (!loadSegments()) {
|
||||
rInfo("Failed to load segments");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Route::loadSegments() {
|
||||
if (!auto_source_) {
|
||||
bool ret = data_dir_.empty() ? loadFromServer() : loadFromLocal();
|
||||
if (ret) {
|
||||
// Trim segments
|
||||
if (route_.begin_segment > 0) {
|
||||
segments_.erase(segments_.begin(), segments_.lower_bound(route_.begin_segment));
|
||||
}
|
||||
if (route_.end_segment >= 0) {
|
||||
segments_.erase(segments_.upper_bound(route_.end_segment), segments_.end());
|
||||
}
|
||||
}
|
||||
return !segments_.empty();
|
||||
}
|
||||
return loadFromAutoSource();
|
||||
}
|
||||
|
||||
bool Route::loadFromAutoSource() {
|
||||
auto origin_prefix = getenv("OPENPILOT_PREFIX");
|
||||
if (origin_prefix) {
|
||||
setenv("OPENPILOT_PREFIX", "", 1);
|
||||
}
|
||||
auto cmd = util::string_format("../auto_source.py \"%s\"", route_string_.c_str());
|
||||
auto log_files = split(util::check_output(cmd), '\n');
|
||||
if (origin_prefix) {
|
||||
setenv("OPENPILOT_PREFIX", origin_prefix, 1);
|
||||
}
|
||||
|
||||
const static std::regex rx(R"(\/(\d+)\/)");
|
||||
for (int i = 0; i < log_files.size(); ++i) {
|
||||
int seg_num = i;
|
||||
std::smatch match;
|
||||
if (std::regex_search(log_files[i], match, rx)) {
|
||||
seg_num = std::stoi(match[1]);
|
||||
}
|
||||
addFileToSegment(seg_num, log_files[i]);
|
||||
}
|
||||
return !segments_.empty();
|
||||
}
|
||||
@@ -121,7 +155,7 @@ bool Route::loadFromJson(const std::string &json) {
|
||||
bool Route::loadFromLocal() {
|
||||
std::string pattern = route_.timestamp + "--";
|
||||
for (const auto &entry : std::filesystem::directory_iterator(data_dir_)) {
|
||||
if (entry.is_directory() && entry.path().filename().string().find(pattern) == 0) {
|
||||
if (entry.is_directory() && entry.path().filename().string().find(pattern) != std::string::npos) {
|
||||
std::string segment = entry.path().string();
|
||||
int seg_num = std::atoi(segment.substr(segment.rfind("--") + 2).c_str());
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ struct SegmentFile {
|
||||
|
||||
class Route {
|
||||
public:
|
||||
Route(const std::string &route, const std::string &data_dir = {});
|
||||
Route(const std::string &route, const std::string &data_dir = {}, bool auto_source = false);
|
||||
bool load();
|
||||
RouteLoadError lastError() const { return err_; }
|
||||
inline const std::string &name() const { return route_.str; }
|
||||
@@ -52,6 +52,8 @@ public:
|
||||
static RouteIdentifier parseRoute(const std::string &str);
|
||||
|
||||
protected:
|
||||
bool loadSegments();
|
||||
bool loadFromAutoSource();
|
||||
bool loadFromLocal();
|
||||
bool loadFromServer(int retries = 3);
|
||||
bool loadFromJson(const std::string &json);
|
||||
@@ -59,8 +61,10 @@ protected:
|
||||
RouteIdentifier route_ = {};
|
||||
std::string data_dir_;
|
||||
std::map<int, SegmentFile> segments_;
|
||||
std::time_t date_time_;
|
||||
std::time_t date_time_ = 0;
|
||||
RouteLoadError err_ = RouteLoadError::None;
|
||||
bool auto_source_ = false;
|
||||
std::string route_string_;
|
||||
};
|
||||
|
||||
class Segment {
|
||||
|
||||
@@ -20,8 +20,8 @@ public:
|
||||
bool isSegmentLoaded(int n) const { return segments.find(n) != segments.end(); }
|
||||
};
|
||||
|
||||
SegmentManager(const std::string &route_name, uint32_t flags, const std::string &data_dir = "")
|
||||
: flags_(flags), route_(route_name, data_dir), event_data_(std::make_shared<EventData>()) {}
|
||||
SegmentManager(const std::string &route_name, uint32_t flags, const std::string &data_dir = "", bool auto_source = false)
|
||||
: flags_(flags), route_(route_name, data_dir, auto_source), event_data_(std::make_shared<EventData>()) {}
|
||||
~SegmentManager();
|
||||
|
||||
bool load();
|
||||
|
||||
Executable
+7
@@ -0,0 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
|
||||
# this is a little nicer than "adb shell" since
|
||||
# "adb shell" doesn't do full terminal emulation
|
||||
adb forward tcp:2222 tcp:22
|
||||
ssh comma@localhost -p 2222
|
||||
@@ -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")
|
||||
|
||||
@@ -40,7 +40,7 @@ class SimulatorBridge(ABC):
|
||||
def __init__(self, dual_camera, high_quality):
|
||||
set_params_enabled()
|
||||
self.params = Params()
|
||||
self.params.put_bool("ExperimentalLongitudinalEnabled", True)
|
||||
self.params.put_bool("AlphaLongitudinalEnabled", True)
|
||||
|
||||
self.rk = Ratekeeper(100, None)
|
||||
|
||||
|
||||
+10
-10
@@ -1,22 +1,22 @@
|
||||
# Run openpilot with webcam on PC
|
||||
|
||||
What's needed:
|
||||
- Ubuntu 24.04 ([WSL2 is not supported](https://github.com/commaai/openpilot/issues/34216))
|
||||
- Ubuntu 24.04 ([WSL2 is not supported](https://github.com/commaai/openpilot/issues/34216)) or macOS
|
||||
- GPU (recommended)
|
||||
- Two USB webcams, at least 720p and 78 degrees FOV (e.g. Logitech C920/C615)
|
||||
- [Car harness](https://comma.ai/shop/products/comma-car-harness) with black panda to connect to your car
|
||||
- [Panda paw](https://comma.ai/shop/products/panda-paw) or USB-A to USB-A cable to connect panda to your computer
|
||||
That's it!
|
||||
- One USB webcam, at least 720p and 78 degrees FOV (e.g. Logitech C920/C615, NexiGo N60)
|
||||
- [Car harness](https://comma.ai/shop/products/comma-car-harness)
|
||||
- [panda](https://comma.ai/shop/panda)
|
||||
- USB-A to USB-A cable to connect panda to your computer
|
||||
|
||||
## Setup openpilot
|
||||
- Follow [this readme](../README.md) to install and build the requirements
|
||||
- Install OpenCL Driver
|
||||
- Install OpenCL Driver (Ubuntu)
|
||||
```
|
||||
sudo apt install pocl-opencl-icd
|
||||
```
|
||||
|
||||
## Connect the hardware
|
||||
- Connect the road facing camera first, then the driver facing camera
|
||||
- Connect the camera first
|
||||
- Connect your computer to panda
|
||||
|
||||
## GO
|
||||
@@ -24,12 +24,12 @@ sudo apt install pocl-opencl-icd
|
||||
USE_WEBCAM=1 system/manager/manager.py
|
||||
```
|
||||
- Start the car, then the UI should show the road webcam's view
|
||||
- Adjust and secure the webcams.
|
||||
- Adjust and secure the webcam
|
||||
- Finish calibration and engage!
|
||||
|
||||
## Specify Cameras
|
||||
|
||||
Use the `ROAD_CAM`, `DRIVER_CAM`, and optional `WIDE_CAM` environment variables to specify which camera is which (ie. `DRIVER_CAM=2` uses `/dev/video2` for the driver-facing camera):
|
||||
Use the `ROAD_CAM` (default 0) and optional `DRIVER_CAM`, `WIDE_CAM` environment variables to specify which camera is which (ie. `ROAD_CAM=1` uses `/dev/video1`, on Ubuntu, for the road camera):
|
||||
```
|
||||
USE_WEBCAM=1 ROAD_CAM=4 WIDE_CAM=6 system/manager/manager.py
|
||||
USE_WEBCAM=1 ROAD_CAM=1 system/manager/manager.py
|
||||
```
|
||||
|
||||
@@ -9,14 +9,19 @@ from cereal import messaging
|
||||
from openpilot.tools.webcam.camera import Camera
|
||||
from openpilot.common.realtime import Ratekeeper
|
||||
|
||||
ROAD_CAM = os.getenv("ROAD_CAM", "0")
|
||||
WIDE_CAM = os.getenv("WIDE_CAM")
|
||||
DRIVER_CAM = os.getenv("DRIVER_CAM")
|
||||
|
||||
CameraType = namedtuple("CameraType", ["msg_name", "stream_type", "cam_id"])
|
||||
|
||||
CAMERAS = [
|
||||
CameraType("roadCameraState", VisionStreamType.VISION_STREAM_ROAD, os.getenv("ROAD_CAM", "0")),
|
||||
CameraType("driverCameraState", VisionStreamType.VISION_STREAM_DRIVER, os.getenv("DRIVER_CAM", "2")),
|
||||
CameraType("roadCameraState", VisionStreamType.VISION_STREAM_ROAD, ROAD_CAM)
|
||||
]
|
||||
if WIDE_CAM:
|
||||
CAMERAS.append(CameraType("wideRoadCameraState", VisionStreamType.VISION_STREAM_WIDE_ROAD, WIDE_CAM))
|
||||
if DRIVER_CAM:
|
||||
CAMERAS.append(CameraType("driverCameraState", VisionStreamType.VISION_STREAM_DRIVER, DRIVER_CAM))
|
||||
|
||||
class Camerad:
|
||||
def __init__(self):
|
||||
|
||||
Reference in New Issue
Block a user