mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-09-12 11:13:46 +08:00
Merge branch 'openpilot/master' into sync-20250108
# Conflicts: # common/base.py # common/comma_connect.py # opendbc_repo # panda
This commit is contained in:
+62
-13
@@ -248,6 +248,7 @@ std::tuple<int, int, bool> BinaryView::getSelection(QModelIndex index) {
|
||||
|
||||
void BinaryViewModel::refresh() {
|
||||
beginResetModel();
|
||||
bit_flip_tracker = {};
|
||||
items.clear();
|
||||
if (auto dbc_msg = dbc()->msg(msg_id)) {
|
||||
row_count = dbc_msg->size;
|
||||
@@ -295,7 +296,7 @@ void BinaryViewModel::updateItem(int row, int col, uint8_t val, const QColor &co
|
||||
void BinaryViewModel::updateState() {
|
||||
const auto &last_msg = can->lastMessage(msg_id);
|
||||
const auto &binary = last_msg.dat;
|
||||
// data size may changed.
|
||||
// Handle size changes in binary data
|
||||
if (binary.size() > row_count) {
|
||||
beginInsertRows({}, row_count, binary.size() - 1);
|
||||
row_count = binary.size();
|
||||
@@ -303,26 +304,74 @@ void BinaryViewModel::updateState() {
|
||||
endInsertRows();
|
||||
}
|
||||
|
||||
const double max_f = 255.0;
|
||||
const double factor = 0.25;
|
||||
const double scaler = max_f / log2(1.0 + factor);
|
||||
for (int i = 0; i < binary.size(); ++i) {
|
||||
auto &bit_flips = heatmap_live_mode ? last_msg.bit_flip_counts : getBitFlipChanges(binary.size());
|
||||
// Find the maximum bit flip count across the message
|
||||
uint32_t max_bit_flip_count = 1; // Default to 1 to avoid division by zero
|
||||
for (const auto &row : bit_flips) {
|
||||
for (uint32_t count : row) {
|
||||
max_bit_flip_count = std::max(max_bit_flip_count, count);
|
||||
}
|
||||
}
|
||||
|
||||
const double max_alpha = 255.0;
|
||||
const double min_alpha_with_signal = 25.0; // Base alpha for small flip counts
|
||||
const double min_alpha_no_signal = 10.0; // Base alpha for small flip counts for no signal bits
|
||||
const double log_factor = 1.0 + 0.2; // Factor for logarithmic scaling
|
||||
const double log_scaler = max_alpha / log2(log_factor * max_bit_flip_count);
|
||||
|
||||
for (size_t i = 0; i < binary.size(); ++i) {
|
||||
for (int j = 0; j < 8; ++j) {
|
||||
auto &item = items[i * column_count + j];
|
||||
int val = ((binary[i] >> (7 - j)) & 1) != 0 ? 1 : 0;
|
||||
// Bit update frequency based highlighting
|
||||
double offset = !item.sigs.empty() ? 50 : 0;
|
||||
auto n = last_msg.last_changes[i].bit_change_counts[j];
|
||||
double min_f = n == 0 ? offset : offset + 25;
|
||||
double alpha = std::clamp(offset + log2(1.0 + factor * (double)n / (double)last_msg.count) * scaler, min_f, max_f);
|
||||
int bit_val = (binary[i] >> (7 - j)) & 1;
|
||||
|
||||
double alpha = item.sigs.empty() ? 0 : min_alpha_with_signal;
|
||||
uint32_t flip_count = bit_flips[i][j];
|
||||
if (flip_count > 0) {
|
||||
double normalized_alpha = log2(1.0 + flip_count * log_factor) * log_scaler;
|
||||
double min_alpha = item.sigs.empty() ? min_alpha_no_signal : min_alpha_with_signal;
|
||||
alpha = std::clamp(normalized_alpha, min_alpha, max_alpha);
|
||||
}
|
||||
|
||||
auto color = item.bg_color;
|
||||
color.setAlpha(alpha);
|
||||
updateItem(i, j, val, color);
|
||||
updateItem(i, j, bit_val, color);
|
||||
}
|
||||
updateItem(i, 8, binary[i], last_msg.colors[i]);
|
||||
}
|
||||
}
|
||||
|
||||
const std::vector<std::array<uint32_t, 8>> &BinaryViewModel::getBitFlipChanges(size_t msg_size) {
|
||||
// Return cached results if time range and data are unchanged
|
||||
auto time_range = can->timeRange();
|
||||
if (bit_flip_tracker.time_range == time_range && !bit_flip_tracker.flip_counts.empty())
|
||||
return bit_flip_tracker.flip_counts;
|
||||
|
||||
bit_flip_tracker.time_range = time_range;
|
||||
bit_flip_tracker.flip_counts.assign(msg_size, std::array<uint32_t, 8>{});
|
||||
|
||||
// Iterate over events within the specified time range and calculate bit flips
|
||||
auto [first, last] = can->eventsInRange(msg_id, time_range);
|
||||
if (std::distance(first, last) <= 1) return bit_flip_tracker.flip_counts;
|
||||
|
||||
std::vector<uint8_t> prev_values((*first)->dat, (*first)->dat + (*first)->size);
|
||||
for (auto it = std::next(first); it != last; ++it) {
|
||||
const CanEvent *event = *it;
|
||||
int size = std::min<int>(msg_size, event->size);
|
||||
for (int i = 0; i < size; ++i) {
|
||||
const uint8_t diff = event->dat[i] ^ prev_values[i];
|
||||
if (!diff) continue;
|
||||
|
||||
auto &bit_flips = bit_flip_tracker.flip_counts[i];
|
||||
for (int bit = 0; bit < 8; ++bit) {
|
||||
if (diff & (1u << bit)) ++bit_flips[7 - bit];
|
||||
}
|
||||
prev_values[i] = event->dat[i];
|
||||
}
|
||||
}
|
||||
|
||||
return bit_flip_tracker.flip_counts;
|
||||
}
|
||||
|
||||
QVariant BinaryViewModel::headerData(int section, Qt::Orientation orientation, int role) const {
|
||||
if (orientation == Qt::Vertical) {
|
||||
switch (role) {
|
||||
@@ -388,7 +437,7 @@ void BinaryItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &op
|
||||
painter->fillRect(option.rect, item->bg_color);
|
||||
}
|
||||
auto color_role = item->sigs.contains(bin_view->hovered_sig) ? QPalette::BrightText : QPalette::Text;
|
||||
painter->setPen(option.palette.color(color_role));
|
||||
painter->setPen(option.palette.color(bin_view->is_message_active ? QPalette::Normal : QPalette::Disabled, color_role));
|
||||
}
|
||||
|
||||
if (item->sigs.size() > 1) {
|
||||
|
||||
@@ -39,6 +39,12 @@ public:
|
||||
Qt::ItemFlags flags(const QModelIndex &index) const override {
|
||||
return (index.column() == column_count - 1) ? Qt::ItemIsEnabled : Qt::ItemIsEnabled | Qt::ItemIsSelectable;
|
||||
}
|
||||
const std::vector<std::array<uint32_t, 8>> &getBitFlipChanges(size_t msg_size);
|
||||
|
||||
struct BitFlipTracker {
|
||||
std::optional<std::pair<double, double>> time_range;
|
||||
std::vector<std::array<uint32_t, 8>> flip_counts;
|
||||
} bit_flip_tracker;
|
||||
|
||||
struct Item {
|
||||
QColor bg_color = QColor(102, 86, 169, 255);
|
||||
@@ -49,7 +55,7 @@ public:
|
||||
bool valid = false;
|
||||
};
|
||||
std::vector<Item> items;
|
||||
|
||||
bool heatmap_live_mode = true;
|
||||
MessageId msg_id;
|
||||
int row_count = 0;
|
||||
const int column_count = 9;
|
||||
@@ -63,8 +69,13 @@ public:
|
||||
void setMessage(const MessageId &message_id);
|
||||
void highlight(const cabana::Signal *sig);
|
||||
QSet<const cabana::Signal*> getOverlappingSignals() const;
|
||||
inline void updateState() { model->updateState(); }
|
||||
void updateState() { model->updateState(); }
|
||||
void paintEvent(QPaintEvent *event) override {
|
||||
is_message_active = can->isMessageActive(model->msg_id);
|
||||
QTableView::paintEvent(event);
|
||||
}
|
||||
QSize minimumSizeHint() const override;
|
||||
void setHeatmapLiveMode(bool live) { model->heatmap_live_mode = live; updateState(); }
|
||||
|
||||
signals:
|
||||
void signalClicked(const cabana::Signal *sig);
|
||||
@@ -86,6 +97,7 @@ private:
|
||||
QModelIndex anchor_index;
|
||||
BinaryViewModel *model;
|
||||
BinaryItemDelegate *delegate;
|
||||
bool is_message_active = false;
|
||||
const cabana::Signal *resize_sig = nullptr;
|
||||
const cabana::Signal *hovered_sig = nullptr;
|
||||
friend class BinaryItemDelegate;
|
||||
|
||||
@@ -842,6 +842,8 @@ void ChartView::setSeriesType(SeriesType type) {
|
||||
}
|
||||
updateSeriesPoints();
|
||||
updateTitle();
|
||||
|
||||
menu->actions()[(int)type]->setChecked(true);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ ChartsWidget::ChartsWidget(QWidget *parent) : QFrame(parent) {
|
||||
main_layout->setSpacing(0);
|
||||
|
||||
// toolbar
|
||||
QToolBar *toolbar = new QToolBar(tr("Charts"), this);
|
||||
toolbar = new QToolBar(tr("Charts"), this);
|
||||
int icon_size = style()->pixelMetric(QStyle::PM_SmallIconSize);
|
||||
toolbar->setIconSize({icon_size, icon_size});
|
||||
|
||||
@@ -34,6 +34,21 @@ ChartsWidget::ChartsWidget(QWidget *parent) : QFrame(parent) {
|
||||
toolbar->addWidget(title_label = new QLabel());
|
||||
title_label->setContentsMargins(0, 0, style()->pixelMetric(QStyle::PM_LayoutHorizontalSpacing), 0);
|
||||
|
||||
auto chart_type_action = toolbar->addAction("");
|
||||
QMenu *chart_type_menu = new QMenu(this);
|
||||
auto types = std::array{tr("Line"), tr("Step"), tr("Scatter")};
|
||||
for (int i = 0; i < types.size(); ++i) {
|
||||
QString type_text = types[i];
|
||||
chart_type_menu->addAction(type_text, this, [=]() {
|
||||
settings.chart_series_type = i;
|
||||
chart_type_action->setText("Type: " + type_text);
|
||||
settingChanged();
|
||||
});
|
||||
}
|
||||
chart_type_action->setText("Type: " + types[settings.chart_series_type]);
|
||||
chart_type_action->setMenu(chart_type_menu);
|
||||
qobject_cast<QToolButton *>(toolbar->widgetForAction(chart_type_action))->setPopupMode(QToolButton::InstantPopup);
|
||||
|
||||
QMenu *menu = new QMenu(this);
|
||||
for (int i = 0; i < MAX_COLUMN_COUNT; ++i) {
|
||||
menu->addAction(tr("%1").arg(i + 1), [=]() { setColumnCount(i + 1); });
|
||||
@@ -42,13 +57,13 @@ ChartsWidget::ChartsWidget(QWidget *parent) : QFrame(parent) {
|
||||
columns_action->setMenu(menu);
|
||||
qobject_cast<QToolButton*>(toolbar->widgetForAction(columns_action))->setPopupMode(QToolButton::InstantPopup);
|
||||
|
||||
QLabel *stretch_label = new QLabel(this);
|
||||
stretch_label->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);
|
||||
toolbar->addWidget(stretch_label);
|
||||
QWidget *spacer = new QWidget(this);
|
||||
spacer->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred);
|
||||
toolbar->addWidget(spacer);
|
||||
|
||||
range_lb_action = toolbar->addWidget(range_lb = new QLabel(this));
|
||||
range_slider = new LogSlider(1000, Qt::Horizontal, this);
|
||||
range_slider->setMaximumWidth(200);
|
||||
range_slider->setFixedWidth(150 * qApp->devicePixelRatio());
|
||||
range_slider->setToolTip(tr("Set the chart range"));
|
||||
range_slider->setRange(1, settings.max_cached_minutes * 60);
|
||||
range_slider->setSingleStep(1);
|
||||
@@ -121,10 +136,12 @@ ChartsWidget::ChartsWidget(QWidget *parent) : QFrame(parent) {
|
||||
setIsDocked(true);
|
||||
newTab();
|
||||
qApp->installEventFilter(this);
|
||||
|
||||
setWhatsThis(tr(R"(
|
||||
<b>Chart view</b><br />
|
||||
<!-- TODO: add descprition here -->
|
||||
<b>Chart View</b><br />
|
||||
<b>Click</b>: Click to seek to a corresponding time.<br />
|
||||
<b>Drag</b>: Zoom into the chart.<br />
|
||||
<b>Shift + Drag</b>: Scrub through the chart to view values.<br />
|
||||
<b>Right Mouse</b>: Open the context menu.<br />
|
||||
)"));
|
||||
}
|
||||
|
||||
@@ -176,6 +193,7 @@ QRect ChartsWidget::chartVisibleRect(ChartView *chart) {
|
||||
}
|
||||
|
||||
void ChartsWidget::showValueTip(double sec) {
|
||||
emit showTip(sec);
|
||||
if (sec < 0 && !value_tip_visible_) return;
|
||||
|
||||
value_tip_visible_ = sec >= 0;
|
||||
@@ -219,7 +237,7 @@ void ChartsWidget::setIsDocked(bool docked) {
|
||||
|
||||
void ChartsWidget::updateToolBar() {
|
||||
title_label->setText(tr("Charts: %1").arg(charts.size()));
|
||||
columns_action->setText(tr("Column: %1").arg(column_count));
|
||||
columns_action->setText(tr("Columns: %1").arg(column_count));
|
||||
range_lb->setText(utils::formatSeconds(max_chart_range));
|
||||
|
||||
bool is_zoomed = can->timeRange().has_value();
|
||||
@@ -241,7 +259,9 @@ void ChartsWidget::settingChanged() {
|
||||
c->setTheme(theme);
|
||||
}
|
||||
}
|
||||
range_slider->setRange(1, settings.max_cached_minutes * 60);
|
||||
if (range_slider->maximum() != settings.max_cached_minutes * 60) {
|
||||
range_slider->setRange(1, settings.max_cached_minutes * 60);
|
||||
}
|
||||
for (auto c : charts) {
|
||||
c->setFixedHeight(settings.chart_height);
|
||||
c->setSeriesType((SeriesType)settings.chart_series_type);
|
||||
@@ -380,7 +400,7 @@ void ChartsWidget::doAutoScroll() {
|
||||
}
|
||||
|
||||
QSize ChartsWidget::minimumSizeHint() const {
|
||||
return QSize(CHART_MIN_WIDTH, QWidget::minimumSizeHint().height());
|
||||
return QSize(CHART_MIN_WIDTH * 1.5 * qApp->devicePixelRatio(), QWidget::minimumSizeHint().height());
|
||||
}
|
||||
|
||||
void ChartsWidget::newChart() {
|
||||
@@ -529,20 +549,14 @@ void ChartsContainer::dropEvent(QDropEvent *event) {
|
||||
|
||||
void ChartsContainer::paintEvent(QPaintEvent *ev) {
|
||||
if (!drop_indictor_pos.isNull() && !childAt(drop_indictor_pos)) {
|
||||
QRect r;
|
||||
QRect r = geometry();
|
||||
r.setHeight(CHART_SPACING);
|
||||
if (auto insert_after = getDropAfter(drop_indictor_pos)) {
|
||||
QRect area = insert_after->geometry();
|
||||
r = QRect(area.left(), area.bottom() + 1, area.width(), CHART_SPACING);
|
||||
} else {
|
||||
r = geometry();
|
||||
r.setHeight(CHART_SPACING);
|
||||
r.moveTop(insert_after->geometry().bottom());
|
||||
}
|
||||
|
||||
QPainter p(this);
|
||||
p.setPen(QPen(palette().highlight(), 2));
|
||||
p.drawLine(r.topLeft() + QPoint(1, 0), r.bottomLeft() + QPoint(1, 0));
|
||||
p.drawLine(r.topLeft() + QPoint(0, r.height() / 2), r.topRight() + QPoint(0, r.height() / 2));
|
||||
p.drawLine(r.topRight(), r.bottomRight());
|
||||
p.fillRect(r, palette().highlight());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include <QLabel>
|
||||
#include <QScrollArea>
|
||||
#include <QTimer>
|
||||
#include <QToolBar>
|
||||
#include <QUndoCommand>
|
||||
#include <QUndoStack>
|
||||
|
||||
@@ -52,6 +53,7 @@ public slots:
|
||||
signals:
|
||||
void toggleChartsDocking();
|
||||
void seriesChanged();
|
||||
void showTip(double seconds);
|
||||
|
||||
private:
|
||||
QSize minimumSizeHint() const override;
|
||||
@@ -88,6 +90,7 @@ private:
|
||||
bool is_docked = true;
|
||||
ToolButton *dock_btn;
|
||||
|
||||
QToolBar *toolbar;
|
||||
QAction *undo_zoom_action;
|
||||
QAction *redo_zoom_action;
|
||||
QAction *reset_zoom_action;
|
||||
|
||||
@@ -5,15 +5,9 @@
|
||||
#include <QPainter>
|
||||
|
||||
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);
|
||||
|
||||
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());
|
||||
|
||||
points.clear();
|
||||
double value = 0;
|
||||
auto [first, last] = can->eventsInRange(msg_id, std::make_pair(last_msg_ts -range, last_msg_ts));
|
||||
for (auto it = first; it != last; ++it) {
|
||||
if (sig->getValue((*it)->dat, (*it)->size, &value)) {
|
||||
points.emplace_back(((*it)->mono_time - (*first)->mono_time) / 1e9, value);
|
||||
|
||||
@@ -45,6 +45,7 @@ cabana::Msg &cabana::Msg::operator=(const cabana::Msg &other) {
|
||||
name = other.name;
|
||||
size = other.size;
|
||||
comment = other.comment;
|
||||
transmitter = other.transmitter;
|
||||
|
||||
for (auto s : sigs) delete s;
|
||||
sigs.clear();
|
||||
|
||||
@@ -8,16 +8,16 @@
|
||||
#include <QMetaType>
|
||||
#include <QString>
|
||||
|
||||
|
||||
const QString UNTITLED = "untitled";
|
||||
const QString DEFAULT_NODE_NAME = "XXX";
|
||||
constexpr int CAN_MAX_DATA_BYTES = 64;
|
||||
|
||||
struct MessageId {
|
||||
uint8_t source = 0;
|
||||
uint32_t address = 0;
|
||||
|
||||
QString toString() const {
|
||||
return QString("%1:%2").arg(source).arg(address, 1, 16);
|
||||
return QString("%1:%2").arg(source).arg(QString::number(address, 16).toUpper());
|
||||
}
|
||||
|
||||
bool operator==(const MessageId &other) const {
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
|
||||
#include <QFormLayout>
|
||||
#include <QMenu>
|
||||
#include <QSpacerItem>
|
||||
#include <QRadioButton>
|
||||
#include <QToolBar>
|
||||
|
||||
#include "tools/cabana/commands.h"
|
||||
#include "tools/cabana/mainwin.h"
|
||||
@@ -20,19 +21,7 @@ DetailWidget::DetailWidget(ChartsWidget *charts, QWidget *parent) : charts(chart
|
||||
tabbar->setContextMenuPolicy(Qt::CustomContextMenu);
|
||||
main_layout->addWidget(tabbar);
|
||||
|
||||
// message title
|
||||
QHBoxLayout *title_layout = new QHBoxLayout();
|
||||
title_layout->setContentsMargins(3, 6, 3, 0);
|
||||
auto spacer = new QSpacerItem(0, 1);
|
||||
title_layout->addItem(spacer);
|
||||
title_layout->addWidget(name_label = new ElidedLabel(this), 1);
|
||||
name_label->setStyleSheet("QLabel{font-weight:bold;}");
|
||||
name_label->setAlignment(Qt::AlignCenter);
|
||||
auto edit_btn = new ToolButton("pencil", tr("Edit Message"));
|
||||
title_layout->addWidget(edit_btn);
|
||||
title_layout->addWidget(remove_btn = new ToolButton("x-lg", tr("Remove Message")));
|
||||
spacer->changeSize(edit_btn->sizeHint().width() * 2 + 9, 1);
|
||||
main_layout->addLayout(title_layout);
|
||||
createToolBar();
|
||||
|
||||
// warning
|
||||
warning_widget = new QWidget(this);
|
||||
@@ -58,8 +47,6 @@ DetailWidget::DetailWidget(ChartsWidget *charts, QWidget *parent) : charts(chart
|
||||
tab_widget->addTab(history_log = new LogsWidget(this), utils::icon("stopwatch"), "&Logs");
|
||||
main_layout->addWidget(tab_widget);
|
||||
|
||||
QObject::connect(edit_btn, &QToolButton::clicked, this, &DetailWidget::editMsg);
|
||||
QObject::connect(remove_btn, &QToolButton::clicked, this, &DetailWidget::removeMsg);
|
||||
QObject::connect(binary_view, &BinaryView::signalHovered, signal_view, &SignalView::signalHovered);
|
||||
QObject::connect(binary_view, &BinaryView::signalClicked, [this](const cabana::Signal *s) { signal_view->selectSignal(s, true); });
|
||||
QObject::connect(binary_view, &BinaryView::editSignal, signal_view->model, &SignalModel::saveSignal);
|
||||
@@ -80,6 +67,41 @@ DetailWidget::DetailWidget(ChartsWidget *charts, QWidget *parent) : charts(chart
|
||||
QObject::connect(charts, &ChartsWidget::seriesChanged, signal_view, &SignalView::updateChartState);
|
||||
}
|
||||
|
||||
void DetailWidget::createToolBar() {
|
||||
QToolBar *toolbar = new QToolBar(this);
|
||||
int icon_size = style()->pixelMetric(QStyle::PM_SmallIconSize);
|
||||
toolbar->setIconSize({icon_size, icon_size});
|
||||
toolbar->addWidget(name_label = new ElidedLabel(this));
|
||||
name_label->setStyleSheet("QLabel{font-weight:bold;}");
|
||||
|
||||
QWidget *spacer = new QWidget();
|
||||
spacer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);
|
||||
toolbar->addWidget(spacer);
|
||||
|
||||
// Heatmap label and radio buttons
|
||||
toolbar->addWidget(new QLabel(tr("Heatmap:"), this));
|
||||
auto *heatmap_live = new QRadioButton(tr("Live"), this);
|
||||
auto *heatmap_all = new QRadioButton(tr("All"), this);
|
||||
heatmap_live->setChecked(true);
|
||||
|
||||
toolbar->addWidget(heatmap_live);
|
||||
toolbar->addWidget(heatmap_all);
|
||||
|
||||
// Edit and remove buttons
|
||||
toolbar->addSeparator();
|
||||
toolbar->addAction(utils::icon("pencil"), tr("Edit Message"), this, &DetailWidget::editMsg);
|
||||
action_remove_msg = toolbar->addAction(utils::icon("x-lg"), tr("Remove Message"), this, &DetailWidget::removeMsg);
|
||||
|
||||
layout()->addWidget(toolbar);
|
||||
|
||||
connect(heatmap_live, &QAbstractButton::toggled, this, [this](bool on) { binary_view->setHeatmapLiveMode(on); });
|
||||
connect(can, &AbstractStream::timeRangeChanged, this, [=](const std::optional<std::pair<double, double>> &range) {
|
||||
auto text = range ? QString("%1 - %2").arg(range->first, 0, 'f', 3).arg(range->second, 0, 'f', 3) : "All";
|
||||
heatmap_all->setText(text);
|
||||
(range ? heatmap_all : heatmap_live)->setChecked(true);
|
||||
});
|
||||
}
|
||||
|
||||
void DetailWidget::showTabBarContextMenu(const QPoint &pt) {
|
||||
int index = tabbar->tabAt(pt);
|
||||
if (index >= 0) {
|
||||
@@ -131,14 +153,11 @@ void DetailWidget::refresh() {
|
||||
for (auto s : binary_view->getOverlappingSignals()) {
|
||||
warnings.push_back(tr("%1 has overlapping bits.").arg(s->name));
|
||||
}
|
||||
} else {
|
||||
warnings.push_back(tr("Drag-Select in binary view to create new signal."));
|
||||
}
|
||||
|
||||
QString msg_name = msg ? QString("%1 (%2)").arg(msg->name, msg->transmitter) : msgName(msg_id);
|
||||
name_label->setText(msg_name);
|
||||
name_label->setToolTip(msg_name);
|
||||
remove_btn->setEnabled(msg != nullptr);
|
||||
action_remove_msg->setEnabled(msg != nullptr);
|
||||
|
||||
if (!warnings.isEmpty()) {
|
||||
warning_label->setText(warnings.join('\n'));
|
||||
@@ -184,8 +203,7 @@ EditMessageDialog::EditMessageDialog(const MessageId &msg_id, const QString &tit
|
||||
name_edit->setValidator(new NameValidator(name_edit));
|
||||
|
||||
form_layout->addRow(tr("Size"), size_spin = new QSpinBox(this));
|
||||
// TODO: limit the maximum?
|
||||
size_spin->setMinimum(1);
|
||||
size_spin->setRange(1, CAN_MAX_DATA_BYTES);
|
||||
size_spin->setValue(size);
|
||||
|
||||
form_layout->addRow(tr("Node"), node = new QLineEdit(this));
|
||||
|
||||
@@ -36,6 +36,7 @@ public:
|
||||
void refresh();
|
||||
|
||||
private:
|
||||
void createToolBar();
|
||||
void showTabBarContextMenu(const QPoint &pt);
|
||||
void editMsg();
|
||||
void removeMsg();
|
||||
@@ -47,7 +48,7 @@ private:
|
||||
QWidget *warning_widget;
|
||||
TabBar *tabbar;
|
||||
QTabWidget *tab_widget;
|
||||
QToolButton *remove_btn;
|
||||
QAction *action_remove_msg;
|
||||
LogsWidget *history_log;
|
||||
BinaryView *binary_view;
|
||||
SignalView *signal_view;
|
||||
|
||||
@@ -191,6 +191,7 @@ void MainWindow::createDockWidgets() {
|
||||
video_splitter->handle(1)->setEnabled(!can->liveStreaming());
|
||||
video_dock->setWidget(video_splitter);
|
||||
QObject::connect(charts_widget, &ChartsWidget::toggleChartsDocking, this, &MainWindow::toggleChartsDocking);
|
||||
QObject::connect(charts_widget, &ChartsWidget::showTip, video_widget, &VideoWidget::showThumbnail);
|
||||
}
|
||||
|
||||
void MainWindow::createStatusBar() {
|
||||
|
||||
@@ -13,21 +13,6 @@
|
||||
|
||||
#include "tools/cabana/commands.h"
|
||||
|
||||
static bool isMessageActive(const MessageId &id) {
|
||||
if (id.source == INVALID_SOURCE) {
|
||||
return false;
|
||||
}
|
||||
// Check if the message is active based on time difference and frequency
|
||||
const auto &m = can->lastMessage(id);
|
||||
float delta = can->currentSec() - m.ts;
|
||||
|
||||
if (m.freq < std::numeric_limits<double>::epsilon()) {
|
||||
return delta < 1.5;
|
||||
}
|
||||
|
||||
return delta < (5.0 / m.freq) + (1.0 / settings.fps);
|
||||
}
|
||||
|
||||
MessagesWidget::MessagesWidget(QWidget *parent) : menu(new QMenu(this)), QWidget(parent) {
|
||||
QVBoxLayout *main_layout = new QVBoxLayout(this);
|
||||
main_layout->setContentsMargins(0, 0, 0, 0);
|
||||
@@ -207,7 +192,7 @@ QVariant MessageListModel::data(const QModelIndex &index, int role) const {
|
||||
switch (index.column()) {
|
||||
case Column::NAME: return item.name;
|
||||
case Column::SOURCE: return item.id.source != INVALID_SOURCE ? QString::number(item.id.source) : NA;
|
||||
case Column::ADDRESS: return QString::number(item.id.address, 16);
|
||||
case Column::ADDRESS: return toHexString(item.id.address);
|
||||
case Column::NODE: return item.node;
|
||||
case Column::FREQ: return item.id.source != INVALID_SOURCE ? getFreq(can->lastMessage(item.id).freq) : NA;
|
||||
case Column::COUNT: return item.id.source != INVALID_SOURCE ? QString::number(can->lastMessage(item.id).count) : NA;
|
||||
@@ -300,7 +285,7 @@ bool MessageListModel::match(const MessageListModel::Item &item) {
|
||||
match = parseRange(txt, item.id.source);
|
||||
break;
|
||||
case Column::ADDRESS:
|
||||
match = QString::number(item.id.address, 16).contains(txt, Qt::CaseInsensitive);
|
||||
match = toHexString(item.id.address).contains(txt, Qt::CaseInsensitive);
|
||||
match = match || parseRange(txt, item.id.address, 16);
|
||||
break;
|
||||
case Column::NODE:
|
||||
@@ -335,7 +320,7 @@ bool MessageListModel::filterAndSort() {
|
||||
std::vector<Item> items;
|
||||
items.reserve(all_messages.size());
|
||||
for (const auto &id : all_messages) {
|
||||
if (show_inactive_messages || isMessageActive(id)) {
|
||||
if (show_inactive_messages || can->isMessageActive(id)) {
|
||||
auto msg = dbc()->msg(id);
|
||||
Item item = {.id = id,
|
||||
.name = msg ? msg->name : UNTITLED,
|
||||
@@ -378,7 +363,7 @@ void MessageListModel::sort(int column, Qt::SortOrder order) {
|
||||
|
||||
void MessageView::drawRow(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const {
|
||||
const auto &item = ((MessageListModel*)model())->items_[index.row()];
|
||||
if (!isMessageActive(item.id)) {
|
||||
if (!can->isMessageActive(item.id)) {
|
||||
QStyleOptionViewItem custom_option = option;
|
||||
custom_option.palette.setBrush(QPalette::Text, custom_option.palette.color(QPalette::Disabled, QPalette::Text));
|
||||
auto color = QApplication::palette().color(QPalette::HighlightedText);
|
||||
|
||||
@@ -89,10 +89,6 @@ SettingsDlg::SettingsDlg(QWidget *parent) : QDialog(parent) {
|
||||
|
||||
groupbox = new QGroupBox("Chart");
|
||||
form_layout = new QFormLayout(groupbox);
|
||||
form_layout->addRow(tr("Default Series Type"), chart_series_type = new QComboBox(this));
|
||||
chart_series_type->addItems({tr("Line"), tr("Step Line"), tr("Scatter")});
|
||||
chart_series_type->setCurrentIndex(settings.chart_series_type);
|
||||
|
||||
form_layout->addRow(tr("Chart Height"), chart_height = new QSpinBox(this));
|
||||
chart_height->setRange(100, 500);
|
||||
chart_height->setSingleStep(10);
|
||||
@@ -132,7 +128,6 @@ void SettingsDlg::save() {
|
||||
}
|
||||
settings.fps = fps->value();
|
||||
settings.max_cached_minutes = cached_minutes->value();
|
||||
settings.chart_series_type = chart_series_type->currentIndex();
|
||||
settings.chart_height = chart_height->value();
|
||||
settings.log_livestream = log_livestream->isChecked();
|
||||
settings.log_path = log_path->text();
|
||||
|
||||
@@ -382,7 +382,7 @@ QWidget *SignalItemDelegate::createEditor(QWidget *parent, const QStyleOptionVie
|
||||
} else if (item->type == SignalModel::Item::Size) {
|
||||
QSpinBox *spin = new QSpinBox(parent);
|
||||
spin->setFrame(false);
|
||||
spin->setRange(1, 64);
|
||||
spin->setRange(1, CAN_MAX_DATA_BYTES);
|
||||
return spin;
|
||||
} else if (item->type == SignalModel::Item::SignalType) {
|
||||
QComboBox *c = new QComboBox(parent);
|
||||
|
||||
@@ -39,7 +39,7 @@ void AbstractStream::updateMasks() {
|
||||
const int size = std::min(mask.size(), m.last_changes.size());
|
||||
for (int i = 0; i < size; ++i) {
|
||||
for (int j = 0; j < 8; ++j) {
|
||||
if (((mask[i] >> (7 - j)) & 1) != 0) m.last_changes[i].bit_change_counts[j] = 0;
|
||||
if (((mask[i] >> (7 - j)) & 1) != 0) m.bit_flip_counts[i][j] = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -59,9 +59,9 @@ size_t AbstractStream::suppressHighlighted() {
|
||||
if (dt < 2.0) {
|
||||
last_change.suppressed = true;
|
||||
}
|
||||
last_change.bit_change_counts.fill(0);
|
||||
cnt += last_change.suppressed;
|
||||
}
|
||||
for (auto &flip_counts : m.bit_flip_counts) flip_counts.fill(0);
|
||||
}
|
||||
return cnt;
|
||||
}
|
||||
@@ -127,8 +127,22 @@ const CanData &AbstractStream::lastMessage(const MessageId &id) const {
|
||||
return it != last_msgs.end() ? it->second : empty_data;
|
||||
}
|
||||
|
||||
bool AbstractStream::isMessageActive(const MessageId &id) const {
|
||||
if (id.source == INVALID_SOURCE) {
|
||||
return false;
|
||||
}
|
||||
// Check if the message is active based on time difference and frequency
|
||||
const auto &m = lastMessage(id);
|
||||
float delta = currentSec() - m.ts;
|
||||
|
||||
if (m.freq < std::numeric_limits<double>::epsilon()) {
|
||||
return delta < 1.5;
|
||||
}
|
||||
|
||||
return delta < (5.0 / m.freq) + (1.0 / settings.fps);
|
||||
}
|
||||
|
||||
void AbstractStream::updateLastMsgsTo(double sec) {
|
||||
std::lock_guard lk(mutex_);
|
||||
current_sec_ = sec;
|
||||
uint64_t last_ts = toMonoTime(sec);
|
||||
std::unordered_map<MessageId, CanData> msgs;
|
||||
@@ -160,10 +174,17 @@ void AbstractStream::updateLastMsgsTo(double sec) {
|
||||
std::any_of(messages_.cbegin(), messages_.cend(),
|
||||
[this](const auto &m) { return !last_msgs.count(m.first); });
|
||||
last_msgs = messages_;
|
||||
mutex_.unlock();
|
||||
|
||||
emit msgsReceived(nullptr, id_changed);
|
||||
resumeStream();
|
||||
|
||||
std::lock_guard lk(mutex_);
|
||||
seek_finished_ = true;
|
||||
seek_finished_cv_.notify_one();
|
||||
}
|
||||
|
||||
void AbstractStream::waitForSeekFinshed() {
|
||||
std::unique_lock lock(mutex_);
|
||||
seek_finished_cv_.wait(lock, [this]() { return seek_finished_; });
|
||||
seek_finished_ = false;
|
||||
}
|
||||
|
||||
const CanEvent *AbstractStream::newEvent(uint64_t mono_time, const cereal::CanData::Reader &c) {
|
||||
@@ -200,6 +221,15 @@ void AbstractStream::mergeEvents(const std::vector<const CanEvent *> &events) {
|
||||
}
|
||||
}
|
||||
|
||||
std::pair<CanEventIter, CanEventIter> AbstractStream::eventsInRange(const MessageId &id, std::optional<std::pair<double, double>> time_range) const {
|
||||
const auto &events = can->events(id);
|
||||
if (!time_range) return {events.begin(), events.end()};
|
||||
|
||||
auto first = std::lower_bound(events.begin(), events.end(), can->toMonoTime(time_range->first), CompareCanEvent());
|
||||
auto last = std::upper_bound(events.begin(), events.end(), can->toMonoTime(time_range->second), CompareCanEvent());
|
||||
return {first, last};
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
enum Color { GREYISH_BLUE, CYAN, RED};
|
||||
@@ -219,15 +249,7 @@ 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);
|
||||
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());
|
||||
|
||||
auto [first, last] = can->eventsInRange(msg_id, std::make_pair(current_sec - 59, current_sec));
|
||||
int count = std::distance(first, last);
|
||||
if (count <= 1) return 0.0;
|
||||
|
||||
@@ -248,9 +270,10 @@ void CanData::compute(const MessageId &msg_id, const uint8_t *can_data, const in
|
||||
}
|
||||
|
||||
if (dat.size() != size) {
|
||||
dat.resize(size);
|
||||
dat.assign(can_data, can_data + size);
|
||||
colors.assign(size, QColor(0, 0, 0, 0));
|
||||
last_changes.resize(size);
|
||||
bit_flip_counts.resize(size);
|
||||
std::for_each(last_changes.begin(), last_changes.end(), [current_sec](auto &c) { c.ts = current_sec; });
|
||||
} else {
|
||||
constexpr int periodic_threshold = 10;
|
||||
@@ -282,10 +305,11 @@ void CanData::compute(const MessageId &msg_id, const uint8_t *can_data, const in
|
||||
}
|
||||
|
||||
// Track bit level changes
|
||||
auto &row_bit_flips = bit_flip_counts[i];
|
||||
const uint8_t diff = (cur ^ last);
|
||||
for (int bit = 0; bit < 8; bit++) {
|
||||
if (diff & (1u << bit)) {
|
||||
++last_change.bit_change_counts[7 - bit];
|
||||
++row_bit_flips[7 - bit];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <condition_variable>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
@@ -33,9 +34,9 @@ struct CanData {
|
||||
int delta = 0;
|
||||
int same_delta_counter = 0;
|
||||
bool suppressed = false;
|
||||
std::array<uint32_t, 8> bit_change_counts;
|
||||
};
|
||||
std::vector<ByteLastChange> last_changes;
|
||||
std::vector<std::array<uint32_t, 8>> bit_flip_counts;
|
||||
double last_freq_update_ts = 0;
|
||||
};
|
||||
|
||||
@@ -53,6 +54,7 @@ struct CompareCanEvent {
|
||||
};
|
||||
|
||||
typedef std::unordered_map<MessageId, std::vector<const CanEvent *>> MessageEventsMap;
|
||||
using CanEventIter = std::vector<const CanEvent *>::const_iterator;
|
||||
|
||||
class AbstractStream : public QObject {
|
||||
Q_OBJECT
|
||||
@@ -81,10 +83,12 @@ public:
|
||||
inline double toSeconds(uint64_t mono_time) const { return std::max(0.0, (mono_time - beginMonoTime()) / 1e9); }
|
||||
|
||||
inline const std::unordered_map<MessageId, CanData> &lastMessages() const { return last_msgs; }
|
||||
bool isMessageActive(const MessageId &id) const;
|
||||
inline const MessageEventsMap &eventsMap() const { return events_; }
|
||||
inline const std::vector<const CanEvent *> &allEvents() const { return all_events_; }
|
||||
const CanData &lastMessage(const MessageId &id) const;
|
||||
const std::vector<const CanEvent *> &events(const MessageId &id) const;
|
||||
std::pair<CanEventIter, CanEventIter> eventsInRange(const MessageId &id, std::optional<std::pair<double, double>> time_range) const;
|
||||
|
||||
size_t suppressHighlighted();
|
||||
void clearSuppressed();
|
||||
@@ -108,7 +112,7 @@ protected:
|
||||
void mergeEvents(const std::vector<const CanEvent *> &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);
|
||||
virtual void resumeStream() {}
|
||||
void waitForSeekFinshed();
|
||||
std::vector<const CanEvent *> all_events_;
|
||||
double current_sec_ = 0;
|
||||
std::optional<std::pair<double, double>> time_range_;
|
||||
@@ -124,6 +128,8 @@ private:
|
||||
|
||||
// Members accessed in multiple threads. (mutex protected)
|
||||
std::mutex mutex_;
|
||||
std::condition_variable seek_finished_cv_;
|
||||
bool seek_finished_ = false;
|
||||
std::set<MessageId> new_msgs_;
|
||||
std::unordered_map<MessageId, CanData> messages_;
|
||||
std::unordered_map<MessageId, std::vector<uint8_t>> masks_;
|
||||
|
||||
@@ -53,9 +53,12 @@ bool ReplayStream::loadRoute(const QString &route, const QString &data_dir, uint
|
||||
|
||||
// Forward replay callbacks to corresponding Qt signals.
|
||||
replay->onSeeking = [this](double sec) { emit seeking(sec); };
|
||||
replay->onSeekedTo = [this](double sec) { emit seekedTo(sec); };
|
||||
replay->onSeekedTo = [this](double sec) {
|
||||
emit seekedTo(sec);
|
||||
waitForSeekFinshed();
|
||||
};
|
||||
replay->onQLogLoaded = [this](std::shared_ptr<LogReader> qlog) { emit qLogLoaded(qlog); };
|
||||
replay->onSegmentsMerged = [this]() { QMetaObject::invokeMethod(this, &ReplayStream::mergeSegments, Qt::QueuedConnection); };
|
||||
replay->onSegmentsMerged = [this]() { QMetaObject::invokeMethod(this, &ReplayStream::mergeSegments, Qt::BlockingQueuedConnection); };
|
||||
|
||||
bool success = replay->load();
|
||||
if (!success) {
|
||||
|
||||
@@ -32,7 +32,6 @@ public:
|
||||
inline float getSpeed() const { return replay->getSpeed(); }
|
||||
inline Replay *getReplay() const { return replay.get(); }
|
||||
inline bool isPaused() const override { return replay->isPaused(); }
|
||||
void resumeStream() override { return replay->resumeStream(); }
|
||||
void pause(bool pause) override;
|
||||
|
||||
signals:
|
||||
|
||||
@@ -163,3 +163,4 @@ private:
|
||||
|
||||
int num_decimals(double num);
|
||||
QString signalToolTip(const cabana::Signal *sig);
|
||||
inline QString toHexString(int value) { return QString("0x%1").arg(QString::number(value, 16).toUpper(), 2, '0'); }
|
||||
|
||||
+111
-78
@@ -31,9 +31,12 @@ static Replay *getReplay() {
|
||||
VideoWidget::VideoWidget(QWidget *parent) : QFrame(parent) {
|
||||
setFrameStyle(QFrame::StyledPanel | QFrame::Plain);
|
||||
auto main_layout = new QVBoxLayout(this);
|
||||
main_layout->setContentsMargins(0, 0, 0, 0);
|
||||
main_layout->setSpacing(0);
|
||||
if (!can->liveStreaming())
|
||||
main_layout->addWidget(createCameraWidget());
|
||||
main_layout->addLayout(createPlaybackController());
|
||||
|
||||
createPlaybackController();
|
||||
|
||||
setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Maximum);
|
||||
QObject::connect(can, &AbstractStream::paused, this, &VideoWidget::updatePlayBtnState);
|
||||
@@ -65,15 +68,19 @@ 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")));
|
||||
void VideoWidget::createPlaybackController() {
|
||||
QToolBar *toolbar = new QToolBar(this);
|
||||
layout()->addWidget(toolbar);
|
||||
|
||||
int icon_size = style()->pixelMetric(QStyle::PM_SmallIconSize);
|
||||
toolbar->setIconSize({icon_size, icon_size});
|
||||
|
||||
toolbar->addAction(utils::icon("rewind"), tr("Seek backward"), []() { can->seekTo(can->currentSec() - 1); });
|
||||
play_toggle_action = toolbar->addAction(utils::icon("play"), tr("Play"), []() { can->pause(!can->isPaused()); });
|
||||
toolbar->addAction(utils::icon("fast-forward"), tr("Seek forward"), []() { can->seekTo(can->currentSec() + 1); });
|
||||
|
||||
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]() {
|
||||
skip_to_end_action = toolbar->addAction(utils::icon("skip-end"), tr("Skip to the end"), this, [this]() {
|
||||
// set speed to 1.0
|
||||
speed_btn->menu()->actions()[7]->setChecked(true);
|
||||
can->pause(false);
|
||||
@@ -81,53 +88,48 @@ QHBoxLayout *VideoWidget::createPlaybackController() {
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
time_display_action = toolbar->addAction("", this, [this]() {
|
||||
settings.absolute_time = !settings.absolute_time;
|
||||
time_display_action->setToolTip(settings.absolute_time ? tr("Elapsed time") : tr("Absolute time"));
|
||||
updateState();
|
||||
});
|
||||
|
||||
QWidget *spacer = new QWidget();
|
||||
spacer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);
|
||||
toolbar->addWidget(spacer);
|
||||
|
||||
if (!can->liveStreaming()) {
|
||||
layout->addWidget(loop_btn = new ToolButton("repeat", tr("Loop playback")));
|
||||
QObject::connect(loop_btn, &QToolButton::clicked, this, &VideoWidget::loopPlaybackClicked);
|
||||
toolbar->addAction(utils::icon("repeat"), tr("Loop playback"), this, &VideoWidget::loopPlaybackClicked);
|
||||
}
|
||||
|
||||
// speed selector
|
||||
layout->addWidget(speed_btn = new QToolButton(this));
|
||||
speed_btn->setAutoRaise(true);
|
||||
createSpeedDropdown(toolbar);
|
||||
}
|
||||
|
||||
void VideoWidget::createSpeedDropdown(QToolBar *toolbar) {
|
||||
toolbar->addWidget(speed_btn = new QToolButton(this));
|
||||
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]() {
|
||||
auto act = speed_btn->menu()->addAction(QString("%1x").arg(speed), this, [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;
|
||||
speed_group->addAction(act);
|
||||
act->setCheckable(true);
|
||||
if (speed == 1.0) {
|
||||
act->setChecked(true);
|
||||
act->trigger();
|
||||
}
|
||||
}
|
||||
|
||||
QFont font = speed_btn->font();
|
||||
font.setBold(true);
|
||||
speed_btn->setFont(font);
|
||||
speed_btn->setMinimumWidth(speed_btn->fontMetrics().width("0.05x ") + style()->pixelMetric(QStyle::PM_MenuButtonIndicator));
|
||||
}
|
||||
|
||||
QWidget *VideoWidget::createCameraWidget() {
|
||||
@@ -150,7 +152,6 @@ QWidget *VideoWidget::createCameraWidget() {
|
||||
|
||||
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);
|
||||
@@ -158,7 +159,7 @@ QWidget *VideoWidget::createCameraWidget() {
|
||||
if (index != -1) cam_widget->setStreamType((VisionStreamType)camera_tab->tabData(index).toInt());
|
||||
});
|
||||
QObject::connect(static_cast<ReplayStream*>(can), &ReplayStream::qLogLoaded, cam_widget, &StreamCameraView::parseQLog, Qt::QueuedConnection);
|
||||
slider->installEventFilter(cam_widget);
|
||||
slider->installEventFilter(this);
|
||||
return w;
|
||||
}
|
||||
|
||||
@@ -180,13 +181,13 @@ void VideoWidget::vipcAvailableStreamsUpdated(std::set<VisionStreamType> streams
|
||||
void VideoWidget::loopPlaybackClicked() {
|
||||
bool is_looping = getReplay()->loop();
|
||||
getReplay()->setLoop(!is_looping);
|
||||
loop_btn->setIcon(!is_looping ? "repeat" : "repeat-1");
|
||||
qobject_cast<QAction*>(sender())->setIcon(utils::icon(!is_looping ? "repeat" : "repeat-1"));
|
||||
}
|
||||
|
||||
void VideoWidget::timeRangeChanged() {
|
||||
const auto time_range = can->timeRange();
|
||||
if (can->liveStreaming()) {
|
||||
skip_to_end_btn->setEnabled(!time_range.has_value());
|
||||
skip_to_end_action->setEnabled(!time_range.has_value());
|
||||
return;
|
||||
}
|
||||
time_range ? slider->setTimeRange(time_range->first, time_range->second)
|
||||
@@ -208,28 +209,50 @@ void VideoWidget::updateState() {
|
||||
if (camera_tab->count() == 0) { // No streams available
|
||||
cam_widget->update(); // Manually refresh to show alert events
|
||||
}
|
||||
time_btn->setText(QString("%1 / %2").arg(formatTime(can->currentSec(), true),
|
||||
time_display_action->setText(QString("%1 / %2").arg(formatTime(can->currentSec(), true),
|
||||
formatTime(slider->maximum() / slider->factor)));
|
||||
} else {
|
||||
time_btn->setText(formatTime(can->currentSec(), true));
|
||||
time_display_action->setText(formatTime(can->currentSec(), true));
|
||||
}
|
||||
}
|
||||
|
||||
void VideoWidget::updatePlayBtnState() {
|
||||
play_btn->setIcon(can->isPaused() ? "play" : "pause");
|
||||
play_btn->setToolTip(can->isPaused() ? tr("Play") : tr("Pause"));
|
||||
play_toggle_action->setIcon(utils::icon(can->isPaused() ? "play" : "pause"));
|
||||
play_toggle_action->setToolTip(can->isPaused() ? tr("Play") : tr("Pause"));
|
||||
}
|
||||
|
||||
void VideoWidget::showThumbnail(double seconds) {
|
||||
if (can->liveStreaming()) return;
|
||||
|
||||
cam_widget->thumbnail_dispaly_time = seconds;
|
||||
slider->thumbnail_dispaly_time = seconds;
|
||||
cam_widget->update();
|
||||
slider->update();
|
||||
}
|
||||
|
||||
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()));
|
||||
showThumbnail(min_sec + static_cast<QMouseEvent *>(event)->pos().x() * (max_sec - min_sec) / slider->width());
|
||||
} else if (event->type() == QEvent::Leave) {
|
||||
showThumbnail(-1);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Slider
|
||||
|
||||
Slider::Slider(QWidget *parent) : QSlider(Qt::Horizontal, parent) {
|
||||
setMouseTracking(true);
|
||||
}
|
||||
|
||||
void Slider::paintEvent(QPaintEvent *ev) {
|
||||
QPainter p(this);
|
||||
QRect r = rect().adjusted(0, 4, 0, -4);
|
||||
|
||||
QStyleOptionSlider opt;
|
||||
initStyleOption(&opt);
|
||||
QRect r = style()->subControlRect(QStyle::CC_Slider, &opt, QStyle::SC_SliderGroove, this);
|
||||
p.fillRect(r, timeline_colors[(int)TimelineType::None]);
|
||||
|
||||
double min = minimum() / factor;
|
||||
double max = maximum() / factor;
|
||||
|
||||
@@ -254,13 +277,19 @@ void Slider::paintEvent(QPaintEvent *ev) {
|
||||
}
|
||||
}
|
||||
|
||||
QStyleOptionSlider opt;
|
||||
opt.initFrom(this);
|
||||
opt.minimum = minimum();
|
||||
opt.maximum = maximum();
|
||||
opt.subControls = QStyle::SC_SliderHandle;
|
||||
opt.sliderPosition = value();
|
||||
style()->drawComplexControl(QStyle::CC_Slider, &opt, &p);
|
||||
|
||||
if (thumbnail_dispaly_time >= 0) {
|
||||
int left = (thumbnail_dispaly_time - min) * width() / (max - min) - 1;
|
||||
QRect rc(left, rect().top() + 1, 2, rect().height() - 2);
|
||||
p.setBrush(palette().highlight());
|
||||
p.setPen(Qt::NoPen);
|
||||
p.drawRoundedRect(rc, 1.5, 1.5);
|
||||
}
|
||||
}
|
||||
|
||||
void Slider::mousePressEvent(QMouseEvent *e) {
|
||||
@@ -272,7 +301,6 @@ void Slider::mousePressEvent(QMouseEvent *e) {
|
||||
}
|
||||
|
||||
// StreamCameraView
|
||||
|
||||
StreamCameraView::StreamCameraView(std::string stream_name, VisionStreamType stream_type, QWidget *parent)
|
||||
: CameraWidget(stream_name, stream_type, parent) {
|
||||
fade_animation = new QPropertyAnimation(this, "overlayOpacity");
|
||||
@@ -294,6 +322,7 @@ void StreamCameraView::parseQLog(std::shared_ptr<LogReader> qlog) {
|
||||
QPixmap generated_thumb = generateThumbnail(thumb, can->toSeconds(thumb_data.getTimestampEof()));
|
||||
std::lock_guard lock(mutex);
|
||||
thumbnails[thumb_data.getTimestampEof()] = generated_thumb;
|
||||
big_thumbnails[thumb_data.getTimestampEof()] = thumb;
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -304,12 +333,15 @@ void StreamCameraView::paintGL() {
|
||||
CameraWidget::paintGL();
|
||||
|
||||
QPainter p(this);
|
||||
if (auto alert = getReplay()->findAlertAtTime(can->currentSec())) {
|
||||
bool scrubbing = false;
|
||||
if (thumbnail_dispaly_time >= 0) {
|
||||
scrubbing = can->isPaused();
|
||||
scrubbing ? drawScrubThumbnail(p) : drawThumbnail(p);
|
||||
}
|
||||
if (auto alert = getReplay()->findAlertAtTime(scrubbing ? thumbnail_dispaly_time : can->currentSec())) {
|
||||
drawAlert(p, rect(), *alert);
|
||||
}
|
||||
if (thumbnail_pt_) {
|
||||
drawThumbnail(p);
|
||||
}
|
||||
|
||||
if (can->isPaused()) {
|
||||
p.setPen(QColor(200, 200, 200, static_cast<int>(255 * fade_animation->currentValue().toFloat())));
|
||||
p.setFont(QFont(font().family(), 16, QFont::Bold));
|
||||
@@ -329,25 +361,37 @@ QPixmap StreamCameraView::generateThumbnail(QPixmap thumb, double seconds) {
|
||||
return scaled;
|
||||
}
|
||||
|
||||
void StreamCameraView::drawThumbnail(QPainter &p) {
|
||||
int pos = std::clamp(thumbnail_pt_->x(), 0, width());
|
||||
auto [min_sec, max_sec] = can->timeRange().value_or(std::make_pair(can->minSeconds(), can->maxSeconds()));
|
||||
double seconds = min_sec + pos * (max_sec - min_sec) / width();
|
||||
void StreamCameraView::drawScrubThumbnail(QPainter &p) {
|
||||
p.fillRect(rect(), Qt::black);
|
||||
auto it = big_thumbnails.lowerBound(can->toMonoTime(thumbnail_dispaly_time));
|
||||
if (it != big_thumbnails.end()) {
|
||||
QPixmap scaled_thumb = it.value().scaled(rect().size(), Qt::KeepAspectRatio, Qt::SmoothTransformation);
|
||||
QRect thumb_rect(rect().center() - scaled_thumb.rect().center(), scaled_thumb.size());
|
||||
p.drawPixmap(thumb_rect.topLeft(), scaled_thumb);
|
||||
drawTime(p, thumb_rect, thumbnail_dispaly_time);
|
||||
}
|
||||
}
|
||||
|
||||
auto it = thumbnails.lowerBound(can->toMonoTime(seconds));
|
||||
void StreamCameraView::drawThumbnail(QPainter &p) {
|
||||
auto it = thumbnails.lowerBound(can->toMonoTime(thumbnail_dispaly_time));
|
||||
if (it != thumbnails.end()) {
|
||||
const QPixmap &thumb = it.value();
|
||||
auto [min_sec, max_sec] = can->timeRange().value_or(std::make_pair(can->minSeconds(), can->maxSeconds()));
|
||||
int pos = (thumbnail_dispaly_time - min_sec) * width() / (max_sec - min_sec);
|
||||
int x = std::clamp(pos - thumb.width() / 2, THUMBNAIL_MARGIN, width() - thumb.width() - THUMBNAIL_MARGIN + 1);
|
||||
int y = height() - thumb.height() - THUMBNAIL_MARGIN;
|
||||
|
||||
p.drawPixmap(x, y, thumb);
|
||||
p.setPen(QPen(palette().color(QPalette::BrightText), 2));
|
||||
p.setFont(QFont(font().family(), 10));
|
||||
p.drawText(x, y, thumb.width(), thumb.height() - THUMBNAIL_MARGIN,
|
||||
Qt::AlignHCenter | Qt::AlignBottom, QString::number(seconds, 'f', 3));
|
||||
drawTime(p, QRect{x, y, thumb.width(), thumb.height()}, thumbnail_dispaly_time);
|
||||
}
|
||||
}
|
||||
|
||||
void StreamCameraView::drawTime(QPainter &p, const QRect &rect, double seconds) {
|
||||
p.setPen(palette().color(QPalette::BrightText));
|
||||
p.setFont(QFont(font().family(), 10));
|
||||
p.drawText(rect.adjusted(0, 0, 0, -THUMBNAIL_MARGIN), Qt::AlignHCenter | Qt::AlignBottom, QString::number(seconds, 'f', 3));
|
||||
}
|
||||
|
||||
void StreamCameraView::drawAlert(QPainter &p, const QRect &rect, const Timeline::Entry &alert) {
|
||||
p.setPen(QPen(palette().color(QPalette::BrightText), 2));
|
||||
QColor color = timeline_colors[int(alert.type)];
|
||||
@@ -360,14 +404,3 @@ void StreamCameraView::drawAlert(QPainter &p, const QRect &rect, const Timeline:
|
||||
p.fillRect(text_rect.left(), r.top(), text_rect.width(), r.height(), color);
|
||||
p.drawText(text_rect, Qt::AlignTop | Qt::AlignHCenter | Qt::TextWordWrap, text);
|
||||
}
|
||||
|
||||
bool StreamCameraView::eventFilter(QObject *, QEvent *event) {
|
||||
if (event->type() == QEvent::MouseMove) {
|
||||
thumbnail_pt_ = static_cast<QMouseEvent *>(event)->pos();
|
||||
update();
|
||||
} else if (event->type() == QEvent::Leave) {
|
||||
thumbnail_pt_ = std::nullopt;
|
||||
update();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
+14
-11
@@ -1,15 +1,14 @@
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
#include <QHBoxLayout>
|
||||
#include <QFrame>
|
||||
#include <QPropertyAnimation>
|
||||
#include <QSlider>
|
||||
#include <QToolBar>
|
||||
#include <QTabBar>
|
||||
|
||||
#include "selfdrive/ui/qt/widgets/cameraview.h"
|
||||
@@ -28,6 +27,7 @@ public:
|
||||
void mousePressEvent(QMouseEvent *e) override;
|
||||
void paintEvent(QPaintEvent *ev) override;
|
||||
const double factor = 1000.0;
|
||||
double thumbnail_dispaly_time = -1;
|
||||
};
|
||||
|
||||
class StreamCameraView : public CameraWidget {
|
||||
@@ -43,11 +43,14 @@ private:
|
||||
QPixmap generateThumbnail(QPixmap thumbnail, double seconds);
|
||||
void drawAlert(QPainter &p, const QRect &rect, const Timeline::Entry &alert);
|
||||
void drawThumbnail(QPainter &p);
|
||||
bool eventFilter(QObject *obj, QEvent *event) override;
|
||||
void drawScrubThumbnail(QPainter &p);
|
||||
void drawTime(QPainter &p, const QRect &rect, double seconds);
|
||||
|
||||
QPropertyAnimation *fade_animation;
|
||||
QMap<uint64_t, QPixmap> big_thumbnails;
|
||||
QMap<uint64_t, QPixmap> thumbnails;
|
||||
std::optional<QPoint> thumbnail_pt_;
|
||||
double thumbnail_dispaly_time = -1;
|
||||
friend class VideoWidget;
|
||||
};
|
||||
|
||||
class VideoWidget : public QFrame {
|
||||
@@ -55,25 +58,25 @@ class VideoWidget : public QFrame {
|
||||
|
||||
public:
|
||||
VideoWidget(QWidget *parnet = nullptr);
|
||||
void showThumbnail(double seconds);
|
||||
|
||||
protected:
|
||||
bool eventFilter(QObject *obj, QEvent *event) override;
|
||||
QString formatTime(double sec, bool include_milliseconds = false);
|
||||
void timeRangeChanged();
|
||||
void updateState();
|
||||
void updatePlayBtnState();
|
||||
QWidget *createCameraWidget();
|
||||
QHBoxLayout *createPlaybackController();
|
||||
void createPlaybackController();
|
||||
void createSpeedDropdown(QToolBar *toolbar);
|
||||
void loopPlaybackClicked();
|
||||
void vipcAvailableStreamsUpdated(std::set<VisionStreamType> streams);
|
||||
|
||||
StreamCameraView *cam_widget;
|
||||
QToolButton *time_btn = nullptr;
|
||||
ToolButton *seek_backward_btn = nullptr;
|
||||
ToolButton *play_btn = nullptr;
|
||||
ToolButton *seek_forward_btn = nullptr;
|
||||
ToolButton *loop_btn = nullptr;
|
||||
QAction *time_display_action = nullptr;
|
||||
QAction *play_toggle_action = nullptr;
|
||||
QToolButton *speed_btn = nullptr;
|
||||
ToolButton *skip_to_end_btn = nullptr;
|
||||
QAction *skip_to_end_action = nullptr;
|
||||
Slider *slider = nullptr;
|
||||
QTabBar *camera_tab = nullptr;
|
||||
};
|
||||
|
||||
@@ -10,8 +10,11 @@ cd "$ROOT"
|
||||
|
||||
# updating uv on macOS results in 403 sometimes
|
||||
function update_uv() {
|
||||
for i in $(seq 1 5);
|
||||
do
|
||||
if ! uv self update --help >/dev/null 2>&1; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
for i in $(seq 1 5); do
|
||||
if uv self update; then
|
||||
return 0
|
||||
else
|
||||
|
||||
@@ -1,90 +0,0 @@
|
||||
# LatencyLogger
|
||||
|
||||
LatencyLogger is a tool to track the time from first pixel to actuation. Timestamps are printed in a table as well as plotted in a graph. Start openpilot with `LOG_TIMESTAMPS=1` set to enable the necessary logging.
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
$ python3 latency_logger.py -h
|
||||
usage: latency_logger.py [-h] [--relative] [--demo] [--plot] [route_or_segment_name]
|
||||
|
||||
A tool for analyzing openpilot's end-to-end latency
|
||||
|
||||
positional arguments:
|
||||
route_or_segment_name
|
||||
The route to print (default: None)
|
||||
|
||||
optional arguments:
|
||||
-h, --help show this help message and exit
|
||||
--relative Make timestamps relative to the start of each frame (default: False)
|
||||
--demo Use the demo route instead of providing one (default: False)
|
||||
--plot If a plot should be generated (default: False)
|
||||
--offset Offset service to better visualize overlap (default: False)
|
||||
```
|
||||
To timestamp an event, use `LOGT("msg")` in c++ code or `cloudlog.timestamp("msg")` in python code. If the print is warning for frameId assignment ambiguity, use `LOGT(frameId ,"msg")`.
|
||||
|
||||
## Examples
|
||||
|
||||
Timestamps are visualized as diamonds
|
||||
|
||||
| | Relative | Absolute |
|
||||
| ------------- | ------------- | ------------- |
|
||||
| Inline |  |  |
|
||||
| Offset |  |  |
|
||||
|
||||
Printed timestamps of a frame with internal durations.
|
||||
```
|
||||
Frame ID: 1202
|
||||
camerad
|
||||
wideRoadCameraState start of frame 0.0
|
||||
roadCameraState start of frame 0.049583
|
||||
wideRoadCameraState published 35.01206
|
||||
WideRoadCamera: Image set 35.020028
|
||||
roadCameraState published 38.508261
|
||||
RoadCamera: Image set 38.520344
|
||||
RoadCamera: Transformed 38.616176
|
||||
wideRoadCameraState.processingTime 3.152403049170971
|
||||
roadCameraState.processingTime 6.453451234847307
|
||||
modeld
|
||||
Image added 40.909841
|
||||
Extra image added 42.515027
|
||||
Execution finished 63.002552
|
||||
modelV2 published 63.148747
|
||||
modelV2.modelExecutionTime 23.62649142742157
|
||||
modelV2.gpuExecutionTimeDEPRECATED 0.0
|
||||
plannerd
|
||||
longitudinalPlan published 69.715999
|
||||
longitudinalPlan.solverExecutionTime 0.5619999719783664
|
||||
controlsd
|
||||
Data sampled 70.217763
|
||||
Events updated 71.037178
|
||||
sendcan published 72.278775
|
||||
controlsState published 72.825226
|
||||
Data sampled 80.008354
|
||||
Events updated 80.787666
|
||||
sendcan published 81.849682
|
||||
controlsState published 82.238323
|
||||
Data sampled 90.521123
|
||||
Events updated 91.626003
|
||||
sendcan published 93.413218
|
||||
controlsState published 94.143989
|
||||
Data sampled 100.991497
|
||||
Events updated 101.973774
|
||||
sendcan published 103.565575
|
||||
controlsState published 104.146088
|
||||
Data sampled 110.284387
|
||||
Events updated 111.183541
|
||||
sendcan published 112.981692
|
||||
controlsState published 113.731994
|
||||
pandad
|
||||
sending sendcan to panda: 250027001751393037323631 81.928119
|
||||
sendcan sent to panda: 250027001751393037323631 82.164834
|
||||
sending sendcan to panda: 250027001751393037323631 93.569986
|
||||
sendcan sent to panda: 250027001751393037323631 93.92795
|
||||
sending sendcan to panda: 250027001751393037323631 103.689167
|
||||
sendcan sent to panda: 250027001751393037323631 104.012235
|
||||
sending sendcan to panda: 250027001751393037323631 113.109555
|
||||
sendcan sent to panda: 250027001751393037323631 113.525487
|
||||
sending sendcan to panda: 250027001751393037323631 122.508434
|
||||
sendcan sent to panda: 250027001751393037323631 122.834314
|
||||
```
|
||||
@@ -1,239 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import json
|
||||
import matplotlib.patches as mpatches
|
||||
import matplotlib.pyplot as plt
|
||||
import sys
|
||||
from bisect import bisect_left, bisect_right
|
||||
from collections import defaultdict
|
||||
|
||||
from openpilot.tools.lib.logreader import LogReader
|
||||
|
||||
DEMO_ROUTE = "9f583b1d93915c31|2022-05-18--10-49-51--0"
|
||||
|
||||
SERVICES = ['camerad', 'modeld', 'plannerd', 'controlsd', 'pandad']
|
||||
MONOTIME_KEYS = ['modelMonoTime', 'lateralPlanMonoTime']
|
||||
MSGQ_TO_SERVICE = {
|
||||
'roadCameraState': 'camerad',
|
||||
'wideRoadCameraState': 'camerad',
|
||||
'modelV2': 'modeld',
|
||||
'longitudinalPlan': 'plannerd',
|
||||
'sendcan': 'controlsd',
|
||||
'controlsState': 'controlsd'
|
||||
}
|
||||
SERVICE_TO_DURATIONS = {
|
||||
'camerad': ['processingTime'],
|
||||
'modeld': ['modelExecutionTime', 'gpuExecutionTimeDEPRECATED'],
|
||||
'plannerd': ['solverExecutionTime'],
|
||||
}
|
||||
|
||||
def read_logs(lr):
|
||||
data = defaultdict(lambda: defaultdict(lambda: defaultdict(list)))
|
||||
mono_to_frame = {}
|
||||
frame_mismatches = []
|
||||
frame_id_fails = 0
|
||||
latest_sendcan_monotime = 0
|
||||
for msg in lr:
|
||||
if msg.which() == 'sendcan':
|
||||
latest_sendcan_monotime = msg.logMonoTime
|
||||
continue
|
||||
|
||||
if msg.which() in MSGQ_TO_SERVICE:
|
||||
service = MSGQ_TO_SERVICE[msg.which()]
|
||||
msg_obj = getattr(msg, msg.which())
|
||||
|
||||
frame_id = -1
|
||||
if hasattr(msg_obj, "frameId"):
|
||||
frame_id = msg_obj.frameId
|
||||
else:
|
||||
continue_outer = False
|
||||
for key in MONOTIME_KEYS:
|
||||
if hasattr(msg_obj, key):
|
||||
if getattr(msg_obj, key) == 0:
|
||||
# Filter out controlsd messages which arrive before the camera loop
|
||||
continue_outer = True
|
||||
elif getattr(msg_obj, key) in mono_to_frame:
|
||||
frame_id = mono_to_frame[getattr(msg_obj, key)]
|
||||
if continue_outer:
|
||||
continue
|
||||
if frame_id == -1:
|
||||
frame_id_fails += 1
|
||||
continue
|
||||
mono_to_frame[msg.logMonoTime] = frame_id
|
||||
data['timestamp'][frame_id][service].append((msg.which()+" published", msg.logMonoTime))
|
||||
|
||||
next_service = SERVICES[SERVICES.index(service)+1]
|
||||
if not data['start'][frame_id][next_service]:
|
||||
data['start'][frame_id][next_service] = msg.logMonoTime
|
||||
data['end'][frame_id][service] = msg.logMonoTime
|
||||
|
||||
if service in SERVICE_TO_DURATIONS:
|
||||
for duration in SERVICE_TO_DURATIONS[service]:
|
||||
data['duration'][frame_id][service].append((msg.which()+"."+duration, getattr(msg_obj, duration)))
|
||||
|
||||
if service == SERVICES[0]:
|
||||
data['timestamp'][frame_id][service].append((msg.which()+" start of frame", msg_obj.timestampSof))
|
||||
if not data['start'][frame_id][service]:
|
||||
data['start'][frame_id][service] = msg_obj.timestampSof
|
||||
elif msg.which() == 'controlsState':
|
||||
# Sendcan is published before controlsState, but the frameId is retrieved in CS
|
||||
data['timestamp'][frame_id][service].append(("sendcan published", latest_sendcan_monotime))
|
||||
elif msg.which() == 'modelV2':
|
||||
if msg_obj.frameIdExtra != frame_id:
|
||||
frame_mismatches.append(frame_id)
|
||||
|
||||
if frame_id_fails > 20:
|
||||
print("Warning, many frameId fetch fails", frame_id_fails)
|
||||
if len(frame_mismatches) > 20:
|
||||
print("Warning, many frame mismatches", len(frame_mismatches))
|
||||
return (data, frame_mismatches)
|
||||
|
||||
# This is not needed in 3.10 as a "key" parameter is added to bisect
|
||||
class KeyifyList:
|
||||
def __init__(self, inner, key):
|
||||
self.inner = inner
|
||||
self.key = key
|
||||
def __len__(self):
|
||||
return len(self.inner)
|
||||
def __getitem__(self, k):
|
||||
return self.key(self.inner[k])
|
||||
|
||||
def find_frame_id(time, service, start_times, end_times):
|
||||
left = bisect_left(KeyifyList(list(start_times.items()),
|
||||
lambda x: x[1][service] if x[1][service] else -1), time) - 1
|
||||
right = bisect_right(KeyifyList(list(end_times.items()),
|
||||
lambda x: x[1][service] if x[1][service] else float("inf")), time)
|
||||
return left, right
|
||||
|
||||
def find_t0(start_times, frame_id=-1):
|
||||
frame_id = frame_id if frame_id > -1 else min(start_times.keys())
|
||||
m = max(start_times.keys())
|
||||
while frame_id <= m:
|
||||
for service in SERVICES:
|
||||
if start_times[frame_id][service]:
|
||||
return start_times[frame_id][service]
|
||||
frame_id += 1
|
||||
raise Exception('No start time has been set')
|
||||
|
||||
def insert_cloudlogs(lr, timestamps, start_times, end_times):
|
||||
# at least one cloudlog must be made in controlsd
|
||||
|
||||
t0 = find_t0(start_times)
|
||||
failed_inserts = 0
|
||||
latest_controls_frameid = 0
|
||||
for msg in lr:
|
||||
if msg.which() == "logMessage":
|
||||
jmsg = json.loads(msg.logMessage)
|
||||
if "timestamp" in jmsg['msg']:
|
||||
time = int(jmsg['msg']['timestamp']['time'])
|
||||
service = jmsg['ctx']['daemon']
|
||||
event = jmsg['msg']['timestamp']['event']
|
||||
if time < t0:
|
||||
# Filter out controlsd messages which arrive before the camera loop
|
||||
continue
|
||||
|
||||
if "frame_id" in jmsg['msg']['timestamp']:
|
||||
timestamps[int(jmsg['msg']['timestamp']['frame_id'])][service].append((event, time))
|
||||
continue
|
||||
|
||||
if service == "pandad":
|
||||
timestamps[latest_controls_frameid][service].append((event, time))
|
||||
end_times[latest_controls_frameid][service] = time
|
||||
else:
|
||||
frame_id = find_frame_id(time, service, start_times, end_times)
|
||||
if frame_id:
|
||||
if frame_id[0] != frame_id[1]:
|
||||
event += " (warning: ambiguity)"
|
||||
frame_id = frame_id[0]
|
||||
if service == 'controlsd':
|
||||
latest_controls_frameid = frame_id
|
||||
timestamps[frame_id][service].append((event, time))
|
||||
else:
|
||||
failed_inserts += 1
|
||||
|
||||
if latest_controls_frameid == 0:
|
||||
print("Warning: failed to bind pandad logs to a frame ID. Add a timestamp cloudlog in controlsd.")
|
||||
elif failed_inserts > len(timestamps):
|
||||
print(f"Warning: failed to bind {failed_inserts} cloudlog timestamps to a frame ID")
|
||||
|
||||
def print_timestamps(timestamps, durations, start_times, relative):
|
||||
t0 = find_t0(start_times)
|
||||
for frame_id in timestamps.keys():
|
||||
print('='*80)
|
||||
print("Frame ID:", frame_id)
|
||||
if relative:
|
||||
t0 = find_t0(start_times, frame_id)
|
||||
|
||||
for service in SERVICES:
|
||||
print(" "+service)
|
||||
events = timestamps[frame_id][service]
|
||||
for event, time in sorted(events, key = lambda x: x[1]):
|
||||
print(" "+f'{event:<53s}{str((time-t0)/1e6):<53s}')
|
||||
for event, time in durations[frame_id][service]:
|
||||
print(" "+f'{event:<53s}{str(time*1000):<53s}')
|
||||
|
||||
def graph_timestamps(timestamps, start_times, end_times, relative, offset_services=False, title=""):
|
||||
plt.rcParams.update({'font.size': 18})
|
||||
|
||||
t0 = find_t0(start_times)
|
||||
fig, ax = plt.subplots()
|
||||
ax.set_xlim(0, 130 if relative else 750)
|
||||
ax.set_ylim(0, 17)
|
||||
ax.set_xlabel('Time (milliseconds)')
|
||||
colors = ['blue', 'green', 'red', 'yellow', 'purple']
|
||||
offsets = [[0, -5*j] for j in range(len(SERVICES))] if offset_services else None
|
||||
height = 0.3 if offset_services else 0.9
|
||||
assert len(colors) == len(SERVICES), 'Each service needs a color'
|
||||
|
||||
points = {"x": [], "y": [], "labels": []}
|
||||
for i, (frame_id, services) in enumerate(timestamps.items()):
|
||||
if relative:
|
||||
t0 = find_t0(start_times, frame_id)
|
||||
service_bars = []
|
||||
for service, events in services.items():
|
||||
if start_times[frame_id][service] and end_times[frame_id][service]:
|
||||
start = start_times[frame_id][service]
|
||||
end = end_times[frame_id][service]
|
||||
service_bars.append(((start-t0)/1e6,(end-start)/1e6))
|
||||
for event in events:
|
||||
points['x'].append((event[1]-t0)/1e6)
|
||||
points['y'].append(i)
|
||||
points['labels'].append(event[0])
|
||||
ax.broken_barh(service_bars, (i-height/2, height), facecolors=(colors), alpha=0.5, offsets=offsets)
|
||||
|
||||
ax.scatter(points['x'], points['y'], marker='d', edgecolor='black')
|
||||
for i, label in enumerate(points['labels']):
|
||||
ax.annotate(label, (points['x'][i], points['y'][i]), textcoords="offset points", xytext=(0,10), ha='center')
|
||||
|
||||
plt.title(title)
|
||||
fig.set_size_inches(18, 9)
|
||||
plt.legend(handles=[mpatches.Patch(color=colors[i], label=SERVICES[i]) for i in range(len(SERVICES))])
|
||||
plt.show()
|
||||
|
||||
def get_timestamps(lr):
|
||||
lr = list(lr)
|
||||
data, frame_mismatches = read_logs(lr)
|
||||
insert_cloudlogs(lr, data['timestamp'], data['start'], data['end'])
|
||||
return data, frame_mismatches
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description="A tool for analyzing openpilot's end-to-end latency",
|
||||
formatter_class = argparse.ArgumentDefaultsHelpFormatter)
|
||||
parser.add_argument("--relative", action="store_true", help="Make timestamps relative to the start of each frame")
|
||||
parser.add_argument("--demo", action="store_true", help="Use the demo route instead of providing one")
|
||||
parser.add_argument("--plot", action="store_true", help="If a plot should be generated")
|
||||
parser.add_argument("--offset", action="store_true", help="Vertically offset service to better visualize overlap")
|
||||
parser.add_argument("route_or_segment_name", nargs='?', help="The route to print")
|
||||
|
||||
if len(sys.argv) == 1:
|
||||
parser.print_help()
|
||||
sys.exit()
|
||||
args = parser.parse_args()
|
||||
|
||||
r = DEMO_ROUTE if args.demo else args.route_or_segment_name.strip()
|
||||
lr = LogReader(r, sort_by_time=True)
|
||||
|
||||
data, _ = get_timestamps(lr)
|
||||
print_timestamps(data['timestamp'], data['duration'], data['start'], args.relative)
|
||||
if args.plot:
|
||||
graph_timestamps(data['timestamp'], data['start'], data['end'], args.relative, offset_services=args.offset, title=r)
|
||||
@@ -1,4 +1,5 @@
|
||||
import os
|
||||
import posixpath
|
||||
import socket
|
||||
from urllib.parse import urlparse
|
||||
|
||||
@@ -8,6 +9,9 @@ DATA_ENDPOINT = os.getenv("DATA_ENDPOINT", "http://data-raw.comma.internal/")
|
||||
|
||||
|
||||
def internal_source_available(url=DATA_ENDPOINT):
|
||||
if os.path.isdir(url):
|
||||
return True
|
||||
|
||||
try:
|
||||
hostname = urlparse(url).hostname
|
||||
port = urlparse(url).port or 80
|
||||
@@ -22,7 +26,7 @@ def internal_source_available(url=DATA_ENDPOINT):
|
||||
|
||||
def resolve_name(fn):
|
||||
if fn.startswith("cd:/"):
|
||||
return fn.replace("cd:/", DATA_ENDPOINT)
|
||||
return posixpath.join(DATA_ENDPOINT, fn[4:])
|
||||
return fn
|
||||
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@ If streaming to PlotJuggler from a replay on your PC, simply run: `./juggle.py -
|
||||
|
||||
For a quick demo, go through the installation step and run this command:
|
||||
|
||||
`./juggle.py --demo --layout=layouts/demo.xml`
|
||||
`./juggle.py --demo --layout=layouts/tuning.xml`
|
||||
|
||||
## Layouts
|
||||
|
||||
|
||||
@@ -1,104 +0,0 @@
|
||||
<?xml version='1.0' encoding='UTF-8'?>
|
||||
<root version="2.3.8">
|
||||
<tabbed_widget parent="main_window" name="Main Window">
|
||||
<Tab tab_name="tab1" containers="1">
|
||||
<Container>
|
||||
<DockSplitter orientation="-" sizes="1" count="1">
|
||||
<DockSplitter orientation="|" sizes="0.5;0.5" count="2">
|
||||
<DockSplitter orientation="-" sizes="0.500497;0.499503" count="2">
|
||||
<DockArea name="...">
|
||||
<plot mode="TimeSeries" style="Lines">
|
||||
<range top="2.762667" bottom="-3.239397" right="56.512723" left="0.000000"/>
|
||||
<limitY/>
|
||||
<curve color="#1f77b4" name="/carState/aEgo"/>
|
||||
<curve color="#17becf" name="/carState/brake"/>
|
||||
</plot>
|
||||
</DockArea>
|
||||
<DockArea name="...">
|
||||
<plot mode="TimeSeries" style="Lines">
|
||||
<range top="5.191867" bottom="-5.724069" right="56.512723" left="0.000000"/>
|
||||
<limitY/>
|
||||
<curve color="#1ac938" name="dv/dt"/>
|
||||
</plot>
|
||||
</DockArea>
|
||||
</DockSplitter>
|
||||
<DockSplitter orientation="-" sizes="0.500497;0.499503" count="2">
|
||||
<DockArea name="...">
|
||||
<plot mode="TimeSeries" style="Lines">
|
||||
<range top="16.065524" bottom="-0.470076" right="56.512723" left="0.000000"/>
|
||||
<limitY/>
|
||||
<curve color="#d62728" name="/carState/vEgo"/>
|
||||
<curve color="#bcbd22" name="/carState/gas"/>
|
||||
</plot>
|
||||
</DockArea>
|
||||
<DockArea name="...">
|
||||
<plot mode="TimeSeries" style="Lines">
|
||||
<range top="1.014703" bottom="-0.012971" right="56.512723" left="0.000000"/>
|
||||
<limitY/>
|
||||
<curve color="#ff7f0e" name="/model/meta/brakeDisengageProb"/>
|
||||
<curve color="#f14cc1" name="/model/meta/engagedProb"/>
|
||||
<curve color="#9467bd" name="/model/meta/steerOverrideProb"/>
|
||||
</plot>
|
||||
</DockArea>
|
||||
</DockSplitter>
|
||||
</DockSplitter>
|
||||
</DockSplitter>
|
||||
</Container>
|
||||
</Tab>
|
||||
<currentTabIndex index="0"/>
|
||||
</tabbed_widget>
|
||||
<use_relative_time_offset enabled="1"/>
|
||||
<!-- - - - - - - - - - - - - - - -->
|
||||
<!-- - - - - - - - - - - - - - - -->
|
||||
<Plugins>
|
||||
<plugin ID="DataLoad CSV">
|
||||
<default time_axis=""/>
|
||||
</plugin>
|
||||
<plugin ID="DataLoad ROS bags">
|
||||
<use_header_stamp value="false"/>
|
||||
<use_renaming_rules value="true"/>
|
||||
<discard_large_arrays value="true"/>
|
||||
<max_array_size value="100"/>
|
||||
</plugin>
|
||||
<plugin ID="DataLoad Rlog"/>
|
||||
<plugin ID="DataLoad ULog"/>
|
||||
<plugin ID="LSL Subscriber"/>
|
||||
<plugin ID="MQTT Subscriber"/>
|
||||
<plugin ID="ROS Topic Subscriber">
|
||||
<use_header_stamp value="false"/>
|
||||
<use_renaming_rules value="true"/>
|
||||
<discard_large_arrays value="true"/>
|
||||
<max_array_size value="100"/>
|
||||
</plugin>
|
||||
<plugin ID="UDP Server"/>
|
||||
<plugin ID="WebSocket Server"/>
|
||||
<plugin ID="ZMQ Subscriber"/>
|
||||
<plugin status="idle" ID="ROS /rosout Visualization"/>
|
||||
<plugin status="idle" ID="ROS Topic Re-Publisher"/>
|
||||
</Plugins>
|
||||
<!-- - - - - - - - - - - - - - - -->
|
||||
<!-- - - - - - - - - - - - - - - -->
|
||||
<customMathEquations>
|
||||
<snippet name="dv/dt">
|
||||
<global>prevX = 0
|
||||
prevY = 0
|
||||
is_first = true</global>
|
||||
<function>if (is_first) then
|
||||
is_first = false
|
||||
prevX = time
|
||||
prevY = value
|
||||
end
|
||||
|
||||
dx = time - prevX
|
||||
dy = value - prevY
|
||||
prevX = time
|
||||
prevY = value
|
||||
|
||||
return dy/dx</function>
|
||||
<linkedSource>/carState/vEgo</linkedSource>
|
||||
</snippet>
|
||||
</customMathEquations>
|
||||
<snippets/>
|
||||
<!-- - - - - - - - - - - - - - - -->
|
||||
</root>
|
||||
|
||||
+28
-41
@@ -106,31 +106,27 @@ void Replay::seekTo(double seconds, bool relative) {
|
||||
rInfo("Seeking to %d s, segment %d", (int)target_time, target_segment);
|
||||
notifyEvent(onSeeking, target_time);
|
||||
|
||||
double seeked_to_sec = -1;
|
||||
interruptStream([&]() {
|
||||
current_segment_ = target_segment;
|
||||
current_segment_.store(target_segment);
|
||||
cur_mono_time_ = route_start_ts_ + target_time * 1e9;
|
||||
seeking_to_ = target_time;
|
||||
|
||||
if (event_data_->isSegmentLoaded(target_segment)) {
|
||||
seeked_to_sec = *seeking_to_;
|
||||
seeking_to_.reset();
|
||||
}
|
||||
seeking_to_.store(target_time, std::memory_order_relaxed);
|
||||
return false;
|
||||
});
|
||||
|
||||
checkSeekProgress(seeked_to_sec);
|
||||
seg_mgr_->setCurrentSegment(target_segment);
|
||||
checkSeekProgress();
|
||||
}
|
||||
|
||||
void Replay::checkSeekProgress(double seeked_to_sec) {
|
||||
if (seeked_to_sec >= 0) {
|
||||
if (onSeekedTo) {
|
||||
onSeekedTo(seeked_to_sec);
|
||||
} else {
|
||||
interruptStream([]() { return true; });
|
||||
}
|
||||
void Replay::checkSeekProgress() {
|
||||
if (!seg_mgr_->getEventData()->isSegmentLoaded(current_segment_.load())) return;
|
||||
|
||||
double seek_to = seeking_to_.exchange(-1.0, std::memory_order_acquire);
|
||||
if (seek_to >= 0 && onSeekedTo) {
|
||||
onSeekedTo(seek_to);
|
||||
}
|
||||
|
||||
// Resume the interrupted stream
|
||||
interruptStream([]() { return true; });
|
||||
}
|
||||
|
||||
void Replay::seekToFlag(FindFlag flag) {
|
||||
@@ -152,29 +148,19 @@ void Replay::pause(bool pause) {
|
||||
void Replay::handleSegmentMerge() {
|
||||
if (exit_) return;
|
||||
|
||||
double seeked_to_sec = -1;
|
||||
interruptStream([&]() {
|
||||
event_data_ = seg_mgr_->getEventData();
|
||||
notifyEvent(onSegmentsMerged);
|
||||
|
||||
bool segment_loaded = event_data_->isSegmentLoaded(current_segment_);
|
||||
if (seeking_to_ && segment_loaded) {
|
||||
seeked_to_sec = *seeking_to_;
|
||||
seeking_to_.reset();
|
||||
return false;
|
||||
}
|
||||
return segment_loaded;
|
||||
});
|
||||
|
||||
checkSeekProgress(seeked_to_sec);
|
||||
if (!stream_thread_.joinable() && !event_data_->events.empty()) {
|
||||
startStream();
|
||||
auto event_data = seg_mgr_->getEventData();
|
||||
if (!stream_thread_.joinable() && !event_data->segments.empty()) {
|
||||
startStream(event_data->segments.begin()->second);
|
||||
}
|
||||
notifyEvent(onSegmentsMerged);
|
||||
|
||||
// Interrupt the stream to handle segment merge
|
||||
interruptStream([]() { return false; });
|
||||
checkSeekProgress();
|
||||
}
|
||||
|
||||
void Replay::startStream() {
|
||||
const auto &cur_segment = event_data_->segments.begin()->second;
|
||||
const auto &events = cur_segment->log->events;
|
||||
void Replay::startStream(const std::shared_ptr<Segment> segment) {
|
||||
const auto &events = segment->log->events;
|
||||
route_start_ts_ = events.front().mono_time;
|
||||
cur_mono_time_ += route_start_ts_ - 1;
|
||||
|
||||
@@ -212,7 +198,7 @@ void Replay::startStream() {
|
||||
if (!hasFlag(REPLAY_FLAG_NO_VIPC)) {
|
||||
std::pair<int, int> camera_size[MAX_CAMERAS] = {};
|
||||
for (auto type : ALL_CAMERAS) {
|
||||
if (auto &fr = cur_segment->frames[type]) {
|
||||
if (auto &fr = segment->frames[type]) {
|
||||
camera_size[type] = {fr->width, fr->height};
|
||||
}
|
||||
}
|
||||
@@ -271,6 +257,7 @@ void Replay::streamThread() {
|
||||
stream_cv_.wait(lk, [this]() { return exit_ || (events_ready_ && !interrupt_requested_); });
|
||||
if (exit_) break;
|
||||
|
||||
event_data_ = seg_mgr_->getEventData();
|
||||
const auto &events = event_data_->events;
|
||||
auto first = std::upper_bound(events.cbegin(), events.cend(), Event(cur_which, cur_mono_time_, {}));
|
||||
if (first == events.cend()) {
|
||||
@@ -308,11 +295,11 @@ std::vector<Event>::const_iterator Replay::publishEvents(std::vector<Event>::con
|
||||
|
||||
for (; !interrupt_requested_ && first != last; ++first) {
|
||||
const Event &evt = *first;
|
||||
int segment = toSeconds(evt.mono_time) / 60;
|
||||
|
||||
if (current_segment_ != segment) {
|
||||
current_segment_ = segment;
|
||||
seg_mgr_->setCurrentSegment(current_segment_);
|
||||
int segment = toSeconds(evt.mono_time) / 60;
|
||||
if (current_segment_.load(std::memory_order_relaxed) != segment) {
|
||||
current_segment_.store(segment, std::memory_order_relaxed);
|
||||
seg_mgr_->setCurrentSegment(segment);
|
||||
}
|
||||
|
||||
// Skip events if socket is not present
|
||||
|
||||
@@ -55,9 +55,8 @@ public:
|
||||
inline const std::string &carFingerprint() const { return car_fingerprint_; }
|
||||
inline const std::shared_ptr<std::vector<Timeline::Entry>> getTimeline() const { return timeline_.getEntries(); }
|
||||
inline const std::optional<Timeline::Entry> findAlertAtTime(double sec) const { return timeline_.findAlertAtTime(sec); }
|
||||
const std::shared_ptr<SegmentManager::EventData> getEventData() const { return event_data_; }
|
||||
const std::shared_ptr<SegmentManager::EventData> getEventData() const { return seg_mgr_->getEventData(); }
|
||||
void installEventFilter(std::function<bool(const Event *)> filter) { event_filter_ = filter; }
|
||||
void resumeStream() { interruptStream([]() { return true; }); }
|
||||
|
||||
// Event callback functions
|
||||
std::function<void()> onSegmentsMerged = nullptr;
|
||||
@@ -68,7 +67,7 @@ public:
|
||||
private:
|
||||
void setupServices(const std::vector<std::string> &allow, const std::vector<std::string> &block);
|
||||
void setupSegmentManager(bool has_filters);
|
||||
void startStream();
|
||||
void startStream(const std::shared_ptr<Segment> segment);
|
||||
void streamThread();
|
||||
void handleSegmentMerge();
|
||||
void interruptStream(const std::function<bool()>& update_fn);
|
||||
@@ -76,7 +75,7 @@ private:
|
||||
std::vector<Event>::const_iterator last);
|
||||
void publishMessage(const Event *e);
|
||||
void publishFrame(const Event *e);
|
||||
void checkSeekProgress(double seeked_to_sec);
|
||||
void checkSeekProgress();
|
||||
|
||||
std::unique_ptr<SegmentManager> seg_mgr_;
|
||||
Timeline timeline_;
|
||||
@@ -86,8 +85,8 @@ private:
|
||||
std::mutex stream_lock_;
|
||||
bool user_paused_ = false;
|
||||
std::condition_variable stream_cv_;
|
||||
int current_segment_ = 0;
|
||||
std::optional<double> seeking_to_;
|
||||
std::atomic<int> current_segment_ = 0;
|
||||
std::atomic<double> seeking_to_ = -1.0;
|
||||
std::atomic<bool> exit_ = false;
|
||||
std::atomic<bool> interrupt_requested_ = false;
|
||||
bool events_ready_ = false;
|
||||
|
||||
@@ -105,7 +105,7 @@ bool SegmentManager::mergeSegments(const SegmentMap::iterator &begin, const Segm
|
||||
merged_event_data->segments[n] = segments_.at(n);
|
||||
}
|
||||
|
||||
event_data_ = merged_event_data;
|
||||
std::atomic_store(&event_data_, std::move(merged_event_data));
|
||||
merged_segments_ = segments_to_merge;
|
||||
|
||||
return true;
|
||||
|
||||
@@ -21,14 +21,14 @@ public:
|
||||
};
|
||||
|
||||
SegmentManager(const std::string &route_name, uint32_t flags, const std::string &data_dir = "")
|
||||
: flags_(flags), route_(route_name, data_dir) {};
|
||||
: flags_(flags), route_(route_name, data_dir), event_data_(std::make_shared<EventData>()) {}
|
||||
~SegmentManager();
|
||||
|
||||
bool load();
|
||||
void setCurrentSegment(int seg_num);
|
||||
void setCallback(const std::function<void()> &callback) { onSegmentMergedCallback_ = callback; }
|
||||
void setFilters(const std::vector<bool> &filters) { filters_ = filters; }
|
||||
const std::shared_ptr<EventData> getEventData() const { return event_data_; }
|
||||
const std::shared_ptr<EventData> getEventData() const { return std::atomic_load(&event_data_); }
|
||||
bool hasSegment(int n) const { return segments_.find(n) != segments_.end(); }
|
||||
|
||||
Route route_;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#include "tools/replay/timeline.h"
|
||||
|
||||
#include <array>
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
|
||||
#include "cereal/gen/cpp/log.capnp.h"
|
||||
|
||||
@@ -74,7 +74,7 @@ void Timeline::buildTimeline(const Route &route, uint64_t route_start_ts, bool l
|
||||
// Sort and finalize the timeline entries
|
||||
auto entries = std::make_shared<std::vector<Entry>>(staging_entries_);
|
||||
std::sort(entries->begin(), entries->end(), [](auto &a, auto &b) { return a.start_time < b.start_time; });
|
||||
timeline_entries_ = entries;
|
||||
std::atomic_store(&timeline_entries_, std::move(entries));
|
||||
|
||||
callback(log); // Notify the callback once the log is processed
|
||||
}
|
||||
|
||||
+11
-11
@@ -27,20 +27,20 @@ public:
|
||||
std::function<void(std::shared_ptr<LogReader>)> callback);
|
||||
std::optional<uint64_t> find(double cur_ts, FindFlag flag) const;
|
||||
std::optional<Entry> findAlertAtTime(double target_time) const;
|
||||
const std::shared_ptr<std::vector<Entry>> getEntries() const { return timeline_entries_; }
|
||||
const std::shared_ptr<std::vector<Entry>> getEntries() const { return std::atomic_load(&timeline_entries_); }
|
||||
|
||||
private:
|
||||
void buildTimeline(const Route &route, uint64_t route_start_ts, bool local_cache,
|
||||
std::function<void(std::shared_ptr<LogReader>)> callback);
|
||||
void updateEngagementStatus(const cereal::SelfdriveState::Reader &cs, std::optional<size_t> &idx, double seconds);
|
||||
void updateAlertStatus(const cereal::SelfdriveState::Reader &cs, std::optional<size_t> &idx, double seconds);
|
||||
void buildTimeline(const Route &route, uint64_t route_start_ts, bool local_cache,
|
||||
std::function<void(std::shared_ptr<LogReader>)> callback);
|
||||
void updateEngagementStatus(const cereal::SelfdriveState::Reader &cs, std::optional<size_t> &idx, double seconds);
|
||||
void updateAlertStatus(const cereal::SelfdriveState::Reader &cs, std::optional<size_t> &idx, double seconds);
|
||||
|
||||
std::thread thread_;
|
||||
std::atomic<bool> should_exit_ = false;
|
||||
std::thread thread_;
|
||||
std::atomic<bool> should_exit_ = false;
|
||||
|
||||
// Temporarily holds entries before they are sorted and finalized
|
||||
std::vector<Entry> staging_entries_;
|
||||
// Temporarily holds entries before they are sorted and finalized
|
||||
std::vector<Entry> staging_entries_;
|
||||
|
||||
// Final sorted timeline entries
|
||||
std::shared_ptr<std::vector<Entry>> timeline_entries_;
|
||||
// Final sorted timeline entries
|
||||
std::shared_ptr<std::vector<Entry>> timeline_entries_;
|
||||
};
|
||||
|
||||
@@ -24,14 +24,14 @@ options:
|
||||
|
||||
Examples using route name to observe accelerometer and qcamera:
|
||||
|
||||
`./run.sh --qcam "a2a0ccea32023010/2023-07-27--13-01-19"`
|
||||
`./run.py --qcam "a2a0ccea32023010/2023-07-27--13-01-19"`
|
||||
|
||||
Examples using segment range (more on [SegmentRange](https://github.com/commaai/openpilot/tree/master/tools/lib)):
|
||||
|
||||
`./run.sh --qcam "a2a0ccea32023010/2023-07-27--13-01-19/2:4"`
|
||||
`./run.py --qcam "a2a0ccea32023010/2023-07-27--13-01-19/2:4"`
|
||||
|
||||
## Cautions:
|
||||
- Showing hevc videos (`--fcam`, `--ecam`, and `--dcam`) are expensive, and it's recommended to use `--qcam` for optimized performance. If possible, limiting your route to a few segments using `SegmentRange` will speed up logging and reduce memory usage
|
||||
|
||||
## Demo
|
||||
`./run.sh --qcam --demo`
|
||||
`./run.py --qcam --demo`
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# TODO: remove this file once Rerun has interface to set log message level
|
||||
set -e
|
||||
|
||||
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )"
|
||||
|
||||
RUST_LOG=warn $DIR/run.py $@
|
||||
|
||||
+13
-15
@@ -1,7 +1,7 @@
|
||||
# Run openpilot with webcam on PC
|
||||
|
||||
What's needed:
|
||||
- Ubuntu 24.04
|
||||
- Ubuntu 24.04 ([WSL2 is not supported](https://github.com/commaai/openpilot/issues/34216))
|
||||
- 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
|
||||
@@ -9,29 +9,27 @@ What's needed:
|
||||
That's it!
|
||||
|
||||
## Setup openpilot
|
||||
- Follow [this readme](../README.md) to install and build the requirements
|
||||
- Install OpenCL Driver
|
||||
```
|
||||
cd ~
|
||||
git clone https://github.com/commaai/openpilot.git
|
||||
```
|
||||
- Follow [this readme](https://github.com/commaai/openpilot/tree/master/tools) to install the requirements
|
||||
- Install [OpenCL Driver](https://registrationcenter-download.intel.com/akdlm/irc_nas/vcp/15532/l_opencl_p_18.1.0.015.tgz)
|
||||
|
||||
## Build openpilot for webcam
|
||||
```
|
||||
cd ~/openpilot
|
||||
USE_WEBCAM=1 scons -j$(nproc)
|
||||
sudo apt install pocl-opencl-icd
|
||||
```
|
||||
|
||||
## Connect the hardware
|
||||
- Connect the road facing camera first, then the driver facing camera
|
||||
- (default indexes are 1 and 2; can be modified in system/camerad/cameras/camera_webcam.cc)
|
||||
- Connect your computer to panda
|
||||
|
||||
## GO
|
||||
```
|
||||
cd ~/openpilot/system/manager
|
||||
NOSENSOR=1 USE_WEBCAM=1 ./manager.py
|
||||
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 (you can run tools/webcam/front_mount_helper.py to help mount the driver camera)
|
||||
- Adjust and secure the webcams.
|
||||
- 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_WEBCAM=1 ROAD_CAM=4 WIDE_CAM=6 system/manager/manager.py
|
||||
```
|
||||
|
||||
@@ -9,14 +9,14 @@ from cereal import messaging
|
||||
from openpilot.tools.webcam.camera import Camera
|
||||
from openpilot.common.realtime import Ratekeeper
|
||||
|
||||
DUAL_CAM = os.getenv("DUAL_CAMERA")
|
||||
WIDE_CAM = os.getenv("WIDE_CAM")
|
||||
CameraType = namedtuple("CameraType", ["msg_name", "stream_type", "cam_id"])
|
||||
CAMERAS = [
|
||||
CameraType("roadCameraState", VisionStreamType.VISION_STREAM_ROAD, os.getenv("CAMERA_ROAD_ID", "/dev/video0")),
|
||||
CameraType("driverCameraState", VisionStreamType.VISION_STREAM_DRIVER, os.getenv("CAMERA_DRIVER_ID", "/dev/video1")),
|
||||
CameraType("roadCameraState", VisionStreamType.VISION_STREAM_ROAD, os.getenv("ROAD_CAM", "0")),
|
||||
CameraType("driverCameraState", VisionStreamType.VISION_STREAM_DRIVER, os.getenv("DRIVER_CAM", "2")),
|
||||
]
|
||||
if DUAL_CAM:
|
||||
CAMERAS.append(CameraType("wideRoadCameraState", VisionStreamType.VISION_STREAM_WIDE_ROAD, DUAL_CAM))
|
||||
if WIDE_CAM:
|
||||
CAMERAS.append(CameraType("wideRoadCameraState", VisionStreamType.VISION_STREAM_WIDE_ROAD, WIDE_CAM))
|
||||
|
||||
class Camerad:
|
||||
def __init__(self):
|
||||
@@ -25,8 +25,9 @@ class Camerad:
|
||||
|
||||
self.cameras = []
|
||||
for c in CAMERAS:
|
||||
print(f"opening {c.msg_name} at {c.cam_id}")
|
||||
cam = Camera(c.msg_name, c.stream_type, c.cam_id)
|
||||
cam_device = f"/dev/video{c.cam_id}"
|
||||
print(f"opening {c.msg_name} at {cam_device}")
|
||||
cam = Camera(c.msg_name, c.stream_type, cam_device)
|
||||
self.cameras.append(cam)
|
||||
self.vipc_server.create_buffers(c.stream_type, 20, cam.W, cam.H)
|
||||
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# export the block below when call manager.py
|
||||
export BLOCK="${BLOCK},camerad"
|
||||
export USE_WEBCAM="1"
|
||||
|
||||
# Change camera index according to your setting
|
||||
export CAMERA_ROAD_ID="/dev/video0"
|
||||
export CAMERA_DRIVER_ID="/dev/video1"
|
||||
#export DUAL_CAMERA="/dev/video2" # optional, camera index for wide road camera
|
||||
|
||||
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )"
|
||||
|
||||
$DIR/camerad.py
|
||||
Reference in New Issue
Block a user