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 |
#pragma once |
||||||
|
|
||||||
#include <QLineEdit> |
#include <QAbstractTableModel> |
||||||
#include <QTableWidget> |
#include <QTableView> |
||||||
#include <QWidget> |
|
||||||
|
|
||||||
#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 { |
class MessagesWidget : public QWidget { |
||||||
Q_OBJECT |
Q_OBJECT |
||||||
|
|
||||||
public: |
public: |
||||||
MessagesWidget(QWidget *parent); |
MessagesWidget(QWidget *parent); |
||||||
|
|
||||||
public slots: |
public slots: |
||||||
void updateState(); |
void dbcSelectionChanged(const QString &dbc_file); |
||||||
|
|
||||||
signals: |
signals: |
||||||
void msgChanged(const CanData *id); |
void msgSelectionChanged(const QString &message_id); |
||||||
|
|
||||||
protected: |
protected: |
||||||
QLineEdit *filter; |
QTableView *table_widget; |
||||||
QTableWidget *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