diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index efa947a91a83d6..6fbccfbdbb7383 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,5 +1,15 @@ + + @@ -96,42 +63,138 @@ VideoWidget::VideoWidget(QWidget *parent) : QFrame(parent) { timeline_colors[(int)TimelineType::AlertCritical].name())); } +QHBoxLayout *VideoWidget::createPlaybackController() { + QHBoxLayout *layout = new QHBoxLayout(); + layout->addWidget(seek_backward_btn = new ToolButton("rewind", tr("Seek backward"))); + layout->addWidget(play_btn = new ToolButton("play", tr("Play"))); + layout->addWidget(seek_forward_btn = new ToolButton("fast-forward", tr("Seek forward"))); + + if (can->liveStreaming()) { + layout->addWidget(skip_to_end_btn = new ToolButton("skip-end", tr("Skip to the end"), this)); + QObject::connect(skip_to_end_btn, &QToolButton::clicked, [this]() { + // set speed to 1.0 + speed_btn->menu()->actions()[7]->setChecked(true); + can->pause(false); + can->seekTo(can->totalSeconds() + 1); + }); + } + + layout->addWidget(time_btn = new QToolButton); + time_btn->setToolTip(settings.absolute_time ? tr("Elapsed time") : tr("Absolute time")); + time_btn->setAutoRaise(true); + layout->addStretch(0); + + if (!can->liveStreaming()) { + layout->addWidget(loop_btn = new ToolButton("repeat", tr("Loop playback"))); + QObject::connect(loop_btn, &QToolButton::clicked, this, &VideoWidget::loopPlaybackClicked); + } + + // speed selector + layout->addWidget(speed_btn = new QToolButton(this)); + speed_btn->setAutoRaise(true); + speed_btn->setMenu(new QMenu(speed_btn)); + speed_btn->setPopupMode(QToolButton::InstantPopup); + QActionGroup *speed_group = new QActionGroup(this); + speed_group->setExclusive(true); + + int max_width = 0; + QFont font = speed_btn->font(); + font.setBold(true); + speed_btn->setFont(font); + QFontMetrics fm(font); + for (float speed : {0.01, 0.02, 0.05, 0.1, 0.2, 0.5, 0.8, 1., 2., 3., 5.}) { + QString name = QString("%1x").arg(speed); + max_width = std::max(max_width, fm.width(name) + fm.horizontalAdvance(QLatin1Char(' ')) * 2); + + QAction *act = new QAction(name, speed_group); + act->setCheckable(true); + QObject::connect(act, &QAction::toggled, [this, speed]() { + can->setSpeed(speed); + speed_btn->setText(QString("%1x ").arg(speed)); + }); + speed_btn->menu()->addAction(act); + if (speed == 1.0)act->setChecked(true); + } + speed_btn->setMinimumWidth(max_width + style()->pixelMetric(QStyle::PM_MenuButtonIndicator)); + + QObject::connect(play_btn, &QToolButton::clicked, []() { can->pause(!can->isPaused()); }); + QObject::connect(seek_backward_btn, &QToolButton::clicked, []() { can->seekTo(can->currentSec() - 1); }); + QObject::connect(seek_forward_btn, &QToolButton::clicked, []() { can->seekTo(can->currentSec() + 1); }); + QObject::connect(time_btn, &QToolButton::clicked, [this]() { + settings.absolute_time = !settings.absolute_time; + time_btn->setToolTip(settings.absolute_time ? tr("Elapsed time") : tr("Absolute time")); + updateState(); + }); + return layout; +} + QWidget *VideoWidget::createCameraWidget() { QWidget *w = new QWidget(this); QVBoxLayout *l = new QVBoxLayout(w); l->setContentsMargins(0, 0, 0, 0); + l->setSpacing(0); + + l->addWidget(camera_tab = new TabBar(w)); + camera_tab->setAutoHide(true); + camera_tab->setExpanding(false); QStackedLayout *stacked = new QStackedLayout(); stacked->setStackingMode(QStackedLayout::StackAll); - stacked->addWidget(cam_widget = new CameraWidget("camerad", can->visionStreamType(), false)); + stacked->addWidget(cam_widget = new CameraWidget("camerad", VISION_STREAM_ROAD, false)); cam_widget->setMinimumHeight(MIN_VIDEO_HEIGHT); cam_widget->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::MinimumExpanding); stacked->addWidget(alert_label = new InfoLabel(this)); l->addLayout(stacked); - // slider controls - auto slider_layout = new QHBoxLayout(); - slider_layout->addWidget(time_label = new QLabel("00:00")); - - slider = new Slider(this); + l->addWidget(slider = new Slider(w)); slider->setSingleStep(0); - slider_layout->addWidget(slider); - - slider_layout->addWidget(end_time_label = new QLabel(this)); - l->addLayout(slider_layout); setMaximumTime(can->totalSeconds()); QObject::connect(slider, &QSlider::sliderReleased, [this]() { can->seekTo(slider->currentSecond()); }); - QObject::connect(slider, &QSlider::valueChanged, [=](int value) { time_label->setText(utils::formatSeconds(slider->currentSecond())); }); QObject::connect(slider, &Slider::updateMaximumTime, this, &VideoWidget::setMaximumTime, Qt::QueuedConnection); + QObject::connect(static_cast(can), &ReplayStream::qLogLoaded, slider, &Slider::parseQLog); QObject::connect(cam_widget, &CameraWidget::clicked, []() { can->pause(!can->isPaused()); }); - QObject::connect(can, &AbstractStream::updated, this, &VideoWidget::updateState); + QObject::connect(cam_widget, &CameraWidget::vipcAvailableStreamsUpdated, this, &VideoWidget::vipcAvailableStreamsUpdated); + QObject::connect(camera_tab, &QTabBar::currentChanged, [this](int index) { + if (index != -1) cam_widget->setStreamType((VisionStreamType)camera_tab->tabData(index).toInt()); + }); return w; } +void VideoWidget::vipcAvailableStreamsUpdated(std::set streams) { + static const QString stream_names[] = { + [VISION_STREAM_ROAD] = "Road camera", + [VISION_STREAM_WIDE_ROAD] = "Wide road camera", + [VISION_STREAM_DRIVER] = "Driver camera"}; + + for (int i = 0; i < streams.size(); ++i) { + if (camera_tab->count() <= i) { + camera_tab->addTab(QString()); + } + int type = *std::next(streams.begin(), i); + camera_tab->setTabText(i, stream_names[type]); + camera_tab->setTabData(i, type); + } + while (camera_tab->count() > streams.size()) { + camera_tab->removeTab(camera_tab->count() - 1); + } +} + +void VideoWidget::loopPlaybackClicked() { + auto replay = qobject_cast(can)->getReplay(); + if (!replay) return; + + if (replay->hasFlag(REPLAY_FLAG_NO_LOOP)) { + replay->removeFlag(REPLAY_FLAG_NO_LOOP); + loop_btn->setIcon("repeat"); + } else { + replay->addFlag(REPLAY_FLAG_NO_LOOP); + loop_btn->setIcon("repeat-1"); + } +} + void VideoWidget::setMaximumTime(double sec) { maximum_time = sec; - end_time_label->setText(utils::formatSeconds(sec)); slider->setTimeRange(0, sec); } @@ -140,24 +203,30 @@ void VideoWidget::updateTimeRange(double min, double max, bool is_zoomed) { skip_to_end_btn->setEnabled(!is_zoomed); return; } + is_zoomed ? slider->setTimeRange(min, max) + : slider->setTimeRange(0, maximum_time); +} - if (!is_zoomed) { - min = 0; - max = maximum_time; - } - end_time_label->setText(utils::formatSeconds(max)); - slider->setTimeRange(min, max); +QString VideoWidget::formatTime(double sec, bool include_milliseconds) { + if (settings.absolute_time) + sec = can->beginDateTime().addMSecs(sec * 1000).toMSecsSinceEpoch() / 1000.0; + return utils::formatSeconds(sec, include_milliseconds, settings.absolute_time); } void VideoWidget::updateState() { - if (!slider->isSliderDown()) { - slider->setCurrentSecond(can->currentSec()); + if (slider) { + if (!slider->isSliderDown()) + slider->setCurrentSecond(can->currentSec()); + alert_label->showAlert(slider->alertInfo(can->currentSec())); + time_btn->setText(QString("%1 / %2").arg(formatTime(can->currentSec(), true), + formatTime(slider->maximum() / slider->factor))); + } else { + time_btn->setText(formatTime(can->currentSec(), true)); } - alert_label->showAlert(slider->alertInfo(can->currentSec())); } void VideoWidget::updatePlayBtnState() { - play_btn->setIcon(utils::icon(can->isPaused() ? "play" : "pause")); + play_btn->setIcon(can->isPaused() ? "play" : "pause"); play_btn->setToolTip(can->isPaused() ? tr("Play") : tr("Pause")); } @@ -165,29 +234,9 @@ void VideoWidget::updatePlayBtnState() { Slider::Slider(QWidget *parent) : thumbnail_label(parent), QSlider(Qt::Horizontal, parent) { setMouseTracking(true); - auto timer = new QTimer(this); - timer->callOnTimeout([this]() { - timeline = can->getTimeline(); - std::sort(timeline.begin(), timeline.end(), [](auto &l, auto &r) { return std::get<2>(l) < std::get<2>(r); }); - update(); - }); - timer->start(2000); - QObject::connect(can, &AbstractStream::eventsMerged, [this]() { - if (!qlog_future) { - qlog_future = std::make_unique>(QtConcurrent::run(this, &Slider::parseQLog)); - } - }); - QObject::connect(qApp, &QApplication::aboutToQuit, [this]() { - abort_parse_qlog = true; - if (qlog_future && qlog_future->isRunning()) { - qDebug() << "stopping thumbnail thread"; - qlog_future->waitForFinished(); - } - }); } AlertInfo Slider::alertInfo(double seconds) { - std::lock_guard lk(thumbnail_lock); uint64_t mono_time = (seconds + can->routeStartTime()) * 1e9; auto alert_it = alerts.lower_bound(mono_time); bool has_alert = (alert_it != alerts.end()) && ((alert_it->first - mono_time) <= 1e8); @@ -195,7 +244,6 @@ AlertInfo Slider::alertInfo(double seconds) { } QPixmap Slider::thumbnail(double seconds) { - std::lock_guard lk(thumbnail_lock); uint64_t mono_time = (seconds + can->routeStartTime()) * 1e9; auto it = thumbnails.lowerBound(mono_time); return it != thumbnails.end() ? it.value() : QPixmap(); @@ -206,36 +254,32 @@ void Slider::setTimeRange(double min, double max) { setRange(min * factor, max * factor); } -void Slider::parseQLog() { - const auto &segments = can->route()->segments(); - for (auto it = segments.rbegin(); it != segments.rend() && !abort_parse_qlog; ++it) { - LogReader log; - std::string qlog = it->second.qlog.toStdString(); - if (!qlog.empty() && log.load(qlog, &abort_parse_qlog, {cereal::Event::Which::THUMBNAIL, cereal::Event::Which::CONTROLS_STATE}, true, 0, 3)) { - if (it == segments.rbegin() && !log.events.empty()) { - double max_time = log.events.back()->mono_time / 1e9 - can->routeStartTime(); - emit updateMaximumTime(max_time); +void Slider::parseQLog(int segnum, std::shared_ptr qlog) { + const auto &segments = qobject_cast(can)->route()->segments(); + if (segments.size() > 0 && segnum == segments.rbegin()->first && !qlog->events.empty()) { + emit updateMaximumTime(qlog->events.back()->mono_time / 1e9 - can->routeStartTime()); + } + + std::mutex mutex; + QtConcurrent::blockingMap(qlog->events.cbegin(), qlog->events.cend(), [&mutex, this](const Event *e) { + if (e->which == cereal::Event::Which::THUMBNAIL) { + auto thumb = e->event.getThumbnail(); + auto data = thumb.getThumbnail(); + if (QPixmap pm; pm.loadFromData(data.begin(), data.size(), "jpeg")) { + QPixmap scaled = pm.scaledToHeight(MIN_VIDEO_HEIGHT - THUMBNAIL_MARGIN * 2, Qt::SmoothTransformation); + std::lock_guard lk(mutex); + thumbnails[thumb.getTimestampEof()] = scaled; } - for (auto ev = log.events.cbegin(); ev != log.events.cend() && !abort_parse_qlog; ++ev) { - if ((*ev)->which == cereal::Event::Which::THUMBNAIL) { - auto thumb = (*ev)->event.getThumbnail(); - auto data = thumb.getThumbnail(); - if (QPixmap pm; pm.loadFromData(data.begin(), data.size(), "jpeg")) { - pm = pm.scaledToHeight(MIN_VIDEO_HEIGHT - THUMBNAIL_MARGIN * 2, Qt::SmoothTransformation); - std::lock_guard lk(thumbnail_lock); - thumbnails[thumb.getTimestampEof()] = pm; - } - } else if ((*ev)->which == cereal::Event::Which::CONTROLS_STATE) { - auto cs = (*ev)->event.getControlsState(); - if (cs.getAlertType().size() > 0 && cs.getAlertText1().size() > 0 && - cs.getAlertSize() != cereal::ControlsState::AlertSize::NONE) { - std::lock_guard lk(thumbnail_lock); - alerts.emplace((*ev)->mono_time, AlertInfo{cs.getAlertStatus(), cs.getAlertText1().cStr(), cs.getAlertText2().cStr()}); - } - } + } else if (e->which == cereal::Event::Which::CONTROLS_STATE) { + auto cs = e->event.getControlsState(); + if (cs.getAlertType().size() > 0 && cs.getAlertText1().size() > 0 && + cs.getAlertSize() != cereal::ControlsState::AlertSize::NONE) { + std::lock_guard lk(mutex); + alerts.emplace(e->mono_time, AlertInfo{cs.getAlertStatus(), cs.getAlertText1().cStr(), cs.getAlertText2().cStr()}); } } - } + }); + update(); } void Slider::paintEvent(QPaintEvent *ev) { @@ -245,7 +289,7 @@ void Slider::paintEvent(QPaintEvent *ev) { double min = minimum() / factor; double max = maximum() / factor; - for (auto [begin, end, type] : timeline) { + for (auto [begin, end, type] : qobject_cast(can)->getReplay()->getTimeline()) { if (begin > max || end < min) continue; r.setLeft(((std::max(min, begin) - min) / (max - min)) * width()); @@ -265,8 +309,7 @@ void Slider::paintEvent(QPaintEvent *ev) { void Slider::mousePressEvent(QMouseEvent *e) { QSlider::mousePressEvent(e); if (e->button() == Qt::LeftButton && !isSliderDown()) { - int value = minimum() + ((maximum() - minimum()) * e->x()) / width(); - setValue(value); + setValue(minimum() + ((maximum() - minimum()) * e->x()) / width()); emit sliderReleased(); } } @@ -276,9 +319,9 @@ void Slider::mouseMoveEvent(QMouseEvent *e) { double seconds = (minimum() + pos * ((maximum() - minimum()) / (double)width())) / factor; QPixmap thumb = thumbnail(seconds); if (!thumb.isNull()) { - int x = std::clamp(pos - thumb.width() / 2, THUMBNAIL_MARGIN, rect().right() - thumb.width() - THUMBNAIL_MARGIN); - int y = -thumb.height(); - thumbnail_label.showPixmap(mapToParent({x, y}), utils::formatSeconds(seconds), thumb, alertInfo(seconds)); + int x = std::clamp(pos - thumb.width() / 2, THUMBNAIL_MARGIN, width() - thumb.width() - THUMBNAIL_MARGIN + 1); + int y = -thumb.height() - THUMBNAIL_MARGIN; + thumbnail_label.showPixmap(mapToParent(QPoint(x, y)), utils::formatSeconds(seconds), thumb, alertInfo(seconds)); } else { thumbnail_label.hide(); } @@ -311,8 +354,7 @@ void InfoLabel::showPixmap(const QPoint &pt, const QString &sec, const QPixmap & second = sec; pixmap = pm; alert_info = alert; - resize(pm.size()); - move(pt); + setGeometry(QRect(pt, pm.size())); setVisible(true); update(); } diff --git a/tools/cabana/videowidget.h b/tools/cabana/videowidget.h index aca44d0ac122cd..69f1edd2bc6072 100644 --- a/tools/cabana/videowidget.h +++ b/tools/cabana/videowidget.h @@ -1,19 +1,18 @@ #pragma once -#include #include #include -#include -#include -#include +#include -#include -#include +#include +#include #include +#include #include #include "selfdrive/ui/qt/widgets/cameraview.h" -#include "tools/cabana/streams/abstractstream.h" +#include "tools/cabana/util.h" +#include "tools/replay/logreader.h" struct AlertInfo { cereal::ControlsState::AlertStatus status; @@ -42,6 +41,9 @@ class Slider : public QSlider { void setTimeRange(double min, double max); AlertInfo alertInfo(double sec); QPixmap thumbnail(double sec); + void parseQLog(int segnum, std::shared_ptr qlog); + + const double factor = 1000.0; signals: void updateMaximumTime(double); @@ -51,15 +53,9 @@ class Slider : public QSlider { void mouseMoveEvent(QMouseEvent *e) override; bool event(QEvent *event) override; void paintEvent(QPaintEvent *ev) override; - void parseQLog(); - const double factor = 1000.0; - std::vector> timeline; - std::mutex thumbnail_lock; - std::atomic abort_parse_qlog = false; QMap thumbnails; std::map alerts; - std::unique_ptr> qlog_future; InfoLabel thumbnail_label; }; @@ -72,16 +68,24 @@ class VideoWidget : public QFrame { void setMaximumTime(double sec); protected: + QString formatTime(double sec, bool include_milliseconds = false); void updateState(); void updatePlayBtnState(); QWidget *createCameraWidget(); + QHBoxLayout *createPlaybackController(); + void loopPlaybackClicked(); + void vipcAvailableStreamsUpdated(std::set streams); CameraWidget *cam_widget; double maximum_time = 0; - QLabel *end_time_label; - QLabel *time_label; - QToolButton *play_btn; - QToolButton *skip_to_end_btn = nullptr; - InfoLabel *alert_label; - Slider *slider; + QToolButton *time_btn = nullptr; + ToolButton *seek_backward_btn = nullptr; + ToolButton *play_btn = nullptr; + ToolButton *seek_forward_btn = nullptr; + ToolButton *loop_btn = nullptr; + QToolButton *speed_btn = nullptr; + ToolButton *skip_to_end_btn = nullptr; + InfoLabel *alert_label = nullptr; + Slider *slider = nullptr; + QTabBar *camera_tab = nullptr; }; diff --git a/tools/gpstest/fuzzy_testing.py b/tools/gpstest/fuzzy_testing.py index 3bad2770cd294b..532fd2d34cc5e0 100755 --- a/tools/gpstest/fuzzy_testing.py +++ b/tools/gpstest/fuzzy_testing.py @@ -33,8 +33,7 @@ def run_remote_checker(lat, lon, alt, duration, ip_addr): return False, None, None matched, log, info = con.root.exposed_run_checker(lat, lon, alt, - timeout=duration, - use_laikad=True) + timeout=duration) con.close() # TODO: might wanna fetch more logs here con = None @@ -43,7 +42,7 @@ def run_remote_checker(lat, lon, alt, duration, ip_addr): stats = defaultdict(int) # type: ignore -keys = ['success', 'failed', 'ublox_fail', 'laikad_fail', 'proc_crash', 'checker_crash'] +keys = ['success', 'failed', 'ublox_fail', 'proc_crash', 'checker_crash'] def print_report(): print("\nFuzzy testing report summary:") @@ -62,10 +61,7 @@ def update_stats(matched, log, info): if log == "CHECKER CRASHED": stats['checker_crash'] += 1 if log == "TIMEOUT": - if "LAIKAD" in info: - stats['laikad_fail'] += 1 - else: # "UBLOX" in info - stats['ublox_fail'] += 1 + stats['ublox_fail'] += 1 def main(ip_addr, continuous_mode, timeout, pos): diff --git a/tools/gpstest/rpc_server.py b/tools/gpstest/rpc_server.py deleted file mode 100644 index 798237142d7511..00000000000000 --- a/tools/gpstest/rpc_server.py +++ /dev/null @@ -1,185 +0,0 @@ -import os -import time -import shutil -from datetime import datetime -from collections import defaultdict -from openpilot.system.hardware.hw import Paths - -import rpyc -from rpyc.utils.server import ThreadedServer - -#from openpilot.common.params import Params -import cereal.messaging as messaging -from openpilot.selfdrive.manager.process_config import managed_processes -from laika.lib.coordinates import ecef2geodetic - -DELTA = 0.001 -ALT_DELTA = 30 -MATCH_NUM = 10 -REPORT_STATS = 10 - -EPHEM_CACHE = "/data/params/d/LaikadEphemerisV3" - -SERVER_LOG_FILE = "/tmp/fuzzy_server.log" -server_log = open(SERVER_LOG_FILE, "w+") - -def slog(msg): - server_log.write(f"{datetime.now().strftime('%H:%M:%S.%f')} | {msg}\n") - server_log.flush() - -def handle_laikad(msg): - if not hasattr(msg, 'correctedMeasurements'): - return None - - num_corr = len(msg.correctedMeasurements) - pos_ecef = msg.positionECEF.value - pos_geo = [] - if len(pos_ecef) > 0: - pos_geo = ecef2geodetic(pos_ecef) - - pos_std = msg.positionECEF.std - pos_valid = msg.positionECEF.valid - - slog(f"{num_corr} {pos_geo} {pos_ecef} {pos_std} {pos_valid}") - return pos_geo, (num_corr, pos_geo, list(pos_ecef), list(msg.positionECEF.std)) - -hw_msgs = 0 -ephem_msgs: dict = defaultdict(int) -def handle_ublox(msg): - global hw_msgs - - d = msg.to_dict() - - if 'hwStatus2' in d: - hw_msgs += 1 - - if 'ephemeris' in d: - ephem_msgs[msg.ephemeris.svId] += 1 - - num_meas = None - if 'measurementReport' in d: - num_meas = msg.measurementReport.numMeas - - return [hw_msgs, ephem_msgs, num_meas] - - -def start_procs(procs): - for p in procs: - managed_processes[p].start() - time.sleep(1) - -def kill_procs(procs, no_retry=False): - for p in procs: - managed_processes[p].stop() - time.sleep(1) - - if not no_retry: - for p in procs: - mp = managed_processes[p].proc - if mp is not None and mp.is_alive(): - managed_processes[p].stop() - time.sleep(3) - -def check_alive_procs(procs): - for p in procs: - mp = managed_processes[p].proc - if mp is None or not mp.is_alive(): - return False, p - return True, None - - -class RemoteCheckerService(rpyc.Service): - def on_connect(self, conn): - pass - - def on_disconnect(self, conn): - #kill_procs(self.procs, no_retry=False) - # this execution is delayed, it will kill the next run of laikad - # TODO: add polling to wait for everything is killed - pass - - def run_checker(self, slat, slon, salt, sockets, procs, timeout): - global hw_msgs, ephem_msgs - hw_msgs = 0 - ephem_msgs = defaultdict(int) - - slog(f"Run test: {slat} {slon} {salt}") - - # quectel_mod = Params().get_bool("UbloxAvailable") - - match_cnt = 0 - msg_cnt = 0 - stats_laikad = [] - stats_ublox = [] - - self.procs = procs - start_procs(procs) - sm = messaging.SubMaster(sockets) - - start_time = time.monotonic() - while True: - sm.update() - - if sm.updated['ubloxGnss']: - stats_ublox.append(handle_ublox(sm['ubloxGnss'])) - - if sm.updated['gnssMeasurements']: - pos_geo, stats = handle_laikad(sm['gnssMeasurements']) - if pos_geo is None or len(pos_geo) == 0: - continue - - match = all(abs(g-s) < DELTA for g,s in zip(pos_geo[:2], [slat, slon], strict=True)) - match &= abs(pos_geo[2] - salt) < ALT_DELTA - if match: - match_cnt += 1 - if match_cnt >= MATCH_NUM: - return True, "MATCH", f"After: {round(time.monotonic() - start_time, 4)}" - - # keep some stats for error reporting - stats_laikad.append(stats) - - if (msg_cnt % 10) == 0: - a, p = check_alive_procs(procs) - if not a: - return False, "PROC CRASH", f"{p}" - msg_cnt += 1 - - if (time.monotonic() - start_time) > timeout: - h = f"LAIKAD: {stats_laikad[-REPORT_STATS:]}" - if len(h) == 0: - h = f"UBLOX: {stats_ublox[-REPORT_STATS:]}" - return False, "TIMEOUT", h - - - def exposed_run_checker(self, slat, slon, salt, timeout=180, use_laikad=True): - try: - procs = [] - sockets = [] - - if use_laikad: - procs.append("laikad") # pigeond, ubloxd # might wanna keep them running - sockets += ['ubloxGnss', 'gnssMeasurements'] - - if os.path.exists(EPHEM_CACHE): - os.remove(EPHEM_CACHE) - shutil.rmtree(Paths.download_cache_root(), ignore_errors=True) - - ret = self.run_checker(slat, slon, salt, sockets, procs, timeout) - kill_procs(procs) - return ret - - except Exception as e: - # always make sure processes get killed - kill_procs(procs) - return False, "CHECKER CRASHED", f"{str(e)}" - - - def exposed_kill_procs(self): - kill_procs(self.procs, no_retry=True) - - -if __name__ == "__main__": - print(f"Sever Log written to: {SERVER_LOG_FILE}") - t = ThreadedServer(RemoteCheckerService, port=18861) - t.start() - diff --git a/tools/gpstest/test_laikad.py b/tools/gpstest/test_laikad.py deleted file mode 100755 index abb47d4bfce717..00000000000000 --- a/tools/gpstest/test_laikad.py +++ /dev/null @@ -1,105 +0,0 @@ -#!/usr/bin/env python3 -import os -import time -import unittest - -import cereal.messaging as messaging -import openpilot.system.sensord.pigeond as pd - -from openpilot.common.params import Params -from openpilot.system.hardware import TICI -from openpilot.selfdrive.manager.process_config import managed_processes -from openpilot.selfdrive.test.helpers import with_processes - - -def wait_for_location(sm, timeout, con=10): - cons_meas = 0 - start_time = time.monotonic() - while (time.monotonic() - start_time) < timeout: - sm.update() - if not sm.updated["gnssMeasurements"]: - continue - - msg = sm["gnssMeasurements"] - cons_meas = (cons_meas + 1) if 'positionECEF' in msg.to_dict() else 0 - if cons_meas >= con: - return True - return False - - -class TestLaikad(unittest.TestCase): - @classmethod - def setUpClass(self): - if not TICI: - raise unittest.SkipTest - - ublox_available = Params().get_bool("UbloxAvailable") - if not ublox_available: - raise unittest.SkipTest - - def setUp(self): - # ensure laikad cold start - Params().remove("LaikadEphemerisV3") - os.environ["LAIKAD_NO_INTERNET"] = "1" - managed_processes['laikad'].start() - - def tearDown(self): - managed_processes['laikad'].stop() - - - @with_processes(['pigeond', 'ubloxd']) - def test_laikad_cold_start(self): - time.sleep(5) - - start_time = time.monotonic() - sm = messaging.SubMaster(["gnssMeasurements"]) - - success = wait_for_location(sm, 60*2, con=10) - duration = time.monotonic() - start_time - - assert success, "Waiting for location timed out (2min)!" - assert duration < 60, f"Received Location {duration}!" - - - @with_processes(['ubloxd']) - def test_laikad_ublox_reset_start(self): - time.sleep(2) - - pigeon, pm = pd.create_pigeon() - pd.init_baudrate(pigeon) - assert pigeon.reset_device(), "Could not reset device!" - - laikad_sock = messaging.sub_sock("gnssMeasurements", timeout=0.1) - ublox_gnss_sock = messaging.sub_sock("ubloxGnss", timeout=0.1) - - pd.init_baudrate(pigeon) - pd.initialize_pigeon(pigeon) - pd.run_receiving(pigeon, pm, 180) - - ublox_msgs = messaging.drain_sock(ublox_gnss_sock) - laikad_msgs = messaging.drain_sock(laikad_sock) - - gps_ephem_cnt = 0 - glonass_ephem_cnt = 0 - for um in ublox_msgs: - if um.ubloxGnss.which() == 'ephemeris': - gps_ephem_cnt += 1 - elif um.ubloxGnss.which() == 'glonassEphemeris': - glonass_ephem_cnt += 1 - - assert gps_ephem_cnt > 0, "NO gps ephemeris collected!" - assert glonass_ephem_cnt > 0, "NO glonass ephemeris collected!" - - pos_meas = 0 - duration = -1 - for lm in laikad_msgs: - pos_meas = (pos_meas + 1) if 'positionECEF' in lm.gnssMeasurements.to_dict() else 0 - if pos_meas > 5: - duration = (lm.logMonoTime - laikad_msgs[0].logMonoTime)*1e-9 - break - - assert pos_meas > 5, "NOT enough positions at end of read!" - assert duration < 120, "Laikad took too long to get a Position!" - -if __name__ == "__main__": - unittest.main() diff --git a/tools/install_python_dependencies.sh b/tools/install_python_dependencies.sh index 07bb8ac9a4f256..6753afffb96563 100755 --- a/tools/install_python_dependencies.sh +++ b/tools/install_python_dependencies.sh @@ -54,8 +54,8 @@ fi eval "$(pyenv init --path)" echo "update pip" -pip install pip==23.2.1 -pip install poetry==1.5.1 +pip install pip==23.3 +pip install poetry==1.6.1 poetry config virtualenvs.prefer-active-python true --local poetry config virtualenvs.in-project true --local diff --git a/tools/lib/framereader.py b/tools/lib/framereader.py index 8ab9e10edc258f..275b9b65b84cf3 100644 --- a/tools/lib/framereader.py +++ b/tools/lib/framereader.py @@ -3,7 +3,6 @@ import pickle import struct import subprocess -import tempfile import threading from enum import IntEnum from functools import wraps @@ -61,16 +60,14 @@ def fingerprint_video(fn): def ffprobe(fn, fmt=None): fn = resolve_name(fn) - cmd = ["ffprobe", - "-v", "quiet", - "-print_format", "json", - "-show_format", "-show_streams"] + cmd = ["ffprobe", "-v", "quiet", "-print_format", "json", "-show_format", "-show_streams"] if fmt: cmd += ["-f", fmt] - cmd += [fn] + cmd += ["-i", "-"] try: - ffprobe_output = subprocess.check_output(cmd) + with FileReader(fn) as f: + ffprobe_output = subprocess.check_output(cmd, input=f.read(4096)) except subprocess.CalledProcessError as e: raise DataUnreadableError(fn) from e @@ -170,31 +167,21 @@ def rgb24tonv12(rgb): def decompress_video_data(rawdat, vid_fmt, w, h, pix_fmt): - # using a tempfile is much faster than proc.communicate for some reason - - with tempfile.TemporaryFile() as tmpf: - tmpf.write(rawdat) - tmpf.seek(0) - - threads = os.getenv("FFMPEG_THREADS", "0") - cuda = os.getenv("FFMPEG_CUDA", "0") == "1" - args = ["ffmpeg", - "-threads", threads, - "-hwaccel", "none" if not cuda else "cuda", - "-c:v", "hevc", - "-vsync", "0", - "-f", vid_fmt, - "-flags2", "showall", - "-i", "pipe:0", - "-threads", threads, - "-f", "rawvideo", - "-pix_fmt", pix_fmt, - "pipe:1"] - with subprocess.Popen(args, stdin=tmpf, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL) as proc: - # dat = proc.communicate()[0] - dat = proc.stdout.read() - if proc.wait() != 0: - raise DataUnreadableError("ffmpeg failed") + threads = os.getenv("FFMPEG_THREADS", "0") + cuda = os.getenv("FFMPEG_CUDA", "0") == "1" + args = ["ffmpeg", "-v", "quiet", + "-threads", threads, + "-hwaccel", "none" if not cuda else "cuda", + "-c:v", "hevc", + "-vsync", "0", + "-f", vid_fmt, + "-flags2", "showall", + "-i", "-", + "-threads", threads, + "-f", "rawvideo", + "-pix_fmt", pix_fmt, + "-"] + dat = subprocess.check_output(args, input=rawdat) if pix_fmt == "rgb24": ret = np.frombuffer(dat, dtype=np.uint8).reshape(-1, h, w, 3) diff --git a/tools/lib/logreader.py b/tools/lib/logreader.py index e528996f3215ec..4af922c77442b2 100755 --- a/tools/lib/logreader.py +++ b/tools/lib/logreader.py @@ -6,11 +6,14 @@ import capnp import warnings +from typing import Iterable, Iterator from cereal import log as capnp_log from openpilot.tools.lib.filereader import FileReader from openpilot.tools.lib.route import Route, SegmentName +LogIterable = Iterable[capnp._DynamicStructReader] + # this is an iterator itself, and uses private variables from LogReader class MultiLogIterator: def __init__(self, log_paths, sort_by_time=False): @@ -30,7 +33,7 @@ def _log_reader(self, i): return self._log_readers[i] - def __iter__(self): + def __iter__(self) -> Iterator[capnp._DynamicStructReader]: return self def _inc(self): @@ -107,7 +110,7 @@ def __init__(self, fn, canonicalize=True, only_union_types=False, sort_by_time=F def from_bytes(cls, dat): return cls("", dat=dat) - def __iter__(self): + def __iter__(self) -> Iterator[capnp._DynamicStructReader]: for ent in self._ents: if self._only_union_types: try: diff --git a/tools/lib/url_file.py b/tools/lib/url_file.py index 2f933e3b7f235c..315ade514bdc80 100644 --- a/tools/lib/url_file.py +++ b/tools/lib/url_file.py @@ -17,6 +17,10 @@ def hash_256(link): return hsh +class URLFileException(Exception): + pass + + class URLFile: _tlocal = threading.local() @@ -158,11 +162,11 @@ def test(debug_type, debug_msg): response_code = c.getinfo(pycurl.RESPONSE_CODE) if response_code == 416: # Requested Range Not Satisfiable - raise Exception(f"Error, range out of bounds {response_code} {headers} ({self._url}): {repr(dats.getvalue())[:500]}") + raise URLFileException(f"Error, range out of bounds {response_code} {headers} ({self._url}): {repr(dats.getvalue())[:500]}") if download_range and response_code != 206: # Partial Content - raise Exception(f"Error, requested range but got unexpected response {response_code} {headers} ({self._url}): {repr(dats.getvalue())[:500]}") + raise URLFileException(f"Error, requested range but got unexpected response {response_code} {headers} ({self._url}): {repr(dats.getvalue())[:500]}") if (not download_range) and response_code != 200: # OK - raise Exception(f"Error {response_code} {headers} ({self._url}): {repr(dats.getvalue())[:500]}") + raise URLFileException(f"Error {response_code} {headers} ({self._url}): {repr(dats.getvalue())[:500]}") ret = dats.getvalue() self._pos += len(ret) diff --git a/tools/replay/replay.cc b/tools/replay/replay.cc index d3a32a2651474b..b83f657e3917b0 100644 --- a/tools/replay/replay.cc +++ b/tools/replay/replay.cc @@ -153,12 +153,10 @@ void Replay::buildTimeline() { const auto &route_segments = route_->segments(); for (auto it = route_segments.cbegin(); it != route_segments.cend() && !exit_; ++it) { - LogReader log; - if (!log.load(it->second.qlog.toStdString(), &exit_, - {cereal::Event::Which::CONTROLS_STATE, cereal::Event::Which::USER_FLAG}, - !hasFlag(REPLAY_FLAG_NO_FILE_CACHE), 0, 3)) continue; + std::shared_ptr log(new LogReader()); + if (!log->load(it->second.qlog.toStdString(), &exit_, {}, !hasFlag(REPLAY_FLAG_NO_FILE_CACHE), 0, 3)) continue; - for (const Event *e : log.events) { + for (const Event *e : log->events) { if (e->which == cereal::Event::Which::CONTROLS_STATE) { auto cs = e->event.getControlsState(); @@ -186,6 +184,8 @@ void Replay::buildTimeline() { timeline.push_back({toSeconds(e->mono_time), toSeconds(e->mono_time), TimelineType::UserFlag}); } } + std::sort(timeline.begin(), timeline.end(), [](auto &l, auto &r) { return std::get<2>(l) < std::get<2>(r); }); + emit qLogLoaded(it->first, log); } } diff --git a/tools/replay/replay.h b/tools/replay/replay.h index 5ed4ff11b5b589..01969b0a9f44ad 100644 --- a/tools/replay/replay.h +++ b/tools/replay/replay.h @@ -44,6 +44,7 @@ enum class FindFlag { enum class TimelineType { None, Engaged, AlertInfo, AlertWarning, AlertCritical, UserFlag }; typedef bool (*replayEventFilter)(const Event *, void *); +Q_DECLARE_METATYPE(std::shared_ptr); class Replay : public QObject { Q_OBJECT @@ -91,6 +92,7 @@ class Replay : public QObject { void streamStarted(); void segmentsMerged(); void seekedTo(double sec); + void qLogLoaded(int segnum, std::shared_ptr qlog); protected slots: void segmentLoadFinished(bool success); diff --git a/tools/replay/ui.py b/tools/replay/ui.py index 43195a481da992..05e8bdec1f4e08 100755 --- a/tools/replay/ui.py +++ b/tools/replay/ui.py @@ -116,9 +116,9 @@ def ui_thread(addr): if yuv_img_raw is None or not yuv_img_raw.data.any(): continue - imgff = np.frombuffer(yuv_img_raw.data, dtype=np.uint8).reshape((vipc_client.height * 3 // 2, vipc_client.width)) + imgff = np.frombuffer(yuv_img_raw.data, dtype=np.uint8).reshape((len(yuv_img_raw.data) // vipc_client.stride, vipc_client.stride)) num_px = vipc_client.width * vipc_client.height - bgr = cv2.cvtColor(imgff, cv2.COLOR_YUV2RGB_NV12) + bgr = cv2.cvtColor(imgff[:vipc_client.height * 3 // 2, :vipc_client.width], cv2.COLOR_YUV2RGB_NV12) zoom_matrix = _BB_TO_FULL_FRAME[num_px] cv2.warpAffine(bgr, zoom_matrix[:2], (img.shape[1], img.shape[0]), dst=img, flags=cv2.WARP_INVERSE_MAP) diff --git a/tools/replay/util.cc b/tools/replay/util.cc index 2c2de69e78e99b..b4f72d053000b4 100644 --- a/tools/replay/util.cc +++ b/tools/replay/util.cc @@ -19,10 +19,7 @@ #include "common/util.h" ReplayMessageHandler message_handler = nullptr; -DownloadProgressHandler download_progress_handler = nullptr; - void installMessageHandler(ReplayMessageHandler handler) { message_handler = handler; } -void installDownloadProgressHandler(DownloadProgressHandler handler) { download_progress_handler = handler; } void logMessage(ReplyMsgType type, const char *fmt, ...) { static std::mutex lock; @@ -94,6 +91,11 @@ size_t write_cb(char *data, size_t size, size_t count, void *userp) { size_t dumy_write_cb(char *data, size_t size, size_t count, void *userp) { return size * count; } struct DownloadStats { + void installDownloadProgressHandler(DownloadProgressHandler handler) { + std::lock_guard lk(lock); + download_progress_handler = handler; + } + void add(const std::string &url, uint64_t total_bytes) { std::lock_guard lk(lock); items[url] = {0, total_bytes}; @@ -121,10 +123,17 @@ struct DownloadStats { std::mutex lock; std::map> items; double prev_tm = 0; + DownloadProgressHandler download_progress_handler = nullptr; }; +static DownloadStats download_stats; + } // namespace +void installDownloadProgressHandler(DownloadProgressHandler handler) { + download_stats.installDownloadProgressHandler(handler); +} + std::string formattedDataSize(size_t size) { if (size < 1024) { return std::to_string(size) + " B"; @@ -167,7 +176,6 @@ std::string getUrlWithoutQuery(const std::string &url) { template bool httpDownload(const std::string &url, T &buf, size_t chunk_size, size_t content_length, std::atomic *abort) { - static DownloadStats download_stats; download_stats.add(url, content_length); int parts = 1; diff --git a/tools/sim/Dockerfile.sim b/tools/sim/Dockerfile.sim index 9fd8a56101b397..7dffa7b5e50c01 100644 --- a/tools/sim/Dockerfile.sim +++ b/tools/sim/Dockerfile.sim @@ -31,6 +31,7 @@ COPY ./panda ${OPENPILOT_PATH}/panda COPY ./selfdrive ${OPENPILOT_PATH}/selfdrive COPY ./system ${OPENPILOT_PATH}/system COPY ./tools ${OPENPILOT_PATH}/tools +COPY ./release ${OPENPILOT_PATH}/release RUN --mount=type=bind,source=.ci_cache/scons_cache,target=/tmp/scons_cache,rw scons -j$(nproc) --cache-readonly