IQ.Pilot Release Commit @ 0b96bd5
This commit is contained in:
222
iqpilot/tools/cabana/ui/dialogs/filedialog.cc
Normal file
222
iqpilot/tools/cabana/ui/dialogs/filedialog.cc
Normal 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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
19
iqpilot/tools/cabana/ui/dialogs/filedialog.h
Normal file
19
iqpilot/tools/cabana/ui/dialogs/filedialog.h
Normal 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();
|
||||
|
||||
}
|
||||
87
iqpilot/tools/cabana/ui/dialogs/messagebox.cc
Normal file
87
iqpilot/tools/cabana/ui/dialogs/messagebox.cc
Normal 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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
19
iqpilot/tools/cabana/ui/dialogs/messagebox.h
Normal file
19
iqpilot/tools/cabana/ui/dialogs/messagebox.h
Normal 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();
|
||||
|
||||
}
|
||||
124
iqpilot/tools/cabana/ui/dialogs/routesdialog.cc
Normal file
124
iqpilot/tools/cabana/ui/dialogs/routesdialog.cc
Normal 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);
|
||||
}
|
||||
45
iqpilot/tools/cabana/ui/dialogs/routesdialog.h
Normal file
45
iqpilot/tools/cabana/ui/dialogs/routesdialog.h
Normal 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_;
|
||||
};
|
||||
108
iqpilot/tools/cabana/ui/dialogs/settingsdialog.cc
Normal file
108
iqpilot/tools/cabana/ui/dialogs/settingsdialog.cc
Normal 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();
|
||||
}
|
||||
24
iqpilot/tools/cabana/ui/dialogs/settingsdialog.h
Normal file
24
iqpilot/tools/cabana/ui/dialogs/settingsdialog.h
Normal 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_;
|
||||
};
|
||||
323
iqpilot/tools/cabana/ui/dialogs/streamselector.cc
Normal file
323
iqpilot/tools/cabana/ui/dialogs/streamselector.cc
Normal 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_);
|
||||
}
|
||||
}
|
||||
106
iqpilot/tools/cabana/ui/dialogs/streamselector.h
Normal file
106
iqpilot/tools/cabana/ui/dialogs/streamselector.h
Normal 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_;
|
||||
};
|
||||
Reference in New Issue
Block a user