IQ.Pilot Release Commit @ 27f668a
This commit is contained in:
@@ -1,10 +1,6 @@
|
||||
import os
|
||||
import sys
|
||||
|
||||
venv_site_packages = os.path.join(Dir("#").abspath, ".venv", "lib", "python3.12", "site-packages")
|
||||
if os.path.isdir(venv_site_packages) and venv_site_packages not in sys.path:
|
||||
sys.path.insert(0, venv_site_packages)
|
||||
|
||||
import imgui
|
||||
import iqdbc
|
||||
import libusb
|
||||
|
||||
@@ -49,7 +49,7 @@ void ReplayStream::mergeSegments() {
|
||||
}
|
||||
|
||||
bool ReplayStream::loadRoute(const std::string &route, const std::string &data_dir, uint32_t replay_flags, bool auto_source) {
|
||||
replay.reset(new Replay(route, {"can", "narrowRoadEncodeIdx", "cabinEncodeIdx", "wideRoadEncodeIdx", "carParams"},
|
||||
replay.reset(new Replay(route, {"can", "roadEncodeIdx", "driverEncodeIdx", "wideRoadEncodeIdx", "carParams"},
|
||||
{}, nullptr, replay_flags, data_dir, auto_source));
|
||||
replay->setSegmentCacheLimit(settings.max_cached_minutes);
|
||||
replay->installEventFilter([this](const Event *event) { return eventFilter(event); });
|
||||
|
||||
@@ -90,3 +90,16 @@ class TestFrontendRemoval:
|
||||
def test_imgui_frontend_is_present(self):
|
||||
assert (CABANA_DIR / "ui" / "app.cc").is_file()
|
||||
assert (CABANA_DIR / "ui" / "main.cc").is_file()
|
||||
|
||||
def test_stream_selector_stays_in_main_window(self):
|
||||
app = read("ui/app.cc")
|
||||
assert "io.ConfigFlags |= ImGuiConfigFlags_ViewportsEnable" not in app
|
||||
|
||||
|
||||
class TestReplayVideo:
|
||||
def test_iqpilot_camera_index_services_are_replayed(self):
|
||||
source = read("streams/replaystream.cc")
|
||||
for service in ("roadEncodeIdx", "driverEncodeIdx", "wideRoadEncodeIdx"):
|
||||
assert f'"{service}"' in source
|
||||
assert '"narrowRoadEncodeIdx"' not in source
|
||||
assert '"cabinEncodeIdx"' not in source
|
||||
|
||||
@@ -144,8 +144,6 @@ public:
|
||||
ImPlot::CreateContext();
|
||||
ImGuiIO &io = ImGui::GetIO();
|
||||
io.ConfigFlags |= ImGuiConfigFlags_DockingEnable;
|
||||
io.ConfigFlags |= ImGuiConfigFlags_ViewportsEnable;
|
||||
io.ConfigViewportsNoDecoration = false;
|
||||
io.IniFilename = nullptr;
|
||||
io.LogFilename = nullptr;
|
||||
if (!ImGui_ImplGlfw_InitForOpenGL(window, true)) {
|
||||
|
||||
@@ -15,19 +15,19 @@
|
||||
#include "tools/cabana/utils/util.h"
|
||||
|
||||
void OpenReplayWidget::draw() {
|
||||
ImGui::AlignTextToFramePadding();
|
||||
ImGui::TextDisabled("Replay a local or Konn3kt route with optional camera streams.");
|
||||
ImGui::Spacing();
|
||||
ImGui::TextUnformatted("Route");
|
||||
ImGui::SameLine();
|
||||
ImGui::SetNextItemWidth(-250.0f);
|
||||
ImGui::SetNextItemWidth(-1.0f);
|
||||
inputText("##route", &route_, "Enter route name or browse for local/remote route");
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("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("Local route...")) {
|
||||
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;
|
||||
@@ -35,6 +35,8 @@ void OpenReplayWidget::draw() {
|
||||
}
|
||||
}));
|
||||
}
|
||||
ImGui::Spacing();
|
||||
ImGui::SeparatorText("Camera streams");
|
||||
checkBox("Road camera", &cameras_[0]);
|
||||
ImGui::SameLine();
|
||||
checkBox("Driver camera", &cameras_[1]);
|
||||
@@ -126,6 +128,8 @@ void OpenPandaWidget::buildConfigForm() {
|
||||
}
|
||||
|
||||
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.");
|
||||
@@ -184,6 +188,8 @@ std::unique_ptr<AbstractStream> OpenPandaWidget::open() {
|
||||
}
|
||||
|
||||
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);
|
||||
@@ -226,6 +232,8 @@ void OpenSocketCanWidget::refreshDevices() {
|
||||
}
|
||||
|
||||
void OpenSocketCanWidget::draw() {
|
||||
ImGui::TextDisabled("Read CAN traffic from a Linux SocketCAN interface.");
|
||||
ImGui::Spacing();
|
||||
ImGui::AlignTextToFramePadding();
|
||||
ImGui::TextUnformatted("Device");
|
||||
ImGui::SameLine();
|
||||
@@ -248,7 +256,6 @@ std::unique_ptr<AbstractStream> OpenSocketCanWidget::open() {
|
||||
void StreamSelector::open(Callback on_done) {
|
||||
on_done_ = std::move(on_done);
|
||||
open_ = true;
|
||||
popup_.reset();
|
||||
first_frame_ = true;
|
||||
dbc_file_.clear();
|
||||
widgets_.clear();
|
||||
@@ -264,32 +271,65 @@ void StreamSelector::open(Callback on_done) {
|
||||
|
||||
void StreamSelector::draw() {
|
||||
if (!open_) return;
|
||||
if (!beginDialog("Open stream", &popup_, ImVec2(640.0f, 0.0f))) 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();
|
||||
ImGui::BeginChild("tab", ImVec2(0, 130.0f));
|
||||
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::AlignTextToFramePadding();
|
||||
ImGui::TextUnformatted("dbc File");
|
||||
ImGui::SameLine();
|
||||
ImGui::SetNextItemWidth(-90.0f);
|
||||
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...")) {
|
||||
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;
|
||||
@@ -302,7 +342,13 @@ void StreamSelector::draw() {
|
||||
bool accepted = false, rejected = false;
|
||||
std::unique_ptr<AbstractStream> stream;
|
||||
bool open_clicked = false;
|
||||
dialogButtons("Open", &open_clicked, &rejected, current != nullptr && current->openEnabled());
|
||||
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;
|
||||
}
|
||||
@@ -312,8 +358,8 @@ void StreamSelector::draw() {
|
||||
FileDialog::draw();
|
||||
MessageBox::draw();
|
||||
|
||||
if (accepted || rejected) ImGui::CloseCurrentPopup();
|
||||
ImGui::EndPopup();
|
||||
ImGui::EndChild();
|
||||
ImGui::End();
|
||||
if (accepted || rejected) {
|
||||
open_ = false;
|
||||
widgets_.clear();
|
||||
|
||||
@@ -98,7 +98,6 @@ public:
|
||||
|
||||
private:
|
||||
bool open_ = false;
|
||||
PopupOwner popup_;
|
||||
bool first_frame_ = false;
|
||||
std::string dbc_file_;
|
||||
std::vector<std::unique_ptr<AbstractOpenStreamWidget>> widgets_;
|
||||
|
||||
@@ -44,6 +44,20 @@ def host():
|
||||
|
||||
class TestFileDownload:
|
||||
|
||||
def test_head_connection_released(self, monkeypatch):
|
||||
class Response:
|
||||
status = 200
|
||||
headers = {"content-length": "4"}
|
||||
released = False
|
||||
|
||||
def release_conn(self):
|
||||
self.released = True
|
||||
|
||||
response = Response()
|
||||
monkeypatch.setattr(URLFile, "_request", lambda self, method, url, headers=None: response)
|
||||
assert URLFile("https://example.com/test").get_length_online() == 4
|
||||
assert response.released
|
||||
|
||||
def test_pipeline_defaults(self, host):
|
||||
# TODO: parameterize the defaults so we don't rely on hard-coded values in xx
|
||||
|
||||
|
||||
@@ -128,10 +128,13 @@ class URLFile:
|
||||
|
||||
def get_length_online(self) -> int:
|
||||
response = self._request('HEAD', self._url)
|
||||
if not (200 <= response.status <= 299):
|
||||
return -1
|
||||
length = response.headers.get('content-length', 0)
|
||||
return int(length)
|
||||
try:
|
||||
if not (200 <= response.status <= 299):
|
||||
return -1
|
||||
length = response.headers.get('content-length', 0)
|
||||
return int(length)
|
||||
finally:
|
||||
response.release_conn()
|
||||
|
||||
def get_length(self) -> int:
|
||||
if self._length is not None:
|
||||
|
||||
@@ -40,6 +40,36 @@ def write_rlog(path: Path, n_frames: int = 200):
|
||||
f.write(msg.to_bytes())
|
||||
|
||||
|
||||
def write_video_rlog(path: Path, n_frames: int):
|
||||
with open(path, "wb") as f:
|
||||
cp = messaging.new_message('carParams')
|
||||
cp.logMonoTime = 1_000_000_000
|
||||
cp.carParams.carFingerprint = "TOYOTA_RAV4_TSS2"
|
||||
cp.carParams.brand = "toyota"
|
||||
f.write(cp.to_bytes())
|
||||
|
||||
for i in range(n_frames):
|
||||
timestamp = 1_000_000_000 + i * 50_000_000
|
||||
msg = messaging.new_message('can', 1)
|
||||
msg.logMonoTime = timestamp
|
||||
msg.can[0].address = 0x1D2
|
||||
msg.can[0].src = 0
|
||||
msg.can[0].dat = bytes([i % 256] * 8)
|
||||
f.write(msg.to_bytes())
|
||||
|
||||
idx = messaging.new_message('roadEncodeIdx')
|
||||
idx.logMonoTime = timestamp
|
||||
idx.roadEncodeIdx.frameId = i
|
||||
idx.roadEncodeIdx.type = 'fullHEVC'
|
||||
idx.roadEncodeIdx.encodeId = i
|
||||
idx.roadEncodeIdx.segmentNum = 0
|
||||
idx.roadEncodeIdx.segmentId = i
|
||||
idx.roadEncodeIdx.segmentIdEncode = i
|
||||
idx.roadEncodeIdx.timestampSof = timestamp
|
||||
idx.roadEncodeIdx.timestampEof = timestamp + 10_000_000
|
||||
f.write(idx.to_bytes())
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def local_route(tmp_path_factory):
|
||||
data_dir = tmp_path_factory.mktemp("routes")
|
||||
@@ -50,6 +80,25 @@ def local_route(tmp_path_factory):
|
||||
return data_dir
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def local_video_route(tmp_path_factory):
|
||||
data_dir = tmp_path_factory.mktemp("video_routes")
|
||||
seg_dir = data_dir / f"{DONGLE_ID}|{TIMESTAMP}--0"
|
||||
seg_dir.mkdir()
|
||||
frame_count = 20
|
||||
result = subprocess.run([
|
||||
"ffmpeg", "-hide_banner", "-loglevel", "error", "-f", "lavfi",
|
||||
"-i", "testsrc=size=320x180:rate=20", "-frames:v", str(frame_count),
|
||||
"-pix_fmt", "yuv420p", "-c:v", "libx265", "-preset", "ultrafast",
|
||||
"-x265-params", "pools=1:frame-threads=1:log-level=error", "-f", "hevc",
|
||||
str(seg_dir / "fcamera.hevc"),
|
||||
], capture_output=True, text=True)
|
||||
if result.returncode != 0:
|
||||
pytest.skip(result.stderr)
|
||||
write_video_rlog(seg_dir / "rlog", frame_count)
|
||||
return data_dir
|
||||
|
||||
|
||||
def run(cmd, timeout=180):
|
||||
return subprocess.run(cmd, capture_output=True, text=True, timeout=timeout, env=os.environ.copy(),
|
||||
cwd=TOOLS_DIR.parent)
|
||||
@@ -62,6 +111,33 @@ def cabana_command(*args):
|
||||
return command
|
||||
|
||||
|
||||
def cabana_output_until(args, expected, timeout=60):
|
||||
master, slave = pty.openpty()
|
||||
proc = subprocess.Popen(cabana_command(*args), stdout=slave, stderr=slave,
|
||||
env=os.environ.copy(), cwd=TOOLS_DIR.parent)
|
||||
os.close(slave)
|
||||
output = bytearray()
|
||||
deadline = time.monotonic() + timeout
|
||||
try:
|
||||
while time.monotonic() < deadline:
|
||||
ready, _, _ = select.select([master], [], [], min(1, deadline - time.monotonic()))
|
||||
if not ready:
|
||||
if proc.poll() is not None:
|
||||
break
|
||||
continue
|
||||
try:
|
||||
output.extend(os.read(master, 4096))
|
||||
except OSError:
|
||||
break
|
||||
if expected.encode() in output:
|
||||
break
|
||||
finally:
|
||||
proc.kill()
|
||||
proc.wait()
|
||||
os.close(master)
|
||||
return output.decode(errors="replace")
|
||||
|
||||
|
||||
def test_jotpluggler_renders_a_local_route(local_route, tmp_path):
|
||||
assert JOTPLUGGLER_BIN.exists(), "jotpluggler not built"
|
||||
out = tmp_path / "plot.png"
|
||||
@@ -74,38 +150,20 @@ def test_jotpluggler_renders_a_local_route(local_route, tmp_path):
|
||||
|
||||
def test_cabana_loads_a_local_route(local_route):
|
||||
assert CABANA_BIN.exists(), "cabana not built"
|
||||
master, slave = pty.openpty()
|
||||
proc = subprocess.Popen(cabana_command("--data_dir", str(local_route), "--no-vipc", ROUTE),
|
||||
stdout=slave, stderr=slave,
|
||||
env=os.environ.copy(), cwd=TOOLS_DIR.parent)
|
||||
os.close(slave)
|
||||
loaded = f"loaded route {ROUTE} with 2 valid segments"
|
||||
output = bytearray()
|
||||
deadline = time.monotonic() + 60
|
||||
try:
|
||||
while time.monotonic() < deadline:
|
||||
ready, _, _ = select.select([master], [], [], min(1, deadline - time.monotonic()))
|
||||
if not ready:
|
||||
if proc.poll() is not None:
|
||||
break
|
||||
continue
|
||||
try:
|
||||
output.extend(os.read(master, 4096))
|
||||
except OSError:
|
||||
break
|
||||
if loaded.encode() in output:
|
||||
break
|
||||
finally:
|
||||
proc.kill()
|
||||
proc.wait()
|
||||
os.close(master)
|
||||
|
||||
out = output.decode(errors="replace")
|
||||
out = cabana_output_until(("--data_dir", str(local_route), "--no-vipc", ROUTE), loaded)
|
||||
assert "failed to load route" not in out, out
|
||||
assert "invalid route format" not in out, out
|
||||
assert loaded in out, out
|
||||
|
||||
|
||||
def test_cabana_replays_local_video(local_video_route):
|
||||
expected = "camera[0] vipc send #1"
|
||||
out = cabana_output_until(("--data_dir", str(local_video_route), ROUTE), expected)
|
||||
assert "failed to get frame" not in out, out
|
||||
assert expected in out, out
|
||||
|
||||
|
||||
def test_replay_logreader_reports_load_stats(local_route):
|
||||
assert shutil.which("python3") is not None
|
||||
header = (TOOLS_DIR / "replay" / "logreader.h").read_text()
|
||||
|
||||
Reference in New Issue
Block a user