From dbf095522f12659129a4e058d8371731f097c8dd Mon Sep 17 00:00:00 2001 From: Dean Lee Date: Tue, 9 Jul 2024 05:50:31 +0800 Subject: [PATCH] cabana: bug fixes and improvements (#32934) bug fixes and improvements old-commit-hash: 36815cc6d536ab8926f6335574ea587947c4e222 --- tools/cabana/chart/chart.cc | 14 ++--- tools/cabana/chart/chart.h | 1 + tools/cabana/chart/chartswidget.cc | 28 ++++++---- tools/cabana/chart/sparkline.cc | 9 +-- tools/cabana/chart/tiplabel.h | 2 + tools/cabana/dbc/dbcmanager.cc | 7 ++- tools/cabana/historylog.cc | 4 +- tools/cabana/streams/abstractstream.cc | 48 ++++++++-------- tools/cabana/streams/abstractstream.h | 30 +++++----- tools/cabana/streams/livestream.cc | 6 +- tools/cabana/streams/livestream.h | 7 ++- tools/cabana/streams/pandastream.h | 6 ++ tools/cabana/streams/replaystream.cc | 13 +---- tools/cabana/streams/replaystream.h | 9 ++- tools/cabana/tools/findsignal.cc | 6 +- tools/cabana/utils/export.cc | 6 +- tools/cabana/videowidget.cc | 77 +++++++++++++++++--------- tools/cabana/videowidget.h | 27 +++++++-- tools/replay/consoleui.cc | 10 ++-- tools/replay/replay.cc | 24 ++++++-- tools/replay/replay.h | 8 ++- 21 files changed, 200 insertions(+), 142 deletions(-) diff --git a/tools/cabana/chart/chart.cc b/tools/cabana/chart/chart.cc index 28d4e068ee..128c33baec 100644 --- a/tools/cabana/chart/chart.cc +++ b/tools/cabana/chart/chart.cc @@ -289,10 +289,9 @@ void ChartView::appendCanEvents(const cabana::Signal *sig, const std::vectorrouteStartTime() * 1e9; for (const CanEvent *e : events) { if (sig->getValue(e->dat, e->size, &value)) { - const double ts = (e->mono_time - std::min(e->mono_time, begin_mono_time)) / 1e9; + const double ts = can->toSeconds(e->mono_time); vals.emplace_back(ts, value); if (!step_vals.empty()) step_vals.emplace_back(ts, step_vals.back().y()); @@ -312,7 +311,7 @@ void ChartView::updateSeries(const cabana::Signal *sig, const MessageEventsMap * auto it = events->find(s.msg_id); if (it == events->end() || it->second.empty()) continue; - if (s.vals.empty() || (it->second.back()->mono_time / 1e9 - can->routeStartTime()) > s.vals.back().x()) { + if (s.vals.empty() || can->toSeconds(it->second.back()->mono_time) > s.vals.back().x()) { appendCanEvents(s.sig, it->second, s.vals, s.step_vals); } else { std::vector vals, step_vals; @@ -500,8 +499,8 @@ void ChartView::mouseReleaseEvent(QMouseEvent *event) { rubber->hide(); auto rect = rubber->geometry().normalized(); // Prevent zooming/seeking past the end of the route - double min = std::clamp(chart()->mapToValue(rect.topLeft()).x(), 0., can->totalSeconds()); - double max = std::clamp(chart()->mapToValue(rect.bottomRight()).x(), 0., can->totalSeconds()); + double min = std::clamp(chart()->mapToValue(rect.topLeft()).x(), can->minSeconds(), can->maxSeconds()); + double max = std::clamp(chart()->mapToValue(rect.bottomRight()).x(), can->minSeconds(), can->maxSeconds()); if (rubber->width() <= 0) { // no rubber dragged, seek to mouse position can->seekTo(min); @@ -531,7 +530,7 @@ void ChartView::mouseMoveEvent(QMouseEvent *ev) { // Scrubbing if (is_scrubbing && QApplication::keyboardModifiers().testFlag(Qt::ShiftModifier)) { if (plot_area.contains(ev->pos())) { - can->seekTo(std::clamp(chart()->mapToValue(ev->pos()).x(), 0., can->totalSeconds())); + can->seekTo(std::clamp(chart()->mapToValue(ev->pos()).x(), can->minSeconds(), can->maxSeconds())); } } @@ -540,8 +539,7 @@ void ChartView::mouseMoveEvent(QMouseEvent *ev) { clearTrackPoints(); if (!is_zooming && plot_area.contains(ev->pos()) && isActiveWindow()) { - const double sec = chart()->mapToValue(ev->pos()).x(); - charts_widget->showValueTip(sec); + charts_widget->showValueTip(secondsAtPoint(ev->pos())); } else if (tip_label->isVisible()) { charts_widget->showValueTip(-1); } diff --git a/tools/cabana/chart/chart.h b/tools/cabana/chart/chart.h index 1bfec6355a..f9472bd4f6 100644 --- a/tools/cabana/chart/chart.h +++ b/tools/cabana/chart/chart.h @@ -39,6 +39,7 @@ public: void showTip(double sec); void hideTip(); void startAnimation(); + double secondsAtPoint(const QPointF &pt) const { return chart()->mapToValue(pt).x(); } struct SigItem { MessageId msg_id; diff --git a/tools/cabana/chart/chartswidget.cc b/tools/cabana/chart/chartswidget.cc index 9822eaa307..27846f9c92 100644 --- a/tools/cabana/chart/chartswidget.cc +++ b/tools/cabana/chart/chartswidget.cc @@ -93,7 +93,7 @@ ChartsWidget::ChartsWidget(QWidget *parent) : QFrame(parent) { current_theme = settings.theme; column_count = std::clamp(settings.chart_column_count, 1, MAX_COLUMN_COUNT); max_chart_range = std::clamp(settings.chart_range, 1, settings.max_cached_minutes * 60); - display_range = {0, max_chart_range}; + display_range = std::make_pair(can->minSeconds(), can->minSeconds() + max_chart_range); range_slider->setValue(max_chart_range); updateToolBar(); @@ -192,10 +192,10 @@ void ChartsWidget::updateState() { if (!time_range.has_value()) { double pos = (cur_sec - display_range.first) / std::max(1.0, max_chart_range); if (pos < 0 || pos > 0.8) { - display_range.first = std::max(0.0, cur_sec - max_chart_range * 0.1); + display_range.first = std::max(can->minSeconds(), cur_sec - max_chart_range * 0.1); } - double max_sec = std::min(display_range.first + max_chart_range, can->totalSeconds()); - display_range.first = std::max(0.0, max_sec - max_chart_range); + double max_sec = std::min(display_range.first + max_chart_range, can->maxSeconds()); + display_range.first = std::max(can->minSeconds(), max_sec - max_chart_range); display_range.second = display_range.first + max_chart_range; } @@ -435,14 +435,20 @@ void ChartsWidget::alignCharts() { bool ChartsWidget::eventFilter(QObject *o, QEvent *e) { if (value_tip_visible_ && e->type() == QEvent::MouseMove) { - auto pos = static_cast(e)->globalPos(); - bool outside_plot_area =std::none_of(charts.begin(), charts.end(), [&pos](auto c) { - return c->chart()->plotArea().contains(c->mapFromGlobal(pos)); - }); - - if (outside_plot_area) { - showValueTip(-1); + bool on_tip = qobject_cast(o) != nullptr; + auto global_pos = static_cast(e)->globalPos(); + + for (const auto &c : charts) { + auto local_pos = c->mapFromGlobal(global_pos); + if (c->chart()->plotArea().contains(local_pos)) { + if (on_tip) { + showValueTip(c->secondsAtPoint(local_pos)); + } + return false; + } } + + showValueTip(-1); } return false; } diff --git a/tools/cabana/chart/sparkline.cc b/tools/cabana/chart/sparkline.cc index 34bc76b7c4..1cadda2bd8 100644 --- a/tools/cabana/chart/sparkline.cc +++ b/tools/cabana/chart/sparkline.cc @@ -8,10 +8,11 @@ void Sparkline::update(const MessageId &msg_id, const cabana::Signal *sig, double last_msg_ts, int range, QSize size) { const auto &msgs = can->events(msg_id); - uint64_t ts = (last_msg_ts + can->routeStartTime()) * 1e9; - uint64_t first_ts = (ts > range * 1e9) ? ts - range * 1e9 : 0; - auto first = std::lower_bound(msgs.cbegin(), msgs.cend(), first_ts, CompareCanEvent()); - auto last = std::upper_bound(first, msgs.cend(), ts, CompareCanEvent()); + + auto range_start = can->toMonoTime(last_msg_ts - range); + auto range_end = can->toMonoTime(last_msg_ts); + auto first = std::lower_bound(msgs.cbegin(), msgs.cend(), range_start, CompareCanEvent()); + auto last = std::upper_bound(first, msgs.cend(), range_end, CompareCanEvent()); if (first == last || size.isEmpty()) { pixmap = QPixmap(); diff --git a/tools/cabana/chart/tiplabel.h b/tools/cabana/chart/tiplabel.h index ac6e09e976..cc96aa9864 100644 --- a/tools/cabana/chart/tiplabel.h +++ b/tools/cabana/chart/tiplabel.h @@ -3,6 +3,8 @@ #include class TipLabel : public QLabel { + Q_OBJECT + public: TipLabel(QWidget *parent = nullptr); void showText(const QPoint &pt, const QString &sec, QWidget *w, const QRect &rect); diff --git a/tools/cabana/dbc/dbcmanager.cc b/tools/cabana/dbc/dbcmanager.cc index 250ec225b5..8e98d95322 100644 --- a/tools/cabana/dbc/dbcmanager.cc +++ b/tools/cabana/dbc/dbcmanager.cc @@ -1,5 +1,6 @@ #include "tools/cabana/dbc/dbcmanager.h" +#include #include #include @@ -124,16 +125,16 @@ cabana::Msg *DBCManager::msg(uint8_t source, const QString &name) { QStringList DBCManager::signalNames() { // Used for autocompletion - QStringList ret; + QSet names; for (auto &f : allDBCFiles()) { for (auto &[_, m] : f->getMessages()) { for (auto sig : m.getSignals()) { - ret << sig->name; + names.insert(sig->name); } } } + QStringList ret = names.values(); ret.sort(); - ret.removeDuplicates(); return ret; } diff --git a/tools/cabana/historylog.cc b/tools/cabana/historylog.cc index 55ba0a4919..7e73da55f0 100644 --- a/tools/cabana/historylog.cc +++ b/tools/cabana/historylog.cc @@ -13,7 +13,7 @@ QVariant HistoryLogModel::data(const QModelIndex &index, int role) const { const auto &m = messages[index.row()]; const int col = index.column(); if (role == Qt::DisplayRole) { - if (col == 0) return QString::number((m.mono_time / (double)1e9) - can->routeStartTime(), 'f', 3); + if (col == 0) return QString::number(can->toSeconds(m.mono_time), 'f', 3); if (!isHexMode()) return sigs[col - 1]->formatValue(m.sig_values[col - 1], false); } else if (role == Qt::TextAlignmentRole) { return (uint32_t)(Qt::AlignRight | Qt::AlignVCenter); @@ -80,7 +80,7 @@ void HistoryLogModel::updateState(bool clear) { messages.clear(); endRemoveRows(); } - uint64_t current_time = (can->lastMessage(msg_id).ts + can->routeStartTime()) * 1e9 + 1; + uint64_t current_time = can->toMonoTime(can->lastMessage(msg_id).ts) + 1; fetchData(messages.begin(), current_time, messages.empty() ? 0 : messages.front().mono_time); } diff --git a/tools/cabana/streams/abstractstream.cc b/tools/cabana/streams/abstractstream.cc index 2584106ce4..ee6a1143c8 100644 --- a/tools/cabana/streams/abstractstream.cc +++ b/tools/cabana/streams/abstractstream.cc @@ -1,6 +1,5 @@ #include "tools/cabana/streams/abstractstream.h" -#include #include #include @@ -20,9 +19,9 @@ AbstractStream::AbstractStream(QObject *parent) : QObject(parent) { assert(parent != nullptr); event_buffer_ = std::make_unique(EVENT_NEXT_BUFFER_SIZE); - QObject::connect(QApplication::instance(), &QCoreApplication::aboutToQuit, this, &AbstractStream::stop); QObject::connect(this, &AbstractStream::privateUpdateLastMsgsSignal, this, &AbstractStream::updateLastMessages, Qt::QueuedConnection); QObject::connect(this, &AbstractStream::seekedTo, this, &AbstractStream::updateLastMsgsTo); + QObject::connect(this, &AbstractStream::seeking, this, [this](double sec) { current_sec_ = sec; }); QObject::connect(dbc(), &DBCManager::DBCFileChanged, this, &AbstractStream::updateMasks); QObject::connect(dbc(), &DBCManager::maskUpdated, this, &AbstractStream::updateMasks); QObject::connect(this, &AbstractStream::streamStarted, [this]() { @@ -64,14 +63,12 @@ void AbstractStream::suppressDefinedSignals(bool suppress) { size_t AbstractStream::suppressHighlighted() { std::lock_guard lk(mutex_); size_t cnt = 0; - const double cur_ts = currentSec(); for (auto &[_, m] : messages_) { for (auto &last_change : m.last_changes) { - const double dt = cur_ts - last_change.ts; + const double dt = current_sec_ - last_change.ts; if (dt < 2.0) { last_change.suppressed = true; } - // clear bit change counts last_change.bit_change_counts.fill(0); cnt += last_change.suppressed; } @@ -90,20 +87,16 @@ void AbstractStream::updateLastMessages() { auto prev_src_size = sources.size(); auto prev_msg_size = last_msgs.size(); std::set msgs; + { std::lock_guard lk(mutex_); - double max_sec = 0; for (const auto &id : new_msgs_) { const auto &can_data = messages_[id]; - max_sec = std::max(max_sec, can_data.ts); + current_sec_ = std::max(current_sec_, can_data.ts); last_msgs[id] = can_data; sources.insert(id.source); } - - if (!new_msgs_.empty()) { - msgs = std::move(new_msgs_); - current_sec_ = max_sec; - } + msgs = std::move(new_msgs_); } if (time_range_ && (current_sec_ < time_range_->first || current_sec_ >= time_range_->second)) { @@ -138,7 +131,7 @@ const std::vector &AbstractStream::events(const MessageId &id) return it != events_.end() ? it->second : empty_events; } -const CanData &AbstractStream::lastMessage(const MessageId &id) { +const CanData &AbstractStream::lastMessage(const MessageId &id) const { static CanData empty_data = {}; auto it = last_msgs.find(id); return it != last_msgs.end() ? it->second : empty_data; @@ -148,15 +141,13 @@ const CanData &AbstractStream::lastMessage(const MessageId &id) { // updateLastMsgsTo is always called in UI thread. void AbstractStream::updateLastMsgsTo(double sec) { current_sec_ = sec; - uint64_t last_ts = (sec + routeStartTime()) * 1e9; + uint64_t last_ts = toMonoTime(sec); std::unordered_map msgs; msgs.reserve(events_.size()); for (const auto &[id, ev] : events_) { auto it = std::upper_bound(ev.begin(), ev.end(), last_ts, CompareCanEvent()); if (it != ev.begin()) { - auto prev = std::prev(it); - double ts = (*prev)->mono_time / 1e9 - routeStartTime(); auto &m = msgs[id]; double freq = 0; // Keep suppressed bits. @@ -167,7 +158,9 @@ void AbstractStream::updateLastMsgsTo(double sec) { std::back_inserter(m.last_changes), [](const auto &change) { return CanData::ByteLastChange{.suppressed = change.suppressed}; }); } - m.compute(id, (*prev)->dat, (*prev)->size, ts, getSpeed(), {}, freq); + + auto prev = std::prev(it); + m.compute(id, (*prev)->dat, (*prev)->size, toSeconds((*prev)->mono_time), getSpeed(), {}, freq); m.count = std::distance(ev.begin(), prev) + 1; } } @@ -213,7 +206,6 @@ void AbstractStream::mergeEvents(const std::vector &events) { all_events_.insert(pos, events.cbegin(), events.cend()); emit eventsMerged(msg_events); } - lastest_event_ts = all_events_.empty() ? 0 : all_events_.back()->mono_time; } namespace { @@ -236,14 +228,18 @@ inline QColor blend(const QColor &a, const QColor &b) { // Calculate the frequency from the past one minute data double calc_freq(const MessageId &msg_id, double current_sec) { const auto &events = can->events(msg_id); - uint64_t cur_mono_time = (can->routeStartTime() + current_sec) * 1e9; - uint64_t first_mono_time = std::max(0, cur_mono_time - 59 * 1e9); - auto first = std::lower_bound(events.begin(), events.end(), first_mono_time, CompareCanEvent()); - auto second = std::lower_bound(first, events.end(), cur_mono_time, CompareCanEvent()); - if (first != events.end() && second != events.end()) { - double duration = ((*second)->mono_time - (*first)->mono_time) / 1e9; - uint32_t count = std::distance(first, second); - return count / std::max(1.0, duration); + if (events.empty()) return 0.0; + + auto current_mono_time = can->toMonoTime(current_sec); + auto start_mono_time = can->toMonoTime(current_sec - 59); + + auto first = std::lower_bound(events.begin(), events.end(), start_mono_time, CompareCanEvent()); + auto last = std::upper_bound(first, events.end(), current_mono_time, CompareCanEvent()); + + int count = std::distance(first, last); + if (count > 1) { + double duration = ((*std::prev(last))->mono_time - (*first)->mono_time) / 1e9; + return count / duration; } return 0; } diff --git a/tools/cabana/streams/abstractstream.h b/tools/cabana/streams/abstractstream.h index 822dd03d84..9e3f92dc5d 100644 --- a/tools/cabana/streams/abstractstream.h +++ b/tools/cabana/streams/abstractstream.h @@ -1,5 +1,6 @@ #pragma once +#include #include #include #include @@ -28,10 +29,10 @@ struct CanData { std::vector colors; struct ByteLastChange { - double ts; - int delta; - int same_delta_counter; - bool suppressed; + double ts = 0; + int delta = 0; + int same_delta_counter = 0; + bool suppressed = false; std::array bit_change_counts; }; std::vector last_changes; @@ -51,12 +52,6 @@ struct CompareCanEvent { constexpr bool operator()(uint64_t ts, const CanEvent *const e) const { return ts < e->mono_time; } }; -struct BusConfig { - int can_speed_kbps = 500; - int data_speed_kbps = 2000; - bool can_fd = false; -}; - typedef std::unordered_map> MessageEventsMap; class AbstractStream : public QObject { @@ -66,15 +61,14 @@ public: AbstractStream(QObject *parent); virtual ~AbstractStream() {} virtual void start() = 0; - virtual void stop() {} virtual bool liveStreaming() const { return true; } virtual void seekTo(double ts) {} virtual QString routeName() const = 0; virtual QString carFingerprint() const { return ""; } virtual QDateTime beginDateTime() const { return {}; } - virtual double routeStartTime() const { return 0; } - inline double currentSec() const { return current_sec_; } - virtual double totalSeconds() const { return lastEventMonoTime() / 1e9 - routeStartTime(); } + virtual uint64_t beginMonoTime() const { return 0; } + virtual double minSeconds() const { return 0; } + virtual double maxSeconds() const { return 0; } virtual void setSpeed(float speed) {} virtual double getSpeed() { return 1; } virtual bool isPaused() const { return false; } @@ -82,10 +76,14 @@ public: void setTimeRange(const std::optional> &range); const std::optional> &timeRange() const { return time_range_; } + inline double currentSec() const { return current_sec_; } + inline uint64_t toMonoTime(double sec) const { return beginMonoTime() + std::max(sec, 0.0) * 1e9; } + inline double toSeconds(uint64_t mono_time) const { return std::max(0.0, (mono_time - beginMonoTime()) / 1e9); } + inline const std::unordered_map &lastMessages() const { return last_msgs; } inline const MessageEventsMap &eventsMap() const { return events_; } inline const std::vector &allEvents() const { return all_events_; } - const CanData &lastMessage(const MessageId &id); + const CanData &lastMessage(const MessageId &id) const; const std::vector &events(const MessageId &id) const; size_t suppressHighlighted(); @@ -111,12 +109,10 @@ protected: void mergeEvents(const std::vector &events); const CanEvent *newEvent(uint64_t mono_time, const cereal::CanData::Reader &c); void updateEvent(const MessageId &id, double sec, const uint8_t *data, uint8_t size); - uint64_t lastEventMonoTime() const { return lastest_event_ts; } std::vector all_events_; double current_sec_ = 0; std::optional> time_range_; - uint64_t lastest_event_ts = 0; private: void updateLastMessages(); diff --git a/tools/cabana/streams/livestream.cc b/tools/cabana/streams/livestream.cc index d9d96e23b0..6bcb1c1d54 100644 --- a/tools/cabana/streams/livestream.cc +++ b/tools/cabana/streams/livestream.cc @@ -92,6 +92,8 @@ void LiveStream::timerEvent(QTimerEvent *event) { // merge events received from live stream thread. std::lock_guard lk(lock); mergeEvents(received_events_); + uint64_t last_received_ts = !received_events_.empty() ? received_events_.back()->mono_time : 0; + lastest_event_ts = std::max(lastest_event_ts, last_received_ts); received_events_.clear(); } if (!all_events_.empty()) { @@ -136,8 +138,8 @@ void LiveStream::updateEvents() { void LiveStream::seekTo(double sec) { sec = std::max(0.0, sec); first_update_ts = nanos_since_boot(); - current_event_ts = first_event_ts = std::min(sec * 1e9 + begin_event_ts, lastEventMonoTime()); - post_last_event = (first_event_ts == lastEventMonoTime()); + current_event_ts = first_event_ts = std::min(sec * 1e9 + begin_event_ts, lastest_event_ts); + post_last_event = (first_event_ts == lastest_event_ts); emit seekedTo((current_event_ts - begin_event_ts) / 1e9); } diff --git a/tools/cabana/streams/livestream.h b/tools/cabana/streams/livestream.h index 9ca7e8d745..24b9285092 100644 --- a/tools/cabana/streams/livestream.h +++ b/tools/cabana/streams/livestream.h @@ -1,5 +1,6 @@ #pragma once +#include #include #include @@ -14,9 +15,10 @@ public: LiveStream(QObject *parent); virtual ~LiveStream(); void start() override; - void stop() override; + void stop(); inline QDateTime beginDateTime() const { return begin_date_time; } - inline double routeStartTime() const override { return begin_event_ts / 1e9; } + inline uint64_t beginMonoTime() const override { return begin_event_ts; } + double maxSeconds() const override { return std::max(1.0, (lastest_event_ts - begin_event_ts) / 1e9); } void setSpeed(float speed) override { speed_ = speed; } double getSpeed() override { return speed_; } bool isPaused() const override { return paused_; } @@ -41,6 +43,7 @@ private: QDateTime begin_date_time; uint64_t begin_event_ts = 0; + uint64_t lastest_event_ts = 0; uint64_t current_event_ts = 0; uint64_t first_event_ts = 0; uint64_t first_update_ts = 0; diff --git a/tools/cabana/streams/pandastream.h b/tools/cabana/streams/pandastream.h index 3e76247680..ad792ec292 100644 --- a/tools/cabana/streams/pandastream.h +++ b/tools/cabana/streams/pandastream.h @@ -12,6 +12,12 @@ const uint32_t speeds[] = {10U, 20U, 50U, 100U, 125U, 250U, 500U, 1000U}; const uint32_t data_speeds[] = {10U, 20U, 50U, 100U, 125U, 250U, 500U, 1000U, 2000U, 5000U}; +struct BusConfig { + int can_speed_kbps = 500; + int data_speed_kbps = 2000; + bool can_fd = false; +}; + struct PandaStreamConfig { QString serial = ""; std::vector bus_config; diff --git a/tools/cabana/streams/replaystream.cc b/tools/cabana/streams/replaystream.cc index 302a25696e..09ba3db417 100644 --- a/tools/cabana/streams/replaystream.cc +++ b/tools/cabana/streams/replaystream.cc @@ -88,16 +88,10 @@ void ReplayStream::start() { replay->start(); } -void ReplayStream::stop() { - if (replay) { - replay->stop(); - } -} - bool ReplayStream::eventFilter(const Event *event) { static double prev_update_ts = 0; if (event->which == cereal::Event::Which::CAN) { - double current_sec = event->mono_time / 1e9 - routeStartTime(); + double current_sec = toSeconds(event->mono_time); capnp::FlatArrayMessageReader reader(event->data); auto e = reader.getRoot(); for (const auto &c : e.getCan()) { @@ -115,11 +109,6 @@ bool ReplayStream::eventFilter(const Event *event) { return true; } -void ReplayStream::seekTo(double ts) { - current_sec_ = ts; - replay->seekTo(std::max(double(0), ts), false); -} - void ReplayStream::pause(bool pause) { replay->pause(pause); emit(pause ? paused() : resume()); diff --git a/tools/cabana/streams/replaystream.h b/tools/cabana/streams/replaystream.h index ced78680d1..217d1ac1d3 100644 --- a/tools/cabana/streams/replaystream.h +++ b/tools/cabana/streams/replaystream.h @@ -16,17 +16,16 @@ class ReplayStream : public AbstractStream { public: ReplayStream(QObject *parent); void start() override; - void stop() override; bool loadRoute(const QString &route, const QString &data_dir, uint32_t replay_flags = REPLAY_FLAG_NONE); bool eventFilter(const Event *event); - void seekTo(double ts) override; + void seekTo(double ts) override { replay->seekTo(std::max(double(0), ts), false); } bool liveStreaming() const override { return false; } inline QString routeName() const override { return replay->route()->name(); } inline QString carFingerprint() const override { return replay->carFingerprint().c_str(); } - double totalSeconds() const override { return replay->totalSeconds(); } + double minSeconds() const override { return replay->minSeconds(); } + double maxSeconds() const { return replay->maxSeconds(); } inline QDateTime beginDateTime() const { return replay->routeDateTime(); } - inline double routeStartTime() const override { return replay->routeStartTime() / (double)1e9; } - inline const Route *route() const { return replay->route(); } + inline uint64_t beginMonoTime() const override { return replay->routeStartNanos(); } inline void setSpeed(float speed) override { replay->setSpeed(speed); } inline float getSpeed() const { return replay->getSpeed(); } inline Replay *getReplay() const { return replay.get(); } diff --git a/tools/cabana/tools/findsignal.cc b/tools/cabana/tools/findsignal.cc index 387a912787..8e1749ba8e 100644 --- a/tools/cabana/tools/findsignal.cc +++ b/tools/cabana/tools/findsignal.cc @@ -46,7 +46,7 @@ void FindSignalModel::search(std::function cmp) { auto it = std::find_if(first, last, [&](const CanEvent *e) { return cmp(get_raw_value(e->dat, e->size, s.sig)); }); if (it != last) { auto values = s.values; - values += QString("(%1, %2)").arg((*it)->mono_time / 1e9 - can->routeStartTime(), 0, 'f', 2).arg(get_raw_value((*it)->dat, (*it)->size, s.sig)); + values += QString("(%1, %2)").arg(can->toSeconds((*it)->mono_time), 0, 'f', 3).arg(get_raw_value((*it)->dat, (*it)->size, s.sig)); std::lock_guard lk(lock); filtered_signals.push_back({.id = s.id, .mono_time = (*it)->mono_time, .sig = s.sig, .values = values}); } @@ -217,10 +217,10 @@ void FindSignalDlg::setInitialSignals() { double first_time_val = first_time_edit->text().toDouble(); double last_time_val = last_time_edit->text().toDouble(); auto [first_sec, last_sec] = std::minmax(first_time_val, last_time_val); - uint64_t first_time = (can->routeStartTime() + first_sec) * 1e9; + uint64_t first_time = can->toMonoTime(first_sec); model->last_time = std::numeric_limits::max(); if (last_sec > 0) { - model->last_time = (can->routeStartTime() + last_sec) * 1e9; + model->last_time = can->toMonoTime(last_sec); } model->initial_signals.clear(); diff --git a/tools/cabana/utils/export.cc b/tools/cabana/utils/export.cc index 10f69ad548..79ca97ba8f 100644 --- a/tools/cabana/utils/export.cc +++ b/tools/cabana/utils/export.cc @@ -10,11 +10,10 @@ namespace utils { void exportToCSV(const QString &file_name, std::optional msg_id) { QFile file(file_name); if (file.open(QIODevice::ReadWrite | QIODevice::Truncate)) { - const uint64_t start_time = can->routeStartTime(); QTextStream stream(&file); stream << "time,addr,bus,data\n"; for (auto e : msg_id ? can->events(*msg_id) : can->allEvents()) { - stream << QString::number((e->mono_time / 1e9) - start_time, 'f', 2) << "," + stream << QString::number(can->toSeconds(e->mono_time), 'f', 3) << "," << "0x" << QString::number(e->address, 16) << "," << e->src << "," << "0x" << QByteArray::fromRawData((const char *)e->dat, e->size).toHex().toUpper() << "\n"; } @@ -30,9 +29,8 @@ void exportSignalsToCSV(const QString &file_name, const MessageId &msg_id) { stream << "," << s->name; stream << "\n"; - const uint64_t start_time = can->routeStartTime(); for (auto e : can->events(msg_id)) { - stream << QString::number((e->mono_time / 1e9) - start_time, 'f', 2) << "," + stream << QString::number(can->toSeconds(e->mono_time), 'f', 3) << "," << "0x" << QString::number(e->address, 16) << "," << e->src; for (auto s : msg->sigs) { double value = 0; diff --git a/tools/cabana/videowidget.cc b/tools/cabana/videowidget.cc index d2f78babc1..8d2f8674b3 100644 --- a/tools/cabana/videowidget.cc +++ b/tools/cabana/videowidget.cc @@ -27,6 +27,13 @@ static const QColor timeline_colors[] = { [(int)TimelineType::AlertCritical] = QColor(199, 0, 57), }; +static Replay *getReplay() { + auto stream = qobject_cast(can); + if (!stream) return nullptr; + + return stream->getReplay(); +} + VideoWidget::VideoWidget(QWidget *parent) : QFrame(parent) { setFrameStyle(QFrame::StyledPanel | QFrame::Plain); auto main_layout = new QVBoxLayout(this); @@ -76,7 +83,7 @@ QHBoxLayout *VideoWidget::createPlaybackController() { // set speed to 1.0 speed_btn->menu()->actions()[7]->setChecked(true); can->pause(false); - can->seekTo(can->totalSeconds() + 1); + can->seekTo(can->maxSeconds() + 1); }); } @@ -141,7 +148,7 @@ QWidget *VideoWidget::createCameraWidget() { QStackedLayout *stacked = new QStackedLayout(); stacked->setStackingMode(QStackedLayout::StackAll); - stacked->addWidget(cam_widget = new CameraWidget("camerad", VISION_STREAM_ROAD, false)); + stacked->addWidget(cam_widget = new StreamCameraView("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)); @@ -149,9 +156,11 @@ QWidget *VideoWidget::createCameraWidget() { l->addWidget(slider = new Slider(w)); slider->setSingleStep(0); + slider->setTimeRange(can->minSeconds(), can->maxSeconds()); - setMaximumTime(can->totalSeconds()); QObject::connect(slider, &QSlider::sliderReleased, [this]() { can->seekTo(slider->currentSecond()); }); + QObject::connect(can, &AbstractStream::paused, cam_widget, [c = cam_widget]() { c->showPausedOverlay(); }); + QObject::connect(can, &AbstractStream::resume, cam_widget, [c = cam_widget]() { c->update(); }); QObject::connect(can, &AbstractStream::eventsMerged, this, [this]() { slider->update(); }); QObject::connect(cam_widget, &CameraWidget::clicked, []() { can->pause(!can->isPaused()); }); QObject::connect(cam_widget, &CameraWidget::vipcAvailableStreamsUpdated, this, &VideoWidget::vipcAvailableStreamsUpdated); @@ -161,7 +170,7 @@ QWidget *VideoWidget::createCameraWidget() { auto replay = static_cast(can)->getReplay(); QObject::connect(replay, &Replay::qLogLoaded, slider, &Slider::parseQLog, Qt::QueuedConnection); - QObject::connect(replay, &Replay::totalSecondsUpdated, this, &VideoWidget::setMaximumTime, Qt::QueuedConnection); + QObject::connect(replay, &Replay::minMaxTimeChanged, this, &VideoWidget::timeRangeChanged, Qt::QueuedConnection); return w; } @@ -185,7 +194,7 @@ void VideoWidget::vipcAvailableStreamsUpdated(std::set streams } void VideoWidget::loopPlaybackClicked() { - auto replay = qobject_cast(can)->getReplay(); + auto replay = getReplay(); if (!replay) return; if (replay->hasFlag(REPLAY_FLAG_NO_LOOP)) { @@ -197,18 +206,15 @@ void VideoWidget::loopPlaybackClicked() { } } -void VideoWidget::setMaximumTime(double sec) { - maximum_time = sec; - slider->setTimeRange(0, sec); -} - -void VideoWidget::timeRangeChanged(const std::optional> &time_range) { +void VideoWidget::timeRangeChanged() { + const auto time_range = can->timeRange(); if (can->liveStreaming()) { skip_to_end_btn->setEnabled(!time_range.has_value()); return; } time_range ? slider->setTimeRange(time_range->first, time_range->second) - : slider->setTimeRange(0, maximum_time); + : slider->setTimeRange(can->minSeconds(), can->maxSeconds()); + updateState(); } QString VideoWidget::formatTime(double sec, bool include_milliseconds) { @@ -219,8 +225,9 @@ QString VideoWidget::formatTime(double sec, bool include_milliseconds) { void VideoWidget::updateState() { if (slider) { - if (!slider->isSliderDown()) + 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))); @@ -242,15 +249,14 @@ Slider::Slider(QWidget *parent) : QSlider(Qt::Horizontal, parent) { } AlertInfo Slider::alertInfo(double seconds) { - uint64_t mono_time = (seconds + can->routeStartTime()) * 1e9; + uint64_t mono_time = can->toMonoTime(seconds); auto alert_it = alerts.lower_bound(mono_time); bool has_alert = (alert_it != alerts.end()) && ((alert_it->first - mono_time) <= 1e8); return has_alert ? alert_it->second : AlertInfo{}; } QPixmap Slider::thumbnail(double seconds) { - uint64_t mono_time = (seconds + can->routeStartTime()) * 1e9; - auto it = thumbnails.lowerBound(mono_time); + auto it = thumbnails.lowerBound(can->toMonoTime(seconds)); return it != thumbnails.end() ? it.value() : QPixmap(); } @@ -298,16 +304,18 @@ void Slider::paintEvent(QPaintEvent *ev) { p.fillRect(r, color); }; - const auto replay = qobject_cast(can)->getReplay(); - for (auto [begin, end, type] : replay->getTimeline()) { - fillRange(begin, end, timeline_colors[(int)type]); - } + auto replay = getReplay(); + if (replay) { + for (auto [begin, end, type] : replay->getTimeline()) { + fillRange(begin, end, timeline_colors[(int)type]); + } - QColor empty_color = palette().color(QPalette::Window); - empty_color.setAlpha(160); - for (const auto &[n, seg] : replay->segments()) { - if (!(seg && seg->isLoaded())) - fillRange(n * 60.0, (n + 1) * 60.0, empty_color); + QColor empty_color = palette().color(QPalette::Window); + empty_color.setAlpha(160); + for (const auto &[n, seg] : replay->segments()) { + if (!(seg && seg->isLoaded())) + fillRange(n * 60.0, (n + 1) * 60.0, empty_color); + } } QStyleOptionSlider opt; @@ -411,3 +419,22 @@ void InfoLabel::paintEvent(QPaintEvent *event) { p.drawText(text_rect, Qt::AlignTop | Qt::AlignHCenter | Qt::TextWordWrap, text); } } + +StreamCameraView::StreamCameraView(std::string stream_name, VisionStreamType stream_type, bool zoom, QWidget *parent) + : CameraWidget(stream_name, stream_type, zoom, parent) { + fade_animation = new QPropertyAnimation(this, "overlayOpacity"); + fade_animation->setDuration(500); + fade_animation->setStartValue(0.2f); + fade_animation->setEndValue(0.7f); +} + +void StreamCameraView::paintGL() { + CameraWidget::paintGL(); + + if (can->isPaused()) { + QPainter p(this); + p.setPen(QColor(200, 200, 200, static_cast(255 * overlay_opacity))); + p.setFont(QFont(font().family(), 16, QFont::Bold)); + p.drawText(rect(), Qt::AlignCenter, tr("PAUSED")); + } +} diff --git a/tools/cabana/videowidget.h b/tools/cabana/videowidget.h index ac34c66007..1e793e9d1d 100644 --- a/tools/cabana/videowidget.h +++ b/tools/cabana/videowidget.h @@ -4,10 +4,12 @@ #include #include #include +#include #include #include #include +#include #include #include @@ -57,16 +59,34 @@ private: InfoLabel *thumbnail_label; }; +class StreamCameraView : public CameraWidget { + Q_OBJECT + Q_PROPERTY(float overlayOpacity READ overlayOpacity WRITE setOverlayOpacity) + +public: + StreamCameraView(std::string stream_name, VisionStreamType stream_type, bool zoom, QWidget *parent = nullptr); + void paintGL() override; + void showPausedOverlay() { fade_animation->start(); } + float overlayOpacity() const { return overlay_opacity; } + void setOverlayOpacity(float opacity) { + overlay_opacity = opacity; + update(); + } + +private: + float overlay_opacity; + QPropertyAnimation *fade_animation; +}; + class VideoWidget : public QFrame { Q_OBJECT public: VideoWidget(QWidget *parnet = nullptr); - void setMaximumTime(double sec); protected: QString formatTime(double sec, bool include_milliseconds = false); - void timeRangeChanged(const std::optional> &time_range); + void timeRangeChanged(); void updateState(); void updatePlayBtnState(); QWidget *createCameraWidget(); @@ -74,8 +94,7 @@ protected: void loopPlaybackClicked(); void vipcAvailableStreamsUpdated(std::set streams); - CameraWidget *cam_widget; - double maximum_time = 0; + StreamCameraView *cam_widget; QToolButton *time_btn = nullptr; ToolButton *seek_backward_btn = nullptr; ToolButton *play_btn = nullptr; diff --git a/tools/replay/consoleui.cc b/tools/replay/consoleui.cc index eaff78c691..21ca0ad74b 100644 --- a/tools/replay/consoleui.cc +++ b/tools/replay/consoleui.cc @@ -171,7 +171,7 @@ void ConsoleUI::updateStatus() { if (status != Status::Paused) { auto events = replay->events(); - uint64_t current_mono_time = replay->routeStartTime() + replay->currentSeconds() * 1e9; + uint64_t current_mono_time = replay->routeStartNanos() + replay->currentSeconds() * 1e9; bool playing = !events->empty() && events->back().mono_time > current_mono_time; status = playing ? Status::Playing : Status::Waiting; } @@ -262,10 +262,10 @@ void ConsoleUI::updateTimeline() { mvwhline(win, 2, 0, ' ', width); wattroff(win, COLOR_PAIR(Color::Disengaged)); - const int total_sec = replay->totalSeconds(); + const int total_sec = replay->maxSeconds() - replay->minSeconds(); for (auto [begin, end, type] : replay->getTimeline()) { - int start_pos = (begin / total_sec) * width; - int end_pos = (end / total_sec) * width; + int start_pos = ((begin - replay->minSeconds()) / total_sec) * width; + int end_pos = ((end - replay->minSeconds()) / total_sec) * width; if (type == TimelineType::Engaged) { mvwchgat(win, 1, start_pos, end_pos - start_pos + 1, A_COLOR, Color::Engaged, NULL); mvwchgat(win, 2, start_pos, end_pos - start_pos + 1, A_COLOR, Color::Engaged, NULL); @@ -280,7 +280,7 @@ void ConsoleUI::updateTimeline() { } } - int cur_pos = ((double)replay->currentSeconds() / total_sec) * width; + int cur_pos = ((replay->currentSeconds() - replay->minSeconds()) / total_sec) * width; wattron(win, COLOR_PAIR(Color::BrightWhite)); mvwaddch(win, 0, cur_pos, ACS_VLINE); mvwaddch(win, 3, cur_pos, ACS_VLINE); diff --git a/tools/replay/replay.cc b/tools/replay/replay.cc index 15daecf01f..4fea879f8b 100644 --- a/tools/replay/replay.cc +++ b/tools/replay/replay.cc @@ -85,6 +85,7 @@ bool Replay::load() { return false; } rInfo("load route %s with %zu valid segments", qPrintable(route_->name()), segments_.size()); + max_seconds_ = (segments_.rbegin()->first + 1) * 60; return true; } @@ -108,7 +109,11 @@ void Replay::seekTo(double seconds, bool relative) { target_time = std::max(double(0.0), target_time); int target_segment = (int)target_time / 60; if (segments_.count(target_segment) == 0) { - rWarning("Can't seek to %d s segment %d is invalid", (int)target_time, target_segment); + rWarning("Can't seek to %.2f s segment %d is invalid", target_time, target_segment); + return true; + } + if (target_time > max_seconds_) { + rWarning("Can't seek to %.2f s, time is invalid", target_time); return true; } @@ -193,15 +198,22 @@ void Replay::buildTimeline() { } } + if (it->first == route_segments.rbegin()->first) { + if (engaged) { + timeline.push_back({toSeconds(engaged_begin), toSeconds(log->events.back().mono_time), TimelineType::Engaged}); + } + if (!alert_type.empty() && alert_size != cereal::ControlsState::AlertSize::NONE) { + timeline.push_back({toSeconds(alert_begin), toSeconds(log->events.back().mono_time), timeline_types[(int)alert_status]}); + } + + max_seconds_ = std::ceil(toSeconds(log->events.back().mono_time)); + emit minMaxTimeChanged(route_segments.cbegin()->first * 60.0, max_seconds_); + } { std::lock_guard lk(timeline_lock); timeline_.insert(timeline_.end(), timeline.begin(), timeline.end()); std::sort(timeline_.begin(), timeline_.end(), [](auto &l, auto &r) { return std::get<2>(l) < std::get<2>(r); }); } - - if (it->first == route_segments.rbegin()->first) { - emit totalSecondsUpdated(toSeconds(log->events.back().mono_time)); - } emit qLogLoaded(log); } } @@ -463,7 +475,7 @@ void Replay::streamThread() { int last_segment = segments_.rbegin()->first; if (current_segment_ >= last_segment && isSegmentMerged(last_segment)) { rInfo("reaches the end of route, restart from beginning"); - QMetaObject::invokeMethod(this, std::bind(&Replay::seekTo, this, 0, false), Qt::QueuedConnection); + QMetaObject::invokeMethod(this, std::bind(&Replay::seekTo, this, minSeconds(), false), Qt::QueuedConnection); } } } diff --git a/tools/replay/replay.h b/tools/replay/replay.h index 7e1815ede0..ac829ffb71 100644 --- a/tools/replay/replay.h +++ b/tools/replay/replay.h @@ -76,9 +76,10 @@ public: inline double currentSeconds() const { return double(cur_mono_time_ - route_start_ts_) / 1e9; } inline QDateTime routeDateTime() const { return route_date_time_; } inline QDateTime currentDateTime() const { return route_date_time_.addSecs(currentSeconds()); } - inline uint64_t routeStartTime() const { return route_start_ts_; } + inline uint64_t routeStartNanos() const { return route_start_ts_; } inline double toSeconds(uint64_t mono_time) const { return (mono_time - route_start_ts_) / 1e9; } - inline int totalSeconds() const { return (!segments_.empty()) ? (segments_.rbegin()->first + 1) * 60 : 0; } + inline double minSeconds() const { return !segments_.empty() ? segments_.begin()->first * 60 : 0; } + inline double maxSeconds() const { return max_seconds_; } inline void setSpeed(float speed) { speed_ = speed; } inline float getSpeed() const { return speed_; } inline const std::vector *events() const { return &events_; } @@ -95,7 +96,7 @@ signals: void seeking(double sec); void seekedTo(double sec); void qLogLoaded(std::shared_ptr qlog); - void totalSecondsUpdated(double sec); + void minMaxTimeChanged(double min_sec, double max_sec); protected slots: void segmentLoadFinished(bool success); @@ -133,6 +134,7 @@ protected: QDateTime route_date_time_; uint64_t route_start_ts_ = 0; std::atomic cur_mono_time_ = 0; + std::atomic max_seconds_ = 0; std::vector events_; std::set merged_segments_;