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_msgspull/214/head
							parent
							
								
									03a065160e
								
							
						
					
					
						commit
						0fa1588f6c
					
				
				 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,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