IQ.Pilot Release Commit @ 0b96bd5

This commit is contained in:
IQ.Lvbs CI [bot]
2026-09-02 16:46:45 -05:00
parent 4fb3da761f
commit 7745f48100
162 changed files with 12835 additions and 9476 deletions

View File

@@ -0,0 +1,222 @@
#include "tools/cabana/ui/dialogs/filedialog.h"
#include <algorithm>
#include <cctype>
#include <system_error>
#include "imgui.h"
#include "imgui_internal.h"
#include "tools/cabana/ui/dialogs/messagebox.h"
#include "tools/cabana/ui/icons.h"
#include "tools/cabana/ui/util.h"
namespace fs = std::filesystem;
namespace FileDialog {
namespace {
bool naturalLess(const std::string &a, const std::string &b) {
auto skip = [](const std::string &s, size_t &i) {
while (i < s.size() && !isalnum(static_cast<unsigned char>(s[i]))) ++i;
};
size_t i = 0, j = 0;
for (;;) {
skip(a, i);
skip(b, j);
if (i >= a.size() || j >= b.size()) break;
if (isdigit(static_cast<unsigned char>(a[i])) && isdigit(static_cast<unsigned char>(b[j]))) {
size_t ie = i, je = j;
while (ie < a.size() && isdigit(static_cast<unsigned char>(a[ie]))) ++ie;
while (je < b.size() && isdigit(static_cast<unsigned char>(b[je]))) ++je;
const unsigned long long na = std::stoull(a.substr(i, ie - i)), nb = std::stoull(b.substr(j, je - j));
if (na != nb) return na < nb;
i = ie;
j = je;
} else {
const int ca = tolower(static_cast<unsigned char>(a[i])), cb = tolower(static_cast<unsigned char>(b[j]));
if (ca != cb) return ca < cb;
++i;
++j;
}
}
const bool a_done = i >= a.size(), b_done = j >= b.size();
if (a_done != b_done) return a_done;
return a < b;
}
enum class Mode { OpenFile, SaveFile, Directory };
struct State {
bool active = false;
Mode mode = Mode::OpenFile;
std::string title;
std::string extension;
fs::path dir;
std::string dir_input;
std::string filename;
std::vector<fs::directory_entry> entries;
Callback callback;
};
State g_state;
PopupOwner g_owner;
void listDir() {
State &s = g_state;
s.entries.clear();
std::error_code ec;
for (const auto &entry : fs::directory_iterator(s.dir, ec)) {
const std::string name = entry.path().filename().string();
if (name.empty() || name[0] == '.') continue;
const bool is_dir = entry.is_directory(ec);
if (!is_dir && s.mode == Mode::Directory) continue;
if (!is_dir && !s.extension.empty() && entry.path().extension() != s.extension) continue;
s.entries.push_back(entry);
}
std::sort(s.entries.begin(), s.entries.end(), [](const auto &a, const auto &b) {
std::error_code sort_ec;
const bool da = a.is_directory(sort_ec), db = b.is_directory(sort_ec);
return da != db ? da : naturalLess(a.path().filename().string(), b.path().filename().string());
});
s.dir_input = s.dir.string();
}
void setDir(const fs::path &dir) {
std::error_code ec;
fs::path d = fs::is_directory(dir, ec) ? fs::absolute(dir, ec) : fs::current_path(ec);
g_state.dir = d.lexically_normal();
listDir();
}
void start(Mode mode, const std::string &title, const fs::path &dir, const std::string &filename,
const std::string &extension, Callback cb) {
State &s = g_state;
s = State{};
s.active = true;
s.mode = mode;
s.title = title;
s.extension = extension;
s.filename = filename;
s.callback = std::move(cb);
g_owner.reset();
setDir(dir);
}
void finish(const std::string &path) {
Callback cb = std::move(g_state.callback);
g_state = State{};
g_owner.reset();
if (cb) cb(path);
}
void accept(const fs::path &path) {
if (g_state.mode == Mode::SaveFile) {
std::error_code ec;
if (fs::exists(path, ec)) {
const std::string name = path.filename().string();
MessageBox::question(g_state.title, name + " already exists.\nDo you want to replace it?", [path](bool ok) {
if (ok) finish(path.string());
});
return;
}
}
finish(path.string());
}
}
void getOpenFileName(const std::string &title, const std::string &dir, const std::string &extension, Callback cb) {
start(Mode::OpenFile, title, dir, "", extension, std::move(cb));
}
void getSaveFileName(const std::string &title, const std::string &default_path, const std::string &extension, Callback cb) {
const fs::path p(default_path);
start(Mode::SaveFile, title, p.parent_path(), p.filename().string(), extension, std::move(cb));
}
void getExistingDirectory(const std::string &title, const std::string &dir, Callback cb) {
start(Mode::Directory, title, dir, "", "", std::move(cb));
}
void draw() {
State &s = g_state;
if (!s.active) return;
const std::string popup_id = s.title + "###FileDialog";
if (!beginDialog(popup_id.c_str(), &g_owner, ImVec2(640.0f, 480.0f), 0)) return;
if (ImGui::Button("Up")) setDir(s.dir.parent_path());
ImGui::SameLine();
ImGui::SetNextItemWidth(-1.0f);
if (inputText("##dir", &s.dir_input, "", ImGuiInputTextFlags_EnterReturnsTrue)) setDir(s.dir_input);
const float footer = ImGui::GetFrameHeightWithSpacing() * (s.mode == Mode::Directory ? 1.0f : 2.0f) + ImGui::GetStyle().ItemSpacing.y;
bool ok = false, cancel = false;
fs::path result, pending_dir;
ImGui::BeginChild("entries", ImVec2(0, -footer), ImGuiChildFlags_Borders);
std::error_code dir_ec;
for (size_t i = 0; i < s.entries.size(); ++i) {
const auto &entry = s.entries[i];
const bool is_dir = entry.is_directory(dir_ec);
const std::string name = entry.path().filename().string();
const std::string label = (is_dir ? std::string(icon::FOLDER) : std::string(icon::FILE_EARMARK)) + " " + name;
ImGui::PushID(static_cast<int>(i));
const bool selected = !is_dir && name == s.filename;
if (ImGui::Selectable(label.c_str(), selected, ImGuiSelectableFlags_AllowDoubleClick)) {
const bool double_clicked = ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left);
if (is_dir) {
if (double_clicked) {
pending_dir = entry.path();
} else if (s.mode == Mode::Directory) {
s.filename = name;
}
} else {
s.filename = name;
if (double_clicked && s.mode == Mode::OpenFile) {
result = entry.path();
ok = true;
}
}
}
ImGui::PopID();
if (ok || !pending_dir.empty()) break;
}
ImGui::EndChild();
if (!pending_dir.empty()) setDir(pending_dir);
if (s.mode != Mode::Directory) {
ImGui::SetNextItemWidth(-90.0f);
if (inputText("##name", &s.filename, "File name", ImGuiInputTextFlags_EnterReturnsTrue)) ok = true;
ImGui::SameLine();
ImGui::TextDisabled("%s", s.extension.empty() ? "*" : ("*" + s.extension).c_str());
}
const char *accept_label = s.mode == Mode::SaveFile ? "Save" : (s.mode == Mode::Directory ? "Choose" : "Open");
dialogButtons(accept_label, &ok, &cancel);
if (ok && result.empty()) {
if (s.mode == Mode::Directory) {
result = s.filename.empty() ? s.dir : s.dir / s.filename;
} else if (!s.filename.empty()) {
result = fs::path(s.filename).is_absolute() ? fs::path(s.filename) : s.dir / s.filename;
if (s.mode == Mode::SaveFile && !s.extension.empty() && result.extension().empty()) result += s.extension;
if (s.mode == Mode::OpenFile && !fs::is_regular_file(result, dir_ec)) ok = false;
} else {
ok = false;
}
}
if (ok || cancel) ImGui::CloseCurrentPopup();
MessageBox::draw();
if (!s.active) ImGui::CloseCurrentPopup();
ImGui::EndPopup();
if (cancel) {
finish("");
} else if (ok) {
accept(result);
}
}
}

View File

@@ -0,0 +1,19 @@
#pragma once
#include <filesystem>
#include <functional>
#include <string>
#include <vector>
namespace FileDialog {
using Callback = std::function<void(const std::string &path)>;
void getOpenFileName(const std::string &title, const std::string &dir, const std::string &extension, Callback cb);
void getSaveFileName(const std::string &title, const std::string &default_path, const std::string &extension, Callback cb);
void getExistingDirectory(const std::string &title, const std::string &dir, Callback cb);
void draw();
}

View File

@@ -0,0 +1,87 @@
#include "tools/cabana/ui/dialogs/messagebox.h"
#include <deque>
#include "imgui.h"
#include "imgui_internal.h"
#include "tools/cabana/ui/util.h"
namespace MessageBox {
namespace {
struct Box {
std::string title;
std::string text;
std::string detailed_text;
bool has_cancel = false;
std::function<void(bool)> on_result;
};
std::deque<Box> g_queue;
bool g_show_details = false;
PopupOwner g_owner;
void push(Box box) { g_queue.push_back(std::move(box)); }
std::function<void(bool)> wrap(std::function<void()> on_close) {
if (!on_close) return nullptr;
return [on_close = std::move(on_close)](bool) { on_close(); };
}
}
void information(const std::string &title, const std::string &text, std::function<void()> on_close) {
push({.title = title, .text = text, .on_result = wrap(std::move(on_close))});
}
void warning(const std::string &title, const std::string &text, const std::string &detailed_text,
std::function<void()> on_close) {
push({.title = title, .text = text, .detailed_text = detailed_text, .on_result = wrap(std::move(on_close))});
}
void question(const std::string &title, const std::string &text, std::function<void(bool)> on_result) {
push({.title = title, .text = text, .has_cancel = true, .on_result = std::move(on_result)});
}
void draw() {
if (g_queue.empty()) return;
Box &box = g_queue.front();
const std::string popup_id = box.title + "###MessageBox";
const bool first = g_owner.popup_id == 0;
if (!g_owner.begin(popup_id.c_str())) return;
const ImGuiStyle &style = ImGui::GetStyle();
const float min_width = ImGui::CalcTextSize(box.title.c_str()).x + style.FramePadding.x * 2 + style.WindowPadding.x * 2;
ImGui::SetNextWindowSizeConstraints(ImVec2(min_width, 0.0f), ImVec2(FLT_MAX, FLT_MAX));
setNextDialogWindow(ImVec2(0.0f, 0.0f));
if (!ImGui::BeginPopupModal(popup_id.c_str(), nullptr, ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoSavedSettings)) return;
if (first) g_show_details = false;
bool result = false, done = false;
ImGui::PushTextWrapPos(ImGui::GetCursorPos().x + 480.0f);
ImGui::TextUnformatted(box.text.c_str());
ImGui::PopTextWrapPos();
if (g_show_details) {
ImGui::InputTextMultiline("##details", box.detailed_text.data(), box.detailed_text.size() + 1,
ImVec2(480.0f, 160.0f), ImGuiInputTextFlags_ReadOnly);
}
ImGui::Separator();
if (!box.detailed_text.empty()) {
if (ImGui::Button(g_show_details ? "Hide Details..." : "Show Details...")) g_show_details = !g_show_details;
ImGui::SameLine();
}
dialogButtons("OK", &result, &done, true, box.has_cancel ? "Cancel" : nullptr);
if (ImGui::IsKeyPressed(ImGuiKey_Enter, false) || ImGui::IsKeyPressed(ImGuiKey_KeypadEnter, false)) result = true;
if (result) done = true;
if (done) ImGui::CloseCurrentPopup();
ImGui::EndPopup();
if (done) {
g_owner.reset();
Box finished = std::move(g_queue.front());
g_queue.pop_front();
if (finished.on_result) finished.on_result(result);
}
}
}

View File

@@ -0,0 +1,19 @@
#pragma once
#include <functional>
#include <string>
namespace MessageBox {
void information(const std::string &title, const std::string &text, std::function<void()> on_close = nullptr);
void warning(const std::string &title, const std::string &text, const std::string &detailed_text = "",
std::function<void()> on_close = nullptr);
void question(const std::string &title, const std::string &text, std::function<void(bool ok)> on_result);
void draw();
}

View File

@@ -0,0 +1,124 @@
#include "tools/cabana/ui/dialogs/routesdialog.h"
#include <utility>
#include "imgui.h"
#include "imgui_internal.h"
#include "tools/cabana/ui/dialogs/messagebox.h"
#include "tools/cabana/ui/util.h"
#include "tools/cabana/utils/util.h"
namespace {
const char *PERIOD_NAMES[] = {"Last week", "Last 2 weeks", "Last month", "Last 6 months", "Preserved"};
const int PERIOD_DAYS[] = {7, 14, 30, 180, -1};
}
void RoutesDialog::open(std::function<void(bool, const std::string &)> on_done) {
on_done_ = std::move(on_done);
open_ = true;
popup_.reset();
s_ = State{};
alive_ = std::make_shared<bool>(true);
routes::fetchDevices([this, alive = std::weak_ptr<bool>(alive_)](std::vector<routes::DeviceInfo> devices, bool success, int error_code) {
utils::runOnMainThread(utils::guarded(alive.lock(), [this, devices = std::move(devices), success, error_code]() {
setDeviceList(devices, success, error_code);
}));
});
}
void RoutesDialog::setDeviceList(const std::vector<routes::DeviceInfo> &devices, bool success, int error_code) {
if (success) {
s_.devices.clear();
for (const auto &device : devices) s_.devices.push_back(device.dongle_id);
s_.devices_loaded = true;
s_.device_index = 0;
fetchRoutes();
} else {
MessageBox::warning("Error", error_code == 401 ? "Unauthorized. Authenticate with iqpilot/tools/lib/auth.py" : "Network error", "",
utils::guarded(alive_, [this]() { finish(false); }));
}
}
void RoutesDialog::fetchRoutes() {
if (!s_.devices_loaded || s_.devices.empty()) return;
s_.routes.clear();
s_.route_index = -1;
s_.empty_text = "Loading...";
const int request_id = ++s_.fetch_id;
auto on_routes = [this, alive = std::weak_ptr<bool>(alive_), request_id](std::vector<routes::RouteInfo> list, bool success, int) {
utils::runOnMainThread(utils::guarded(alive.lock(), [this, list = std::move(list), success, request_id]() {
if (s_.fetch_id == request_id) setRouteList(list, success);
}));
};
routes::fetchRoutes(s_.devices[s_.device_index], PERIOD_DAYS[s_.period_index], std::move(on_routes));
}
void RoutesDialog::setRouteList(const std::vector<routes::RouteInfo> &list, bool success) {
if (success) {
for (const auto &route : list) {
const int mins = static_cast<int>((route.end_ms - route.start_ms) / 60000);
s_.routes.push_back({routes::formatUnixMs(route.start_ms) + " " + std::to_string(mins) + "min", route.name});
}
if (!s_.routes.empty()) s_.route_index = 0;
} else {
MessageBox::warning("Error", "Failed to fetch routes. Check your network connection.", "",
utils::guarded(alive_, [this]() { finish(false); }));
}
s_.empty_text = "No items";
}
void RoutesDialog::finish(bool accepted) {
alive_.reset();
open_ = false;
auto on_done = std::move(on_done_);
if (on_done) on_done(accepted, accepted && s_.route_index >= 0 ? s_.routes[s_.route_index].name : "");
}
void RoutesDialog::draw() {
if (!open_) return;
if (!beginDialog("Remote routes", &popup_, ImVec2(480.0f, 420.0f))) return;
ImGui::AlignTextToFramePadding();
ImGui::TextUnformatted("Device");
ImGui::SameLine();
ImGui::SetNextItemWidth(-1.0f);
if (s_.devices_loaded) {
if (comboBox("##device", &s_.device_index, s_.devices)) fetchRoutes();
} else {
int idx = 0;
ImGui::BeginDisabled();
comboBox("##device", &idx, {"Loading..."});
ImGui::EndDisabled();
}
ImGui::SetNextItemWidth(-1.0f);
if (ImGui::Combo("##period", &s_.period_index, PERIOD_NAMES, IM_ARRAYSIZE(PERIOD_NAMES))) fetchRoutes();
bool accepted = false, rejected = false;
const float footer = ImGui::GetFrameHeightWithSpacing() + ImGui::GetStyle().ItemSpacing.y;
ImGui::BeginChild("routes", ImVec2(0, -footer), ImGuiChildFlags_Borders);
if (s_.routes.empty()) {
const ImVec2 size = ImGui::CalcTextSize(s_.empty_text.c_str());
const ImVec2 avail = ImGui::GetContentRegionAvail();
ImGui::SetCursorPos(ImVec2((avail.x - size.x) * 0.5f, (avail.y - size.y) * 0.5f));
ImGui::TextUnformatted(s_.empty_text.c_str());
}
for (int i = 0; i < static_cast<int>(s_.routes.size()); ++i) {
ImGui::PushID(i);
if (ImGui::Selectable(s_.routes[i].label.c_str(), s_.route_index == i, ImGuiSelectableFlags_AllowDoubleClick)) {
s_.route_index = i;
if (ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) accepted = true;
}
ImGui::PopID();
}
ImGui::EndChild();
dialogButtons("OK", &accepted, &rejected);
MessageBox::draw();
if (accepted || rejected || !open_) ImGui::CloseCurrentPopup();
ImGui::EndPopup();
if (accepted || rejected) finish(accepted);
}

View File

@@ -0,0 +1,45 @@
#pragma once
#include <functional>
#include <memory>
#include <string>
#include <vector>
#include "tools/cabana/routes.h"
#include "tools/cabana/ui/util.h"
class RoutesDialog {
public:
void open(std::function<void(bool accepted, const std::string &route)> on_done);
void draw();
private:
void setDeviceList(const std::vector<routes::DeviceInfo> &devices, bool success, int error_code);
void setRouteList(const std::vector<routes::RouteInfo> &list, bool success);
void fetchRoutes();
void finish(bool accepted);
struct RouteItem {
std::string label;
std::string name;
};
struct State {
bool devices_loaded = false;
std::vector<std::string> devices;
int device_index = 0;
int period_index = 0;
std::vector<RouteItem> routes;
int route_index = -1;
std::string empty_text = "No items";
int fetch_id = 0;
};
bool open_ = false;
PopupOwner popup_;
State s_;
std::function<void(bool, const std::string &)> on_done_;
std::shared_ptr<bool> alive_;
};

View File

@@ -0,0 +1,108 @@
#include "tools/cabana/ui/dialogs/settingsdialog.h"
#include <algorithm>
#include <utility>
#include "imgui.h"
#include "imgui_internal.h"
#include "tools/cabana/settings.h"
#include "tools/cabana/ui/dialogs/filedialog.h"
#include "tools/cabana/ui/util.h"
#include "tools/cabana/utils/util.h"
namespace {
const int MIN_CACHE_MINUTES = 30;
const int MAX_CACHE_MINUTES = 120;
enum FormLabel { THEME, CACHED_MINUTES, DRAG_DIRECTION, CHART_HEIGHT, FORM_LABEL_COUNT };
const char *FORM_LABELS[FORM_LABEL_COUNT] = {"Color Theme", "Max Cached Minutes", "Drag Direction", "Chart Height"};
float formLabelWidth() {
float w = 0.0f;
for (const char *label : FORM_LABELS) w = std::max(w, ImGui::CalcTextSize(label).x);
return w + ImGui::GetStyle().ItemSpacing.x * 2;
}
void formRow(FormLabel label, float label_width) {
ImGui::AlignTextToFramePadding();
ImGui::TextUnformatted(FORM_LABELS[label]);
ImGui::SameLine(label_width);
ImGui::SetNextItemWidth(-FLT_MIN);
}
}
void SettingsDialog::open() {
theme_ = settings.theme;
cached_minutes_ = settings.max_cached_minutes;
drag_direction_ = settings.drag_direction;
chart_height_ = settings.chart_height;
log_livestream_ = settings.log_livestream;
log_path_ = settings.log_path;
open_ = true;
popup_.reset();
}
void SettingsDialog::draw() {
if (!open_) return;
if (!beginDialog("Settings", &popup_, ImVec2(400.0f, 0.0f))) return;
const float label_width = formLabelWidth();
ImGui::SeparatorText("General");
static const char *themes[] = {"Light", "Dark"};
formRow(THEME, label_width);
int theme_index = theme_ - LIGHT_THEME;
if (ImGui::Combo("##theme", &theme_index, themes, IM_ARRAYSIZE(themes))) theme_ = theme_index + LIGHT_THEME;
formRow(CACHED_MINUTES, label_width);
if (ImGui::InputInt("##cached_minutes", &cached_minutes_, 1, 10)) {
cached_minutes_ = std::clamp(cached_minutes_, MIN_CACHE_MINUTES, MAX_CACHE_MINUTES);
}
ImGui::SeparatorText("New Signal Settings");
static const char *directions[] = {"MSB First", "LSB First", "Always Little Endian", "Always Big Endian"};
formRow(DRAG_DIRECTION, label_width);
ImGui::Combo("##drag_direction", &drag_direction_, directions, IM_ARRAYSIZE(directions));
ImGui::SeparatorText("Chart");
formRow(CHART_HEIGHT, label_width);
if (ImGui::InputInt("##chart_height", &chart_height_, 10, 10)) chart_height_ = std::clamp(chart_height_, 100, 500);
checkBox("Enable live stream logging", &log_livestream_);
ImGui::BeginDisabled(!log_livestream_);
ImGui::SetNextItemWidth(-90.0f);
inputText("##log_path", &log_path_, "", ImGuiInputTextFlags_ReadOnly);
ImGui::SameLine();
if (ImGui::Button("Browse...")) {
FileDialog::getExistingDirectory("Log File Location", utils::homePath(), [this](const std::string &fn) {
if (!fn.empty()) log_path_ = fn;
});
}
ImGui::EndDisabled();
ImGui::Separator();
bool accepted = false, done = false;
dialogButtons("OK", &accepted, &done);
if (accepted) {
save();
done = true;
}
FileDialog::draw();
if (done) {
open_ = false;
ImGui::CloseCurrentPopup();
}
ImGui::EndPopup();
}
void SettingsDialog::save() {
if (std::exchange(settings.theme, theme_) != settings.theme) applyTheme(settings.theme);
settings.max_cached_minutes = cached_minutes_;
settings.chart_height = chart_height_;
settings.log_livestream = log_livestream_;
settings.log_path = log_path_;
settings.drag_direction = (Settings::DragDirection)drag_direction_;
settings.changed();
}

View File

@@ -0,0 +1,24 @@
#pragma once
#include <string>
#include "tools/cabana/core/settings.h"
#include "tools/cabana/ui/util.h"
class SettingsDialog {
public:
void open();
void draw();
private:
void save();
bool open_ = false;
PopupOwner popup_;
int theme_ = 0;
int cached_minutes_ = 0;
int drag_direction_ = 0;
int chart_height_ = 0;
bool log_livestream_ = false;
std::string log_path_;
};

View File

@@ -0,0 +1,323 @@
#include "tools/cabana/ui/dialogs/streamselector.h"
#include <algorithm>
#include <filesystem>
#include <fstream>
#include "imgui.h"
#include "imgui_internal.h"
#include "tools/cabana/settings.h"
#include "tools/cabana/streams/devicestream.h"
#include "tools/cabana/streams/replaystream.h"
#include "tools/cabana/ui/dialogs/filedialog.h"
#include "tools/cabana/ui/dialogs/messagebox.h"
#include "tools/cabana/ui/util.h"
#include "tools/cabana/utils/util.h"
void OpenReplayWidget::draw() {
ImGui::AlignTextToFramePadding();
ImGui::TextUnformatted("Route");
ImGui::SameLine();
ImGui::SetNextItemWidth(-250.0f);
inputText("##route", &route_, "Enter route name or browse for local/remote route");
ImGui::SameLine();
if (ImGui::Button("Remote route...")) {
routes_dialog_.open(utils::guarded(alive_, [this](bool accepted, const std::string &route) {
if (accepted) route_ = route;
}));
}
ImGui::SameLine();
if (ImGui::Button("Local route...")) {
FileDialog::getExistingDirectory("Open Local Route", settings.last_route_dir, utils::guarded(alive_, [this](const std::string &dir) {
if (!dir.empty()) {
route_ = dir;
settings.last_route_dir = std::filesystem::absolute(dir).parent_path().string();
}
}));
}
checkBox("Road camera", &cameras_[0]);
ImGui::SameLine();
checkBox("Driver camera", &cameras_[1]);
ImGui::SameLine();
checkBox("Wide road camera", &cameras_[2]);
}
void OpenReplayWidget::drawPopups() {
routes_dialog_.draw();
}
std::unique_ptr<AbstractStream> OpenReplayWidget::open() {
std::string route = route_;
std::string data_dir;
if (auto idx = route.rfind('/'); idx != std::string::npos && util::file_exists(route)) {
data_dir = route.substr(0, idx + 1);
route = route.substr(idx + 1);
}
bool is_valid_format = Route::parseRoute(route).str.size() > 0;
if (!is_valid_format) {
MessageBox::warning("Warning", "Invalid route format: '" + route + "'");
} else {
auto replay_stream = std::make_unique<ReplayStream>();
Connection err = replay_stream->error.connect([](const std::string &msg) {
MessageBox::warning("Error", msg);
});
uint32_t flags = REPLAY_FLAG_NONE;
if (cameras_[1]) flags |= REPLAY_FLAG_DCAM;
if (cameras_[2]) flags |= REPLAY_FLAG_ECAM;
if (flags == REPLAY_FLAG_NONE && !cameras_[0]) flags = REPLAY_FLAG_NO_VIPC;
if (replay_stream->loadRoute(route, data_dir, flags)) {
return replay_stream;
}
}
return nullptr;
}
namespace {
const uint32_t speeds[] = {10U, 20U, 50U, 100U, 125U, 250U, 500U, 1000U};
const uint32_t data_speeds[] = {10U, 20U, 50U, 100U, 125U, 250U, 500U, 1000U, 2000U, 5000U};
}
OpenPandaWidget::OpenPandaWidget() {
if (can && dynamic_cast<PandaStream *>(can) != nullptr) {
already_connected_ = true;
return;
}
refreshSerials();
buildConfigForm();
}
void OpenPandaWidget::refreshSerials() {
serials_ = Panda::list();
serial_index_ = 0;
}
void OpenPandaWidget::buildConfigForm() {
std::string serial = serial_index_ < static_cast<int>(serials_.size()) ? serials_[serial_index_] : "";
has_fd_ = false;
has_panda_ = !serial.empty();
if (has_panda_) {
try {
Panda panda(serial);
has_fd_ = (panda.hw_type == cereal::PandaState::PandaType::RED_PANDA) || (panda.hw_type == cereal::PandaState::PandaType::RED_PANDA_V2);
} catch (const std::exception &e) {
fprintf(stderr, "failed to open panda %s\n", serial.c_str());
has_panda_ = false;
}
}
if (has_panda_) {
config.serial = serial;
config.bus_config.resize(3);
can_speed_index_.assign(3, 0);
data_speed_index_.assign(3, 0);
for (int i = 0; i < 3; i++) {
for (int j = 0; j < static_cast<int>(std::size(speeds)); j++) {
if (speeds[j] == config.bus_config[i].can_speed_kbps) can_speed_index_[i] = j;
}
for (int j = 0; j < static_cast<int>(std::size(data_speeds)); j++) {
if (data_speeds[j] == config.bus_config[i].data_speed_kbps) data_speed_index_[i] = j;
}
}
} else {
config.serial = "";
}
}
void OpenPandaWidget::draw() {
if (already_connected_) {
ImGui::Text("Already connected to %s.", can->routeName().c_str());
ImGui::TextUnformatted("Close the current connection via [File menu -> Close Stream] before connecting to another Panda.");
return;
}
ImGui::AlignTextToFramePadding();
ImGui::TextUnformatted("Serial");
ImGui::SameLine();
ImGui::SetNextItemWidth(-100.0f);
if (comboBox("##serial", &serial_index_, serials_)) buildConfigForm();
ImGui::SameLine();
if (ImGui::Button("Refresh")) {
refreshSerials();
buildConfigForm();
}
if (!has_panda_) {
ImGui::TextUnformatted("No panda found");
return;
}
for (int i = 0; i < static_cast<int>(config.bus_config.size()); i++) {
ImGui::PushID(i);
ImGui::AlignTextToFramePadding();
ImGui::Text("Bus %d:", i);
ImGui::SameLine();
ImGui::TextUnformatted("CAN Speed (kbps):");
ImGui::SameLine();
ImGui::SetNextItemWidth(90.0f);
if (comboBox("##can_speed", &can_speed_index_[i], speeds, (int)std::size(speeds))) {
config.bus_config[i].can_speed_kbps = speeds[can_speed_index_[i]];
}
if (has_fd_) {
ImGui::SameLine();
checkBox("CAN-FD", &config.bus_config[i].can_fd);
ImGui::SameLine();
ImGui::TextUnformatted("Data Speed (kbps):");
ImGui::SameLine();
ImGui::BeginDisabled(!config.bus_config[i].can_fd);
ImGui::SetNextItemWidth(90.0f);
if (comboBox("##data_speed", &data_speed_index_[i], data_speeds, (int)std::size(data_speeds))) {
config.bus_config[i].data_speed_kbps = data_speeds[data_speed_index_[i]];
}
ImGui::EndDisabled();
}
ImGui::PopID();
}
}
std::unique_ptr<AbstractStream> OpenPandaWidget::open() {
try {
return std::make_unique<PandaStream>(config);
} catch (std::exception &e) {
MessageBox::warning("Warning", std::string("Failed to connect to panda: '") + e.what() + "'");
return nullptr;
}
}
void OpenDeviceWidget::draw() {
ImGui::RadioButton("MSGQ", &mode_, 0);
ImGui::RadioButton("ZMQ", &mode_, 1);
ImGui::RadioButton("Bridge", &mode_, 2);
const float label_width = ImGui::GetFrameHeight() + ImGui::GetStyle().ItemInnerSpacing.x +
std::max(ImGui::CalcTextSize("MSGQ").x, ImGui::CalcTextSize("ZMQ").x) +
ImGui::GetStyle().ItemInnerSpacing.x;
ImGui::SameLine(label_width);
ImGui::BeginDisabled(mode_ == 0);
ImGui::SetNextItemWidth(-1.0f);
validatedText("##ip", &ip_address_, validateIpAddress, "Enter device Ip Address", ipValidator);
ImGui::EndDisabled();
}
std::unique_ptr<AbstractStream> OpenDeviceWidget::open() {
std::string ip = ip_address_.empty() ? "127.0.0.1" : ip_address_;
const DeviceStream::Mode modes[] = {DeviceStream::Mode::Msgq, DeviceStream::Mode::Zmq, DeviceStream::Mode::Bridge};
return std::make_unique<DeviceStream>(modes[mode_], ip);
}
#ifdef __linux__
OpenSocketCanWidget::OpenSocketCanWidget() {
refreshDevices();
}
void OpenSocketCanWidget::refreshDevices() {
devices_.clear();
std::error_code ec;
for (const auto &entry : std::filesystem::directory_iterator("/sys/class/net", ec)) {
std::ifstream type_file(entry.path() / "type");
int type = 0;
if (type_file >> type && type == 280) {
devices_.push_back(entry.path().filename().string());
}
}
device_index_ = 0;
config.device = devices_.empty() ? "" : devices_[0];
}
void OpenSocketCanWidget::draw() {
ImGui::AlignTextToFramePadding();
ImGui::TextUnformatted("Device");
ImGui::SameLine();
ImGui::SetNextItemWidth(300.0f);
if (comboBox("##device", &device_index_, devices_)) config.device = devices_[device_index_];
ImGui::SameLine();
if (ImGui::Button("Refresh", ImVec2(100.0f, 0.0f))) refreshDevices();
}
std::unique_ptr<AbstractStream> OpenSocketCanWidget::open() {
try {
return std::make_unique<SocketCanStream>(config);
} catch (std::exception &e) {
MessageBox::warning("Warning", std::string("Failed to connect to SocketCAN device: '") + e.what() + "'");
return nullptr;
}
}
#endif
void StreamSelector::open(Callback on_done) {
on_done_ = std::move(on_done);
open_ = true;
popup_.reset();
first_frame_ = true;
dbc_file_.clear();
widgets_.clear();
widgets_.push_back(std::make_unique<OpenReplayWidget>());
widgets_.push_back(std::make_unique<OpenPandaWidget>());
#ifdef __linux__
if (SocketCanStream::available()) {
widgets_.push_back(std::make_unique<OpenSocketCanWidget>());
}
#endif
widgets_.push_back(std::make_unique<OpenDeviceWidget>());
}
void StreamSelector::draw() {
if (!open_) return;
if (!beginDialog("Open stream", &popup_, ImVec2(640.0f, 0.0f))) return;
AbstractOpenStreamWidget *current = nullptr;
if (ImGui::BeginTabBar("streams")) {
for (auto &w : widgets_) {
ImGuiTabItemFlags tab_flags = (first_frame_ && w == widgets_.front()) ? ImGuiTabItemFlags_SetSelected : 0;
if (ImGui::BeginTabItem(w->title(), nullptr, tab_flags)) {
current = w.get();
ImGui::BeginChild("tab", ImVec2(0, 130.0f));
w->draw();
ImGui::EndChild();
ImGui::EndTabItem();
}
}
ImGui::EndTabBar();
}
first_frame_ = false;
ImGui::AlignTextToFramePadding();
ImGui::TextUnformatted("dbc File");
ImGui::SameLine();
ImGui::SetNextItemWidth(-90.0f);
inputText("##dbc", &dbc_file_, "Choose a dbc file to open", ImGuiInputTextFlags_ReadOnly);
ImGui::SameLine();
if (ImGui::Button("Browse...")) {
FileDialog::getOpenFileName("Open File", settings.last_dir, ".dbc", [this](const std::string &fn) {
if (!fn.empty()) {
dbc_file_ = fn;
settings.last_dir = std::filesystem::absolute(fn).parent_path().string();
}
});
}
ImGui::Separator();
bool accepted = false, rejected = false;
std::unique_ptr<AbstractStream> stream;
bool open_clicked = false;
dialogButtons("Open", &open_clicked, &rejected, current != nullptr && current->openEnabled());
if (open_clicked) {
if (stream = current->open(); stream) accepted = true;
}
if (current) current->drawPopups();
FileDialog::draw();
MessageBox::draw();
if (accepted || rejected) ImGui::CloseCurrentPopup();
ImGui::EndPopup();
if (accepted || rejected) {
open_ = false;
widgets_.clear();
auto on_done = std::move(on_done_);
if (on_done) on_done(std::move(stream), dbc_file_);
}
}

View File

@@ -0,0 +1,106 @@
#pragma once
#include <functional>
#include <memory>
#include <string>
#include <vector>
#include "tools/cabana/streams/abstractstream.h"
#include "tools/cabana/streams/pandastream.h"
#ifdef __linux__
#include "tools/cabana/streams/socketcanstream.h"
#endif
#include "tools/cabana/ui/dialogs/routesdialog.h"
#include "tools/cabana/ui/util.h"
class AbstractOpenStreamWidget {
public:
virtual ~AbstractOpenStreamWidget() = default;
virtual const char *title() const = 0;
virtual void draw() = 0;
virtual void drawPopups() {}
virtual std::unique_ptr<AbstractStream> open() = 0;
virtual bool openEnabled() const { return true; }
};
class OpenReplayWidget : public AbstractOpenStreamWidget {
public:
const char *title() const override { return "Replay"; }
void draw() override;
void drawPopups() override;
std::unique_ptr<AbstractStream> open() override;
private:
std::string route_;
bool cameras_[3] = {true, false, false};
RoutesDialog routes_dialog_;
std::shared_ptr<bool> alive_ = std::make_shared<bool>(true);
};
class OpenPandaWidget : public AbstractOpenStreamWidget {
public:
OpenPandaWidget();
const char *title() const override { return "Panda"; }
void draw() override;
std::unique_ptr<AbstractStream> open() override;
bool openEnabled() const override { return !already_connected_; }
private:
void refreshSerials();
void buildConfigForm();
bool already_connected_ = false;
std::vector<std::string> serials_;
int serial_index_ = 0;
bool has_panda_ = false;
bool has_fd_ = false;
std::vector<int> can_speed_index_, data_speed_index_;
PandaStreamConfig config = {};
};
class OpenDeviceWidget : public AbstractOpenStreamWidget {
public:
const char *title() const override { return "Device"; }
void draw() override;
std::unique_ptr<AbstractStream> open() override;
private:
int mode_ = 1;
std::string ip_address_;
};
#ifdef __linux__
class OpenSocketCanWidget : public AbstractOpenStreamWidget {
public:
OpenSocketCanWidget();
const char *title() const override { return "SocketCAN"; }
void draw() override;
std::unique_ptr<AbstractStream> open() override;
private:
void refreshDevices();
std::vector<std::string> devices_;
int device_index_ = 0;
SocketCanStreamConfig config = {};
};
#endif
class StreamSelector {
public:
using Callback = std::function<void(std::unique_ptr<AbstractStream> stream, const std::string &dbc_file)>;
void open(Callback on_done);
void draw();
private:
bool open_ = false;
PopupOwner popup_;
bool first_frame_ = false;
std::string dbc_file_;
std::vector<std::unique_ptr<AbstractOpenStreamWidget>> widgets_;
Callback on_done_;
};