Cabana: stable initial release (#26004)
* increase form size & fix wrong charts number
* set max axisy to 1.0 if no value
* show 'close' button in floating window
* alwasy show scroll bar
* complete the logs
* more
* increase size to 50
* keep logs for all messages
* more
* rename signal
* better height
* avoid flicker
* dont call setupdatesenabled
* filter dbc files bye typing
* remove all charts if dbc file changed
* fix wrong idx
* bolder dbc filename
* update chart if signal has been edited
* new signals signalAdded,signalUpdated
* split class Parser into CanMessages and DBCManager
* cleanup
* updateState after set message
* cleanup
* emit msgUpdated
* clear history log if selected range changed
* always update time
* change title layout
* show selected range
hide title bar if no charts
less space between title and chart
* custome historylogmodel for extreme fast update
* move historylog to seperate file
* 2 decimal
* cleanup
cleanup
* left click on the chart to set start time
* todo
* show tooltip for header item&cleanup binaryview
add hline to signal form
* better paint
* cleanup signals/slots
* better range if min==max
* set historylog's minheight to 300
* 3x faster,sortable message list.
* zero copy in queued connection
* proxymodel
* clear log if loop to the begin
* simplify history log
* remove icon
* remove assets
* hide linemarker on initialization
* rubber width may less than 0
* dont zoom char if selected range is too small
* cleanup messageslist
* don't zoom chart if selected range less than 500ms
* typo
* check boundary
* check msg_id
* capital first letter
* move history log out of scrollarea
* Show only one form at a time
* auto scroll to header
d
* reduce msg size
entire row clickable
rename filter_msgs
old-commit-hash: 0fa1588f6c
taco
parent
92fab023b3
commit
62adf87f40
25 changed files with 999 additions and 674 deletions
@ -0,0 +1,123 @@ |
||||
#include "tools/cabana/canmessages.h" |
||||
|
||||
#include <QDebug> |
||||
|
||||
Q_DECLARE_METATYPE(std::vector<CanData>); |
||||
|
||||
CANMessages *can = nullptr; |
||||
|
||||
CANMessages::CANMessages(QObject *parent) : QObject(parent) { |
||||
can = this; |
||||
|
||||
qRegisterMetaType<std::vector<CanData>>(); |
||||
QObject::connect(this, &CANMessages::received, this, &CANMessages::process, Qt::QueuedConnection); |
||||
} |
||||
|
||||
CANMessages::~CANMessages() { |
||||
replay->stop(); |
||||
} |
||||
|
||||
static bool event_filter(const Event *e, void *opaque) { |
||||
CANMessages *c = (CANMessages *)opaque; |
||||
return c->eventFilter(e); |
||||
} |
||||
|
||||
bool CANMessages::loadRoute(const QString &route, const QString &data_dir, bool use_qcam) { |
||||
replay = new Replay(route, {"can", "roadEncodeIdx"}, {}, nullptr, use_qcam ? REPLAY_FLAG_QCAMERA : 0, data_dir, this); |
||||
replay->installEventFilter(event_filter, this); |
||||
QObject::connect(replay, &Replay::segmentsMerged, this, &CANMessages::segmentsMerged); |
||||
if (replay->load()) { |
||||
replay->start(); |
||||
return true; |
||||
} |
||||
return false; |
||||
} |
||||
|
||||
void CANMessages::process(QHash<QString, std::deque<CanData>> *messages) { |
||||
for (auto it = messages->begin(); it != messages->end(); ++it) { |
||||
++counters[it.key()]; |
||||
auto &msgs = can_msgs[it.key()]; |
||||
const auto &new_msgs = it.value(); |
||||
if (msgs.size() == CAN_MSG_LOG_SIZE || can_msgs[it.key()].size() == 0) { |
||||
msgs = std::move(new_msgs); |
||||
} else { |
||||
msgs.insert(msgs.begin(), std::make_move_iterator(new_msgs.begin()), std::make_move_iterator(new_msgs.end())); |
||||
while (msgs.size() >= CAN_MSG_LOG_SIZE) { |
||||
msgs.pop_back(); |
||||
} |
||||
} |
||||
} |
||||
delete messages; |
||||
|
||||
if (current_sec < begin_sec || current_sec > end_sec) { |
||||
// loop replay in selected range.
|
||||
seekTo(begin_sec); |
||||
} else { |
||||
emit updated(); |
||||
} |
||||
} |
||||
|
||||
bool CANMessages::eventFilter(const Event *event) { |
||||
static double prev_update_sec = 0; |
||||
// drop packets when the GUI thread is calling seekTo. to make sure the current_sec is accurate.
|
||||
if (!seeking && event->which == cereal::Event::Which::CAN) { |
||||
if (!received_msgs) { |
||||
received_msgs.reset(new QHash<QString, std::deque<CanData>>); |
||||
received_msgs->reserve(1000); |
||||
} |
||||
|
||||
current_sec = (event->mono_time - replay->routeStartTime()) / (double)1e9; |
||||
auto can_events = event->event.getCan(); |
||||
for (const auto &c : can_events) { |
||||
QString id = QString("%1:%2").arg(c.getSrc()).arg(c.getAddress(), 1, 16); |
||||
auto &list = (*received_msgs)[id]; |
||||
while (list.size() >= CAN_MSG_LOG_SIZE) { |
||||
list.pop_back(); |
||||
} |
||||
CanData &data = list.emplace_front(); |
||||
data.ts = current_sec; |
||||
data.bus_time = c.getBusTime(); |
||||
data.dat.append((char *)c.getDat().begin(), c.getDat().size()); |
||||
} |
||||
|
||||
if (current_sec < prev_update_sec || (current_sec - prev_update_sec) > 1.0 / FPS) { |
||||
prev_update_sec = current_sec; |
||||
// use pointer to avoid data copy in queued connection.
|
||||
emit received(received_msgs.release()); |
||||
} |
||||
} |
||||
return true; |
||||
} |
||||
|
||||
void CANMessages::seekTo(double ts) { |
||||
seeking = true; |
||||
replay->seekTo(ts, false); |
||||
seeking = false; |
||||
} |
||||
|
||||
void CANMessages::setRange(double min, double max) { |
||||
if (begin_sec != min || end_sec != max) { |
||||
begin_sec = min; |
||||
end_sec = max; |
||||
is_zoomed = begin_sec != event_begin_sec || end_sec != event_end_sec; |
||||
emit rangeChanged(min, max); |
||||
} |
||||
} |
||||
|
||||
void CANMessages::segmentsMerged() { |
||||
auto events = replay->events(); |
||||
if (!events || events->empty()) return; |
||||
|
||||
auto it = std::find_if(events->begin(), events->end(), [=](const Event *e) { return e->which == cereal::Event::Which::CAN; }); |
||||
event_begin_sec = it == events->end() ? 0 : ((*it)->mono_time - replay->routeStartTime()) / (double)1e9; |
||||
event_end_sec = double(events->back()->mono_time - replay->routeStartTime()) / 1e9; |
||||
if (!is_zoomed) { |
||||
begin_sec = event_begin_sec; |
||||
end_sec = event_end_sec; |
||||
} |
||||
emit eventsMerged(); |
||||
} |
||||
|
||||
void CANMessages::resetRange() { |
||||
setRange(event_begin_sec, event_end_sec); |
||||
} |
@ -0,0 +1,84 @@ |
||||
#pragma once |
||||
|
||||
#include <atomic> |
||||
#include <deque> |
||||
#include <map> |
||||
|
||||
#include <QHash> |
||||
|
||||
#include "tools/replay/replay.h" |
||||
|
||||
const int FPS = 10; |
||||
const int CAN_MSG_LOG_SIZE = 100; |
||||
|
||||
struct CanData { |
||||
double ts; |
||||
uint16_t bus_time; |
||||
QByteArray dat; |
||||
}; |
||||
|
||||
class CANMessages : public QObject { |
||||
Q_OBJECT |
||||
|
||||
public: |
||||
CANMessages(QObject *parent); |
||||
~CANMessages(); |
||||
bool loadRoute(const QString &route, const QString &data_dir, bool use_qcam); |
||||
void seekTo(double ts); |
||||
void resetRange(); |
||||
void setRange(double min, double max); |
||||
bool eventFilter(const Event *event); |
||||
|
||||
inline std::pair<double, double> range() const { return {begin_sec, end_sec}; } |
||||
inline double totalSeconds() const { return replay->totalSeconds(); } |
||||
inline double routeStartTime() const { return replay->routeStartTime() / (double)1e9; } |
||||
inline double currentSec() const { return current_sec; } |
||||
inline bool isZoomed() const { return is_zoomed; } |
||||
inline const std::deque<CanData> &messages(const QString &id) { return can_msgs[id]; } |
||||
inline const CanData &lastMessage(const QString &id) { return can_msgs[id].front(); } |
||||
|
||||
inline const std::vector<Event *> *events() const { return replay->events(); } |
||||
inline void setSpeed(float speed) { replay->setSpeed(speed); } |
||||
inline bool isPaused() const { return replay->isPaused(); } |
||||
inline void pause(bool pause) { replay->pause(pause); } |
||||
inline const std::vector<std::tuple<int, int, TimelineType>> getTimeline() { return replay->getTimeline(); } |
||||
|
||||
signals: |
||||
void eventsMerged(); |
||||
void rangeChanged(double min, double max); |
||||
void updated(); |
||||
void received(QHash<QString, std::deque<CanData>> *); |
||||
|
||||
public: |
||||
QMap<QString, std::deque<CanData>> can_msgs; |
||||
std::unique_ptr<QHash<QString, std::deque<CanData>>> received_msgs = nullptr; |
||||
QHash<QString, uint32_t> counters; |
||||
|
||||
protected: |
||||
void process(QHash<QString, std::deque<CanData>> *); |
||||
void segmentsMerged(); |
||||
|
||||
std::atomic<double> current_sec = 0.; |
||||
std::atomic<bool> seeking = false; |
||||
double begin_sec = 0; |
||||
double end_sec = 0; |
||||
double event_begin_sec = 0; |
||||
double event_end_sec = 0; |
||||
bool is_zoomed = false; |
||||
Replay *replay = nullptr; |
||||
}; |
||||
|
||||
inline QString toHex(const QByteArray &dat) { |
||||
return dat.toHex(' ').toUpper(); |
||||
} |
||||
inline char toHex(uint value) { |
||||
return "0123456789ABCDEF"[value & 0xF]; |
||||
} |
||||
|
||||
inline const QString &getColor(int i) { |
||||
static const QString SIGNAL_COLORS[] = {"#9FE2BF", "#40E0D0", "#6495ED", "#CCCCFF", "#FF7F50", "#FFBF00"}; |
||||
return SIGNAL_COLORS[i % std::size(SIGNAL_COLORS)]; |
||||
} |
||||
|
||||
// A global pointer referring to the unique CANMessages object
|
||||
extern CANMessages *can; |
@ -0,0 +1,117 @@ |
||||
#include "tools/cabana/dbcmanager.h" |
||||
|
||||
#include <QVector> |
||||
|
||||
DBCManager::DBCManager(QObject *parent) : QObject(parent) {} |
||||
|
||||
DBCManager::~DBCManager() {} |
||||
|
||||
void DBCManager::open(const QString &dbc_file_name) { |
||||
dbc_name = dbc_file_name; |
||||
dbc = const_cast<DBC *>(dbc_lookup(dbc_name.toStdString())); |
||||
msg_map.clear(); |
||||
for (auto &msg : dbc->msgs) { |
||||
msg_map[msg.address] = &msg; |
||||
} |
||||
emit DBCFileChanged(); |
||||
} |
||||
|
||||
void save(const QString &dbc_file_name) { |
||||
// TODO: save DBC to file
|
||||
} |
||||
|
||||
void DBCManager::updateMsg(const QString &id, const QString &name, uint32_t size) { |
||||
auto m = const_cast<Msg *>(msg(id)); |
||||
if (m) { |
||||
m->name = name.toStdString(); |
||||
m->size = size; |
||||
} else { |
||||
uint32_t address = addressFromId(id); |
||||
dbc->msgs.push_back({.address = address, .name = name.toStdString(), .size = size}); |
||||
msg_map[address] = &dbc->msgs.back(); |
||||
} |
||||
emit msgUpdated(id); |
||||
} |
||||
|
||||
void DBCManager::addSignal(const QString &id, const Signal &sig) { |
||||
if (Msg *m = const_cast<Msg *>(msg(id))) { |
||||
m->sigs.push_back(sig); |
||||
emit signalAdded(id, QString::fromStdString(sig.name)); |
||||
} |
||||
} |
||||
|
||||
void DBCManager::updateSignal(const QString &id, const QString &sig_name, const Signal &sig) { |
||||
if (Signal *s = const_cast<Signal *>(signal(id, sig_name))) { |
||||
*s = sig; |
||||
emit signalUpdated(id, sig_name); |
||||
} |
||||
} |
||||
|
||||
void DBCManager::removeSignal(const QString &id, const QString &sig_name) { |
||||
if (Msg *m = const_cast<Msg *>(msg(id))) { |
||||
auto it = std::find_if(m->sigs.begin(), m->sigs.end(), [=](auto &sig) { return sig_name == sig.name.c_str(); }); |
||||
if (it != m->sigs.end()) { |
||||
m->sigs.erase(it); |
||||
emit signalRemoved(id, sig_name); |
||||
} |
||||
} |
||||
} |
||||
|
||||
const Signal *DBCManager::signal(const QString &id, const QString &sig_name) const { |
||||
if (auto m = msg(id)) { |
||||
auto it = std::find_if(m->sigs.begin(), m->sigs.end(), [&](auto &s) { return sig_name == s.name.c_str(); }); |
||||
if (it != m->sigs.end()) |
||||
return &(*it); |
||||
} |
||||
return nullptr; |
||||
} |
||||
|
||||
uint32_t DBCManager::addressFromId(const QString &id) { |
||||
return id.mid(id.indexOf(':') + 1).toUInt(nullptr, 16); |
||||
} |
||||
|
||||
DBCManager *dbc() { |
||||
static DBCManager dbc_manager(nullptr); |
||||
return &dbc_manager; |
||||
} |
||||
|
||||
// helper functions
|
||||
|
||||
static QVector<int> BIG_ENDIAN_START_BITS = []() { |
||||
QVector<int> ret; |
||||
for (int i = 0; i < 64; i++) |
||||
for (int j = 7; j >= 0; j--) |
||||
ret.push_back(j + i * 8); |
||||
return ret; |
||||
}(); |
||||
|
||||
int bigEndianStartBitsIndex(int start_bit) { |
||||
return BIG_ENDIAN_START_BITS[start_bit]; |
||||
} |
||||
|
||||
int bigEndianBitIndex(int index) { |
||||
return BIG_ENDIAN_START_BITS.indexOf(index); |
||||
} |
||||
|
||||
double get_raw_value(uint8_t *data, size_t data_size, const Signal &sig) { |
||||
int64_t val = 0; |
||||
|
||||
int i = sig.msb / 8; |
||||
int bits = sig.size; |
||||
while (i >= 0 && i < data_size && bits > 0) { |
||||
int lsb = (int)(sig.lsb / 8) == i ? sig.lsb : i * 8; |
||||
int msb = (int)(sig.msb / 8) == i ? sig.msb : (i + 1) * 8 - 1; |
||||
int size = msb - lsb + 1; |
||||
|
||||
uint64_t d = (data[i] >> (lsb - (i * 8))) & ((1ULL << size) - 1); |
||||
val |= d << (bits - size); |
||||
|
||||
bits -= size; |
||||
i = sig.is_little_endian ? i - 1 : i + 1; |
||||
} |
||||
if (sig.is_signed) { |
||||
val -= ((val >> (sig.size - 1)) & 0x1) ? (1ULL << sig.size) : 0; |
||||
} |
||||
double value = val * sig.factor + sig.offset; |
||||
return value; |
||||
} |
@ -0,0 +1,51 @@ |
||||
#pragma once |
||||
|
||||
#include <QObject> |
||||
|
||||
#include "opendbc/can/common_dbc.h" |
||||
|
||||
class DBCManager : public QObject { |
||||
Q_OBJECT |
||||
|
||||
public: |
||||
DBCManager(QObject *parent); |
||||
~DBCManager(); |
||||
|
||||
void open(const QString &dbc_file_name); |
||||
void save(const QString &dbc_file_name); |
||||
|
||||
const Signal *signal(const QString &id, const QString &sig_name) const; |
||||
void addSignal(const QString &id, const Signal &sig); |
||||
void updateSignal(const QString &id, const QString &sig_name, const Signal &sig); |
||||
void removeSignal(const QString &id, const QString &sig_name); |
||||
|
||||
static uint32_t addressFromId(const QString &id); |
||||
inline static std::vector<std::string> allDBCNames() { return get_dbc_names(); } |
||||
inline QString name() const { return dbc_name; } |
||||
|
||||
void updateMsg(const QString &id, const QString &name, uint32_t size); |
||||
inline const Msg *msg(const QString &id) const { return msg(addressFromId(id)); } |
||||
inline const Msg *msg(uint32_t address) const { |
||||
auto it = msg_map.find(address); |
||||
return it != msg_map.end() ? it->second : nullptr; |
||||
} |
||||
|
||||
signals: |
||||
void signalAdded(const QString &id, const QString &sig_name); |
||||
void signalRemoved(const QString &id, const QString &sig_name); |
||||
void signalUpdated(const QString &id, const QString &sig_name); |
||||
void msgUpdated(const QString &id); |
||||
void DBCFileChanged(); |
||||
|
||||
private: |
||||
QString dbc_name; |
||||
DBC *dbc = nullptr; |
||||
std::unordered_map<uint32_t, const Msg *> msg_map; |
||||
}; |
||||
|
||||
// TODO: Add helper function in dbc.h
|
||||
double get_raw_value(uint8_t *data, size_t data_size, const Signal &sig); |
||||
int bigEndianStartBitsIndex(int start_bit); |
||||
int bigEndianBitIndex(int index); |
||||
|
||||
DBCManager *dbc(); |
@ -0,0 +1,91 @@ |
||||
#include "tools/cabana/historylog.h" |
||||
|
||||
#include <QHeaderView> |
||||
#include <QVBoxLayout> |
||||
|
||||
QVariant HistoryLogModel::data(const QModelIndex &index, int role) const { |
||||
if (role == Qt::DisplayRole) { |
||||
const auto &can_msgs = can->messages(msg_id); |
||||
if (index.row() < can_msgs.size()) { |
||||
const auto &can_data = can_msgs[index.row()]; |
||||
auto msg = dbc()->msg(msg_id); |
||||
if (msg && index.column() < msg->sigs.size()) { |
||||
return get_raw_value((uint8_t *)can_data.dat.begin(), can_data.dat.size(), msg->sigs[index.column()]); |
||||
} else { |
||||
return toHex(can_data.dat); |
||||
} |
||||
} |
||||
} |
||||
return {}; |
||||
} |
||||
|
||||
void HistoryLogModel::setMessage(const QString &message_id) { |
||||
beginResetModel(); |
||||
msg_id = message_id; |
||||
const auto msg = dbc()->msg(message_id); |
||||
column_count = msg && !msg->sigs.empty() ? msg->sigs.size() : 1; |
||||
row_count = 0; |
||||
endResetModel(); |
||||
|
||||
updateState(); |
||||
} |
||||
|
||||
QVariant HistoryLogModel::headerData(int section, Qt::Orientation orientation, int role) const { |
||||
if (orientation == Qt::Horizontal) { |
||||
auto msg = dbc()->msg(msg_id); |
||||
if (msg && section < msg->sigs.size()) { |
||||
if (role == Qt::BackgroundRole) { |
||||
return QBrush(QColor(getColor(section))); |
||||
} else if (role == Qt::DisplayRole || role == Qt::ToolTipRole) { |
||||
return QString::fromStdString(msg->sigs[section].name); |
||||
} |
||||
} |
||||
} else if (role == Qt::DisplayRole) { |
||||
const auto &can_msgs = can->messages(msg_id); |
||||
if (section < can_msgs.size()) { |
||||
return QString::number(can_msgs[section].ts, 'f', 2); |
||||
} |
||||
} |
||||
return {}; |
||||
} |
||||
|
||||
void HistoryLogModel::updateState() { |
||||
if (msg_id.isEmpty()) return; |
||||
|
||||
const auto &can_msgs = can->messages(msg_id); |
||||
int prev_row_count = row_count; |
||||
row_count = can_msgs.size(); |
||||
int delta = row_count - prev_row_count; |
||||
if (delta > 0) { |
||||
beginInsertRows({}, prev_row_count, row_count - 1); |
||||
endInsertRows(); |
||||
} else if (delta < 0) { |
||||
beginRemoveRows({}, row_count, prev_row_count - 1); |
||||
endRemoveRows(); |
||||
} |
||||
if (row_count > 0) { |
||||
emit dataChanged(index(0, 0), index(row_count - 1, column_count - 1)); |
||||
emit headerDataChanged(Qt::Vertical, 0, row_count - 1); |
||||
} |
||||
} |
||||
|
||||
HistoryLog::HistoryLog(QWidget *parent) : QWidget(parent) { |
||||
QVBoxLayout *main_layout = new QVBoxLayout(this); |
||||
main_layout->setContentsMargins(0, 0, 0, 0); |
||||
model = new HistoryLogModel(this); |
||||
table = new QTableView(this); |
||||
table->setModel(model); |
||||
table->horizontalHeader()->setStretchLastSection(true); |
||||
table->setEditTriggers(QAbstractItemView::NoEditTriggers); |
||||
table->setStyleSheet("QTableView::item { border:0px; padding-left:5px; padding-right:5px; }"); |
||||
table->verticalHeader()->setStyleSheet("QHeaderView::section {padding-left: 5px; padding-right: 5px;min-width:40px;}"); |
||||
main_layout->addWidget(table); |
||||
} |
||||
|
||||
void HistoryLog::setMessage(const QString &message_id) { |
||||
model->setMessage(message_id); |
||||
} |
||||
|
||||
void HistoryLog::updateState() { |
||||
model->updateState(); |
||||
} |
@ -0,0 +1,37 @@ |
||||
#pragma once |
||||
|
||||
#include <QTableView> |
||||
|
||||
#include "tools/cabana/canmessages.h" |
||||
#include "tools/cabana/dbcmanager.h" |
||||
|
||||
class HistoryLogModel : public QAbstractTableModel { |
||||
Q_OBJECT |
||||
|
||||
public: |
||||
HistoryLogModel(QObject *parent) : QAbstractTableModel(parent) {} |
||||
void setMessage(const QString &message_id); |
||||
void updateState(); |
||||
QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; |
||||
int columnCount(const QModelIndex &parent = QModelIndex()) const override { return column_count; } |
||||
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const; |
||||
int rowCount(const QModelIndex &parent = QModelIndex()) const override { return row_count; } |
||||
|
||||
private: |
||||
QString msg_id; |
||||
int row_count = 0; |
||||
int column_count = 0; |
||||
}; |
||||
|
||||
class HistoryLog : public QWidget { |
||||
Q_OBJECT |
||||
|
||||
public: |
||||
HistoryLog(QWidget *parent); |
||||
void setMessage(const QString &message_id); |
||||
void updateState(); |
||||
|
||||
private: |
||||
QTableView *table; |
||||
HistoryLogModel *model; |
||||
}; |
@ -1,24 +1,38 @@ |
||||
#pragma once |
||||
|
||||
#include <QLineEdit> |
||||
#include <QTableWidget> |
||||
#include <QWidget> |
||||
#include <QAbstractTableModel> |
||||
#include <QTableView> |
||||
|
||||
#include "tools/cabana/parser.h" |
||||
#include "tools/cabana/canmessages.h" |
||||
|
||||
class MessageListModel : public QAbstractTableModel { |
||||
Q_OBJECT |
||||
|
||||
public: |
||||
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 4; } |
||||
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const; |
||||
int rowCount(const QModelIndex &parent = QModelIndex()) const override { return row_count; } |
||||
void updateState(); |
||||
|
||||
private: |
||||
int row_count = 0; |
||||
}; |
||||
|
||||
class MessagesWidget : public QWidget { |
||||
Q_OBJECT |
||||
|
||||
public: |
||||
public: |
||||
MessagesWidget(QWidget *parent); |
||||
|
||||
public slots: |
||||
void updateState(); |
||||
public slots: |
||||
void dbcSelectionChanged(const QString &dbc_file); |
||||
|
||||
signals: |
||||
void msgChanged(const CanData *id); |
||||
signals: |
||||
void msgSelectionChanged(const QString &message_id); |
||||
|
||||
protected: |
||||
QLineEdit *filter; |
||||
QTableWidget *table_widget; |
||||
protected: |
||||
QTableView *table_widget; |
||||
MessageListModel *model; |
||||
}; |
||||
|
@ -1,181 +0,0 @@ |
||||
#include "tools/cabana/parser.h" |
||||
|
||||
#include <QDebug> |
||||
|
||||
#include "cereal/messaging/messaging.h" |
||||
|
||||
Parser *parser = nullptr; |
||||
|
||||
static bool event_filter(const Event *e, void *opaque) { |
||||
Parser *p = (Parser*)opaque; |
||||
return p->eventFilter(e); |
||||
} |
||||
|
||||
Parser::Parser(QObject *parent) : QObject(parent) { |
||||
parser = this; |
||||
|
||||
qRegisterMetaType<std::vector<CanData>>(); |
||||
QObject::connect(this, &Parser::received, this, &Parser::process, Qt::QueuedConnection); |
||||
} |
||||
|
||||
Parser::~Parser() { |
||||
replay->stop(); |
||||
} |
||||
|
||||
bool Parser::loadRoute(const QString &route, const QString &data_dir, bool use_qcam) { |
||||
replay = new Replay(route, {"can", "roadEncodeIdx"}, {}, nullptr, use_qcam ? REPLAY_FLAG_QCAMERA : 0, data_dir, this); |
||||
replay->installEventFilter(event_filter, this); |
||||
QObject::connect(replay, &Replay::segmentsMerged, this, &Parser::segmentsMerged); |
||||
if (replay->load()) { |
||||
replay->start(); |
||||
return true; |
||||
} |
||||
return false; |
||||
} |
||||
|
||||
void Parser::openDBC(const QString &name) { |
||||
dbc_name = name; |
||||
dbc = const_cast<DBC *>(dbc_lookup(name.toStdString())); |
||||
counters.clear(); |
||||
msg_map.clear(); |
||||
for (auto &msg : dbc->msgs) { |
||||
msg_map[msg.address] = &msg; |
||||
} |
||||
} |
||||
|
||||
void Parser::process(std::vector<CanData> msgs) { |
||||
static double prev_update_ts = 0; |
||||
|
||||
for (const auto &can_data : msgs) { |
||||
can_msgs[can_data.id] = can_data; |
||||
++counters[can_data.id]; |
||||
|
||||
if (can_data.id == current_msg_id) { |
||||
while (history_log.size() >= LOG_SIZE) { |
||||
history_log.pop_back(); |
||||
} |
||||
history_log.push_front(can_data); |
||||
} |
||||
} |
||||
double now = millis_since_boot(); |
||||
if ((now - prev_update_ts) > 1000.0 / FPS) { |
||||
prev_update_ts = now; |
||||
emit updated(); |
||||
} |
||||
|
||||
if (current_sec < begin_sec || current_sec > end_sec) { |
||||
// loop replay in selected range.
|
||||
seekTo(begin_sec); |
||||
} |
||||
} |
||||
|
||||
bool Parser::eventFilter(const Event *event) { |
||||
// drop packets when the GUI thread is calling seekTo. to make sure the current_sec is accurate.
|
||||
if (!seeking && event->which == cereal::Event::Which::CAN) { |
||||
current_sec = (event->mono_time - replay->routeStartTime()) / (double)1e9; |
||||
|
||||
auto can = event->event.getCan(); |
||||
msgs_buf.clear(); |
||||
msgs_buf.reserve(can.size()); |
||||
|
||||
for (const auto &c : can) { |
||||
CanData &data = msgs_buf.emplace_back(); |
||||
data.address = c.getAddress(); |
||||
data.bus_time = c.getBusTime(); |
||||
data.source = c.getSrc(); |
||||
data.dat.append((char *)c.getDat().begin(), c.getDat().size()); |
||||
data.id = QString("%1:%2").arg(data.source).arg(data.address, 1, 16); |
||||
data.ts = current_sec; |
||||
} |
||||
emit received(msgs_buf); |
||||
} |
||||
return true; |
||||
} |
||||
|
||||
void Parser::seekTo(double ts) { |
||||
seeking = true; |
||||
replay->seekTo(ts, false); |
||||
seeking = false; |
||||
} |
||||
|
||||
void Parser::addNewMsg(const Msg &msg) { |
||||
dbc->msgs.push_back(msg); |
||||
msg_map[dbc->msgs.back().address] = &dbc->msgs.back(); |
||||
} |
||||
|
||||
void Parser::removeSignal(const QString &id, const QString &sig_name) { |
||||
Msg *msg = const_cast<Msg *>(getMsg(id)); |
||||
if (!msg) return; |
||||
|
||||
auto it = std::find_if(msg->sigs.begin(), msg->sigs.end(), [=](auto &sig) { return sig_name == sig.name.c_str(); }); |
||||
if (it != msg->sigs.end()) { |
||||
msg->sigs.erase(it); |
||||
emit signalRemoved(id, sig_name); |
||||
} |
||||
} |
||||
|
||||
uint32_t Parser::addressFromId(const QString &id) { |
||||
return id.mid(id.indexOf(':') + 1).toUInt(nullptr, 16); |
||||
} |
||||
|
||||
const Signal *Parser::getSig(const QString &id, const QString &sig_name) { |
||||
if (auto msg = getMsg(id)) { |
||||
auto it = std::find_if(msg->sigs.begin(), msg->sigs.end(), [&](auto &s) { return sig_name == s.name.c_str(); }); |
||||
if (it != msg->sigs.end()) { |
||||
return &(*it); |
||||
} |
||||
} |
||||
return nullptr; |
||||
} |
||||
|
||||
void Parser::setRange(double min, double max) { |
||||
if (begin_sec != min || end_sec != max) { |
||||
begin_sec = min; |
||||
end_sec = max; |
||||
is_zoomed = begin_sec != event_begin_sec || end_sec != event_end_sec; |
||||
emit rangeChanged(min, max); |
||||
} |
||||
} |
||||
|
||||
void Parser::segmentsMerged() { |
||||
auto events = replay->events(); |
||||
if (!events || events->empty()) return; |
||||
|
||||
auto it = std::find_if(events->begin(), events->end(), [=](const Event *e) { return e->which == cereal::Event::Which::CAN; }); |
||||
event_begin_sec = it == events->end() ? 0 : ((*it)->mono_time - replay->routeStartTime()) / (double)1e9; |
||||
event_end_sec = double(events->back()->mono_time - replay->routeStartTime()) / 1e9; |
||||
if (!is_zoomed) { |
||||
begin_sec = event_begin_sec; |
||||
end_sec = event_end_sec; |
||||
} |
||||
emit eventsMerged(); |
||||
} |
||||
|
||||
void Parser::resetRange() { |
||||
setRange(event_begin_sec, event_end_sec); |
||||
} |
||||
|
||||
void Parser::setCurrentMsg(const QString &id) { |
||||
current_msg_id = id; |
||||
history_log.clear(); |
||||
} |
||||
|
||||
// helper functions
|
||||
|
||||
static QVector<int> BIG_ENDIAN_START_BITS = []() { |
||||
QVector<int> ret; |
||||
for (int i = 0; i < 64; i++) { |
||||
for (int j = 7; j >= 0; j--) { |
||||
ret.push_back(j + i * 8); |
||||
} |
||||
} |
||||
return ret; |
||||
}(); |
||||
|
||||
int bigEndianStartBitsIndex(int start_bit) { |
||||
return BIG_ENDIAN_START_BITS[start_bit]; |
||||
} |
||||
|
||||
int bigEndianBitIndex(int index) { |
||||
return BIG_ENDIAN_START_BITS.indexOf(index); |
||||
} |
@ -1,95 +0,0 @@ |
||||
#pragma once |
||||
|
||||
#include <atomic> |
||||
#include <map> |
||||
|
||||
#include <QApplication> |
||||
#include <QHash> |
||||
#include <QObject> |
||||
|
||||
#include "opendbc/can/common.h" |
||||
#include "opendbc/can/common_dbc.h" |
||||
#include "tools/replay/replay.h" |
||||
|
||||
const int FPS = 20; |
||||
const static int LOG_SIZE = 25; |
||||
|
||||
struct CanData { |
||||
QString id; |
||||
double ts; |
||||
uint32_t address; |
||||
uint16_t bus_time; |
||||
uint8_t source; |
||||
QByteArray dat; |
||||
}; |
||||
|
||||
class Parser : public QObject { |
||||
Q_OBJECT |
||||
|
||||
public: |
||||
Parser(QObject *parent); |
||||
~Parser(); |
||||
static uint32_t addressFromId(const QString &id); |
||||
bool eventFilter(const Event *event); |
||||
bool loadRoute(const QString &route, const QString &data_dir, bool use_qcam); |
||||
void openDBC(const QString &name); |
||||
void saveDBC(const QString &name) {} |
||||
void addNewMsg(const Msg &msg); |
||||
void removeSignal(const QString &id, const QString &sig_name); |
||||
void seekTo(double ts); |
||||
const Signal *getSig(const QString &id, const QString &sig_name); |
||||
void setRange(double min, double max); |
||||
void resetRange(); |
||||
void setCurrentMsg(const QString &id); |
||||
inline std::pair<double, double> range() const { return {begin_sec, end_sec}; } |
||||
inline double currentSec() const { return current_sec; } |
||||
inline bool isZoomed() const { return is_zoomed; } |
||||
inline const Msg *getMsg(const QString &id) { return getMsg(addressFromId(id)); } |
||||
inline const Msg *getMsg(uint32_t address) { |
||||
auto it = msg_map.find(address); |
||||
return it != msg_map.end() ? it->second : nullptr; |
||||
} |
||||
|
||||
signals: |
||||
void showPlot(const QString &id, const QString &name); |
||||
void hidePlot(const QString &id, const QString &name); |
||||
void signalRemoved(const QString &id, const QString &sig_name); |
||||
void eventsMerged(); |
||||
void rangeChanged(double min, double max); |
||||
void received(std::vector<CanData> can); |
||||
void updated(); |
||||
|
||||
public: |
||||
Replay *replay = nullptr; |
||||
QHash<QString, uint64_t> counters; |
||||
std::map<QString, CanData> can_msgs; |
||||
QList<CanData> history_log; |
||||
|
||||
protected: |
||||
void process(std::vector<CanData> can); |
||||
void segmentsMerged(); |
||||
|
||||
std::atomic<double> current_sec = 0.; |
||||
std::atomic<bool> seeking = false; |
||||
QString dbc_name; |
||||
double begin_sec = 0; |
||||
double end_sec = 0; |
||||
double event_begin_sec = 0; |
||||
double event_end_sec = 0; |
||||
bool is_zoomed = false; |
||||
DBC *dbc = nullptr; |
||||
std::map<uint32_t, const Msg *> msg_map; |
||||
QString current_msg_id; |
||||
std::vector<CanData> msgs_buf; |
||||
}; |
||||
|
||||
Q_DECLARE_METATYPE(std::vector<CanData>); |
||||
|
||||
// TODO: Add helper function in dbc.h
|
||||
int bigEndianStartBitsIndex(int start_bit); |
||||
int bigEndianBitIndex(int index); |
||||
inline QString toHex(const QByteArray &dat) { |
||||
return dat.toHex(' ').toUpper(); |
||||
} |
||||
|
||||
extern Parser *parser; |
Loading…
Reference in new issue