Files
IQ.Pilot/iqpilot/tools/cabana/ui/dialogs/streamselector.cc
2026-09-03 18:17:35 -05:00

370 lines
13 KiB
C++

#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::TextDisabled("Replay a local or Konn3kt route with optional camera streams.");
ImGui::Spacing();
ImGui::TextUnformatted("Route");
ImGui::SetNextItemWidth(-1.0f);
inputText("##route", &route_, "Enter route name or browse for local/remote route");
const float route_button_width = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f;
if (ImGui::Button("Browse Konn3kt routes", ImVec2(route_button_width, 34.0f))) {
routes_dialog_.open(utils::guarded(alive_, [this](bool accepted, const std::string &route) {
if (accepted) route_ = route;
}));
}
ImGui::SameLine();
if (ImGui::Button("Choose local route", ImVec2(-1.0f, 34.0f))) {
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();
}
}));
}
ImGui::Spacing();
ImGui::SeparatorText("Camera streams");
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() {
ImGui::TextDisabled("Connect directly to a Panda and configure each CAN bus.");
ImGui::Spacing();
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::TextDisabled("Connect to a running IQ.Pilot instance or a local message queue.");
ImGui::Spacing();
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::TextDisabled("Read CAN traffic from a Linux SocketCAN interface.");
ImGui::Spacing();
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;
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;
const ImGuiViewport *viewport = ImGui::GetMainViewport();
ImGui::SetNextWindowPos(viewport->WorkPos);
ImGui::SetNextWindowSize(viewport->WorkSize);
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f));
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f);
const ImGuiWindowFlags page_flags = ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove |
ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoDocking;
if (!ImGui::Begin("##stream_selector_page", nullptr, page_flags)) {
ImGui::End();
ImGui::PopStyleVar(2);
return;
}
ImGui::PopStyleVar(2);
const ImVec2 available = ImGui::GetContentRegionAvail();
const ImVec2 card_size(std::clamp(available.x - 80.0f, 680.0f, 920.0f),
std::clamp(available.y - 80.0f, 470.0f, 570.0f));
ImGui::SetCursorPos(ImVec2(std::max(24.0f, (available.x - card_size.x) * 0.5f),
std::max(24.0f, (available.y - card_size.y) * 0.5f)));
ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, 12.0f);
ImGui::PushStyleVar(ImGuiStyleVar_ChildBorderSize, 1.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(28.0f, 24.0f));
ImGui::BeginChild("##stream_selector_card", card_size, ImGuiChildFlags_Borders, ImGuiWindowFlags_NoScrollbar);
ImGui::PopStyleVar(3);
ImGui::PushFont(boldFont(), 26.0f);
ImGui::TextUnformatted("Open Cabana");
ImGui::PopFont();
ImGui::TextDisabled("Choose a data source to inspect CAN traffic, signals, and video.");
ImGui::Spacing();
ImGui::Spacing();
AbstractOpenStreamWidget *current = nullptr;
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(16.0f, 7.0f));
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();
const float content_height = std::max(170.0f, card_size.y - 300.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(18.0f, 16.0f));
ImGui::BeginChild("tab", ImVec2(0, content_height), ImGuiChildFlags_Borders);
w->draw();
ImGui::EndChild();
ImGui::PopStyleVar();
ImGui::EndTabItem();
}
}
ImGui::EndTabBar();
}
ImGui::PopStyleVar();
first_frame_ = false;
ImGui::SeparatorText("DBC file");
ImGui::SetNextItemWidth(-120.0f);
inputText("##dbc", &dbc_file_, "Choose a dbc file to open", ImGuiInputTextFlags_ReadOnly);
ImGui::SameLine();
if (ImGui::Button("Browse...", ImVec2(-1.0f, 0.0f))) {
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;
const float open_width = 180.0f;
if (ImGui::Button("Cancel", ImVec2(100.0f, 38.0f))) rejected = true;
ImGui::SameLine();
ImGui::SetCursorPosX(ImGui::GetContentRegionMax().x - open_width);
ImGui::BeginDisabled(current == nullptr || !current->openEnabled());
if (ImGui::Button("Open source", ImVec2(open_width, 38.0f))) open_clicked = true;
ImGui::EndDisabled();
if (open_clicked) {
if (stream = current->open(); stream) accepted = true;
}
if (current) current->drawPopups();
FileDialog::draw();
MessageBox::draw();
ImGui::EndChild();
ImGui::End();
if (accepted || rejected) {
open_ = false;
widgets_.clear();
auto on_done = std::move(on_done_);
if (on_done) on_done(std::move(stream), dbc_file_);
}
}