Release 260111

This commit is contained in:
Comma Device
2026-01-11 18:23:29 +08:00
commit 3721ecbf8a
2601 changed files with 855070 additions and 0 deletions

98
tools/cabana/README.md Normal file
View File

@@ -0,0 +1,98 @@
# Cabana
Cabana is a tool developed to view raw CAN data. One use for this is creating and editing [CAN Dictionaries](http://socialledge.com/sjsu/index.php/DBC_Format) (DBC files), and the tool provides direct integration with [commaai/opendbc](https://github.com/commaai/opendbc) (a collection of DBC files), allowing you to load the DBC files direct from source, and save to your fork. In addition, you can load routes from [comma connect](https://connect.comma.ai).
## Usage Instructions
```bash
$ ./cabana -h
Usage: ./cabana [options] route
Options:
-h, --help Displays help on commandline options.
--help-all Displays help including Qt specific options.
--demo use a demo route instead of providing your own
--qcam load qcamera
--ecam load wide road camera
--msgq read can messages from msgq
--panda read can messages from panda
--panda-serial <panda-serial> read can messages from panda with given serial
--socketcan <socketcan> read can messages from given SocketCAN device
--zmq <ip-address> read can messages from zmq at the specified ip-address
messages
--data_dir <data_dir> local directory with routes
--no-vipc do not output video
--dbc <dbc> dbc file to open
Arguments:
route the drive to replay. find your drives at
connect.comma.ai
```
## Examples
### Running Cabana in Demo Mode
To run Cabana using a built-in demo route, use the following command:
```shell
cabana --demo
```
### Loading a Specific Route
To load a specific route for replay, provide the route as an argument:
```shell
cabana "a2a0ccea32023010|2023-07-27--13-01-19"
```
Replace "0ccea32023010|2023-07-27--13-01-19" with your desired route identifier.
### Running Cabana with multiple cameras
To run Cabana with multiple cameras, use the following command:
```shell
cabana "a2a0ccea32023010|2023-07-27--13-01-19" --dcam --ecam
```
### Streaming CAN Messages from a comma Device
[SSH into your device](https://github.com/commaai/openpilot/wiki/SSH) and start the bridge with the following command:
```shell
cd /data/openpilot/cereal/messaging/
./bridge &
```
Then Run Cabana with the device's IP address:
```shell
cabana --zmq <ipaddress>
```
Replace &lt;ipaddress&gt; with your comma device's IP address.
While streaming from the device, Cabana will log the CAN messages to a local directory. By default, this directory is ~/cabana_live_stream/. You can change the log directory in Cabana by navigating to menu -> tools -> settings.
After disconnecting from the device, you can replay the logged CAN messages from the stream selector dialog -> browse local route.
### Streaming CAN Messages from Panda
To read CAN messages from a connected Panda, use the following command:
```shell
cabana --panda
```
### Using the Stream Selector Dialog
If you run Cabana without any arguments, a stream selector dialog will pop up, allowing you to choose the stream.
```shell
cabana
```
## Additional Information
For more information, see the [openpilot wiki](https://github.com/commaai/openpilot/wiki/Cabana)

View File

@@ -0,0 +1,6 @@
<!DOCTYPE RCC><RCC version="1.0">
<qresource>
<file alias="bootstrap-icons.svg">../../../third_party/bootstrap/bootstrap-icons.svg</file>
<file>cabana-icon.png</file>
</qresource>
</RCC>

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

104
tools/cabana/binaryview.h Normal file
View File

@@ -0,0 +1,104 @@
#pragma once
#include <tuple>
#include <vector>
#include <QList>
#include <QSet>
#include <QStyledItemDelegate>
#include <QTableView>
#include "tools/cabana/dbc/dbcmanager.h"
#include "tools/cabana/streams/abstractstream.h"
class BinaryItemDelegate : public QStyledItemDelegate {
public:
BinaryItemDelegate(QObject *parent);
void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override;
bool hasSignal(const QModelIndex &index, int dx, int dy, const cabana::Signal *sig) const;
void drawSignalCell(QPainter* painter, const QStyleOptionViewItem &option, const QModelIndex &index, const cabana::Signal *sig) const;
QFont small_font, hex_font;
std::array<QStaticText, 256> hex_text_table;
std::array<QStaticText, 2> bin_text_table;
};
class BinaryViewModel : public QAbstractTableModel {
public:
BinaryViewModel(QObject *parent) : QAbstractTableModel(parent) {}
void refresh();
void updateState();
void updateItem(int row, int col, uint8_t val, const QColor &color);
QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
int rowCount(const QModelIndex &parent = QModelIndex()) const override { return row_count; }
int columnCount(const QModelIndex &parent = QModelIndex()) const override { return column_count; }
QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override {
return createIndex(row, column, (void *)&items[row * column_count + column]);
}
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);
bool is_msb = false;
bool is_lsb = false;
uint8_t val;
QList<const cabana::Signal *> sigs;
bool valid = false;
};
std::vector<Item> items;
bool heatmap_live_mode = true;
MessageId msg_id;
int row_count = 0;
const int column_count = 9;
};
class BinaryView : public QTableView {
Q_OBJECT
public:
BinaryView(QWidget *parent = nullptr);
void setMessage(const MessageId &message_id);
void highlight(const cabana::Signal *sig);
QSet<const cabana::Signal*> getOverlappingSignals() const;
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);
void signalHovered(const cabana::Signal *sig);
void editSignal(const cabana::Signal *origin_s, cabana::Signal &s);
void showChart(const MessageId &id, const cabana::Signal *sig, bool show, bool merge);
private:
void addShortcuts();
void refresh();
std::tuple<int, int, bool> getSelection(QModelIndex index);
void setSelection(const QRect &rect, QItemSelectionModel::SelectionFlags flags) override;
void mousePressEvent(QMouseEvent *event) override;
void mouseMoveEvent(QMouseEvent *event) override;
void mouseReleaseEvent(QMouseEvent *event) override;
void leaveEvent(QEvent *event) override;
void highlightPosition(const QPoint &pt);
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;
};

123
tools/cabana/chart/chart.h Normal file
View File

@@ -0,0 +1,123 @@
#pragma once
#include <tuple>
#include <utility>
#include <vector>
#include <QMenu>
#include <QGraphicsPixmapItem>
#include <QGraphicsProxyWidget>
#include <QtCharts/QChartView>
#include <QtCharts/QLegendMarker>
#include <QtCharts/QLineSeries>
#include <QtCharts/QScatterSeries>
#include <QtCharts/QValueAxis>
using namespace QtCharts;
#include "tools/cabana/chart/tiplabel.h"
#include "tools/cabana/dbc/dbcmanager.h"
#include "tools/cabana/streams/abstractstream.h"
enum class SeriesType {
Line = 0,
StepLine,
Scatter
};
class ChartsWidget;
class ChartView : public QChartView {
Q_OBJECT
public:
ChartView(const std::pair<double, double> &x_range, ChartsWidget *parent = nullptr);
void addSignal(const MessageId &msg_id, const cabana::Signal *sig);
bool hasSignal(const MessageId &msg_id, const cabana::Signal *sig) const;
void updateSeries(const cabana::Signal *sig = nullptr, const MessageEventsMap *msg_new_events = nullptr);
void updatePlot(double cur, double min, double max);
void setSeriesType(SeriesType type);
void updatePlotArea(int left, bool force = false);
void showTip(double sec);
void hideTip();
void startAnimation();
double secondsAtPoint(const QPointF &pt) const { return chart()->mapToValue(pt).x(); }
struct SigItem {
MessageId msg_id;
const cabana::Signal *sig = nullptr;
QXYSeries *series = nullptr;
std::vector<QPointF> vals;
std::vector<QPointF> step_vals;
QPointF track_pt{};
SegmentTree segment_tree;
double min = 0;
double max = 0;
};
signals:
void axisYLabelWidthChanged(int w);
private slots:
void signalUpdated(const cabana::Signal *sig);
void manageSignals();
void handleMarkerClicked();
void msgUpdated(MessageId id);
void msgRemoved(MessageId id) { removeIf([=](auto &s) { return s.msg_id.address == id.address && !dbc()->msg(id); }); }
void signalRemoved(const cabana::Signal *sig) { removeIf([=](auto &s) { return s.sig == sig; }); }
private:
void appendCanEvents(const cabana::Signal *sig, const std::vector<const CanEvent *> &events,
std::vector<QPointF> &vals, std::vector<QPointF> &step_vals);
void createToolButtons();
void addSeries(QXYSeries *series);
void contextMenuEvent(QContextMenuEvent *event) override;
void mousePressEvent(QMouseEvent *event) override;
void mouseReleaseEvent(QMouseEvent *event) override;
void mouseMoveEvent(QMouseEvent *ev) override;
void dragEnterEvent(QDragEnterEvent *event) override;
void dragLeaveEvent(QDragLeaveEvent *event) override { drawDropIndicator(false); }
void dragMoveEvent(QDragMoveEvent *event) override;
void dropEvent(QDropEvent *event) override;
void resizeEvent(QResizeEvent *event) override;
QSize sizeHint() const override;
void updateAxisY();
void updateTitle();
void resetChartCache();
void setTheme(QChart::ChartTheme theme);
void paintEvent(QPaintEvent *event) override;
void drawForeground(QPainter *painter, const QRectF &rect) override;
void drawBackground(QPainter *painter, const QRectF &rect) override;
void drawDropIndicator(bool draw) { if (std::exchange(can_drop, draw) != can_drop) viewport()->update(); }
void drawSignalValue(QPainter *painter);
void drawTimeline(QPainter *painter);
void drawRubberBandTimeRange(QPainter *painter);
std::tuple<double, double, int> getNiceAxisNumbers(qreal min, qreal max, int tick_count);
qreal niceNumber(qreal x, bool ceiling);
QXYSeries *createSeries(SeriesType type, QColor color);
void setSeriesColor(QXYSeries *, QColor color);
void updateSeriesPoints();
void removeIf(std::function<bool(const SigItem &)> predicate);
inline void clearTrackPoints() { for (auto &s : sigs) s.track_pt = {}; }
int y_label_width = 0;
int align_to = 0;
QValueAxis *axis_x;
QValueAxis *axis_y;
QMenu *menu;
QAction *split_chart_act;
QAction *close_act;
QGraphicsPixmapItem *move_icon;
QGraphicsProxyWidget *close_btn_proxy;
QGraphicsProxyWidget *manage_btn_proxy;
TipLabel *tip_label;
std::vector<SigItem> sigs;
double cur_sec = 0;
SeriesType series_type = SeriesType::Line;
bool is_scrubbing = false;
bool resume_after_scrub = false;
QPixmap chart_pixmap;
bool can_drop = false;
double tooltip_x = -1;
QFont signal_value_font;
ChartsWidget *charts_widget;
friend class ChartsWidget;
};

View File

@@ -0,0 +1,129 @@
#pragma once
#include <unordered_map>
#include <utility>
#include <QGridLayout>
#include <QLabel>
#include <QScrollArea>
#include <QTimer>
#include <QToolBar>
#include <QUndoCommand>
#include <QUndoStack>
#include "tools/cabana/chart/signalselector.h"
#include "tools/cabana/dbc/dbcmanager.h"
#include "tools/cabana/streams/abstractstream.h"
const int CHART_MIN_WIDTH = 300;
const QString CHART_MIME_TYPE = "application/x-cabanachartview";
class ChartView;
class ChartsWidget;
class ChartsContainer : public QWidget {
public:
ChartsContainer(ChartsWidget *parent);
void dragEnterEvent(QDragEnterEvent *event) override;
void dropEvent(QDropEvent *event) override;
void dragLeaveEvent(QDragLeaveEvent *event) override { drawDropIndicator({}); }
void drawDropIndicator(const QPoint &pt) { drop_indictor_pos = pt; update(); }
void paintEvent(QPaintEvent *ev) override;
ChartView *getDropAfter(const QPoint &pos) const;
QGridLayout *charts_layout;
ChartsWidget *charts_widget;
QPoint drop_indictor_pos;
};
class ChartsWidget : public QFrame {
Q_OBJECT
public:
ChartsWidget(QWidget *parent = nullptr);
void showChart(const MessageId &id, const cabana::Signal *sig, bool show, bool merge);
inline bool hasSignal(const MessageId &id, const cabana::Signal *sig) { return findChart(id, sig) != nullptr; }
public slots:
void setColumnCount(int n);
void removeAll();
void timeRangeChanged(const std::optional<std::pair<double, double>> &time_range);
void setIsDocked(bool dock);
signals:
void toggleChartsDocking();
void seriesChanged();
void showTip(double seconds);
private:
QSize minimumSizeHint() const override;
bool event(QEvent *event) override;
void alignCharts();
void newChart();
ChartView *createChart(int pos = 0);
void removeChart(ChartView *chart);
void splitChart(ChartView *chart);
QRect chartVisibleRect(ChartView *chart);
void eventsMerged(const MessageEventsMap &new_events);
void updateState();
void zoomReset();
void startAutoScroll();
void stopAutoScroll();
void doAutoScroll();
void updateToolBar();
void updateTabBar();
void setMaxChartRange(int value);
void updateLayout(bool force = false);
void settingChanged();
void showValueTip(double sec);
bool eventFilter(QObject *obj, QEvent *event) override;
void newTab();
void removeTab(int index);
inline QList<ChartView *> &currentCharts() { return tab_charts[tabbar->tabData(tabbar->currentIndex()).toInt()]; }
ChartView *findChart(const MessageId &id, const cabana::Signal *sig);
QLabel *title_label;
QLabel *range_lb;
LogSlider *range_slider;
QAction *range_lb_action;
QAction *range_slider_action;
bool is_docked = true;
ToolButton *dock_btn;
QToolBar *toolbar;
QAction *undo_zoom_action;
QAction *redo_zoom_action;
QAction *reset_zoom_action;
ToolButton *reset_zoom_btn;
QUndoStack *zoom_undo_stack;
ToolButton *remove_all_btn;
QList<ChartView *> charts;
std::unordered_map<int, QList<ChartView *>> tab_charts;
TabBar *tabbar;
ChartsContainer *charts_container;
QScrollArea *charts_scroll;
uint32_t max_chart_range = 0;
std::pair<double, double> display_range;
QAction *columns_action;
int column_count = 1;
int current_column_count = 0;
int auto_scroll_count = 0;
QTimer *auto_scroll_timer;
QTimer *align_timer;
int current_theme = 0;
bool value_tip_visible_ = false;
friend class ChartView;
friend class ChartsContainer;
};
class ZoomCommand : public QUndoCommand {
public:
ZoomCommand(std::pair<double, double> range) : range(range), QUndoCommand() {
prev_range = can->timeRange();
setText(QObject::tr("Zoom to %1-%2").arg(range.first, 0, 'f', 2).arg(range.second, 0, 'f', 2));
}
void undo() override { can->setTimeRange(prev_range); }
void redo() override { can->setTimeRange(range); }
std::optional<std::pair<double, double>> prev_range, range;
};

View File

@@ -0,0 +1,30 @@
#pragma once
#include <QComboBox>
#include <QDialog>
#include <QListWidget>
#include "tools/cabana/dbc/dbcmanager.h"
class SignalSelector : public QDialog {
public:
struct ListItem : public QListWidgetItem {
ListItem(const MessageId &msg_id, const cabana::Signal *sig, QListWidget *parent) : msg_id(msg_id), sig(sig), QListWidgetItem(parent) {}
MessageId msg_id;
const cabana::Signal *sig;
};
SignalSelector(QString title, QWidget *parent);
QList<ListItem *> seletedItems();
inline void addSelected(const MessageId &id, const cabana::Signal *sig) { addItemToList(selected_list, id, sig, true); }
private:
void updateAvailableList(int index);
void addItemToList(QListWidget *parent, const MessageId id, const cabana::Signal *sig, bool show_msg_name = false);
void add(QListWidgetItem *item);
void remove(QListWidgetItem *item);
QComboBox *msgs_combo;
QListWidget *available_list;
QListWidget *selected_list;
};

View File

@@ -0,0 +1,25 @@
#pragma once
#include <QPixmap>
#include <QPointF>
#include <vector>
#include "tools/cabana/dbc/dbc.h"
#include "tools/cabana/streams/abstractstream.h"
class Sparkline {
public:
void update(const MessageId &msg_id, const cabana::Signal *sig, double last_msg_ts, int range, QSize size);
inline double freq() const { return freq_; }
bool isEmpty() const { return pixmap.isNull(); }
QPixmap pixmap;
double min_val = 0;
double max_val = 0;
private:
void render(const QColor &color, int range, QSize size);
std::vector<QPointF> points;
double freq_ = 0;
};

View File

@@ -0,0 +1,12 @@
#pragma once
#include <QLabel>
class TipLabel : public QLabel {
Q_OBJECT
public:
TipLabel(QWidget *parent = nullptr);
void showText(const QPoint &pt, const QString &sec, QWidget *w, const QRect &rect);
void paintEvent(QPaintEvent *ev) override;
};

72
tools/cabana/commands.h Normal file
View File

@@ -0,0 +1,72 @@
#pragma once
#include <utility>
#include <QUndoCommand>
#include <QUndoStack>
#include "tools/cabana/dbc/dbcmanager.h"
#include "tools/cabana/streams/abstractstream.h"
class EditMsgCommand : public QUndoCommand {
public:
EditMsgCommand(const MessageId &id, const QString &name, int size, const QString &node,
const QString &comment, QUndoCommand *parent = nullptr);
void undo() override;
void redo() override;
private:
const MessageId id;
QString old_name, new_name, old_comment, new_comment, old_node, new_node;
int old_size = 0, new_size = 0;
};
class RemoveMsgCommand : public QUndoCommand {
public:
RemoveMsgCommand(const MessageId &id, QUndoCommand *parent = nullptr);
void undo() override;
void redo() override;
private:
const MessageId id;
cabana::Msg message;
};
class AddSigCommand : public QUndoCommand {
public:
AddSigCommand(const MessageId &id, const cabana::Signal &sig, QUndoCommand *parent = nullptr);
void undo() override;
void redo() override;
private:
const MessageId id;
bool msg_created = false;
cabana::Signal signal = {};
};
class RemoveSigCommand : public QUndoCommand {
public:
RemoveSigCommand(const MessageId &id, const cabana::Signal *sig, QUndoCommand *parent = nullptr);
void undo() override;
void redo() override;
private:
const MessageId id;
QList<cabana::Signal> sigs;
};
class EditSignalCommand : public QUndoCommand {
public:
EditSignalCommand(const MessageId &id, const cabana::Signal *sig, const cabana::Signal &new_sig, QUndoCommand *parent = nullptr);
void undo() override;
void redo() override;
private:
const MessageId id;
QList<std::pair<cabana::Signal, cabana::Signal>> sigs; // QList<{old_sig, new_sig}>
};
namespace UndoStack {
QUndoStack *instance();
inline void push(QUndoCommand *cmd) { instance()->push(cmd); }
};

120
tools/cabana/dbc/dbc.h Normal file
View File

@@ -0,0 +1,120 @@
#pragma once
#include <limits>
#include <utility>
#include <vector>
#include <QColor>
#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(QString::number(address, 16).toUpper());
}
bool operator==(const MessageId &other) const {
return source == other.source && address == other.address;
}
bool operator!=(const MessageId &other) const {
return !(*this == other);
}
bool operator<(const MessageId &other) const {
return std::tie(source, address) < std::tie(other.source, other.address);
}
bool operator>(const MessageId &other) const {
return std::tie(source, address) > std::tie(other.source, other.address);
}
};
uint qHash(const MessageId &item);
Q_DECLARE_METATYPE(MessageId);
template <>
struct std::hash<MessageId> {
std::size_t operator()(const MessageId &k) const noexcept { return qHash(k); }
};
typedef std::vector<std::pair<double, QString>> ValueDescription;
namespace cabana {
class Signal {
public:
Signal() = default;
Signal(const Signal &other) = default;
void update();
bool getValue(const uint8_t *data, size_t data_size, double *val) const;
QString formatValue(double value, bool with_unit = true) const;
bool operator==(const cabana::Signal &other) const;
inline bool operator!=(const cabana::Signal &other) const { return !(*this == other); }
enum class Type {
Normal = 0,
Multiplexed,
Multiplexor
};
Type type = Type::Normal;
QString name;
int start_bit, msb, lsb, size;
double factor = 1.0;
double offset = 0;
bool is_signed;
bool is_little_endian;
double min, max;
QString unit;
QString comment;
QString receiver_name;
ValueDescription val_desc;
int precision = 0;
QColor color;
// Multiplexed
int multiplex_value = 0;
Signal *multiplexor = nullptr;
};
class Msg {
public:
Msg() = default;
Msg(const Msg &other) { *this = other; }
~Msg();
cabana::Signal *addSignal(const cabana::Signal &sig);
cabana::Signal *updateSignal(const QString &sig_name, const cabana::Signal &sig);
void removeSignal(const QString &sig_name);
Msg &operator=(const Msg &other);
int indexOf(const cabana::Signal *sig) const;
cabana::Signal *sig(const QString &sig_name) const;
QString newSignalName();
void update();
inline const std::vector<cabana::Signal *> &getSignals() const { return sigs; }
uint32_t address;
QString name;
uint32_t size;
QString comment;
QString transmitter;
std::vector<cabana::Signal *> sigs;
std::vector<uint8_t> mask;
cabana::Signal *multiplexor = nullptr;
};
} // namespace cabana
// Helper functions
double get_raw_value(const uint8_t *data, size_t data_size, const cabana::Signal &sig);
void updateMsbLsb(cabana::Signal &s);
inline int flipBitPos(int start_bit) { return 8 * (start_bit / 8) + 7 - start_bit % 8; }
inline QString doubleToString(double value) { return QString::number(value, 'g', std::numeric_limits<double>::digits10); }

View File

@@ -0,0 +1,44 @@
#pragma once
#include <map>
#include <QTextStream>
#include "tools/cabana/dbc/dbc.h"
class DBCFile {
public:
DBCFile(const QString &dbc_file_name);
DBCFile(const QString &name, const QString &content);
~DBCFile() {}
bool save();
bool saveAs(const QString &new_filename);
bool writeContents(const QString &fn);
QString generateDBC();
void updateMsg(const MessageId &id, const QString &name, uint32_t size, const QString &node, const QString &comment);
inline void removeMsg(const MessageId &id) { msgs.erase(id.address); }
inline const std::map<uint32_t, cabana::Msg> &getMessages() const { return msgs; }
cabana::Msg *msg(uint32_t address);
cabana::Msg *msg(const QString &name);
inline cabana::Msg *msg(const MessageId &id) { return msg(id.address); }
cabana::Signal *signal(uint32_t address, const QString &name);
inline QString name() const { return name_.isEmpty() ? "untitled" : name_; }
inline bool isEmpty() const { return msgs.empty() && name_.isEmpty(); }
QString filename;
private:
void parse(const QString &content);
cabana::Msg *parseBO(const QString &line);
void parseSG(const QString &line, cabana::Msg *current_msg, int &multiplexor_cnt);
void parseCM_BO(const QString &line, const QString &content, const QString &raw_line, const QTextStream &stream);
void parseCM_SG(const QString &line, const QString &content, const QString &raw_line, const QTextStream &stream);
void parseVAL(const QString &line);
QString header;
std::map<uint32_t, cabana::Msg> msgs;
QString name_;
};

View File

@@ -0,0 +1,69 @@
#pragma once
#include <QObject>
#include <memory>
#include <map>
#include <set>
#include "tools/cabana/dbc/dbcfile.h"
typedef std::set<int> SourceSet;
const SourceSet SOURCE_ALL = {-1};
const int INVALID_SOURCE = 0xff;
inline bool operator<(const std::shared_ptr<DBCFile> &l, const std::shared_ptr<DBCFile> &r) { return l.get() < r.get(); }
class DBCManager : public QObject {
Q_OBJECT
public:
DBCManager(QObject *parent) : QObject(parent) {}
~DBCManager() {}
bool open(const SourceSet &sources, const QString &dbc_file_name, QString *error = nullptr);
bool open(const SourceSet &sources, const QString &name, const QString &content, QString *error = nullptr);
void close(const SourceSet &sources);
void close(DBCFile *dbc_file);
void closeAll();
void addSignal(const MessageId &id, const cabana::Signal &sig);
void updateSignal(const MessageId &id, const QString &sig_name, const cabana::Signal &sig);
void removeSignal(const MessageId &id, const QString &sig_name);
void updateMsg(const MessageId &id, const QString &name, uint32_t size, const QString &node, const QString &comment);
void removeMsg(const MessageId &id);
QString newMsgName(const MessageId &id);
QString newSignalName(const MessageId &id);
const std::map<uint32_t, cabana::Msg> &getMessages(uint8_t source);
cabana::Msg *msg(const MessageId &id);
cabana::Msg* msg(uint8_t source, const QString &name);
QStringList signalNames();
inline int dbcCount() { return allDBCFiles().size(); }
int nonEmptyDBCCount();
const SourceSet sources(const DBCFile *dbc_file) const;
DBCFile *findDBCFile(const uint8_t source);
inline DBCFile *findDBCFile(const MessageId &id) { return findDBCFile(id.source); }
std::set<DBCFile *> allDBCFiles();
signals:
void signalAdded(MessageId id, const cabana::Signal *sig);
void signalRemoved(const cabana::Signal *sig);
void signalUpdated(const cabana::Signal *sig);
void msgUpdated(MessageId id);
void msgRemoved(MessageId id);
void DBCFileChanged();
void maskUpdated();
private:
std::map<int, std::shared_ptr<DBCFile>> dbc_files;
};
DBCManager *dbc();
QString toString(const SourceSet &ss);
inline QString msgName(const MessageId &id) {
auto msg = dbc()->msg(id);
return msg ? msg->name : UNTITLED;
}

View File

@@ -0,0 +1,39 @@
#!/usr/bin/env python3
import argparse
import json
from opendbc.car import Bus
from opendbc.car.fingerprints import MIGRATION
from opendbc.car.values import PLATFORMS
def generate_dbc_dict() -> dict[str, str]:
dbc_map = {}
for platform in PLATFORMS.values():
if platform != "MOCK":
if Bus.pt in platform.config.dbc_dict:
dbc_map[platform.name] = platform.config.dbc_dict[Bus.pt]
elif Bus.main in platform.config.dbc_dict:
dbc_map[platform.name] = platform.config.dbc_dict[Bus.main]
elif Bus.party in platform.config.dbc_dict:
dbc_map[platform.name] = platform.config.dbc_dict[Bus.party]
else:
raise ValueError("Unknown main type")
for m in MIGRATION:
if MIGRATION[m] in dbc_map:
dbc_map[m] = dbc_map[MIGRATION[m]]
return dbc_map
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Generate mapping for all car fingerprints to DBC names and outputs json file",
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument("--out", required=True, help="Generated json filepath")
args = parser.parse_args()
with open(args.out, 'w') as f:
f.write(json.dumps(dict(sorted(generate_dbc_dict().items())), indent=2))
print(f"Generated and written to {args.out}")

View File

@@ -0,0 +1,70 @@
#pragma once
#include <QDialogButtonBox>
#include <QSplitter>
#include <QTabWidget>
#include <QTextEdit>
#include <set>
#include "selfdrive/ui/qt/widgets/controls.h"
#include "tools/cabana/binaryview.h"
#include "tools/cabana/chart/chartswidget.h"
#include "tools/cabana/historylog.h"
#include "tools/cabana/signalview.h"
class EditMessageDialog : public QDialog {
public:
EditMessageDialog(const MessageId &msg_id, const QString &title, int size, QWidget *parent);
void validateName(const QString &text);
MessageId msg_id;
QString original_name;
QDialogButtonBox *btn_box;
QLineEdit *name_edit;
QLineEdit *node;
QTextEdit *comment_edit;
QLabel *error_label;
QSpinBox *size_spin;
};
class DetailWidget : public QWidget {
Q_OBJECT
public:
DetailWidget(ChartsWidget *charts, QWidget *parent);
void setMessage(const MessageId &message_id);
void refresh();
private:
void createToolBar();
void showTabBarContextMenu(const QPoint &pt);
void editMsg();
void removeMsg();
void updateState(const std::set<MessageId> *msgs = nullptr);
MessageId msg_id;
QLabel *warning_icon, *warning_label;
ElidedLabel *name_label;
QWidget *warning_widget;
TabBar *tabbar;
QTabWidget *tab_widget;
QAction *action_remove_msg;
LogsWidget *history_log;
BinaryView *binary_view;
SignalView *signal_view;
ChartsWidget *charts;
QSplitter *splitter;
};
class CenterWidget : public QWidget {
Q_OBJECT
public:
CenterWidget(QWidget *parent);
void setMessage(const MessageId &msg_id);
void clear();
private:
QWidget *createWelcomeWidget();
DetailWidget *detail_widget = nullptr;
QWidget *welcome_widget = nullptr;
};

81
tools/cabana/historylog.h Normal file
View File

@@ -0,0 +1,81 @@
#pragma once
#include <deque>
#include <vector>
#include <QComboBox>
#include <QHeaderView>
#include <QLineEdit>
#include <QTableView>
#include "tools/cabana/dbc/dbcmanager.h"
#include "tools/cabana/streams/abstractstream.h"
class HeaderView : public QHeaderView {
public:
HeaderView(Qt::Orientation orientation, QWidget *parent = nullptr) : QHeaderView(orientation, parent) {}
QSize sectionSizeFromContents(int logicalIndex) const override;
void paintSection(QPainter *painter, const QRect &rect, int logicalIndex) const;
};
class HistoryLogModel : public QAbstractTableModel {
Q_OBJECT
public:
HistoryLogModel(QObject *parent) : QAbstractTableModel(parent) {}
void setMessage(const MessageId &message_id);
void updateState(bool clear = false);
void setFilter(int sig_idx, const QString &value, std::function<bool(double, double)> cmp);
QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
void fetchMore(const QModelIndex &parent) override;
bool canFetchMore(const QModelIndex &parent) const override;
int rowCount(const QModelIndex &parent = QModelIndex()) const override { return messages.size(); }
int columnCount(const QModelIndex &parent = QModelIndex()) const override { return !isHexMode() ? sigs.size() + 1 : 2; }
inline bool isHexMode() const { return sigs.empty() || hex_mode; }
void reset();
void setHexMode(bool hex_mode);
struct Message {
uint64_t mono_time = 0;
std::vector<double> sig_values;
std::vector<uint8_t> data;
std::vector<QColor> colors;
};
void fetchData(std::deque<Message>::iterator insert_pos, uint64_t from_time, uint64_t min_time);
MessageId msg_id;
CanData hex_colors;
const int batch_size = 50;
int filter_sig_idx = -1;
double filter_value = 0;
std::function<bool(double, double)> filter_cmp = nullptr;
std::deque<Message> messages;
std::vector<cabana::Signal *> sigs;
bool hex_mode = false;
};
class LogsWidget : public QFrame {
Q_OBJECT
public:
LogsWidget(QWidget *parent);
void setMessage(const MessageId &message_id) { model->setMessage(message_id); }
void updateState() { model->updateState(); }
void showEvent(QShowEvent *event) override { model->updateState(true); }
private slots:
void filterChanged();
void exportToCSV();
void modelReset();
private:
QTableView *logs;
HistoryLogModel *model;
QComboBox *signals_cb, *comp_box, *display_type_cb;
QLineEdit *value_edit;
QWidget *filters_widget;
ToolButton *export_btn;
MessageBytesDelegate *delegate;
};

110
tools/cabana/mainwin.h Normal file
View File

@@ -0,0 +1,110 @@
#pragma once
#include <QDockWidget>
#include <QJsonDocument>
#include <QMainWindow>
#include <QMenu>
#include <QProgressBar>
#include <QSplitter>
#include <QStatusBar>
#include <set>
#include "tools/cabana/chart/chartswidget.h"
#include "tools/cabana/dbc/dbcmanager.h"
#include "tools/cabana/detailwidget.h"
#include "tools/cabana/messageswidget.h"
#include "tools/cabana/videowidget.h"
#include "tools/cabana/tools/findsimilarbits.h"
class MainWindow : public QMainWindow {
Q_OBJECT
public:
MainWindow(AbstractStream *stream, const QString &dbc_file);
void toggleChartsDocking();
void showStatusMessage(const QString &msg, int timeout = 0) { statusBar()->showMessage(msg, timeout); }
void loadFile(const QString &fn, SourceSet s = SOURCE_ALL);
ChartsWidget *charts_widget = nullptr;
public slots:
void selectAndOpenStream();
void openStream(AbstractStream *stream, const QString &dbc_file = {});
void closeStream();
void exportToCSV();
void newFile(SourceSet s = SOURCE_ALL);
void openFile(SourceSet s = SOURCE_ALL);
void loadDBCFromOpendbc(const QString &name);
void save();
void saveAs();
void saveToClipboard();
signals:
void showMessage(const QString &msg, int timeout);
void updateProgressBar(uint64_t cur, uint64_t total, bool success);
protected:
bool eventFilter(QObject *obj, QEvent *event) override;
void remindSaveChanges();
void closeFile(SourceSet s = SOURCE_ALL);
void closeFile(DBCFile *dbc_file);
void saveFile(DBCFile *dbc_file);
void saveFileAs(DBCFile *dbc_file);
void saveFileToClipboard(DBCFile *dbc_file);
void loadFingerprints();
void loadFromClipboard(SourceSet s = SOURCE_ALL, bool close_all = true);
void updateRecentFiles(const QString &fn);
void updateRecentFileMenu();
void createActions();
void createDockWindows();
void createStatusBar();
void createShortcuts();
void closeEvent(QCloseEvent *event) override;
void DBCFileChanged();
void updateDownloadProgress(uint64_t cur, uint64_t total, bool success);
void setOption();
void findSimilarBits();
void findSignal();
void undoStackCleanChanged(bool clean);
void onlineHelp();
void toggleFullScreen();
void updateStatus();
void updateLoadSaveMenus();
void createDockWidgets();
void eventsMerged();
VideoWidget *video_widget = nullptr;
QDockWidget *video_dock;
QDockWidget *messages_dock;
MessagesWidget *messages_widget = nullptr;
CenterWidget *center_widget;
QWidget *floating_window = nullptr;
QVBoxLayout *charts_layout;
QProgressBar *progress_bar;
QLabel *status_label;
QJsonDocument fingerprint_to_dbc;
QSplitter *video_splitter = nullptr;
enum { MAX_RECENT_FILES = 15 };
QMenu *open_recent_menu = nullptr;
QMenu *manage_dbcs_menu = nullptr;
QMenu *tools_menu = nullptr;
QAction *close_stream_act = nullptr;
QAction *export_to_csv_act = nullptr;
QAction *save_dbc = nullptr;
QAction *save_dbc_as = nullptr;
QAction *copy_dbc_to_clipboard = nullptr;
QString car_fingerprint;
QByteArray default_state;
};
class HelpOverlay : public QWidget {
Q_OBJECT
public:
HelpOverlay(MainWindow *parent);
protected:
void drawHelpForWidget(QPainter &painter, QWidget *w);
void paintEvent(QPaintEvent *event) override;
void mouseReleaseEvent(QMouseEvent *event) override;
bool eventFilter(QObject *obj, QEvent *event) override;
};

View File

@@ -0,0 +1,121 @@
#pragma once
#include <algorithm>
#include <optional>
#include <set>
#include <vector>
#include <QAbstractTableModel>
#include <QHeaderView>
#include <QLineEdit>
#include <QMenu>
#include <QTreeView>
#include <QWheelEvent>
#include "tools/cabana/dbc/dbcmanager.h"
#include "tools/cabana/streams/abstractstream.h"
class MessageListModel : public QAbstractTableModel {
Q_OBJECT
public:
enum Column {
NAME = 0,
SOURCE,
ADDRESS,
NODE,
FREQ,
COUNT,
DATA,
};
MessageListModel(QObject *parent) : QAbstractTableModel(parent) {}
QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override;
int columnCount(const QModelIndex &parent = QModelIndex()) const override { return Column::DATA + 1; }
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const;
int rowCount(const QModelIndex &parent = QModelIndex()) const override { return items_.size(); }
void sort(int column, Qt::SortOrder order = Qt::AscendingOrder) override;
void setFilterStrings(const QMap<int, QString> &filters);
void showInactivemessages(bool show);
void msgsReceived(const std::set<MessageId> *new_msgs, bool has_new_ids);
bool filterAndSort();
void dbcModified();
struct Item {
MessageId id;
QString name;
QString node;
bool operator==(const Item &other) const {
return id == other.id && name == other.name && node == other.node;
}
};
std::vector<Item> items_;
bool show_inactive_messages = true;
private:
void sortItems(std::vector<MessageListModel::Item> &items);
bool match(const MessageListModel::Item &id);
QMap<int, QString> filters_;
std::set<MessageId> dbc_messages_;
int sort_column = 0;
Qt::SortOrder sort_order = Qt::AscendingOrder;
int sort_threshold_ = 0;
};
class MessageView : public QTreeView {
Q_OBJECT
public:
MessageView(QWidget *parent) : QTreeView(parent) {}
void updateBytesSectionSize();
protected:
void drawRow(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override;
void drawBranches(QPainter *painter, const QRect &rect, const QModelIndex &index) const override {}
void dataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector<int> &roles = QVector<int>()) override;
void wheelEvent(QWheelEvent *event) override;
};
class MessageViewHeader : public QHeaderView {
// https://stackoverflow.com/a/44346317
Q_OBJECT
public:
MessageViewHeader(QWidget *parent);
void updateHeaderPositions();
void updateGeometries() override;
QSize sizeHint() const override;
void updateFilters();
QMap<int, QLineEdit *> editors;
};
class MessagesWidget : public QWidget {
Q_OBJECT
public:
MessagesWidget(QWidget *parent);
void selectMessage(const MessageId &message_id);
QByteArray saveHeaderState() const { return view->header()->saveState(); }
bool restoreHeaderState(const QByteArray &state) const { return view->header()->restoreState(state); }
void suppressHighlighted();
signals:
void msgSelectionChanged(const MessageId &message_id);
void titleChanged(const QString &title);
protected:
QWidget *createToolBar();
void headerContextMenuEvent(const QPoint &pos);
void menuAboutToShow();
void setMultiLineBytes(bool multi);
void updateTitle();
MessageView *view;
MessageViewHeader *header;
MessageBytesDelegate *delegate;
std::optional<MessageId> current_msg_id;
MessageListModel *model;
QPushButton *suppress_add;
QPushButton *suppress_clear;
QMenu *menu;
};

67
tools/cabana/settings.h Normal file
View File

@@ -0,0 +1,67 @@
#pragma once
#include <QByteArray>
#include <QComboBox>
#include <QDialog>
#include <QGroupBox>
#include <QLineEdit>
#include <QSpinBox>
#define LIGHT_THEME 1
#define DARK_THEME 2
class Settings : public QObject {
Q_OBJECT
public:
enum DragDirection {
MsbFirst,
LsbFirst,
AlwaysLE,
AlwaysBE,
};
Settings();
~Settings();
bool absolute_time = false;
int fps = 10;
int max_cached_minutes = 30;
int chart_height = 200;
int chart_column_count = 1;
int chart_range = 3 * 60; // 3 minutes
int chart_series_type = 0;
int theme = 0;
int sparkline_range = 15; // 15 seconds
bool multiple_lines_hex = false;
bool log_livestream = true;
bool suppress_defined_signals = false;
QString log_path;
QString last_dir;
QString last_route_dir;
QByteArray geometry;
QByteArray video_splitter_state;
QByteArray window_state;
QStringList recent_files;
QByteArray message_header_state;
DragDirection drag_direction = MsbFirst;
signals:
void changed();
};
class SettingsDlg : public QDialog {
public:
SettingsDlg(QWidget *parent);
void save();
QSpinBox *fps;
QSpinBox *cached_minutes;
QSpinBox *chart_height;
QComboBox *chart_series_type;
QComboBox *theme;
QGroupBox *log_livestream;
QLineEdit *log_path;
QComboBox *drag_direction;
};
extern Settings settings;

147
tools/cabana/signalview.h Normal file
View File

@@ -0,0 +1,147 @@
#pragma once
#include <memory>
#include <set>
#include <utility>
#include <QAbstractItemModel>
#include <QLabel>
#include <QLineEdit>
#include <QSlider>
#include <QStyledItemDelegate>
#include <QTableWidget>
#include <QTreeView>
#include "tools/cabana/chart/chartswidget.h"
#include "tools/cabana/chart/sparkline.h"
class SignalModel : public QAbstractItemModel {
Q_OBJECT
public:
struct Item {
enum Type {Root, Sig, Name, Size, Node, Endian, Signed, Offset, Factor, SignalType, MultiplexValue, ExtraInfo, Unit, Comment, Min, Max, Desc };
~Item() { qDeleteAll(children); }
inline int row() { return parent->children.indexOf(this); }
Type type = Type::Root;
Item *parent = nullptr;
QList<Item *> children;
const cabana::Signal *sig = nullptr;
QString title;
bool highlight = false;
QString sig_val = "-";
Sparkline sparkline;
};
SignalModel(QObject *parent);
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
int columnCount(const QModelIndex &parent = QModelIndex()) const override { return 2; }
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override;
QModelIndex parent(const QModelIndex &index) const override;
Qt::ItemFlags flags(const QModelIndex &index) const override;
bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override;
void setMessage(const MessageId &id);
void setFilter(const QString &txt);
bool saveSignal(const cabana::Signal *origin_s, cabana::Signal &s);
Item *getItem(const QModelIndex &index) const;
int signalRow(const cabana::Signal *sig) const;
private:
void insertItem(SignalModel::Item *root_item, int pos, const cabana::Signal *sig);
void handleSignalAdded(MessageId id, const cabana::Signal *sig);
void handleSignalUpdated(const cabana::Signal *sig);
void handleSignalRemoved(const cabana::Signal *sig);
void handleMsgChanged(MessageId id);
void refresh();
MessageId msg_id;
QString filter_str;
std::unique_ptr<Item> root;
friend class SignalView;
friend class SignalItemDelegate;
};
class ValueDescriptionDlg : public QDialog {
public:
ValueDescriptionDlg(const ValueDescription &descriptions, QWidget *parent);
ValueDescription val_desc;
private:
struct Delegate : public QStyledItemDelegate {
Delegate(QWidget *parent) : QStyledItemDelegate(parent) {}
QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const override;
};
void save();
QTableWidget *table;
};
class SignalItemDelegate : public QStyledItemDelegate {
public:
SignalItemDelegate(QObject *parent);
void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override;
QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override;
QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const override;
void updateEditorGeometry(QWidget *editor, const QStyleOptionViewItem &option, const QModelIndex &index) const override;
void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const override;
QValidator *name_validator, *double_validator, *node_validator;
QFont label_font, minmax_font;
const int color_label_width = 18;
mutable QSize button_size;
};
class SignalView : public QFrame {
Q_OBJECT
public:
SignalView(ChartsWidget *charts, QWidget *parent);
void setMessage(const MessageId &id);
void signalHovered(const cabana::Signal *sig);
void updateChartState();
void selectSignal(const cabana::Signal *sig, bool expand = false);
void rowClicked(const QModelIndex &index);
SignalModel *model = nullptr;
signals:
void highlight(const cabana::Signal *sig);
void showChart(const MessageId &id, const cabana::Signal *sig, bool show, bool merge);
private:
void rowsChanged();
void resizeEvent(QResizeEvent* event) override;
void updateToolBar();
void setSparklineRange(int value);
void handleSignalAdded(MessageId id, const cabana::Signal *sig);
void handleSignalUpdated(const cabana::Signal *sig);
void updateState(const std::set<MessageId> *msgs = nullptr);
std::pair<QModelIndex, QModelIndex> visibleSignalRange();
struct TreeView : public QTreeView {
TreeView(QWidget *parent) : QTreeView(parent) {}
void rowsInserted(const QModelIndex &parent, int start, int end) override {
((SignalView *)parentWidget())->rowsChanged();
// update widget geometries in QTreeView::rowsInserted
QTreeView::rowsInserted(parent, start, end);
}
void dataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector<int> &roles = QVector<int>()) override {
// Bypass the slow call to QTreeView::dataChanged.
QAbstractItemView::dataChanged(topLeft, bottomRight, roles);
}
void leaveEvent(QEvent *event) override {
emit static_cast<SignalView *>(parentWidget())->highlight(nullptr);
QTreeView::leaveEvent(event);
}
};
int max_value_width = 0;
int value_column_width = 0;
TreeView *tree;
QLabel *sparkline_label;
QSlider *sparkline_range_slider;
QLineEdit *filter_edit;
ChartsWidget *charts;
QLabel *signal_count_lb;
SignalItemDelegate *delegate;
};

View File

@@ -0,0 +1,157 @@
#pragma once
#include <algorithm>
#include <array>
#include <condition_variable>
#include <memory>
#include <mutex>
#include <optional>
#include <set>
#include <unordered_map>
#include <utility>
#include <vector>
#include <QColor>
#include <QDateTime>
#include "cereal/messaging/messaging.h"
#include "tools/cabana/dbc/dbcmanager.h"
#include "tools/cabana/utils/util.h"
#include "tools/replay/util.h"
struct CanData {
void compute(const MessageId &msg_id, const uint8_t *dat, const int size, double current_sec,
double playback_speed, const std::vector<uint8_t> &mask, double in_freq = 0);
double ts = 0.;
uint32_t count = 0;
double freq = 0;
std::vector<uint8_t> dat;
std::vector<QColor> colors;
struct ByteLastChange {
double ts = 0;
int delta = 0;
int same_delta_counter = 0;
bool suppressed = false;
};
std::vector<ByteLastChange> last_changes;
std::vector<std::array<uint32_t, 8>> bit_flip_counts;
double last_freq_update_ts = 0;
};
struct CanEvent {
uint8_t src;
uint32_t address;
uint64_t mono_time;
uint8_t size;
uint8_t dat[];
};
struct CompareCanEvent {
constexpr bool operator()(const CanEvent *const e, uint64_t ts) const { return e->mono_time < ts; }
constexpr bool operator()(uint64_t ts, const CanEvent *const e) const { return ts < e->mono_time; }
};
typedef std::unordered_map<MessageId, std::vector<const CanEvent *>> MessageEventsMap;
using CanEventIter = std::vector<const CanEvent *>::const_iterator;
class AbstractStream : public QObject {
Q_OBJECT
public:
AbstractStream(QObject *parent);
virtual ~AbstractStream() {}
virtual void start() = 0;
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 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; }
virtual void pause(bool pause) {}
void setTimeRange(const std::optional<std::pair<double, double>> &range);
const std::optional<std::pair<double, double>> &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<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();
void suppressDefinedSignals(bool suppress);
signals:
void paused();
void resume();
void seeking(double sec);
void seekedTo(double sec);
void timeRangeChanged(const std::optional<std::pair<double, double>> &range);
void eventsMerged(const MessageEventsMap &events_map);
void msgsReceived(const std::set<MessageId> *new_msgs, bool has_new_ids);
void sourcesUpdated(const SourceSet &s);
void privateUpdateLastMsgsSignal();
public:
SourceSet sources;
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);
void waitForSeekFinshed();
std::vector<const CanEvent *> all_events_;
double current_sec_ = 0;
std::optional<std::pair<double, double>> time_range_;
private:
void updateLastMessages();
void updateLastMsgsTo(double sec);
void updateMasks();
MessageEventsMap events_;
std::unordered_map<MessageId, CanData> last_msgs;
std::unique_ptr<MonotonicBuffer> event_buffer_;
// 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_;
};
class AbstractOpenStreamWidget : public QWidget {
Q_OBJECT
public:
AbstractOpenStreamWidget(QWidget *parent = nullptr) : QWidget(parent) {}
virtual AbstractStream *open() = 0;
signals:
void enableOpenButton(bool);
};
class DummyStream : public AbstractStream {
Q_OBJECT
public:
DummyStream(QObject *parent) : AbstractStream(parent) {}
QString routeName() const override { return tr("No Stream"); }
void start() override {}
};
// A global pointer referring to the unique AbstractStream object
extern AbstractStream *can;

View File

@@ -0,0 +1,28 @@
#pragma once
#include "tools/cabana/streams/livestream.h"
class DeviceStream : public LiveStream {
Q_OBJECT
public:
DeviceStream(QObject *parent, QString address = {});
inline QString routeName() const override {
return QString("Live Streaming From %1").arg(zmq_address.isEmpty() ? "127.0.0.1" : zmq_address);
}
protected:
void streamThread() override;
const QString zmq_address;
};
class OpenDeviceWidget : public AbstractOpenStreamWidget {
Q_OBJECT
public:
OpenDeviceWidget(QWidget *parent = nullptr);
AbstractStream *open() override;
private:
QLineEdit *ip_address;
QButtonGroup *group;
};

View File

@@ -0,0 +1,56 @@
#pragma once
#include <algorithm>
#include <memory>
#include <vector>
#include <QBasicTimer>
#include "tools/cabana/streams/abstractstream.h"
class LiveStream : public AbstractStream {
Q_OBJECT
public:
LiveStream(QObject *parent);
virtual ~LiveStream();
void start() override;
void stop();
inline QDateTime beginDateTime() const { return begin_date_time; }
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_; }
void pause(bool pause) override;
void seekTo(double sec) override;
protected:
virtual void streamThread() = 0;
void handleEvent(kj::ArrayPtr<capnp::word> event);
private:
void startUpdateTimer();
void timerEvent(QTimerEvent *event) override;
void updateEvents();
std::mutex lock;
QThread *stream_thread;
std::vector<const CanEvent *> received_events_;
int timer_id;
QBasicTimer update_timer;
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;
bool post_last_event = true;
double speed_ = 1;
bool paused_ = false;
struct Logger;
std::unique_ptr<Logger> logger;
};

View File

@@ -0,0 +1,57 @@
#pragma once
#include <memory>
#include <vector>
#include <QComboBox>
#include <QFormLayout>
#include "tools/cabana/streams/livestream.h"
#include "selfdrive/pandad/panda.h"
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<BusConfig> bus_config;
};
class PandaStream : public LiveStream {
Q_OBJECT
public:
PandaStream(QObject *parent, PandaStreamConfig config_ = {});
~PandaStream() { stop(); }
inline QString routeName() const override {
return QString("Panda: %1").arg(config.serial);
}
protected:
bool connect();
void streamThread() override;
std::unique_ptr<Panda> panda;
PandaStreamConfig config = {};
};
class OpenPandaWidget : public AbstractOpenStreamWidget {
Q_OBJECT
public:
OpenPandaWidget(QWidget *parent = nullptr);
AbstractStream *open() override;
private:
void refreshSerials();
void buildConfigForm();
QComboBox *serial_edit;
QFormLayout *form_layout;
PandaStreamConfig config = {};
};

View File

@@ -0,0 +1,57 @@
#pragma once
#include <QCheckBox>
#include <algorithm>
#include <memory>
#include <set>
#include <vector>
#include "common/prefix.h"
#include "tools/cabana/streams/abstractstream.h"
#include "tools/replay/replay.h"
Q_DECLARE_METATYPE(std::shared_ptr<LogReader>);
class ReplayStream : public AbstractStream {
Q_OBJECT
public:
ReplayStream(QObject *parent);
void start() override { replay->start(); }
bool loadRoute(const QString &route, const QString &data_dir, uint32_t replay_flags = REPLAY_FLAG_NONE);
bool eventFilter(const Event *event);
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 QString::fromStdString(replay->route().name()); }
inline QString carFingerprint() const override { return replay->carFingerprint().c_str(); }
double minSeconds() const override { return replay->minSeconds(); }
double maxSeconds() const { return replay->maxSeconds(); }
inline QDateTime beginDateTime() const { return QDateTime::fromSecsSinceEpoch(replay->routeDateTime()); }
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(); }
inline bool isPaused() const override { return replay->isPaused(); }
void pause(bool pause) override;
signals:
void qLogLoaded(std::shared_ptr<LogReader> qlog);
private:
void mergeSegments();
std::unique_ptr<Replay> replay = nullptr;
std::set<int> processed_segments;
std::unique_ptr<OpenpilotPrefix> op_prefix;
};
class OpenReplayWidget : public AbstractOpenStreamWidget {
Q_OBJECT
public:
OpenReplayWidget(QWidget *parent = nullptr);
AbstractStream *open() override;
private:
QLineEdit *route_edit;
std::vector<QCheckBox *> cameras;
};

View File

@@ -0,0 +1,26 @@
#pragma once
#include <QComboBox>
#include <QDialog>
#include "selfdrive/ui/qt/api.h"
class RouteListWidget;
class OneShotHttpRequest;
class RoutesDialog : public QDialog {
Q_OBJECT
public:
RoutesDialog(QWidget *parent);
QString route();
protected:
void parseDeviceList(const QString &json, bool success, QNetworkReply::NetworkError err);
void parseRouteList(const QString &json, bool success, QNetworkReply::NetworkError err);
void fetchRoutes();
QComboBox *device_list_;
QComboBox *period_selector_;
RouteListWidget *route_list_;
OneShotHttpRequest *route_requester_;
};

View File

@@ -0,0 +1,47 @@
#pragma once
#include <memory>
#include <QtSerialBus/QCanBus>
#include <QtSerialBus/QCanBusDevice>
#include <QtSerialBus/QCanBusDeviceInfo>
#include <QComboBox>
#include "tools/cabana/streams/livestream.h"
struct SocketCanStreamConfig {
QString device = ""; // TODO: support multiple devices/buses at once
};
class SocketCanStream : public LiveStream {
Q_OBJECT
public:
SocketCanStream(QObject *parent, SocketCanStreamConfig config_ = {});
~SocketCanStream() { stop(); }
static bool available();
inline QString routeName() const override {
return QString("Live Streaming From Socket CAN %1").arg(config.device);
}
protected:
void streamThread() override;
bool connect();
SocketCanStreamConfig config = {};
std::unique_ptr<QCanBusDevice> device;
};
class OpenSocketCanWidget : public AbstractOpenStreamWidget {
Q_OBJECT
public:
OpenSocketCanWidget(QWidget *parent = nullptr);
AbstractStream *open() override;
private:
void refreshDevices();
QComboBox *device_edit;
SocketCanStreamConfig config = {};
};

View File

@@ -0,0 +1,24 @@
#pragma once
#include <QDialogButtonBox>
#include <QDialog>
#include <QLineEdit>
#include <QTabWidget>
#include "tools/cabana/streams/abstractstream.h"
class StreamSelector : public QDialog {
Q_OBJECT
public:
StreamSelector(QWidget *parent = nullptr);
void addStreamWidget(AbstractOpenStreamWidget *w, const QString &title);
QString dbcFile() const { return dbc_file->text(); }
AbstractStream *stream() const { return stream_; }
private:
AbstractStream *stream_ = nullptr;
QLineEdit *dbc_file;
QTabWidget *tab;
QDialogButtonBox *btn_box;
};

View File

@@ -0,0 +1,64 @@
#pragma once
#include <algorithm>
#include <limits>
#include <QAbstractTableModel>
#include <QCheckBox>
#include <QLabel>
#include <QPushButton>
#include <QTableView>
#include "tools/cabana/commands.h"
#include "tools/cabana/settings.h"
class FindSignalModel : public QAbstractTableModel {
public:
struct SearchSignal {
MessageId id = {};
uint64_t mono_time = 0;
cabana::Signal sig = {};
double value = 0.;
QStringList values;
};
FindSignalModel(QObject *parent) : QAbstractTableModel(parent) {}
QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
int columnCount(const QModelIndex &parent = QModelIndex()) const override { return 3; }
int rowCount(const QModelIndex &parent = QModelIndex()) const override { return std::min(filtered_signals.size(), 300); }
void search(std::function<bool(double)> cmp);
void reset();
void undo();
QList<SearchSignal> filtered_signals;
QList<SearchSignal> initial_signals;
QList<QList<SearchSignal>> histories;
uint64_t last_time = std::numeric_limits<uint64_t>::max();
};
class FindSignalDlg : public QDialog {
Q_OBJECT
public:
FindSignalDlg(QWidget *parent);
signals:
void openMessage(const MessageId &id);
private:
void search();
void modelReset();
void setInitialSignals();
void customMenuRequested(const QPoint &pos);
QLineEdit *value1, *value2, *factor_edit, *offset_edit;
QLineEdit *bus_edit, *address_edit, *first_time_edit, *last_time_edit;
QComboBox *compare_cb;
QSpinBox *min_size, *max_size;
QCheckBox *litter_endian, *is_signed;
QPushButton *search_btn, *reset_btn, *undo_btn;
QGroupBox *properties_group, *message_group;
QTableView *view;
QLabel *to_label, *stats_label;
FindSignalModel *model;
};

View File

@@ -0,0 +1,34 @@
#pragma once
#include <QComboBox>
#include <QDialog>
#include <QLineEdit>
#include <QSpinBox>
#include <QTableWidget>
#include "tools/cabana/dbc/dbcmanager.h"
class FindSimilarBitsDlg : public QDialog {
Q_OBJECT
public:
FindSimilarBitsDlg(QWidget *parent);
signals:
void openMessage(const MessageId &msg_id);
private:
struct mismatched_struct {
uint32_t address, byte_idx, bit_idx, mismatches, total;
float perc;
};
QList<mismatched_struct> calcBits(uint8_t bus, uint32_t selected_address, int byte_idx, int bit_idx, uint8_t find_bus,
bool equal, int min_msgs_cnt);
void find();
QTableWidget *table;
QComboBox *src_bus_combo, *find_bus_combo, *msg_cb, *equal_combo;
QSpinBox *byte_idx_sb, *bit_idx_sb;
QPushButton *search_btn;
QLineEdit *min_msgs;
};

View File

@@ -0,0 +1,10 @@
#pragma once
#include <optional>
#include "tools/cabana/dbc/dbcmanager.h"
namespace utils {
void exportToCSV(const QString &file_name, std::optional<MessageId> msg_id = std::nullopt);
void exportSignalsToCSV(const QString &file_name, const MessageId &msg_id);
} // namespace utils

166
tools/cabana/utils/util.h Normal file
View File

@@ -0,0 +1,166 @@
#pragma once
#include <array>
#include <cmath>
#include <vector>
#include <utility>
#include <QApplication>
#include <QByteArray>
#include <QDoubleValidator>
#include <QFont>
#include <QFontMetrics>
#include <QPainter>
#include <QRegExpValidator>
#include <QSocketNotifier>
#include <QStaticText>
#include <QStringBuilder>
#include <QStyledItemDelegate>
#include <QToolButton>
#include "tools/cabana/dbc/dbc.h"
#include "tools/cabana/settings.h"
class LogSlider : public QSlider {
Q_OBJECT
public:
LogSlider(double factor, Qt::Orientation orientation, QWidget *parent = nullptr) : factor(factor), QSlider(orientation, parent) {}
void setRange(double min, double max) {
log_min = factor * std::log10(min);
log_max = factor * std::log10(max);
QSlider::setRange(min, max);
setValue(QSlider::value());
}
int value() const {
double v = log_min + (log_max - log_min) * ((QSlider::value() - minimum()) / double(maximum() - minimum()));
return std::lround(std::pow(10, v / factor));
}
void setValue(int v) {
double log_v = std::clamp(factor * std::log10(v), log_min, log_max);
v = minimum() + (maximum() - minimum()) * ((log_v - log_min) / (log_max - log_min));
QSlider::setValue(v);
}
private:
double factor, log_min = 0, log_max = 1;
};
enum {
ColorsRole = Qt::UserRole + 1,
BytesRole = Qt::UserRole + 2
};
class SegmentTree {
public:
SegmentTree() = default;
void build(const std::vector<QPointF> &arr);
inline std::pair<double, double> minmax(int left, int right) const { return get_minmax(1, 0, size - 1, left, right); }
private:
std::pair<double, double> get_minmax(int n, int left, int right, int range_left, int range_right) const;
void build_tree(const std::vector<QPointF> &arr, int n, int left, int right);
std::vector<std::pair<double, double>> tree;
int size = 0;
};
class MessageBytesDelegate : public QStyledItemDelegate {
Q_OBJECT
public:
MessageBytesDelegate(QObject *parent, bool multiple_lines = false);
void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override;
QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override;
bool multipleLines() const { return multiple_lines; }
void setMultipleLines(bool v) { multiple_lines = v; }
QSize sizeForBytes(int n) const;
private:
std::array<QStaticText, 256> hex_text_table;
QFontMetrics font_metrics;
QFont fixed_font;
QSize byte_size = {};
bool multiple_lines = false;
int h_margin, v_margin;
};
class NameValidator : public QRegExpValidator {
Q_OBJECT
public:
NameValidator(QObject *parent=nullptr);
QValidator::State validate(QString &input, int &pos) const override;
};
class DoubleValidator : public QDoubleValidator {
Q_OBJECT
public:
DoubleValidator(QObject *parent = nullptr);
};
namespace utils {
QPixmap icon(const QString &id);
void setTheme(int theme);
QString formatSeconds(double sec, bool include_milliseconds = false, bool absolute_time = false);
inline void drawStaticText(QPainter *p, const QRect &r, const QStaticText &text) {
auto size = (r.size() - text.size()) / 2;
p->drawStaticText(r.left() + size.width(), r.top() + size.height(), text);
}
inline QString toHex(const std::vector<uint8_t> &dat, char separator = '\0') {
return QByteArray::fromRawData((const char *)dat.data(), dat.size()).toHex(separator).toUpper();
}
}
class ToolButton : public QToolButton {
Q_OBJECT
public:
ToolButton(const QString &icon, const QString &tooltip = {}, QWidget *parent = nullptr) : QToolButton(parent) {
setIcon(icon);
setToolTip(tooltip);
setAutoRaise(true);
const int metric = QApplication::style()->pixelMetric(QStyle::PM_SmallIconSize);
setIconSize({metric, metric});
theme = settings.theme;
connect(&settings, &Settings::changed, this, &ToolButton::updateIcon);
}
void setIcon(const QString &icon) {
icon_str = icon;
QToolButton::setIcon(utils::icon(icon_str));
}
private:
void updateIcon() { if (std::exchange(theme, settings.theme) != theme) setIcon(icon_str); }
QString icon_str;
int theme;
};
class TabBar : public QTabBar {
Q_OBJECT
public:
TabBar(QWidget *parent) : QTabBar(parent) {}
int addTab(const QString &text);
private:
void closeTabClicked();
};
class UnixSignalHandler : public QObject {
Q_OBJECT
public:
UnixSignalHandler(QObject *parent = nullptr);
~UnixSignalHandler();
static void signalHandler(int s);
public slots:
void handleSigTerm();
private:
inline static int sig_fd[2] = {};
QSocketNotifier *sn;
};
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'); }

View File

@@ -0,0 +1,82 @@
#pragma once
#include <memory>
#include <set>
#include <string>
#include <utility>
#include <QFrame>
#include <QPropertyAnimation>
#include <QSlider>
#include <QToolBar>
#include <QTabBar>
#include "selfdrive/ui/qt/widgets/cameraview.h"
#include "tools/cabana/utils/util.h"
#include "tools/replay/logreader.h"
#include "tools/cabana/streams/replaystream.h"
class Slider : public QSlider {
Q_OBJECT
public:
Slider(QWidget *parent);
double currentSecond() const { return value() / factor; }
void setCurrentSecond(double sec) { setValue(sec * factor); }
void setTimeRange(double min, double max) { setRange(min * factor, max * factor); }
void mousePressEvent(QMouseEvent *e) override;
void paintEvent(QPaintEvent *ev) override;
const double factor = 1000.0;
double thumbnail_dispaly_time = -1;
};
class StreamCameraView : public CameraWidget {
Q_OBJECT
public:
StreamCameraView(std::string stream_name, VisionStreamType stream_type, QWidget *parent = nullptr);
void paintGL() override;
void showPausedOverlay() { fade_animation->start(); }
void parseQLog(std::shared_ptr<LogReader> qlog);
private:
QPixmap generateThumbnail(QPixmap thumbnail, double seconds);
void drawAlert(QPainter &p, const QRect &rect, const Timeline::Entry &alert);
void drawThumbnail(QPainter &p);
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;
double thumbnail_dispaly_time = -1;
friend class VideoWidget;
};
class VideoWidget : public QFrame {
Q_OBJECT
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();
void createPlaybackController();
void createSpeedDropdown(QToolBar *toolbar);
void loopPlaybackClicked();
void vipcAvailableStreamsUpdated(std::set<VisionStreamType> streams);
StreamCameraView *cam_widget;
QAction *time_display_action = nullptr;
QAction *play_toggle_action = nullptr;
QToolButton *speed_btn = nullptr;
QAction *skip_to_end_action = nullptr;
Slider *slider = nullptr;
QTabBar *camera_tab = nullptr;
};