openpilot is an open source driver assistance system. openpilot performs the functions of Automated Lane Centering and Adaptive Cruise Control for over 200 supported car makes and models.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

349 lines
13 KiB

3 weeks ago
import os
import re
import uuid
import threading
import numpy as np
from collections import deque
3 weeks ago
import dearpygui.dearpygui as dpg
from abc import ABC, abstractmethod
from openpilot.tools.jotpluggler.data import DataManager
3 weeks ago
class ViewPanel(ABC):
"""Abstract base class for all view panels that can be displayed in a plot container"""
def __init__(self, panel_id: str = None):
self.panel_id = panel_id or str(uuid.uuid4())
self.title = "Untitled Panel"
@abstractmethod
def create_ui(self, parent_tag: str):
pass
@abstractmethod
def destroy_ui(self):
pass
@abstractmethod
def get_panel_type(self) -> str:
pass
class TimeSeriesPanel(ViewPanel):
3 weeks ago
def __init__(self, data_manager: DataManager, playback_manager, panel_id: str | None = None):
3 weeks ago
super().__init__(panel_id)
self.data_manager = data_manager
self.playback_manager = playback_manager
self.title = "Time Series Plot"
3 weeks ago
self.plot_tag: str | None = None
self.x_axis_tag: str | None = None
self.y_axis_tag: str | None = None
self.timeline_indicator_tag: str | None = None
3 weeks ago
self._ui_created = False
self._series_data: dict[str, tuple] = {}
self.data_manager.add_observer(self.on_data_loaded)
3 weeks ago
def create_ui(self, parent_tag: str):
self.plot_tag = f"plot_{self.panel_id}"
self.x_axis_tag = f"{self.plot_tag}_x_axis"
self.y_axis_tag = f"{self.plot_tag}_y_axis"
self.timeline_indicator_tag = f"{self.plot_tag}_timeline"
with dpg.plot(height=-1, width=-1, tag=self.plot_tag, parent=parent_tag, drop_callback=self._on_series_drop, payload_type="TIMESERIES_PAYLOAD"):
dpg.add_plot_legend()
dpg.add_plot_axis(dpg.mvXAxis, no_label=True, tag=self.x_axis_tag)
dpg.add_plot_axis(dpg.mvYAxis, no_label=True, tag=self.y_axis_tag)
3 weeks ago
timeline_series_tag = dpg.add_inf_line_series(x=[0], label="Timeline", parent=self.y_axis_tag, tag=self.timeline_indicator_tag)
dpg.bind_item_theme(timeline_series_tag, "global_timeline_theme")
for series_path in list(self._series_data.keys()):
self.add_series(series_path)
3 weeks ago
self._ui_created = True
def update_timeline_indicator(self, current_time_s: float):
if not self._ui_created or not dpg.does_item_exist(self.timeline_indicator_tag):
return
dpg.set_value(self.timeline_indicator_tag, [[current_time_s], [0]])
3 weeks ago
for series_path, (rel_time_array, value_array) in self._series_data.items():
position = np.searchsorted(rel_time_array, current_time_s, side='right') - 1
value = None
3 weeks ago
if position >= 0 and (current_time_s - rel_time_array[position]) <= 1.0:
value = value_array[position]
3 weeks ago
if value is not None:
if isinstance(value, float):
formatted_value = f"{value:.4f}" if abs(value) < 1000 else f"{value:.3e}"
else:
formatted_value = str(value)
3 weeks ago
series_tag = f"series_{self.panel_id}_{series_path}"
legend_label = f"{series_path}: {formatted_value}"
3 weeks ago
if dpg.does_item_exist(series_tag):
dpg.configure_item(series_tag, label=legend_label)
3 weeks ago
def add_series(self, series_path: str, update: bool = False) -> bool:
if update or series_path not in self._series_data:
self._series_data[series_path] = self.data_manager.get_timeseries(series_path)
3 weeks ago
rel_time_array, value_array = self._series_data[series_path]
series_tag = f"series_{self.panel_id}_{series_path}"
3 weeks ago
if dpg.does_item_exist(series_tag):
dpg.set_value(series_tag, [rel_time_array, value_array])
else:
line_series_tag = dpg.add_line_series(x=rel_time_array, y=value_array, label=series_path, parent=self.y_axis_tag, tag=series_tag)
dpg.bind_item_theme(line_series_tag, "global_line_theme")
dpg.fit_axis_data(self.x_axis_tag)
dpg.fit_axis_data(self.y_axis_tag)
3 weeks ago
return True
def destroy_ui(self):
if self.plot_tag and dpg.does_item_exist(self.plot_tag):
dpg.delete_item(self.plot_tag)
self._ui_created = False
def get_panel_type(self) -> str:
return "timeseries"
def clear_all_series(self):
for series_path in list(self._series_data.keys()):
3 weeks ago
self.remove_series(series_path)
def remove_series(self, series_path: str):
if series_path in self._series_data:
series_tag = f"series_{self.panel_id}_{series_path}"
3 weeks ago
if dpg.does_item_exist(series_tag):
dpg.delete_item(series_tag)
del self._series_data[series_path]
3 weeks ago
def on_data_loaded(self, data: dict):
for series_path in list(self._series_data.keys()):
self.add_series(series_path, update=True)
3 weeks ago
def _on_series_drop(self, sender, app_data, user_data):
self.add_series(app_data)
3 weeks ago
class DataTreeNode:
def __init__(self, name: str, full_path: str = ""):
self.name = name
self.full_path = full_path
3 weeks ago
self.children: dict[str, DataTreeNode] = {}
3 weeks ago
self.is_leaf = False
self.child_count = 0
2 weeks ago
self.is_plottable_cached: bool | None = None
self.ui_created = False
self.ui_tag: str | None = None
3 weeks ago
class DataTreeView:
MAX_ITEMS_PER_FRAME = 50
3 weeks ago
def __init__(self, data_manager: DataManager, ui_lock: threading.Lock):
self.data_manager = data_manager
self.ui_lock = ui_lock
self.current_search = ""
self.data_tree = DataTreeNode(name="root")
self.ui_render_queue: deque[tuple[DataTreeNode, str, str, bool]] = deque() # (node, parent_tag, search_term, is_leaf)
self.visible_expanded_nodes: set[str] = set()
self.created_leaf_paths: set[str] = set()
self._all_paths_cache: list[str] = []
self._previous_paths_set: set[str] = set()
self.data_manager.add_observer(self._on_data_loaded)
def _on_data_loaded(self, data: dict):
with self.ui_lock:
if data.get('segment_added'):
current_paths = set(self.data_manager.get_all_paths())
new_paths = current_paths - self._previous_paths_set
if new_paths:
self._all_paths_cache = list(current_paths)
if not self._previous_paths_set:
self._populate_tree()
else:
self._add_paths_to_tree(new_paths, incremental=True)
self._previous_paths_set = current_paths.copy()
def _populate_tree(self):
self._clear_ui()
search_term = self.current_search.strip().lower()
self.data_tree = self._add_paths_to_tree(self._all_paths_cache, incremental=False)
for child in sorted(self.data_tree.children.values(), key=self._natural_sort_key):
self.ui_render_queue.append((child, "data_tree_container", search_term, child.is_leaf))
def _add_paths_to_tree(self, paths, incremental=False):
search_term = self.current_search.strip().lower()
filtered_paths = [path for path in paths if self._should_show_path(path, search_term)]
2 weeks ago
target_tree = self.data_tree if incremental else DataTreeNode(name="root")
if not filtered_paths:
2 weeks ago
return target_tree
nodes_to_update = set() if incremental else None
for path in sorted(filtered_paths):
parts = path.split('/')
current_node = target_tree
current_path_prefix = ""
for i, part in enumerate(parts):
current_path_prefix = f"{current_path_prefix}/{part}" if current_path_prefix else part
if part not in current_node.children:
current_node.children[part] = DataTreeNode(name=part, full_path=current_path_prefix)
if incremental:
nodes_to_update.add(current_node)
current_node = current_node.children[part]
if incremental and i < len(parts) - 1:
nodes_to_update.add(current_node)
if not current_node.is_leaf:
current_node.is_leaf = True
if incremental:
nodes_to_update.add(current_node)
self._calculate_child_counts(target_tree)
if incremental:
self._queue_new_ui_items(filtered_paths, search_term)
return target_tree
def _queue_new_ui_items(self, new_paths, search_term):
for path in new_paths:
parts = path.split('/')
parent_path = '/'.join(parts[:-1]) if len(parts) > 1 else ""
if parent_path == "" or parent_path in self.visible_expanded_nodes:
parent_tag = "data_tree_container" if parent_path == "" else f"tree_{parent_path}"
if dpg.does_item_exist(parent_tag):
node = self.data_tree
for part in parts:
node = node.children[part]
self.ui_render_queue.append((node, parent_tag, search_term, True))
def update_frame(self):
items_processed = 0
while self.ui_render_queue and items_processed < self.MAX_ITEMS_PER_FRAME: # process up to MAX_ITEMS_PER_FRAME to maintain perforamnce
node, parent_tag, search_term, is_leaf = self.ui_render_queue.popleft()
if is_leaf:
self._create_leaf_ui(node, parent_tag)
else:
self._create_node_ui(node, parent_tag, search_term)
items_processed += 1
3 weeks ago
def search_data(self, search_term: str):
self.current_search = search_term
self._all_paths_cache = self.data_manager.get_all_paths()
self._previous_paths_set = set(self._all_paths_cache) # Reset tracking after search
self._populate_tree()
3 weeks ago
def _clear_ui(self):
dpg.delete_item("data_tree_container", children_only=True)
self.ui_render_queue.clear()
self.visible_expanded_nodes.clear()
self.created_leaf_paths.clear()
def _calculate_child_counts(self, node: DataTreeNode):
if node.is_leaf:
node.child_count = 0
else:
node.child_count = len(node.children)
for child in node.children.values():
self._calculate_child_counts(child)
3 weeks ago
def _create_node_ui(self, node: DataTreeNode, parent_tag: str, search_term: str):
if node.is_leaf:
self._create_leaf_ui(node, parent_tag)
else:
self._create_tree_node_ui(node, parent_tag, search_term)
3 weeks ago
def _create_tree_node_ui(self, node: DataTreeNode, parent_tag: str, search_term: str):
if not dpg.does_item_exist(parent_tag):
return
node_tag = f"tree_{node.full_path}"
node.ui_tag = node_tag
3 weeks ago
label = f"{node.name} ({node.child_count} fields)"
should_open = (bool(search_term) and len(search_term) > 1 and any(search_term in path for path in self._get_descendant_paths(node)))
3 weeks ago
with dpg.tree_node(label=label, parent=parent_tag, tag=node_tag, default_open=should_open, open_on_arrow=True, open_on_double_click=True) as tree_node:
with dpg.item_handler_registry() as handler:
dpg.add_item_toggled_open_handler(callback=lambda s, d, u: self._on_node_expanded(node, search_term))
dpg.bind_item_handler_registry(tree_node, handler)
3 weeks ago
node.ui_created = True
3 weeks ago
if should_open:
self.visible_expanded_nodes.add(node.full_path)
self._queue_children(node, node_tag, search_term)
3 weeks ago
def _create_leaf_ui(self, node: DataTreeNode, parent_tag: str):
if not dpg.does_item_exist(parent_tag):
return
half_split_size = dpg.get_item_rect_size("data_pool_window")[0] // 2
with dpg.group(parent=parent_tag, horizontal=True, xoffset=half_split_size, tag=f"group_{node.full_path}") as draggable_group:
dpg.add_text(node.name)
dpg.add_text("N/A", tag=f"value_{node.full_path}")
2 weeks ago
if node.is_plottable_cached is None:
node.is_plottable_cached = self.data_manager.is_plottable(node.full_path)
2 weeks ago
if node.is_plottable_cached:
with dpg.drag_payload(parent=draggable_group, drag_data=node.full_path, payload_type="TIMESERIES_PAYLOAD"):
dpg.add_text(f"Plot: {node.full_path}")
3 weeks ago
node.ui_created = True
node.ui_tag = f"value_{node.full_path}"
self.created_leaf_paths.add(node.full_path)
def _queue_children(self, node: DataTreeNode, parent_tag: str, search_term: str):
for child in sorted(node.children.values(), key=self._natural_sort_key):
self.ui_render_queue.append((child, parent_tag, search_term, child.is_leaf))
def _on_node_expanded(self, node: DataTreeNode, search_term: str):
node_tag = f"tree_{node.full_path}"
if not dpg.does_item_exist(node_tag):
return
is_expanded = dpg.get_value(node_tag)
if is_expanded:
if node.full_path not in self.visible_expanded_nodes:
self.visible_expanded_nodes.add(node.full_path)
self._queue_children(node, node_tag, search_term)
else:
self.visible_expanded_nodes.discard(node.full_path)
self._remove_children_from_queue(node.full_path)
def _remove_children_from_queue(self, collapsed_node_path: str):
2 weeks ago
new_queue: deque[tuple] = deque()
for node, parent_tag, search_term, is_leaf in self.ui_render_queue:
# Keep items that are not children of the collapsed node
if not node.full_path.startswith(collapsed_node_path + "/"):
new_queue.append((node, parent_tag, search_term, is_leaf))
self.ui_render_queue = new_queue
def _should_show_path(self, path: str, search_term: str) -> bool:
if 'DEPRECATED' in path and not os.environ.get('SHOW_DEPRECATED'):
return False
return not search_term or search_term in path.lower()
3 weeks ago
def _natural_sort_key(self, node: DataTreeNode):
node_type_key = node.is_leaf
parts = [int(p) if p.isdigit() else p.lower() for p in re.split(r'(\d+)', node.name) if p]
return (node_type_key, parts)
3 weeks ago
def _get_descendant_paths(self, node: DataTreeNode):
3 weeks ago
for child_name, child_node in node.children.items():
child_name_lower = child_name.lower()
if child_node.is_leaf:
yield child_name_lower
else:
for path in self._get_descendant_paths(child_node):
3 weeks ago
yield f"{child_name_lower}/{path}"