308 lines
11 KiB
C++
308 lines
11 KiB
C++
#include "tools/cabana/ui/widgets/historylog.h"
|
|
|
|
#include <algorithm>
|
|
#include <cstdio>
|
|
#include <iterator>
|
|
|
|
#include "tools/cabana/commands.h"
|
|
#include "tools/cabana/settings.h"
|
|
#include "tools/cabana/ui/dialogs/filedialog.h"
|
|
#include "tools/cabana/ui/icons.h"
|
|
#include "tools/cabana/ui/util.h"
|
|
#include "tools/cabana/ui/widgets/messagebytes.h"
|
|
#include "tools/cabana/utils/export.h"
|
|
#include "tools/cabana/utils/strings.h"
|
|
|
|
namespace {
|
|
|
|
constexpr int BATCH_SIZE = 50;
|
|
constexpr float DISPLAY_TYPE_WIDTH = 90.0f;
|
|
constexpr float SIGNALS_WIDTH = 160.0f;
|
|
constexpr float COMPARE_WIDTH = 50.0f;
|
|
|
|
std::string formatTime(uint64_t mono_time) {
|
|
char buf[32] = {};
|
|
snprintf(buf, sizeof(buf), "%.3f", can->toSeconds(mono_time));
|
|
return buf;
|
|
}
|
|
|
|
}
|
|
|
|
LogsWidget::LogsWidget() {
|
|
connections_.push_back(can->seekedTo.connect([this](double) { reset(); }));
|
|
connections_.push_back(dbc()->fileChanged.connect([this]() { reset(); }));
|
|
connections_.push_back(UndoStack::instance()->indexChanged.connect([this]() { reset(); }));
|
|
}
|
|
|
|
void LogsWidget::setMessage(const MessageId &message_id) {
|
|
msg_id_ = message_id;
|
|
reset();
|
|
}
|
|
|
|
void LogsWidget::reset() {
|
|
sigs_.clear();
|
|
if (auto dbc_msg = dbc()->msg(msg_id_)) sigs_ = dbc_msg->getSignals();
|
|
messages_.clear();
|
|
hex_colors_ = {};
|
|
signals_cb_ = comp_box_ = 0;
|
|
value_edit_.clear();
|
|
value_edit_modified_ = false;
|
|
export_btn_enabled_ = false;
|
|
selected_row_ = selected_col_ = -1;
|
|
setFilter(0, "", nullptr);
|
|
}
|
|
|
|
void LogsWidget::setFilter(int sig_idx, const std::string &value, std::function<bool(double, double)> cmp) {
|
|
filter_sig_idx_ = sig_idx;
|
|
filter_value_ = utils::toDouble(value);
|
|
filter_cmp_ = value.empty() ? nullptr : cmp;
|
|
load(true);
|
|
}
|
|
|
|
void LogsWidget::load(bool clear) {
|
|
if (clear && !messages_.empty()) {
|
|
messages_.clear();
|
|
selected_row_ = selected_col_ = -1;
|
|
}
|
|
const uint64_t current_time = can->toMonoTime(can->lastMessage(msg_id_).ts) + 1;
|
|
fetch(messages_.begin(), current_time, messages_.empty() ? 0 : messages_.front().mono_time);
|
|
}
|
|
|
|
bool LogsWidget::canFetchMore() const {
|
|
const auto &events = can->events(msg_id_);
|
|
return !events.empty() && !messages_.empty() && messages_.back().mono_time > events.front()->mono_time;
|
|
}
|
|
|
|
void LogsWidget::fetch(std::deque<Message>::iterator insert_pos, uint64_t from_time, uint64_t min_time) {
|
|
const auto &events = can->events(msg_id_);
|
|
auto first = std::upper_bound(events.rbegin(), events.rend(), from_time, [](uint64_t ts, auto e) { return ts > e->mono_time; });
|
|
|
|
std::vector<Message> msgs;
|
|
std::vector<double> values(sigs_.size());
|
|
msgs.reserve(BATCH_SIZE);
|
|
for (; first != events.rend() && (*first)->mono_time > min_time; ++first) {
|
|
const CanEvent *e = *first;
|
|
for (int i = 0; i < sigs_.size(); ++i) {
|
|
sigs_[i]->getValue(e->dat, e->size, &values[i]);
|
|
}
|
|
if (!filter_cmp_ || filter_cmp_(values[filter_sig_idx_], filter_value_)) {
|
|
msgs.emplace_back(Message{e->mono_time, values, {e->dat, e->dat + e->size}});
|
|
if (msgs.size() >= BATCH_SIZE && min_time == 0) break;
|
|
}
|
|
}
|
|
if (msgs.empty()) return;
|
|
|
|
if (hexMode() && (min_time > 0 || messages_.empty())) {
|
|
const auto freq = can->lastMessage(msg_id_).freq;
|
|
const std::vector<uint8_t> no_mask;
|
|
for (auto &m : msgs) {
|
|
hex_colors_.compute(msg_id_, m.data.data(), m.data.size(), m.mono_time / (double)1e9, can->getSpeed(), no_mask, freq);
|
|
m.colors = hex_colors_.colors;
|
|
}
|
|
}
|
|
const int pos = std::distance(messages_.begin(), insert_pos);
|
|
messages_.insert(insert_pos, std::move_iterator(msgs.begin()), std::move_iterator(msgs.end()));
|
|
export_btn_enabled_ = true;
|
|
|
|
if (selected_row_ >= pos) selected_row_ += msgs.size();
|
|
}
|
|
|
|
void LogsWidget::filterChanged() {
|
|
if (value_edit_.empty() && !value_edit_modified_) return;
|
|
|
|
std::function<bool(double, double)> cmp = nullptr;
|
|
switch (comp_box_) {
|
|
case 0: cmp = std::greater<double>{}; break;
|
|
case 1: cmp = std::equal_to<double>{}; break;
|
|
case 2: cmp = [](double l, double r) { return l != r; }; break;
|
|
case 3: cmp = std::less<double>{}; break;
|
|
}
|
|
setFilter(signals_cb_, value_edit_, cmp);
|
|
}
|
|
|
|
void LogsWidget::exportToCSV() {
|
|
std::string dir = settings.last_dir + "/" + can->routeName() + "_" + msgName(msg_id_) + ".csv";
|
|
FileDialog::getSaveFileName("Export " + msgName(msg_id_) + " to CSV file", dir, ".csv", [this](const std::string &fn) {
|
|
if (!fn.empty()) {
|
|
hexMode() ? utils::exportToCSV(fn, msg_id_) : utils::exportSignalsToCSV(fn, msg_id_);
|
|
}
|
|
});
|
|
}
|
|
|
|
void LogsWidget::draw() {
|
|
const ImGuiStyle &style = ImGui::GetStyle();
|
|
|
|
|
|
const float export_w = ImGui::CalcTextSize(icon::FILETYPE_CSV).x + style.FramePadding.x * 2;
|
|
if (!sigs_.empty()) {
|
|
const float clear_w = value_edit_.empty() ? 0.0f : ImGui::CalcTextSize(icon::X).x + style.FramePadding.x * 2;
|
|
const float fixed = DISPLAY_TYPE_WIDTH + SIGNALS_WIDTH + COMPARE_WIDTH + clear_w + style.ItemSpacing.x * 4 + export_w;
|
|
const float value_w = std::clamp(ImGui::GetContentRegionAvail().x - fixed, 30.0f, 120.0f);
|
|
|
|
ImGui::SetNextItemWidth(DISPLAY_TYPE_WIDTH);
|
|
if (ImGui::Combo("##display_type", &display_type_cb_, "Signal\0Hex\0")) {
|
|
hex_mode_ = display_type_cb_;
|
|
reset();
|
|
}
|
|
ImGui::SetItemTooltip("Display signal value or raw hex value");
|
|
ImGui::SameLine();
|
|
std::string sig_items;
|
|
for (auto s : sigs_) {
|
|
sig_items += s->name;
|
|
sig_items += '\0';
|
|
}
|
|
sig_items += '\0';
|
|
ImGui::SetNextItemWidth(SIGNALS_WIDTH);
|
|
if (ImGui::Combo("##signals", &signals_cb_, sig_items.c_str())) filterChanged();
|
|
ImGui::SameLine();
|
|
ImGui::SetNextItemWidth(COMPARE_WIDTH);
|
|
if (ImGui::Combo("##comp", &comp_box_, ">\0=\0!=\0<\0")) filterChanged();
|
|
ImGui::SameLine();
|
|
ImGui::SetNextItemWidth(value_w);
|
|
if (clearableInput("##value", &value_edit_, "", doubleValidator)) {
|
|
value_edit_modified_ = true;
|
|
filterChanged();
|
|
}
|
|
}
|
|
alignRight(export_w);
|
|
ImGui::BeginDisabled(!export_btn_enabled_);
|
|
if (ImGui::Button(icon::FILETYPE_CSV)) exportToCSV();
|
|
ImGui::EndDisabled();
|
|
disabledItemTooltip("Export to CSV file...");
|
|
|
|
ImGui::Separator();
|
|
drawTable();
|
|
}
|
|
|
|
std::string LogsWidget::headerText(int column) const {
|
|
if (column == 0) return "Time";
|
|
if (hexMode()) return "Data";
|
|
std::string text = sigs_[column - 1]->name;
|
|
if (!sigs_[column - 1]->unit.empty()) text += " (" + sigs_[column - 1]->unit + ")";
|
|
std::replace(text.begin(), text.end(), '_', ' ');
|
|
return text;
|
|
}
|
|
|
|
ImVec2 LogsWidget::headerSize(int column, float viewport_width) const {
|
|
const ImVec2 time_text_size = ImGui::CalcTextSize("000000.000");
|
|
const ImVec2 time_col_size(time_text_size.x + 10, time_text_size.y + 6);
|
|
if (column == 0) return time_col_size;
|
|
const int default_size = std::max(100, (int)((viewport_width - time_col_size.x) / (columnCount() - 1)));
|
|
const ImVec2 rect = ImGui::CalcTextSize(headerText(column).c_str(), nullptr, false, default_size);
|
|
return ImVec2{std::max(rect.x + 10, (float)default_size), rect.y + 6};
|
|
}
|
|
|
|
void LogsWidget::drawHeaderCell(ImDrawList *dl, const ImRect &rect, int column) const {
|
|
if (column > 0 && !hexMode()) {
|
|
CabanaColor bg = sigs_[column - 1]->color;
|
|
bg.a = 128;
|
|
dl->AddRectFilled(rect.Min, rect.Max, toImU32(bg));
|
|
}
|
|
const std::string text = headerText(column);
|
|
const ImU32 color = isDarkTheme() ? toImU32(DarkTheme::bright_text) : ImGui::GetColorU32(ImGuiCol_Text);
|
|
|
|
const ImRect r(rect.Min.x + 5, rect.Min.y + 3, rect.Max.x - 5, rect.Max.y - 3);
|
|
ImFont *font = ImGui::GetFont();
|
|
const float font_size = ImGui::GetFontSize();
|
|
const float wrap_width = std::max(1.0f, r.GetWidth());
|
|
const char *s = text.c_str();
|
|
const char *end = s + text.size();
|
|
float y = r.Min.y;
|
|
dl->PushClipRect(rect.Min, rect.Max, true);
|
|
while (s < end) {
|
|
const char *line_end = font->CalcWordWrapPosition(font_size, s, end, wrap_width);
|
|
if (line_end == s) line_end = s + 1;
|
|
const float w = ImGui::CalcTextSize(s, line_end).x;
|
|
dl->AddText(font, font_size, ImVec2(r.Max.x - w, y), color, s, line_end);
|
|
y += ImGui::GetTextLineHeight();
|
|
s = line_end;
|
|
while (s < end && ImCharIsBlankA(*s)) s++;
|
|
if (s < end && *s == '\n') s++;
|
|
}
|
|
dl->PopClipRect();
|
|
}
|
|
|
|
void LogsWidget::drawTable() {
|
|
const ImGuiStyle &style = ImGui::GetStyle();
|
|
const int cols = columnCount();
|
|
|
|
|
|
const float header_width = ImGui::GetContentRegionAvail().x - style.CellPadding.x * 2 * cols -
|
|
(vscrollbar_visible_ ? style.ScrollbarSize : 0.0f);
|
|
|
|
std::vector<ImVec2> sizes(cols);
|
|
float header_height = 0;
|
|
for (int i = 0; i < cols; ++i) {
|
|
sizes[i] = headerSize(i, header_width);
|
|
header_height = std::max(header_height, sizes[i].y);
|
|
}
|
|
if (hexMode() && !messages_.empty()) {
|
|
sizes[1].x = std::max(sizes[1].x, bytesCellSize(messages_.front().data.size(), false).x);
|
|
}
|
|
const float row_height = bytesCellSize(8, false).y;
|
|
|
|
|
|
|
|
ImGuiTableFlags flags = ImGuiTableFlags_ScrollY | ImGuiTableFlags_ScrollX | ImGuiTableFlags_BordersInner |
|
|
ImGuiTableFlags_SizingFixedFit;
|
|
|
|
if (messages_.empty()) flags &= ~ImGuiTableFlags_BordersInnerV;
|
|
|
|
float inner_width = 0;
|
|
for (int i = 0; i < cols; ++i) inner_width += sizes[i].x + style.CellPadding.x * 2;
|
|
|
|
bool fetch_more = false;
|
|
if (ImGui::BeginTable("logs", cols, flags, ImVec2(0, 0), inner_width)) {
|
|
ImGui::TableSetupScrollFreeze(0, 1);
|
|
for (int i = 0; i < cols; ++i) {
|
|
ImGui::TableSetupColumn(headerText(i).c_str(), ImGuiTableColumnFlags_WidthFixed, sizes[i].x);
|
|
}
|
|
ImGuiTable *table = ImGui::GetCurrentTable();
|
|
ImDrawList *painter = ImGui::GetWindowDrawList();
|
|
|
|
ImGui::TableNextRow(ImGuiTableRowFlags_Headers, header_height);
|
|
for (int i = 0; i < cols; ++i) {
|
|
if (!ImGui::TableSetColumnIndex(i)) continue;
|
|
drawHeaderCell(painter, ImGui::TableGetCellBgRect(table, i), i);
|
|
ImGui::Dummy(ImVec2(0, header_height - style.CellPadding.y * 2));
|
|
}
|
|
|
|
ImGuiListClipper clipper;
|
|
clipper.Begin(messages_.size(), row_height);
|
|
while (clipper.Step()) {
|
|
for (int row = clipper.DisplayStart; row < clipper.DisplayEnd; ++row) {
|
|
const auto &m = messages_[row];
|
|
ImGui::TableNextRow(0, row_height);
|
|
|
|
ImGui::PushID((void *)(uintptr_t)m.mono_time);
|
|
for (int col = 0; col < cols; ++col) {
|
|
if (!ImGui::TableSetColumnIndex(col)) continue;
|
|
|
|
const bool cell_selected = selected_row_ == row && selected_col_ == col;
|
|
ImGui::PushID(col);
|
|
if (viewSelectable("##cell", cell_selected, ImGuiSelectableFlags_AllowOverlap, ImVec2(0, row_height - style.CellPadding.y * 2))) {
|
|
selected_row_ = row;
|
|
selected_col_ = col;
|
|
}
|
|
ImGui::PopID();
|
|
const ImRect rect = ImGui::TableGetCellBgRect(table, col);
|
|
if (col == 0) {
|
|
drawTextCell(painter, rect, formatTime(m.mono_time), cell_selected, false);
|
|
} else if (hexMode()) {
|
|
drawBytesCell(painter, rect, m.data, &m.colors, cell_selected, false, false);
|
|
} else {
|
|
drawTextCell(painter, rect, sigs_[col - 1]->formatValue(m.sig_values[col - 1], false), cell_selected, false);
|
|
}
|
|
}
|
|
ImGui::PopID();
|
|
}
|
|
|
|
if (clipper.DisplayEnd >= (int)messages_.size()) fetch_more = true;
|
|
}
|
|
if (ImGui::GetScrollY() >= ImGui::GetScrollMaxY()) fetch_more = true;
|
|
vscrollbar_visible_ = table->InnerWindow->ScrollbarY;
|
|
ImGui::EndTable();
|
|
}
|
|
if (fetch_more && canFetchMore()) fetch(messages_.end(), messages_.back().mono_time, 0);
|
|
}
|