#include "tools/cabana/ui/dialogs/streamselector.h" #include #include #include #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 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(); 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(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(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(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(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(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 OpenPandaWidget::open() { try { return std::make_unique(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 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(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 OpenSocketCanWidget::open() { try { return std::make_unique(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()); widgets_.push_back(std::make_unique()); #ifdef __linux__ if (SocketCanStream::available()) { widgets_.push_back(std::make_unique()); } #endif widgets_.push_back(std::make_unique()); } 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 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_); } }