IQ.Pilot Release Commit @ 661a2de
This commit is contained in:
@@ -17,12 +17,14 @@ from iqdbc.car.fingerprints import MIGRATION
|
||||
from iqdbc.car.values import PLATFORMS
|
||||
from openpilot.common.basedir import BASEDIR
|
||||
|
||||
Import('env', 'arch', 'common', 'messaging', 'visionipc', 'cereal', 'replay_lib')
|
||||
Import('env', 'arch', 'common', 'messaging', 'visionipc', 'cereal', 'replay_lib', 'ffmpeg_libs')
|
||||
|
||||
jot_env = env.Clone()
|
||||
# imgui.MESA_DIR only exists on the larch64 wheel; skip missing paths to avoid an ld search-path warning
|
||||
jot_env["LIBPATH"] += [p for p in [imgui.MESA_DIR, libusb.LIB_DIR] if os.path.isdir(p)]
|
||||
jot_env["CPPPATH"] += [imgui.INCLUDE_DIR, libusb.INCLUDE_DIR]
|
||||
# IQ.Pilot patch: no comma-deps-bootstrap-icons wheel here, so BOOTSTRAP_ICONS_TTF is
|
||||
# not defined; icons.cc reads the TTF vendored under tools/jotpluggler/assets instead.
|
||||
jot_env["CXXFLAGS"] += [
|
||||
"-DGLFW_INCLUDE_NONE",
|
||||
'-DJOTP_REPO_ROOT=\'"%s"\'' % os.path.realpath(BASEDIR),
|
||||
@@ -104,16 +106,15 @@ event_extractors = jot_env.Command("generated_event_extractors.h", [
|
||||
jot_env.PrettyAction(generate_event_extractors, 'GEN'),
|
||||
)
|
||||
|
||||
libs = [replay_lib, common, messaging, visionipc, cereal, File(f"{imgui.LIB_DIR}/libimgui.a"), File(f"{imgui.LIB_DIR}/libglfw3.a"),
|
||||
# IQ.Pilot patch: iqpilot's replay_lib resolves URL/api work via libcurl
|
||||
# (vs upstream's Python downloader), so jotpluggler needs to link curl too.
|
||||
# iqpilot's api.cc additionally uses OpenSSL primitives (PEM/RSA/SHA256) for
|
||||
# JWT signing, and visionipc references OpenCL — both must also be linked.
|
||||
"avformat", "avcodec", "avutil", "x264", "yuv", "z", "bz2", "zstd", "curl", "ssl", "crypto", "m", "pthread", "usb-1.0"]
|
||||
# IQ.Pilot patch: iqpilot's replay_lib resolves route/API work via libcurl (vs upstream's
|
||||
# Python downloader), so jotpluggler needs curl; api.cc signs konn3kt JWTs with OpenSSL
|
||||
# primitives, and visionipc references OpenCL. Both must also be linked.
|
||||
libs = [replay_lib, common, messaging, visionipc, cereal, File(f"{imgui.LIB_DIR}/libimgui.a"), File(f"{imgui.LIB_DIR}/libglfw3.a")] + \
|
||||
ffmpeg_libs + ["bz2", "zstd", "curl", "ssl", "crypto", "m", "pthread", "usb-1.0"]
|
||||
if arch == "Darwin":
|
||||
jot_env["FRAMEWORKS"] = ["OpenGL", "OpenCL", "Cocoa", "IOKit", "CoreFoundation", "CoreVideo", "CoreMedia", "VideoToolbox"]
|
||||
else:
|
||||
libs += ["GL", "OpenCL", "dl", "va", "va-drm", "drm"]
|
||||
libs += ["GL", "OpenCL", "dl"]
|
||||
|
||||
program = jot_env.Program("jotpluggler", jot_env.Glob("*.cc"), LIBS=libs)
|
||||
jot_env.Depends(program, generated_dbc_stamp)
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include "tools/jotpluggler/common.h"
|
||||
#include "tools/jotpluggler/internal.h"
|
||||
#include "tools/jotpluggler/map.h"
|
||||
#include "tools/jotpluggler/thumbnail.h"
|
||||
#include "system/hardware/hw.h"
|
||||
#include "imgui_impl_glfw.h"
|
||||
|
||||
@@ -1033,10 +1034,12 @@ bool apply_special_item_to_pane(WorkspaceTab *tab, TabUiState *tab_state, int pa
|
||||
if (pane.title == UNTITLED_PANE_TITLE || previous_kind != PaneKind::Plot) {
|
||||
pane.title = spec->label;
|
||||
}
|
||||
} else {
|
||||
} else if (spec->kind == PaneKind::Camera) {
|
||||
pane.title = spec->label;
|
||||
resize_tab_pane_state(tab_state, tab->panes.size());
|
||||
tab_state->camera_panes[static_cast<size_t>(pane_index)].fit_to_pane = true;
|
||||
} else {
|
||||
pane.title = spec->label;
|
||||
}
|
||||
tab_state->active_pane_index = pane_index;
|
||||
return true;
|
||||
@@ -1565,6 +1568,8 @@ void draw_pane_windows(AppSession *session, UiState *state) {
|
||||
}
|
||||
if (pane.kind == PaneKind::Map) {
|
||||
draw_map_pane(session, state, &pane, static_cast<int>(i));
|
||||
} else if (pane.kind == PaneKind::Thumbnail) {
|
||||
draw_thumbnail_pane(session, state);
|
||||
} else if (pane.kind == PaneKind::Camera) {
|
||||
draw_camera_pane(session, state, tab_state, static_cast<int>(i), pane);
|
||||
} else {
|
||||
@@ -1847,6 +1852,7 @@ int run(const Options &options) {
|
||||
for (std::unique_ptr<CameraFeedView> &feed : session.pane_camera_feeds) {
|
||||
feed = std::make_unique<CameraFeedView>();
|
||||
}
|
||||
session.thumbnail_view = std::make_unique<ThumbnailView>();
|
||||
sync_camera_feeds(&session);
|
||||
|
||||
if (session.async_route_loading) {
|
||||
@@ -1892,6 +1898,7 @@ int run(const Options &options) {
|
||||
for (std::unique_ptr<CameraFeedView> &feed : session.pane_camera_feeds) {
|
||||
feed.reset();
|
||||
}
|
||||
session.thumbnail_view.reset();
|
||||
return 0;
|
||||
} catch (const std::exception &err) {
|
||||
std::cerr << err.what() << "\n";
|
||||
|
||||
@@ -81,6 +81,7 @@ struct Curve {
|
||||
enum class PaneKind : uint8_t {
|
||||
Plot,
|
||||
Map,
|
||||
Thumbnail,
|
||||
Camera,
|
||||
};
|
||||
|
||||
@@ -141,9 +142,15 @@ struct CameraFeedIndex {
|
||||
std::vector<CameraFrameIndexEntry> entries;
|
||||
};
|
||||
|
||||
struct ThumbnailFrame {
|
||||
double timestamp = 0.0;
|
||||
int segment = -1;
|
||||
std::vector<uint8_t> jpeg;
|
||||
};
|
||||
|
||||
enum class LogOrigin : uint8_t {
|
||||
Log,
|
||||
Android,
|
||||
OperatingSystem,
|
||||
Alert,
|
||||
};
|
||||
|
||||
@@ -318,6 +325,7 @@ struct RouteData {
|
||||
CameraFeedIndex driver_camera;
|
||||
CameraFeedIndex wide_road_camera;
|
||||
CameraFeedIndex qroad_camera;
|
||||
std::vector<ThumbnailFrame> thumbnails;
|
||||
GpsTrace gps_trace;
|
||||
std::vector<LogEntry> logs;
|
||||
std::vector<TimelineEntry> timeline;
|
||||
@@ -445,6 +453,7 @@ bool icon_menu_item(const char *glyph,
|
||||
|
||||
class AsyncRouteLoader;
|
||||
class CameraFeedView;
|
||||
class ThumbnailView;
|
||||
class StreamPoller;
|
||||
class MapDataManager;
|
||||
|
||||
@@ -486,6 +495,7 @@ struct AppSession {
|
||||
std::unique_ptr<AsyncRouteLoader> route_loader;
|
||||
std::unique_ptr<StreamPoller> stream_poller;
|
||||
std::array<std::unique_ptr<CameraFeedView>, 4> pane_camera_feeds;
|
||||
std::unique_ptr<ThumbnailView> thumbnail_view;
|
||||
std::unique_ptr<MapDataManager> map_data;
|
||||
bool async_route_loading = false;
|
||||
double next_stream_custom_refresh_time = 0.0;
|
||||
@@ -885,3 +895,20 @@ private:
|
||||
struct Impl;
|
||||
std::unique_ptr<Impl> impl_;
|
||||
};
|
||||
|
||||
class ThumbnailView {
|
||||
public:
|
||||
ThumbnailView();
|
||||
~ThumbnailView();
|
||||
|
||||
ThumbnailView(const ThumbnailView &) = delete;
|
||||
ThumbnailView &operator=(const ThumbnailView &) = delete;
|
||||
|
||||
void setThumbnails(const std::vector<ThumbnailFrame> &thumbnails);
|
||||
void update(double tracker_time);
|
||||
void drawSized(ImVec2 size, bool loading);
|
||||
|
||||
private:
|
||||
struct Impl;
|
||||
std::unique_ptr<Impl> impl_;
|
||||
};
|
||||
|
||||
@@ -47,11 +47,11 @@ const char *special_item_label(std::string_view item_id) {
|
||||
}
|
||||
|
||||
bool pane_kind_is_special(PaneKind kind) {
|
||||
return kind == PaneKind::Map || kind == PaneKind::Camera;
|
||||
return kind == PaneKind::Map || kind == PaneKind::Thumbnail || kind == PaneKind::Camera;
|
||||
}
|
||||
|
||||
bool is_default_special_title(std::string_view title) {
|
||||
if (title == "Map") return true;
|
||||
if (title == "Map" || title == "Thumbnail") return true;
|
||||
return std::any_of(kCameraViewSpecs.begin(), kCameraViewSpecs.end(), [&](const CameraViewSpec &spec) {
|
||||
return title == spec.label;
|
||||
});
|
||||
@@ -162,14 +162,12 @@ void open_external_url(std::string_view url) {
|
||||
}
|
||||
}
|
||||
|
||||
std::string route_useradmin_url(const RouteIdentifier &route_id) {
|
||||
return route_id.empty() ? std::string()
|
||||
: "https://useradmin.comma.ai/?onebox=" + route_id.dongle_id + "%7C" + route_id.log_id;
|
||||
}
|
||||
|
||||
std::string route_connect_url(const RouteIdentifier &route_id) {
|
||||
return route_id.empty() ? std::string()
|
||||
: "https://connect.comma.ai/" + route_id.canonical();
|
||||
// IQ.Pilot patch: iqpilot routes live on konn3kt, not comma connect. konn3kt has no
|
||||
// useradmin equivalent, so that link is gone; the share link keeps connect's
|
||||
// <dongle_id>/<log_id> path shape (see tools/lib/logreader.py parse_indirect).
|
||||
std::string route_konn3kt_url(const RouteIdentifier &route_id) {
|
||||
static const std::string host = util::getenv("KONN3KT_APP_HOST", "https://konn3kt.com");
|
||||
return route_id.empty() ? std::string() : host + "/" + route_id.canonical();
|
||||
}
|
||||
|
||||
std::string route_google_maps_url(const GpsTrace &trace) {
|
||||
|
||||
@@ -28,8 +28,9 @@ inline constexpr std::array<CameraViewSpec, 4> kCameraViewSpecs = {{
|
||||
{CameraViewKind::QRoad, "qRoad Camera", "qroad", "qroad", "camera_qroad", &RouteData::qroad_camera},
|
||||
}};
|
||||
|
||||
inline constexpr std::array<SpecialItemSpec, 5> kSpecialItemSpecs = {{
|
||||
inline constexpr std::array<SpecialItemSpec, 6> kSpecialItemSpecs = {{
|
||||
{"map", "Map", PaneKind::Map, CameraViewKind::Road},
|
||||
{"thumbnail", "Thumbnail", PaneKind::Thumbnail, CameraViewKind::Road},
|
||||
{kCameraViewSpecs[0].special_item_id, kCameraViewSpecs[0].label, PaneKind::Camera, kCameraViewSpecs[0].view},
|
||||
{kCameraViewSpecs[1].special_item_id, kCameraViewSpecs[1].label, PaneKind::Camera, kCameraViewSpecs[1].view},
|
||||
{kCameraViewSpecs[2].special_item_id, kCameraViewSpecs[2].label, PaneKind::Camera, kCameraViewSpecs[2].view},
|
||||
@@ -62,6 +63,5 @@ bool app_begin_popup_modal(const char *name,
|
||||
bool *p_open = nullptr,
|
||||
ImGuiWindowFlags flags = ImGuiWindowFlags_AlwaysAutoResize);
|
||||
void open_external_url(std::string_view url);
|
||||
std::string route_useradmin_url(const RouteIdentifier &route_id);
|
||||
std::string route_connect_url(const RouteIdentifier &route_id);
|
||||
std::string route_konn3kt_url(const RouteIdentifier &route_id);
|
||||
std::string route_google_maps_url(const GpsTrace &trace);
|
||||
|
||||
@@ -276,6 +276,7 @@ inline void Database::parseSg(const std::string &line, int line_number, Message
|
||||
signal.type = Signal::Type::Multiplexor;
|
||||
} else if (!indicator.empty() && indicator.front() == 'm') {
|
||||
signal.type = Signal::Type::Multiplexed;
|
||||
// IQ.Pilot patch: guard a bare "m" indicator; std::stoi("") throws.
|
||||
const std::string mux = indicator.substr(1);
|
||||
signal.multiplex_value = mux.empty() ? 0 : std::stoi(mux);
|
||||
} else {
|
||||
|
||||
@@ -62,6 +62,8 @@ class Generator:
|
||||
def __init__(self, event_schema):
|
||||
self.event_schema = event_schema
|
||||
self.fixed_paths = []
|
||||
self.event_base_slots = {}
|
||||
self.static_enums = []
|
||||
self.tmp_index = 0
|
||||
self.lines = []
|
||||
self.emits_memo = {}
|
||||
@@ -103,9 +105,13 @@ class Generator:
|
||||
self.emit(indent, f"append_dynamic_scalar_point({path_expr}, tm, {double_expr}, series);")
|
||||
else:
|
||||
slot = self.add_fixed_path(path)
|
||||
if kind == "Enum":
|
||||
self.emit_enum_capture(indent, cxx_string(path), enum_names(schema))
|
||||
self.emit(indent, f"append_fixed_scalar_point(&series->fixed_series[{slot}], tm, {double_expr});")
|
||||
names = enum_names(schema) if kind == "Enum" else []
|
||||
if names:
|
||||
enum_index = len(self.static_enums)
|
||||
self.static_enums.append(names)
|
||||
self.emit(indent, f"append_fixed_enum_point({slot}, {enum_index}, tm, {double_expr}, series);")
|
||||
else:
|
||||
self.emit(indent, f"append_fixed_scalar_point(&series->fixed_series[{slot}], tm, {double_expr});")
|
||||
return
|
||||
|
||||
if type_kind == "struct":
|
||||
@@ -144,9 +150,13 @@ class Generator:
|
||||
self.emit(indent, f"if ({' && '.join(conditions)}) {{")
|
||||
indent += 2
|
||||
|
||||
value_var = self.tmp("value")
|
||||
self.emit(indent, f"const auto {value_var} = {get_call};")
|
||||
self.emit_node(indent, type_kind, type_proto, value_schema, value_var, field_path, field_path_expr, dynamic_path)
|
||||
# Scalar getters are only consumed once. Emitting them directly avoids
|
||||
# thousands of single-use locals in the generated extractor.
|
||||
value_expr = get_call
|
||||
if kind is None:
|
||||
value_expr = self.tmp("value")
|
||||
self.emit(indent, f"const auto {value_expr} = {get_call};")
|
||||
self.emit_node(indent, type_kind, type_proto, value_schema, value_expr, field_path, field_path_expr, dynamic_path)
|
||||
|
||||
if conditions:
|
||||
indent -= 2
|
||||
@@ -249,35 +259,47 @@ class Generator:
|
||||
self.emit(indent + 2, "}")
|
||||
self.emit(indent, "}")
|
||||
self.emit(indent, "if (skip_raw_can) {")
|
||||
self.emit(indent + 2, "return true;")
|
||||
self.emit(indent + 2, "return;")
|
||||
self.emit(indent, "}")
|
||||
|
||||
def emit_event_case(self, field_name):
|
||||
def emit_event_reader(self, field_name):
|
||||
field = self.event_schema.fields[field_name]
|
||||
proto = field.proto
|
||||
type_kind = field_type(field)
|
||||
type_proto = field_type_proto(field)
|
||||
kind = scalar_kind(type_proto)
|
||||
schema = field.schema if kind == "Enum" or type_kind in NESTED_TYPE_KINDS else None
|
||||
self.emit(4, f"case static_cast<cereal::Event::Which>({proto.discriminantValue}): {{")
|
||||
valid_slot = self.add_fixed_path(f"/{field_name}/valid")
|
||||
mono_slot = self.add_fixed_path(f"/{field_name}/logMonoTime")
|
||||
seconds_slot = self.add_fixed_path(f"/{field_name}/t")
|
||||
self.emit(6, f"append_fixed_scalar_point(&series->fixed_series[{valid_slot}], tm, event.getValid() ? 1.0 : 0.0);")
|
||||
self.emit(6, f"append_fixed_scalar_point(&series->fixed_series[{mono_slot}], tm, static_cast<double>(event.getLogMonoTime()));")
|
||||
self.emit(6, f"append_fixed_scalar_point(&series->fixed_series[{seconds_slot}], tm, tm);")
|
||||
if field_name in {"can", "sendcan"}:
|
||||
self.emit_can_special(6, field_name)
|
||||
if self.node_emits(type_kind, type_proto, schema):
|
||||
self.add_fixed_path(f"/{field_name}/logMonoTime")
|
||||
self.add_fixed_path(f"/{field_name}/t")
|
||||
self.event_base_slots[proto.discriminantValue] = valid_slot
|
||||
|
||||
emits_payload = self.node_emits(type_kind, type_proto, schema)
|
||||
if field_name not in {"can", "sendcan"} and not emits_payload:
|
||||
return None
|
||||
|
||||
reader_name = f"append_event_{proto.discriminantValue}"
|
||||
needs_can = field_name in {"can", "sendcan"}
|
||||
header_index = len(self.lines)
|
||||
self.emit(0, "")
|
||||
if needs_can:
|
||||
self.emit_can_special(2, field_name)
|
||||
if emits_payload:
|
||||
payload = self.tmp("payload")
|
||||
self.emit(6, f"const auto {payload} = event.{accessor('get', field_name)}();")
|
||||
self.emit_node(6, type_kind, type_proto, schema, payload, f"/{field_name}", None, False)
|
||||
self.emit(6, "return true;")
|
||||
self.emit(4, "}")
|
||||
self.emit(2, f"const auto {payload} = event.{accessor('get', field_name)}();")
|
||||
self.emit_node(2, type_kind, type_proto, schema, payload, f"/{field_name}", None, False)
|
||||
self.emit(0, "}")
|
||||
self.emit(0, "")
|
||||
if needs_can:
|
||||
signature = "const cereal::Event::Reader &event, const dbc::Database *can_dbc, bool skip_raw_can, double tm, SeriesAccumulator *series"
|
||||
else:
|
||||
signature = "const cereal::Event::Reader &event, double tm, SeriesAccumulator *series"
|
||||
self.lines[header_index] = f"__attribute__((noinline)) void {reader_name}({signature}) {{"
|
||||
return reader_name, needs_can
|
||||
|
||||
def generate(self):
|
||||
self.lines = []
|
||||
self.emit(0, "// Generated by tools/jotpluggler/generate_event_extractors.py; do not edit.")
|
||||
self.emit(0, "// Generated by openpilot/tools/jotpluggler/generate_event_extractors.py; do not edit.")
|
||||
self.emit(0, "")
|
||||
self.emit(0, "const std::vector<std::string> &static_event_fixed_paths() {")
|
||||
self.emit(2, "static const std::vector<std::string> paths = {")
|
||||
@@ -300,11 +322,66 @@ class Generator:
|
||||
self.emit(2, "}")
|
||||
self.emit(0, "}")
|
||||
self.emit(0, "")
|
||||
self.emit(0, "__attribute__((noinline)) void append_fixed_enum_point(size_t series_slot, size_t enum_index, double tm, double value, SeriesAccumulator *series);") # noqa: E501
|
||||
self.emit(0, "")
|
||||
|
||||
self.emit(0, "// Keep each event payload behind its own optimizer boundary. Combining the")
|
||||
self.emit(0, "// whole schema into one function creates much more code and runs slower.")
|
||||
event_readers = {}
|
||||
for field_name in self.event_schema.union_fields:
|
||||
event_readers[field_name] = self.emit_event_reader(field_name)
|
||||
|
||||
self.emit(0, "static const std::initializer_list<std::string_view> static_event_enum_names[] = {")
|
||||
for names in self.static_enums:
|
||||
names_expr = "{" + ", ".join(cxx_string(name) for name in names) + "}"
|
||||
self.emit(2, f"{names_expr},")
|
||||
self.emit(0, "};")
|
||||
self.emit(0, "")
|
||||
self.emit(0, "__attribute__((noinline)) void append_fixed_enum_point(size_t series_slot, size_t enum_index, double tm, double value, SeriesAccumulator *series) {") # noqa: E501
|
||||
self.emit(2, "RouteSeries *fixed_series = &series->fixed_series[series_slot];")
|
||||
self.emit(2, "capture_static_enum_info(fixed_series->path, static_event_enum_names[enum_index], series);")
|
||||
self.emit(2, "fixed_series->times.push_back(tm);")
|
||||
self.emit(2, "fixed_series->values.push_back(value);")
|
||||
self.emit(0, "}")
|
||||
self.emit(0, "")
|
||||
|
||||
self.emit(0, "bool append_event_static_reader(cereal::Event::Which which, const cereal::Event::Reader &event, const dbc::Database *can_dbc, bool skip_raw_can, double time_offset, SeriesAccumulator *series) {") # noqa: E501
|
||||
self.emit(2, "const double tm = static_cast<double>(event.getLogMonoTime()) / 1.0e9 - time_offset;")
|
||||
self.emit(2, "const auto log_mono_time = event.getLogMonoTime();")
|
||||
self.emit(2, "const double tm = static_cast<double>(log_mono_time) / 1.0e9 - time_offset;")
|
||||
|
||||
invalid_slot = "static_cast<size_t>(-1)"
|
||||
max_discriminant = max(self.event_base_slots)
|
||||
base_slots = [self.event_base_slots.get(i, invalid_slot) for i in range(max_discriminant + 1)]
|
||||
self.emit(2, "static constexpr size_t event_base_slots[] = {")
|
||||
for slot in base_slots:
|
||||
self.emit(4, f"{slot},")
|
||||
self.emit(2, "};")
|
||||
self.emit(2, "const size_t event_index = static_cast<size_t>(which);")
|
||||
self.emit(2, "if (event_index >= sizeof(event_base_slots) / sizeof(event_base_slots[0])) {")
|
||||
self.emit(4, "return false;")
|
||||
self.emit(2, "}")
|
||||
self.emit(2, "const size_t base_slot = event_base_slots[event_index];")
|
||||
self.emit(2, f"if (base_slot == {invalid_slot}) {{")
|
||||
self.emit(4, "return false;")
|
||||
self.emit(2, "}")
|
||||
self.emit(2, "RouteSeries *base_series = &series->fixed_series[base_slot];")
|
||||
self.emit(2, "base_series[0].times.push_back(tm);")
|
||||
self.emit(2, "base_series[0].values.push_back(event.getValid() ? 1.0 : 0.0);")
|
||||
self.emit(2, "base_series[1].times.push_back(tm);")
|
||||
self.emit(2, "base_series[1].values.push_back(static_cast<double>(log_mono_time));")
|
||||
self.emit(2, "base_series[2].times.push_back(tm);")
|
||||
self.emit(2, "base_series[2].values.push_back(tm);")
|
||||
self.emit(2, "switch (which) {")
|
||||
for field_name in self.event_schema.union_fields:
|
||||
self.emit_event_case(field_name)
|
||||
field = self.event_schema.fields[field_name]
|
||||
self.emit(4, f"case static_cast<cereal::Event::Which>({field.proto.discriminantValue}):")
|
||||
reader = event_readers[field_name]
|
||||
if reader is not None:
|
||||
if reader[1]:
|
||||
self.emit(6, f"{reader[0]}(event, can_dbc, skip_raw_can, tm, series);")
|
||||
else:
|
||||
self.emit(6, f"{reader[0]}(event, tm, series);")
|
||||
self.emit(6, "return true;")
|
||||
self.emit(4, "default:")
|
||||
self.emit(6, "return false;")
|
||||
self.emit(2, "}")
|
||||
@@ -323,6 +400,8 @@ if __name__ == "__main__":
|
||||
repo_root = Path(sys.argv[1]).resolve()
|
||||
output = Path(sys.argv[2])
|
||||
capnp.remove_import_hook()
|
||||
# IQ.Pilot patch: iqpilot's tree is not nested under openpilot/ and car.capnp lives
|
||||
# in cereal/ alongside log.capnp, so no extra import path is needed.
|
||||
log = capnp.load(str(repo_root / "cereal" / "log.capnp"))
|
||||
generated = Generator(log.Event.schema).generate()
|
||||
output.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
void icon_add_font(float size, bool merge, const ImFont *base_font) {
|
||||
// IQ.Pilot patch: iqpilot's third_party/bootstrap is git-lfs and the TTF isn't
|
||||
// checked in, so we vendor the font alongside jotpluggler instead of relying on
|
||||
// third_party/bootstrap/bootstrap-icons.ttf.
|
||||
// upstream's BOOTSTRAP_ICONS_TTF define from the comma-deps-bootstrap-icons wheel.
|
||||
const std::filesystem::path ttf = repo_root() / "tools" / "jotpluggler" / "assets" / "bootstrap-icons.ttf";
|
||||
ImGuiIO &io = ImGui::GetIO();
|
||||
ImFontConfig config;
|
||||
|
||||
@@ -62,6 +62,8 @@ json11::Json workspace_node_to_json(const WorkspaceNode &node, const WorkspaceTa
|
||||
};
|
||||
if (pane.kind == PaneKind::Map) {
|
||||
obj["kind"] = "map";
|
||||
} else if (pane.kind == PaneKind::Thumbnail) {
|
||||
obj["kind"] = "thumbnail";
|
||||
} else if (pane.kind == PaneKind::Camera) {
|
||||
obj["kind"] = "camera";
|
||||
obj["camera_view"] = camera_view_spec(pane.camera_view).layout_name;
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
#include "imgui_impl_opengl3.h"
|
||||
#include "imgui_impl_opengl3_loader.h"
|
||||
#include "implot.h"
|
||||
#include "libyuv.h"
|
||||
#include "common/yuv.h"
|
||||
#include "msgq_repo/msgq/ipc.h"
|
||||
#include "tools/replay/framereader.h"
|
||||
|
||||
@@ -1173,18 +1173,14 @@ struct CameraFeedView::Impl {
|
||||
result.width = reader->width;
|
||||
result.height = reader->height;
|
||||
result.rgba.resize(static_cast<size_t>(result.width) * static_cast<size_t>(result.height) * 4U, 0);
|
||||
// IQ.Pilot patch: vendored libyuv lacks NV12ToABGR; use NV12ToARGB + in-place ARGBToABGR swap.
|
||||
libyuv::NV12ToARGB(decode_buffer.y,
|
||||
static_cast<int>(decode_buffer.stride),
|
||||
decode_buffer.uv,
|
||||
static_cast<int>(decode_buffer.stride),
|
||||
result.rgba.data(),
|
||||
result.width * 4,
|
||||
result.width,
|
||||
result.height);
|
||||
libyuv::ARGBToABGR(result.rgba.data(), result.width * 4,
|
||||
result.rgba.data(), result.width * 4,
|
||||
result.width, result.height);
|
||||
yuv::nv12_to_rgba(decode_buffer.y,
|
||||
static_cast<int>(decode_buffer.stride),
|
||||
decode_buffer.uv,
|
||||
static_cast<int>(decode_buffer.stride),
|
||||
result.rgba.data(),
|
||||
result.width * 4,
|
||||
result.width,
|
||||
result.height);
|
||||
result.success = true;
|
||||
result.decode_ms = std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - decode_begin).count();
|
||||
publish_result(*request, std::move(result));
|
||||
@@ -1207,18 +1203,14 @@ struct CameraFeedView::Impl {
|
||||
.height = reader->height,
|
||||
};
|
||||
prefetched.rgba.resize(static_cast<size_t>(prefetched.width) * static_cast<size_t>(prefetched.height) * 4U, 0);
|
||||
// IQ.Pilot patch: vendored libyuv lacks NV12ToABGR; use NV12ToARGB + in-place ARGBToABGR swap.
|
||||
libyuv::NV12ToARGB(decode_buffer.y,
|
||||
static_cast<int>(decode_buffer.stride),
|
||||
decode_buffer.uv,
|
||||
static_cast<int>(decode_buffer.stride),
|
||||
prefetched.rgba.data(),
|
||||
prefetched.width * 4,
|
||||
prefetched.width,
|
||||
prefetched.height);
|
||||
libyuv::ARGBToABGR(prefetched.rgba.data(), prefetched.width * 4,
|
||||
prefetched.rgba.data(), prefetched.width * 4,
|
||||
prefetched.width, prefetched.height);
|
||||
yuv::nv12_to_rgba(decode_buffer.y,
|
||||
static_cast<int>(decode_buffer.stride),
|
||||
decode_buffer.uv,
|
||||
static_cast<int>(decode_buffer.stride),
|
||||
prefetched.rgba.data(),
|
||||
prefetched.width * 4,
|
||||
prefetched.width,
|
||||
prefetched.height);
|
||||
remember_cached_result(prefetched);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,9 @@ void sync_camera_feeds(AppSession *session) {
|
||||
session->pane_camera_feeds[i]->setCameraIndex(session->route_data.*(kCameraViewSpecs[i].route_member), kCameraViewSpecs[i].view);
|
||||
}
|
||||
}
|
||||
if (session->thumbnail_view) {
|
||||
session->thumbnail_view->setThumbnails(session->route_data.thumbnails);
|
||||
}
|
||||
}
|
||||
|
||||
void apply_route_data(AppSession *session, UiState *state, RouteData route_data) {
|
||||
@@ -456,8 +459,7 @@ void draw_route_info_popup(AppSession *session, UiState *state, ImVec2 anchor) {
|
||||
|
||||
const char *copy_icon = icon::CLIPBOARD;
|
||||
const char *link_icon = icon::BOX_ARROW_UP_RIGHT;
|
||||
const std::string useradmin_label = std::string("Useradmin ") + link_icon;
|
||||
const std::string connect_label = std::string("comma connect ") + link_icon;
|
||||
const std::string konn3kt_label = std::string("konn3kt ") + link_icon;
|
||||
if (ImGui::Button(copy_icon, ImVec2(34.0f, 26.0f))) {
|
||||
ImGui::SetClipboardText(session->route_id.canonical().c_str());
|
||||
state->status_text = "Copied route to clipboard";
|
||||
@@ -470,14 +472,9 @@ void draw_route_info_popup(AppSession *session, UiState *state, ImVec2 anchor) {
|
||||
ImGui::EndTooltip();
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button(useradmin_label.c_str(), ImVec2(132.0f, 26.0f))) {
|
||||
open_external_url(route_useradmin_url(session->route_id));
|
||||
state->status_text = "Opened useradmin";
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button(connect_label.c_str(), ImVec2(156.0f, 26.0f))) {
|
||||
open_external_url(route_connect_url(session->route_id));
|
||||
state->status_text = "Opened comma connect";
|
||||
if (ImGui::Button(konn3kt_label.c_str(), ImVec2(120.0f, 26.0f))) {
|
||||
open_external_url(route_konn3kt_url(session->route_id));
|
||||
state->status_text = "Opened konn3kt";
|
||||
}
|
||||
|
||||
ImGui::Spacing();
|
||||
|
||||
@@ -24,10 +24,11 @@
|
||||
|
||||
#include "common/util.h"
|
||||
#include "third_party/json11/json11.hpp"
|
||||
#include "tools/replay/api.h"
|
||||
#include "tools/replay/logreader.h"
|
||||
// IQ.Pilot patch: iqpilot's tools/replay has no py_downloader; route file listing is
|
||||
// served directly by CommaApi2::httpGet against the IQ.Pilot konn3kt API.
|
||||
// served by the konn3kt CommaApi2 helper, which returns the same JSON shape (and the
|
||||
// same {"error": ...} envelope) as upstream's PyDownloader.
|
||||
#include "tools/replay/api.h"
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
@@ -100,6 +101,7 @@ struct LoadedRouteArtifacts {
|
||||
std::vector<CanMessageData> can_messages;
|
||||
std::vector<LogEntry> logs;
|
||||
std::vector<TimelineEntry> timeline;
|
||||
std::vector<ThumbnailFrame> thumbnails;
|
||||
std::unordered_map<std::string, EnumInfo> enum_info;
|
||||
};
|
||||
|
||||
@@ -263,7 +265,8 @@ RouteSelection parse_route_selection(std::string route_name) {
|
||||
if (separator == "/") {
|
||||
size_t pos = range_str.find(':');
|
||||
int begin_segment = 0;
|
||||
if (!parse_segment_number(range_str.substr(0, pos), &begin_segment)) {
|
||||
const std::string begin_str = range_str.substr(0, pos);
|
||||
if (!begin_str.empty() && !parse_segment_number(begin_str, &begin_segment)) {
|
||||
return {};
|
||||
}
|
||||
route.begin_segment = begin_segment;
|
||||
@@ -330,11 +333,7 @@ std::map<int, SegmentLogs> load_segments_from_json(const json11::Json &json) {
|
||||
}
|
||||
|
||||
std::map<int, SegmentLogs> load_segments_from_server(const RouteSelection &route) {
|
||||
// IQ.Pilot patch: replaces upstream PyDownloader::getRouteFiles with the existing
|
||||
// iqpilot CommaApi2 helper (same JSON shape: /v1/route/<name>/files).
|
||||
const std::string url = CommaApi2::BASE_URL + "/v1/route/" + route.canonical_name + "/files";
|
||||
long response_code = 0;
|
||||
const std::string result = CommaApi2::httpGet(url, &response_code);
|
||||
const std::string result = CommaApi2::getRouteFiles(route.canonical_name);
|
||||
if (result.empty()) throw std::runtime_error("Failed to fetch route files for " + route.canonical_name);
|
||||
|
||||
std::string parse_error;
|
||||
@@ -438,7 +437,7 @@ std::array<uint8_t, 3> parse_color(std::string_view color) {
|
||||
return out;
|
||||
}
|
||||
|
||||
uint8_t android_priority_to_level(uint8_t priority) {
|
||||
uint8_t operating_system_priority_to_level(uint8_t priority) {
|
||||
switch (priority) {
|
||||
case 2:
|
||||
case 3:
|
||||
@@ -497,7 +496,7 @@ void append_timeline_entry(std::vector<TimelineEntry> *timeline, double mono_tim
|
||||
});
|
||||
}
|
||||
|
||||
double android_wall_time_seconds(uint64_t timestamp) {
|
||||
double operating_system_wall_time_seconds(uint64_t timestamp) {
|
||||
if (timestamp == 0) return 0.0;
|
||||
if (timestamp > 1000000000000ULL) return static_cast<double>(timestamp) / 1.0e9;
|
||||
if (timestamp > 1000000000ULL) return static_cast<double>(timestamp) / 1.0e6;
|
||||
@@ -615,13 +614,15 @@ void append_log_event(cereal::Event::Which which,
|
||||
logs->push_back(std::move(entry));
|
||||
break;
|
||||
}
|
||||
// IQ.Pilot patch: upstream renamed cereal's androidLog -> operatingSystemLog (#38209);
|
||||
// iqpilot's cereal still carries the original field name. Only the accessor differs.
|
||||
case cereal::Event::Which::ANDROID_LOG: {
|
||||
const auto android = event.getAndroidLog();
|
||||
auto entry = make_entry(LogOrigin::Android, android_priority_to_level(android.getPriority()));
|
||||
entry.wall_time = android_wall_time_seconds(android.getTs());
|
||||
entry.source = android.hasTag() ? android.getTag().cStr() : "android";
|
||||
entry.message = android.hasMessage() ? android.getMessage().cStr() : std::string();
|
||||
entry.context = "pid=" + std::to_string(android.getPid()) + ", tid=" + std::to_string(android.getTid());
|
||||
const auto operating_system_log = event.getAndroidLog();
|
||||
auto entry = make_entry(LogOrigin::OperatingSystem, operating_system_priority_to_level(operating_system_log.getPriority()));
|
||||
entry.wall_time = operating_system_wall_time_seconds(operating_system_log.getTs());
|
||||
entry.source = operating_system_log.hasTag() ? operating_system_log.getTag().cStr() : "operating_system";
|
||||
entry.message = operating_system_log.hasMessage() ? operating_system_log.getMessage().cStr() : std::string();
|
||||
entry.context = "pid=" + std::to_string(operating_system_log.getPid()) + ", tid=" + std::to_string(operating_system_log.getTid());
|
||||
if (!entry.message.empty()) {
|
||||
std::string err;
|
||||
if (const auto p = json11::Json::parse(entry.message, err); err.empty() && p.is_object()) {
|
||||
@@ -629,10 +630,10 @@ void append_log_event(cereal::Event::Which which,
|
||||
if (p["SYSLOG_IDENTIFIER"].is_string() && !p["SYSLOG_IDENTIFIER"].string_value().empty())
|
||||
entry.source = p["SYSLOG_IDENTIFIER"].string_value();
|
||||
if (auto pri = json_int_value(p["PRIORITY"]); pri.has_value())
|
||||
entry.level = android_priority_to_level(*pri);
|
||||
entry.level = operating_system_priority_to_level(*pri);
|
||||
if (auto ts = json_u64_value(p["__REALTIME_TIMESTAMP"]); ts.has_value())
|
||||
entry.wall_time = android_wall_time_seconds(*ts);
|
||||
entry.context = format_journal_context(p, android.getPid(), android.getTid());
|
||||
entry.wall_time = operating_system_wall_time_seconds(*ts);
|
||||
entry.context = format_journal_context(p, operating_system_log.getPid(), operating_system_log.getTid());
|
||||
}
|
||||
}
|
||||
logs->push_back(std::move(entry));
|
||||
@@ -690,6 +691,25 @@ std::vector<LogEntry> extract_segment_logs(const std::vector<Event> &events) {
|
||||
return logs;
|
||||
}
|
||||
|
||||
std::vector<ThumbnailFrame> extract_segment_thumbnails(const std::vector<Event> &events, int segment) {
|
||||
std::vector<ThumbnailFrame> thumbnails;
|
||||
for (const Event &event_record : events) {
|
||||
if (event_record.which != cereal::Event::Which::THUMBNAIL) continue;
|
||||
with_parseable_event(event_record.data, [&](const cereal::Event::Reader &event) {
|
||||
const auto thumbnail = event.getThumbnail();
|
||||
const auto jpeg = thumbnail.getThumbnail();
|
||||
if (jpeg.size() == 0) return;
|
||||
const uint64_t timestamp = thumbnail.getTimestampEof();
|
||||
ThumbnailFrame frame;
|
||||
frame.timestamp = static_cast<double>(timestamp != 0 ? timestamp : event.getLogMonoTime()) / 1.0e9;
|
||||
frame.segment = segment;
|
||||
frame.jpeg.assign(jpeg.begin(), jpeg.end());
|
||||
thumbnails.push_back(std::move(frame));
|
||||
});
|
||||
}
|
||||
return thumbnails;
|
||||
}
|
||||
|
||||
RouteMetadata extract_segment_metadata(const std::vector<Event> &events) {
|
||||
RouteMetadata metadata;
|
||||
for (const Event &event_record : events) {
|
||||
@@ -802,6 +822,8 @@ Pane parse_dock_area(const json11::Json &dock_area_node) {
|
||||
const std::string kind = dock_area_node["kind"].string_value();
|
||||
if (kind == "map") {
|
||||
pane.kind = PaneKind::Map;
|
||||
} else if (kind == "thumbnail") {
|
||||
pane.kind = PaneKind::Thumbnail;
|
||||
} else if (kind == "camera") {
|
||||
pane.kind = PaneKind::Camera;
|
||||
const std::string camera_view = dock_area_node["camera_view"].string_value();
|
||||
@@ -910,7 +932,9 @@ void append_scalar_point(RouteSeries *series,
|
||||
series->values.push_back(value);
|
||||
}
|
||||
|
||||
void append_fixed_scalar_point(RouteSeries *series, double tm, double value) {
|
||||
// This has thousands of generated call sites. Inlining it duplicates vector
|
||||
// growth logic throughout the extractor and is slower both to compile and run.
|
||||
__attribute__((noinline)) void append_fixed_scalar_point(RouteSeries *series, double tm, double value) {
|
||||
series->times.push_back(tm);
|
||||
series->values.push_back(value);
|
||||
}
|
||||
@@ -1173,6 +1197,7 @@ RouteData build_route_data(std::vector<RouteSeries> &&series_list,
|
||||
std::vector<CanMessageData> &&can_messages,
|
||||
std::vector<LogEntry> &&logs,
|
||||
std::vector<TimelineEntry> &&timeline,
|
||||
std::vector<ThumbnailFrame> &&thumbnails,
|
||||
std::unordered_map<std::string, EnumInfo> &&enum_info,
|
||||
std::string car_fingerprint,
|
||||
std::string dbc_name) {
|
||||
@@ -1239,6 +1264,14 @@ RouteData build_route_data(std::vector<RouteSeries> &&series_list,
|
||||
route_data.x_min = timeline.front().start_time;
|
||||
route_data.x_max = timeline.back().end_time;
|
||||
}
|
||||
std::sort(thumbnails.begin(), thumbnails.end(), [](const ThumbnailFrame &a, const ThumbnailFrame &b) {
|
||||
return a.timestamp < b.timestamp;
|
||||
});
|
||||
if (!route_data.has_time_range && !thumbnails.empty()) {
|
||||
route_data.has_time_range = true;
|
||||
route_data.x_min = thumbnails.front().timestamp;
|
||||
route_data.x_max = thumbnails.back().timestamp;
|
||||
}
|
||||
|
||||
if (route_data.has_time_range) {
|
||||
const double time_offset = route_data.x_min;
|
||||
@@ -1260,6 +1293,9 @@ RouteData build_route_data(std::vector<RouteSeries> &&series_list,
|
||||
entry.start_time -= time_offset;
|
||||
entry.end_time -= time_offset;
|
||||
}
|
||||
for (ThumbnailFrame &thumbnail : thumbnails) {
|
||||
thumbnail.timestamp -= time_offset;
|
||||
}
|
||||
route_data.x_max -= time_offset;
|
||||
route_data.x_min = 0.0;
|
||||
}
|
||||
@@ -1277,6 +1313,7 @@ RouteData build_route_data(std::vector<RouteSeries> &&series_list,
|
||||
merged_timeline.push_back(std::move(entry));
|
||||
}
|
||||
route_data.timeline = std::move(merged_timeline);
|
||||
route_data.thumbnails = std::move(thumbnails);
|
||||
std::sort(can_messages.begin(), can_messages.end(), [](const CanMessageData &a, const CanMessageData &b) {
|
||||
return std::make_tuple(a.id.service, a.id.bus, a.id.address)
|
||||
< std::make_tuple(b.id.service, b.id.bus, b.id.address);
|
||||
@@ -1530,6 +1567,7 @@ LoadedRouteArtifacts load_route_series_parallel(
|
||||
SeriesAccumulator series;
|
||||
std::vector<LogEntry> logs;
|
||||
std::vector<TimelineEntry> timeline;
|
||||
std::vector<ThumbnailFrame> thumbnails;
|
||||
};
|
||||
|
||||
const std::vector<std::pair<int, SegmentLogs>> segment_list(segments.begin(), segments.end());
|
||||
@@ -1579,15 +1617,12 @@ LoadedRouteArtifacts load_route_series_parallel(
|
||||
continue;
|
||||
}
|
||||
|
||||
// IQ.Pilot patch: iqpilot's LogReader does not expose load-time telemetry
|
||||
// (download/decompress/parse seconds, compressed/decompressed sizes). Stub
|
||||
// these to 0 so the loader UI still renders without per-segment diagnostics.
|
||||
segment_stats.download_seconds = 0.0;
|
||||
segment_stats.decompress_seconds = 0.0;
|
||||
segment_stats.parse_seconds = 0.0;
|
||||
segment_stats.compressed_bytes = 0;
|
||||
segment_stats.decompressed_bytes = 0;
|
||||
stats->bytes_downloaded.fetch_add(0);
|
||||
segment_stats.download_seconds = reader.download_seconds();
|
||||
segment_stats.decompress_seconds = reader.decompress_seconds();
|
||||
segment_stats.parse_seconds = reader.parse_seconds();
|
||||
segment_stats.compressed_bytes = reader.compressed_size();
|
||||
segment_stats.decompressed_bytes = reader.decompressed_size();
|
||||
stats->bytes_downloaded.fetch_add(reader.compressed_size());
|
||||
stats->segments_downloaded.fetch_add(1);
|
||||
stats->publish(RouteLoadStage::DownloadingSegment, index, std::to_string(segment_number));
|
||||
|
||||
@@ -1595,6 +1630,7 @@ LoadedRouteArtifacts load_route_series_parallel(
|
||||
results[index].series = extract_segment_series(reader.events, schema, can_dbc, skip_raw_can, worker_budget, segment_workers);
|
||||
results[index].logs = extract_segment_logs(reader.events);
|
||||
results[index].timeline = extract_segment_timeline(reader.events);
|
||||
results[index].thumbnails = extract_segment_thumbnails(reader.events, segment_number);
|
||||
segment_stats.extract_seconds = std::chrono::duration<double>(LoadStats::Clock::now() - extract_start).count();
|
||||
segment_stats.event_count = reader.events.size();
|
||||
segment_stats.series_count = populated_series_count(results[index].series);
|
||||
@@ -1621,6 +1657,7 @@ LoadedRouteArtifacts load_route_series_parallel(
|
||||
}
|
||||
std::vector<LogEntry> logs;
|
||||
std::vector<TimelineEntry> timeline;
|
||||
std::vector<ThumbnailFrame> thumbnails;
|
||||
for (SegmentResult &result : results) {
|
||||
if (!result.logs.empty()) {
|
||||
logs.insert(logs.end(),
|
||||
@@ -1632,12 +1669,18 @@ LoadedRouteArtifacts load_route_series_parallel(
|
||||
std::make_move_iterator(result.timeline.begin()),
|
||||
std::make_move_iterator(result.timeline.end()));
|
||||
}
|
||||
if (!result.thumbnails.empty()) {
|
||||
thumbnails.insert(thumbnails.end(),
|
||||
std::make_move_iterator(result.thumbnails.begin()),
|
||||
std::make_move_iterator(result.thumbnails.end()));
|
||||
}
|
||||
}
|
||||
LoadedRouteArtifacts artifacts;
|
||||
artifacts.series = collect_series(std::move(merged));
|
||||
artifacts.can_messages = std::move(merged.can_messages);
|
||||
artifacts.logs = std::move(logs);
|
||||
artifacts.timeline = std::move(timeline);
|
||||
artifacts.thumbnails = std::move(thumbnails);
|
||||
artifacts.enum_info = std::move(merged.enum_info);
|
||||
stats->merge_end = LoadStats::Clock::now();
|
||||
return artifacts;
|
||||
@@ -1843,6 +1886,7 @@ RouteData load_route_data(const std::string &route_name,
|
||||
std::move(artifacts.can_messages),
|
||||
std::move(artifacts.logs),
|
||||
std::move(artifacts.timeline),
|
||||
std::move(artifacts.thumbnails),
|
||||
std::move(artifacts.enum_info),
|
||||
metadata.car_fingerprint,
|
||||
resolved_dbc);
|
||||
|
||||
53
tools/jotpluggler/test_jotpluggler.py
Normal file
53
tools/jotpluggler/test_jotpluggler.py
Normal file
@@ -0,0 +1,53 @@
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
JOTPLUGGLER_DIR = Path(__file__).parent
|
||||
JOTPLUGGLER_BIN = JOTPLUGGLER_DIR / "jotpluggler"
|
||||
|
||||
pytestmark = pytest.mark.skipif(not JOTPLUGGLER_BIN.exists(),
|
||||
reason="jotpluggler not built (scons -u tools/jotpluggler)")
|
||||
|
||||
|
||||
def run_jotpluggler(*args):
|
||||
return subprocess.run([str(JOTPLUGGLER_BIN), *args], cwd=JOTPLUGGLER_DIR,
|
||||
capture_output=True, text=True, timeout=60)
|
||||
|
||||
|
||||
class TestJotpluggler:
|
||||
def test_help(self):
|
||||
result = run_jotpluggler("-h")
|
||||
assert result.returncode == 0, result.stderr
|
||||
assert "Usage:" in result.stderr
|
||||
|
||||
def test_generated_dbcs_materialized(self):
|
||||
# the build materializes iqdbc's *_generated.dbc files; layout.cc and
|
||||
# sketch_layout.cc both resolve DBC names against this directory
|
||||
generated = JOTPLUGGLER_DIR / "generated_dbcs"
|
||||
assert generated.is_dir()
|
||||
assert list(generated.glob("*.dbc")), "no generated DBCs — iqdbc create_all did not run"
|
||||
|
||||
def test_car_fingerprint_header_uses_iqdbc_platforms(self):
|
||||
header = (JOTPLUGGLER_DIR / "car_fingerprint_to_dbc.h").read_text()
|
||||
assert "kCarFingerprintToDbc" in header
|
||||
assert "dbc_for_car_fingerprint" in header
|
||||
|
||||
def test_bootstrap_icons_vendored(self):
|
||||
# IQ.Pilot vendors the TTF because third_party/bootstrap is git-lfs; icons.cc
|
||||
# reads this exact path instead of upstream's BOOTSTRAP_ICONS_TTF define
|
||||
assert (JOTPLUGGLER_DIR / "assets" / "bootstrap-icons.ttf").is_file()
|
||||
|
||||
def test_no_comma_connect_links(self):
|
||||
# iqpilot routes live on konn3kt; the comma connect / useradmin buttons are gone
|
||||
common = (JOTPLUGGLER_DIR / "common.cc").read_text()
|
||||
assert "connect.comma.ai" not in common
|
||||
assert "useradmin.comma.ai" not in common
|
||||
assert "route_konn3kt_url" in common
|
||||
|
||||
def test_route_files_go_through_konn3kt_api(self):
|
||||
# upstream calls PyDownloader::getRouteFiles here; iqpilot has no py_downloader
|
||||
sketch = (JOTPLUGGLER_DIR / "sketch_layout.cc").read_text()
|
||||
assert "PyDownloader::" not in sketch
|
||||
assert "py_downloader.h" not in sketch
|
||||
assert "CommaApi2::getRouteFiles" in sketch
|
||||
253
tools/jotpluggler/thumbnail.cc
Normal file
253
tools/jotpluggler/thumbnail.cc
Normal file
@@ -0,0 +1,253 @@
|
||||
#include "tools/jotpluggler/thumbnail.h"
|
||||
|
||||
#include "imgui_impl_opengl3_loader.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
|
||||
extern "C" {
|
||||
#include <libavcodec/avcodec.h>
|
||||
#include <libavutil/pixfmt.h>
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
bool decode_jpeg(const std::vector<uint8_t> &jpeg, int *width, int *height, std::vector<uint8_t> *rgba) {
|
||||
if (jpeg.empty()) return false;
|
||||
|
||||
const AVCodec *codec = avcodec_find_decoder(AV_CODEC_ID_MJPEG);
|
||||
AVCodecContext *context = codec != nullptr ? avcodec_alloc_context3(codec) : nullptr;
|
||||
AVFrame *frame = av_frame_alloc();
|
||||
AVPacket *packet = av_packet_alloc();
|
||||
if (context == nullptr || frame == nullptr || packet == nullptr) {
|
||||
av_packet_free(&packet);
|
||||
av_frame_free(&frame);
|
||||
avcodec_free_context(&context);
|
||||
return false;
|
||||
}
|
||||
|
||||
const bool packet_ready = jpeg.size() <= static_cast<size_t>(std::numeric_limits<int>::max())
|
||||
&& av_new_packet(packet, static_cast<int>(jpeg.size())) >= 0;
|
||||
if (packet_ready) {
|
||||
std::copy(jpeg.begin(), jpeg.end(), packet->data);
|
||||
}
|
||||
const bool decoded = packet_ready
|
||||
&& avcodec_open2(context, codec, nullptr) >= 0
|
||||
&& avcodec_send_packet(context, packet) >= 0
|
||||
&& avcodec_receive_frame(context, frame) >= 0;
|
||||
if (!decoded || frame->width <= 0 || frame->height <= 0) {
|
||||
av_packet_free(&packet);
|
||||
av_frame_free(&frame);
|
||||
avcodec_free_context(&context);
|
||||
return false;
|
||||
}
|
||||
|
||||
int chroma_x_shift = 0;
|
||||
int chroma_y_shift = 0;
|
||||
switch (static_cast<AVPixelFormat>(frame->format)) {
|
||||
case AV_PIX_FMT_YUV420P:
|
||||
case AV_PIX_FMT_YUVJ420P:
|
||||
chroma_x_shift = 1;
|
||||
chroma_y_shift = 1;
|
||||
break;
|
||||
case AV_PIX_FMT_YUV422P:
|
||||
case AV_PIX_FMT_YUVJ422P:
|
||||
chroma_x_shift = 1;
|
||||
break;
|
||||
case AV_PIX_FMT_YUV444P:
|
||||
case AV_PIX_FMT_YUVJ444P:
|
||||
break;
|
||||
default:
|
||||
av_packet_free(&packet);
|
||||
av_frame_free(&frame);
|
||||
avcodec_free_context(&context);
|
||||
return false;
|
||||
}
|
||||
|
||||
*width = frame->width;
|
||||
*height = frame->height;
|
||||
rgba->resize(static_cast<size_t>(*width) * static_cast<size_t>(*height) * 4U);
|
||||
const bool full_range = frame->color_range == AVCOL_RANGE_JPEG
|
||||
|| frame->format == AV_PIX_FMT_YUVJ420P
|
||||
|| frame->format == AV_PIX_FMT_YUVJ422P
|
||||
|| frame->format == AV_PIX_FMT_YUVJ444P;
|
||||
for (int y = 0; y < *height; ++y) {
|
||||
const uint8_t *y_row = frame->data[0] + y * frame->linesize[0];
|
||||
const uint8_t *u_row = frame->data[1] + (y >> chroma_y_shift) * frame->linesize[1];
|
||||
const uint8_t *v_row = frame->data[2] + (y >> chroma_y_shift) * frame->linesize[2];
|
||||
uint8_t *out = rgba->data() + static_cast<size_t>(y) * static_cast<size_t>(*width) * 4U;
|
||||
for (int x = 0; x < *width; ++x) {
|
||||
const double luma = full_range ? static_cast<double>(y_row[x])
|
||||
: 1.164383 * (static_cast<double>(y_row[x]) - 16.0);
|
||||
const double u = static_cast<double>(u_row[x >> chroma_x_shift]) - 128.0;
|
||||
const double v = static_cast<double>(v_row[x >> chroma_x_shift]) - 128.0;
|
||||
const double red = luma + (full_range ? 1.402 : 1.596027) * v;
|
||||
const double green = luma - (full_range ? 0.344136 : 0.391762) * u
|
||||
- (full_range ? 0.714136 : 0.812968) * v;
|
||||
const double blue = luma + (full_range ? 1.772 : 2.017232) * u;
|
||||
out[x * 4 + 0] = static_cast<uint8_t>(std::clamp(std::lround(red), 0L, 255L));
|
||||
out[x * 4 + 1] = static_cast<uint8_t>(std::clamp(std::lround(green), 0L, 255L));
|
||||
out[x * 4 + 2] = static_cast<uint8_t>(std::clamp(std::lround(blue), 0L, 255L));
|
||||
out[x * 4 + 3] = 255;
|
||||
}
|
||||
}
|
||||
|
||||
av_packet_free(&packet);
|
||||
av_frame_free(&frame);
|
||||
avcodec_free_context(&context);
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string format_thumbnail_time(double seconds) {
|
||||
const int rounded = std::max(0, static_cast<int>(std::lround(seconds)));
|
||||
const int hours = rounded / 3600;
|
||||
const int minutes = (rounded % 3600) / 60;
|
||||
const int secs = rounded % 60;
|
||||
if (hours > 0) {
|
||||
return util::string_format("%d:%02d:%02d", hours, minutes, secs);
|
||||
}
|
||||
return util::string_format("%02d:%02d", minutes, secs);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
struct ThumbnailView::Impl {
|
||||
~Impl() {
|
||||
destroy_texture();
|
||||
}
|
||||
|
||||
void setThumbnails(const std::vector<ThumbnailFrame> &next_thumbnails) {
|
||||
destroy_texture();
|
||||
thumbnails = &next_thumbnails;
|
||||
displayed_index = -1;
|
||||
failed_index = -1;
|
||||
}
|
||||
|
||||
void update(double tracker_time) {
|
||||
if (thumbnails == nullptr || thumbnails->empty()) return;
|
||||
auto it = std::lower_bound(thumbnails->begin(), thumbnails->end(), tracker_time,
|
||||
[](const ThumbnailFrame &frame, double time) {
|
||||
return frame.timestamp < time;
|
||||
});
|
||||
if (it == thumbnails->end()) {
|
||||
it = std::prev(thumbnails->end());
|
||||
} else if (it != thumbnails->begin()) {
|
||||
const auto previous = std::prev(it);
|
||||
if (std::abs(previous->timestamp - tracker_time) <= std::abs(it->timestamp - tracker_time)) {
|
||||
it = previous;
|
||||
}
|
||||
}
|
||||
const int index = static_cast<int>(std::distance(thumbnails->begin(), it));
|
||||
if (index == displayed_index || index == failed_index) return;
|
||||
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
std::vector<uint8_t> rgba;
|
||||
if (!decode_jpeg(it->jpeg, &width, &height, &rgba)) {
|
||||
failed_index = index;
|
||||
return;
|
||||
}
|
||||
|
||||
if (texture == 0) {
|
||||
glGenTextures(1, &texture);
|
||||
}
|
||||
glBindTexture(GL_TEXTURE_2D, texture);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||||
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, rgba.data());
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
texture_width = width;
|
||||
texture_height = height;
|
||||
displayed_index = index;
|
||||
failed_index = -1;
|
||||
}
|
||||
|
||||
void drawSized(ImVec2 size, bool loading) const {
|
||||
size.x = std::max(1.0f, size.x);
|
||||
size.y = std::max(1.0f, size.y);
|
||||
ImGui::InvisibleButton("##thumbnail_sized", size);
|
||||
const ImVec2 pane_min = ImGui::GetItemRectMin();
|
||||
const ImVec2 pane_max = ImGui::GetItemRectMax();
|
||||
ImDrawList *draw_list = ImGui::GetWindowDrawList();
|
||||
draw_list->AddRectFilled(pane_min, pane_max, IM_COL32(24, 24, 24, 255));
|
||||
|
||||
if (texture != 0 && texture_width > 0 && texture_height > 0) {
|
||||
const float scale = std::min(size.x / static_cast<float>(texture_width),
|
||||
size.y / static_cast<float>(texture_height));
|
||||
const ImVec2 image_size(static_cast<float>(texture_width) * scale,
|
||||
static_cast<float>(texture_height) * scale);
|
||||
const ImVec2 image_min(pane_min.x + (size.x - image_size.x) * 0.5f,
|
||||
pane_min.y + (size.y - image_size.y) * 0.5f);
|
||||
const ImVec2 image_max(image_min.x + image_size.x, image_min.y + image_size.y);
|
||||
draw_list->AddImage(static_cast<ImTextureID>(texture), image_min, image_max);
|
||||
|
||||
if (thumbnails != nullptr && displayed_index >= 0
|
||||
&& displayed_index < static_cast<int>(thumbnails->size())) {
|
||||
const ThumbnailFrame &frame = (*thumbnails)[static_cast<size_t>(displayed_index)];
|
||||
const std::string label = util::string_format("%s · segment %d · %d/%zu",
|
||||
format_thumbnail_time(frame.timestamp).c_str(),
|
||||
frame.segment,
|
||||
displayed_index + 1,
|
||||
thumbnails->size());
|
||||
const ImVec2 text_size = ImGui::CalcTextSize(label.c_str());
|
||||
const ImVec2 label_min(image_min.x, std::max(image_min.y, image_max.y - text_size.y - 14.0f));
|
||||
draw_list->AddRectFilled(label_min, image_max, IM_COL32(0, 0, 0, 175));
|
||||
draw_list->AddText(ImVec2(label_min.x + 7.0f, label_min.y + 7.0f), IM_COL32_WHITE, label.c_str());
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const bool has_thumbnails = thumbnails != nullptr && !thumbnails->empty();
|
||||
const char *label = loading ? "loading" : (has_thumbnails ? "invalid thumbnail" : "no thumbnails");
|
||||
const ImVec2 text_size = ImGui::CalcTextSize(label);
|
||||
draw_list->AddText(ImVec2(pane_min.x + (size.x - text_size.x) * 0.5f,
|
||||
pane_min.y + (size.y - text_size.y) * 0.5f),
|
||||
IM_COL32(187, 187, 187, 255), label);
|
||||
}
|
||||
|
||||
void destroy_texture() {
|
||||
if (texture != 0) {
|
||||
glDeleteTextures(1, &texture);
|
||||
}
|
||||
texture = 0;
|
||||
texture_width = 0;
|
||||
texture_height = 0;
|
||||
}
|
||||
|
||||
const std::vector<ThumbnailFrame> *thumbnails = nullptr;
|
||||
int displayed_index = -1;
|
||||
int failed_index = -1;
|
||||
GLuint texture = 0;
|
||||
int texture_width = 0;
|
||||
int texture_height = 0;
|
||||
};
|
||||
|
||||
ThumbnailView::ThumbnailView() : impl_(std::make_unique<Impl>()) {}
|
||||
ThumbnailView::~ThumbnailView() = default;
|
||||
|
||||
void ThumbnailView::setThumbnails(const std::vector<ThumbnailFrame> &thumbnails) {
|
||||
impl_->setThumbnails(thumbnails);
|
||||
}
|
||||
|
||||
void ThumbnailView::update(double tracker_time) {
|
||||
impl_->update(tracker_time);
|
||||
}
|
||||
|
||||
void ThumbnailView::drawSized(ImVec2 size, bool loading) {
|
||||
impl_->drawSized(size, loading);
|
||||
}
|
||||
|
||||
void draw_thumbnail_pane(AppSession *session, UiState *state) {
|
||||
if (session->thumbnail_view == nullptr) {
|
||||
ImGui::TextDisabled("Thumbnails unavailable");
|
||||
return;
|
||||
}
|
||||
if (state->has_tracker_time) {
|
||||
session->thumbnail_view->update(state->tracker_time);
|
||||
}
|
||||
session->thumbnail_view->drawSized(ImGui::GetContentRegionAvail(), session->async_route_loading);
|
||||
}
|
||||
5
tools/jotpluggler/thumbnail.h
Normal file
5
tools/jotpluggler/thumbnail.h
Normal file
@@ -0,0 +1,5 @@
|
||||
#pragma once
|
||||
|
||||
#include "tools/jotpluggler/app.h"
|
||||
|
||||
void draw_thumbnail_pane(AppSession *session, UiState *state);
|
||||
Reference in New Issue
Block a user