Skip to content

Commit 0224f7c

Browse files
xela-95GiulioRomualdinicktrem
authored
Add real-time capability to visualizer (part 1) #100
Co-authored-by: Giulio Romualdi <giulio.romualdi@gmail.com> Co-authored-by: Nick <nicholas.tremaroli@iit.it>
1 parent 946aae6 commit 0224f7c

11 files changed

Lines changed: 783 additions & 102 deletions

File tree

robot_log_visualizer/__main__.py

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,11 @@
55
# Released under the terms of the BSD 3-Clause License
66

77
import sys
8-
import os
98

109
# GUI
1110
from robot_log_visualizer.ui.gui import RobotViewerMainWindow
1211
from PyQt5.QtWidgets import QApplication
1312

14-
from robot_log_visualizer.file_reader.signal_provider import SignalProvider
15-
1613
# Meshcat
1714
from robot_log_visualizer.robot_visualizer.meshcat_provider import MeshcatProvider
1815

@@ -24,29 +21,21 @@ def main():
2421
"plot_animation": 0.03,
2522
}
2623

27-
# instantiate device_manager
28-
signal_provider = SignalProvider(period=thread_periods["signal_provider"])
29-
30-
meshcat_provider = MeshcatProvider(
31-
period=thread_periods["meshcat_provider"], signal_provider=signal_provider
32-
)
24+
meshcat_provider = MeshcatProvider(period=thread_periods["meshcat_provider"])
3325

3426
# instantiate a QApplication
3527
app = QApplication(sys.argv)
3628

3729
# instantiate the main window
3830
gui = RobotViewerMainWindow(
39-
signal_provider=signal_provider,
31+
signal_provider_period=thread_periods["signal_provider"],
4032
meshcat_provider=meshcat_provider,
4133
animation_period=thread_periods["plot_animation"],
4234
)
4335

4436
# show the main window
4537
gui.show()
4638

47-
signal_provider.start()
48-
meshcat_provider.start()
49-
5039
return app.exec_()
5140

5241

robot_log_visualizer/plotter/pyqtgraph_viewer_canvas.py

Lines changed: 72 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,14 @@
44

55
from __future__ import annotations
66

7-
from typing import Dict, Tuple, Sequence, Iterable
7+
from typing import Dict, Iterable, Sequence, Tuple
88

9-
from PyQt5 import QtCore, QtWidgets # type: ignore
10-
import pyqtgraph as pg # type: ignore
119
import numpy as np
10+
import pyqtgraph as pg # type: ignore
11+
from PyQt5 import QtCore, QtWidgets # type: ignore
1212

1313
from robot_log_visualizer.plotter.color_palette import ColorPalette
14+
from robot_log_visualizer.signal_provider.signal_provider import ProviderType
1415

1516
# ------------------------------------------------------------------------
1617
# Type aliases
@@ -31,7 +32,6 @@ class PyQtGraphViewerCanvas(QtWidgets.QWidget):
3132
def __init__(
3233
self,
3334
parent: QtWidgets.QWidget | None,
34-
signal_provider,
3535
period: float,
3636
*,
3737
click_radius: float | None = None,
@@ -41,16 +41,14 @@ def __init__(
4141
4242
Args:
4343
parent: Parent widget or *None*.
44-
signal_provider: Object exposing ``data``, ``initial_time``,
45-
``end_time`` and a **dynamic** ``current_time`` attribute.
4644
period: Update period for the vertical line (seconds).
4745
click_radius: Override :pyattr:`DEFAULT_RADIUS`.
4846
marker_size: Override :pyattr:`DEFAULT_MARKER_SIZE`.
4947
"""
5048
super().__init__(parent)
5149

5250
# injected dependencies
53-
self._signal_provider = signal_provider
51+
self._signal_provider = None
5452
self._period_ms: int = int(period * 1000)
5553
self._click_radius: float = click_radius or self.DEFAULT_RADIUS
5654
self._marker_size: int = marker_size or self.DEFAULT_MARKER_SIZE
@@ -66,21 +64,58 @@ def __init__(
6664
self._connect_signals()
6765

6866
# -------------------------------------------------------------#
69-
# Public API (called from the outside) #
67+
# Public API (called from the outside) #
7068
# -------------------------------------------------------------#
69+
70+
def set_signal_provider(self, signal_provider) -> None:
71+
"""Set the signal provider to fetch data from.
72+
73+
Args:
74+
signal_provider: An instance of `SignalProvider`.
75+
"""
76+
77+
if signal_provider is None:
78+
return
79+
80+
self._signal_provider = signal_provider
81+
82+
# Connect to real-time updates for real-time provider
83+
if self._signal_provider.provider_type == ProviderType.REALTIME:
84+
self._signal_provider.update_index_signal.connect(
85+
self._update_realtime_curves
86+
)
87+
7188
def update_plots(self, paths: Sequence[Path], legends: Sequence[Legend]) -> None:
7289
"""Synchronise plots with the *paths* list.
7390
7491
New items are added, disappeared items removed. Existing ones are
7592
left untouched to avoid flicker.
7693
"""
94+
if self._signal_provider is None:
95+
return
96+
97+
# For real-time provider, update the set of selected signals to buffer
98+
if self._signal_provider.provider_type == ProviderType.REALTIME:
99+
selected_keys = ["::".join(path) for path in paths]
100+
self._signal_provider.add_signals_to_buffer(selected_keys)
101+
77102
self._add_missing_curves(paths, legends)
78103
self._remove_obsolete_curves(paths)
79-
# Always show the full time span
80-
self._plot.setXRange(
81-
0.0,
82-
self._signal_provider.end_time - self._signal_provider.initial_time,
83-
)
104+
105+
# Set the X axis range based on the provider type
106+
if self._signal_provider.provider_type == ProviderType.REALTIME:
107+
# For real-time data, show a fixed window with 0 set at the right edge for the latest data
108+
self._plot.setXRange(-self._signal_provider.realtime_fixed_plot_window, 0.0)
109+
# Disable mouse panning on x axis
110+
self._plot.plotItem.vb.setMouseEnabled(x=False, y=True)
111+
# For real-time data enable autoscaling of Y axis
112+
self._plot.plotItem.vb.enableAutoRange(axis=pg.ViewBox.YAxis, enable=True)
113+
else:
114+
# Default behavior
115+
self._plot.setXRange(
116+
0.0,
117+
self._signal_provider.end_time - self._signal_provider.initial_time,
118+
)
84119

85120
# The following trio is wired to whoever controls the replay/stream
86121
def pause_animation(self) -> None: # noqa: D401
@@ -173,8 +208,32 @@ def _remove_obsolete_curves(self, paths: Sequence[Path]) -> None:
173208

174209
def _update_vline(self) -> None:
175210
"""Move the vertical line to ``current_time``."""
211+
if self._signal_provider is None:
212+
return
213+
176214
self._vline.setValue(self._signal_provider.current_time)
177215

216+
def _update_realtime_curves(self):
217+
"""Update all curves with the latest data from the signal provider."""
218+
219+
if self._signal_provider is None:
220+
return
221+
for key, curve in self._curves.items():
222+
# Drill down to the data array using the path
223+
path = key.split("/")
224+
data = self._signal_provider.data
225+
for subkey in path[:-1]:
226+
data = data[subkey]
227+
try:
228+
y = data["data"][:, int(path[-1])]
229+
except (IndexError, ValueError):
230+
y = data["data"][:]
231+
232+
# Set the 0 of the x axis to the latest timestamp
233+
latest_time = data["timestamps"][-1] if len(data["timestamps"]) > 0 else 0
234+
x = data["timestamps"] - latest_time
235+
curve.setData(x, y)
236+
178237
def _on_mouse_click(self, event) -> None: # noqa: N802
179238
"""Handle a left‑click: select or unselect the nearest data point."""
180239
if event.button() != QtCore.Qt.MouseButton.LeftButton:

robot_log_visualizer/robot_visualizer/meshcat_provider.py

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,21 @@
22
# This software may be modified and distributed under the terms of the
33
# Released under the terms of the BSD 3-Clause License
44

5-
from PyQt5.QtCore import QThread, QMutex, QMutexLocker
6-
75
import os
86
import re
9-
from pathlib import Path
10-
11-
import numpy as np
127
import time
8+
from pathlib import Path
139

1410
import idyntree.swig as idyn
11+
import numpy as np
1512
from idyntree.visualize import MeshcatVisualizer
13+
from PyQt5.QtCore import QMutex, QMutexLocker, QThread
1614

1715
from robot_log_visualizer.utils.utils import PeriodicThreadState
1816

1917

2018
class MeshcatProvider(QThread):
21-
def __init__(self, signal_provider, period):
19+
def __init__(self, period):
2220
QThread.__init__(self)
2321

2422
self._state = PeriodicThreadState.pause
@@ -29,12 +27,17 @@ def __init__(self, signal_provider, period):
2927
self.meshcat_visualizer_mutex = QMutex()
3028

3129
self._is_model_loaded = False
32-
self._signal_provider = signal_provider
30+
self._signal_provider = None
3331

3432
self.model_path = ""
3533
self.custom_package_dir = ""
3634
self.base_frame = ""
37-
self.env_list = ["GAZEBO_MODEL_PATH", "ROS_PACKAGE_PATH", "AMENT_PREFIX_PATH"]
35+
self.env_list = [
36+
"GAZEBO_MODEL_PATH",
37+
"GZ_SIM_RESOURCE_PATH",
38+
"ROS_PACKAGE_PATH",
39+
"AMENT_PREFIX_PATH",
40+
]
3841
self._registered_3d_points = set()
3942
self._registered_3d_trajectories = dict()
4043
self._register_3d_arrow = set()
@@ -87,6 +90,9 @@ def unregister_3d_arrow(self, arrow_path):
8790
self._register_3d_arrow.remove(arrow_path)
8891
self._meshcat_visualizer.delete(shape_name=arrow_path)
8992

93+
def set_signal_provider(self, signal_provider):
94+
self._signal_provider = signal_provider
95+
9096
def load_model(self, considered_joints, model_name, base_frame=None):
9197
def get_model_path_from_envs(env_list):
9298
return [
@@ -148,7 +154,7 @@ def find_model_joints(model_name, considered_joints):
148154
model_filenames = [
149155
folder_model_path / Path(f)
150156
for f in os.listdir(folder_model_path.absolute())
151-
if re.search("[a-zA-Z0-9_]*\.urdf", f)
157+
if re.search(r"[a-zA-Z0-9_]*\.urdf", f)
152158
]
153159

154160
if model_filenames:
@@ -158,6 +164,9 @@ def find_model_joints(model_name, considered_joints):
158164

159165
# If the model is not found we exit
160166
if not model_found_in_env_folders:
167+
print(
168+
f"MeshcatProvider: Unable to find the model {model_name} in the environment folders."
169+
)
161170
return False
162171

163172
self.model_joints_index = find_model_joints(
@@ -234,6 +243,9 @@ def run(self):
234243

235244
index = self._signal_provider.index
236245
if self.state == PeriodicThreadState.running and self._is_model_loaded:
246+
if len(self._signal_provider) == 0:
247+
time.sleep(self._period)
248+
continue
237249
robot_state = self._signal_provider.get_robot_state_at_index(index)
238250
self.meshcat_visualizer_mutex.lock()
239251
# These are the robot measured joint positions in radians
File renamed without changes.

0 commit comments

Comments
 (0)