Skip to content

Commit c8a1b5a

Browse files
Add bookmark system & Integrate toast message into thunderscope (UBC-Thunderbots#3418)
* add stubs and new proto * investigate missing logs * add listener to bookmark * work? * refactor proto_player.py * finish refactoring for proto_player.py * refactor bookmark visual * display bookmark visual * fix time stamp calculation * fix bookmark visual * update documentations * use kwargs as argument list * optimize bookmark button * [pre-commit.ci lite] apply automatic fixes * add documentation * add shortcut doc * add toast msg dependency * [pre-commit.ci lite] apply automatic fixes * build toast helper and apply * [pre-commit.ci lite] apply automatic fixes * upgrade pyqt6-qt * fix index test * add _ms suffix * adjust offset * add None as return type * remove magic number * [pre-commit.ci lite] apply automatic fixes --------- Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
1 parent f794df7 commit c8a1b5a

26 files changed

+399
-60
lines changed

environment_setup/ubuntu20_requirements.txt

+1
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@ python-Levenshtein==0.25.1
88
psutil==5.9.0
99
PyOpenGL==3.1.6
1010
ruff==0.5.5
11+
pyqt-toast-notification==1.3.2

environment_setup/ubuntu22_requirements.txt

+1
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,4 @@ psutil==5.9.0
99
PyOpenGL==3.1.6
1010
numpy==1.26.4
1111
ruff==0.5.5
12+
pyqt-toast-notification==1.3.2

environment_setup/ubuntu24_requirements.txt

+1
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@ python-Levenshtein==0.25.1
66
psutil==5.9.0
77
PyOpenGL==3.1.6
88
ruff==0.5.5
9+
pyqt-toast-notification==1.3.2

src/proto/BUILD

+2
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ proto_library(
1616
"play.proto",
1717
"power_frame_msg.proto",
1818
"primitive.proto",
19+
"replay_bookmark.proto",
1920
"robot_crash_msg.proto",
2021
"robot_log_msg.proto",
2122
"robot_statistic.proto",
@@ -55,6 +56,7 @@ py_proto_library(
5556
"parameters.proto",
5657
"play.proto",
5758
"primitive.proto",
59+
"replay_bookmark.proto",
5860
"robot_statistic.proto",
5961
"robot_status_msg.proto",
6062
"tactic.proto",

src/proto/replay_bookmark.proto

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
syntax = "proto3";
2+
3+
package TbotsProto;
4+
5+
import "proto/tbots_timestamp_msg.proto";
6+
7+
message ReplayBookmark
8+
{
9+
Timestamp timestamp = 1;
10+
}

src/software/backend/unix_simulator_backend.cpp

+5
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,11 @@ UnixSimulatorBackend::UnixSimulatorBackend(
5151
robot_crash_listener.reset(new ThreadedProtoUnixListener<TbotsProto::RobotCrash>(
5252
runtime_dir + ROBOT_CRASH_PATH, [](TbotsProto::RobotCrash& v) {}, proto_logger));
5353

54+
replay_bookmark_listener.reset(
55+
new ThreadedProtoUnixListener<TbotsProto::ReplayBookmark>(
56+
runtime_dir + REPLAY_BOOKMARK_PATH, [](TbotsProto::ReplayBookmark& v) {},
57+
proto_logger));
58+
5459
// Protobuf Outputs
5560
world_output.reset(new ThreadedProtoUnixSender<TbotsProto::World>(
5661
runtime_dir + WORLD_PATH, proto_logger));

src/software/backend/unix_simulator_backend.h

+4
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,14 @@
33
#include <mutex>
44

55
#include "proto/parameters.pb.h"
6+
#include "proto/replay_bookmark.pb.h"
67
#include "proto/robot_crash_msg.pb.h"
78
#include "proto/robot_log_msg.pb.h"
89
#include "proto/robot_status_msg.pb.h"
910
#include "proto/sensor_msg.pb.h"
1011
#include "proto/tbots_software_msgs.pb.h"
1112
#include "proto/validation.pb.h"
13+
#include "proto/world.pb.h"
1214
#include "software/backend/backend.h"
1315
#include "software/logger/proto_logger.h"
1416
#include "software/networking/unix/threaded_proto_unix_listener.hpp"
@@ -50,6 +52,8 @@ class UnixSimulatorBackend : public Backend, public Subject<TbotsProto::Thunderb
5052
std::unique_ptr<ThreadedProtoUnixListener<TbotsProto::RobotLog>> robot_log_listener;
5153
std::unique_ptr<ThreadedProtoUnixListener<TbotsProto::RobotCrash>>
5254
robot_crash_listener;
55+
std::unique_ptr<ThreadedProtoUnixListener<TbotsProto::ReplayBookmark>>
56+
replay_bookmark_listener;
5357

5458
// Outputs
5559
std::unique_ptr<ThreadedProtoUnixSender<TbotsProto::World>> world_output;

src/software/constants.h

+1
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ const std::string SIMULATOR_STATE_PATH = "/simulator_state";
3232
const std::string VALIDATION_PROTO_SET_PATH = "/validation_proto_set";
3333
const std::string ROBOT_LOG_PATH = "/robot_log";
3434
const std::string ROBOT_CRASH_PATH = "/robot_crash";
35+
const std::string REPLAY_BOOKMARK_PATH = "/replay_bookmark";
3536
const std::string DYNAMIC_PARAMETER_UPDATE_REQUEST_PATH = "/dynamic_parameter_request";
3637
const std::string DYNAMIC_PARAMETER_UPDATE_RESPONSE_PATH = "/dynamic_parameter_response";
3738
const std::string WORLD_STATE_RECEIVED_TRIGGER_PATH = "/world_state_received_trigger";

src/software/py_constants.cpp

+1
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ PYBIND11_MODULE(py_constants, m)
6363
m.attr("VALIDATION_PROTO_SET_PATH") = VALIDATION_PROTO_SET_PATH;
6464
m.attr("ROBOT_LOG_PATH") = ROBOT_LOG_PATH;
6565
m.attr("ROBOT_CRASH_PATH") = ROBOT_CRASH_PATH;
66+
m.attr("REPLAY_BOOKMARK_PATH") = REPLAY_BOOKMARK_PATH;
6667
m.attr("UNIX_BUFFER_SIZE") = UNIX_BUFFER_SIZE;
6768
m.attr("DYNAMIC_PARAMETER_UPDATE_REQUEST_PATH") =
6869
DYNAMIC_PARAMETER_UPDATE_REQUEST_PATH;

src/software/thunderscope/binary_context_managers/full_system.py

+1
Original file line numberDiff line numberDiff line change
@@ -216,5 +216,6 @@ def setup_proto_unix_io(self, proto_unix_io: ProtoUnixIO) -> None:
216216
(VALIDATION_PROTO_SET_PATH, ValidationProtoSet),
217217
(ROBOT_LOG_PATH, RobotLog),
218218
(ROBOT_CRASH_PATH, RobotCrash),
219+
(REPLAY_BOOKMARK_PATH, ReplayBookmark),
219220
]:
220221
proto_unix_io.attach_unix_sender(self.full_system_runtime_dir, *arg)

src/software/thunderscope/common/BUILD

+8
Original file line numberDiff line numberDiff line change
@@ -44,3 +44,11 @@ py_library(
4444
":frametime_counter",
4545
],
4646
)
47+
48+
py_library(
49+
name = "toast_msg_helper",
50+
srcs = ["toast_msg_helper.py"],
51+
deps = [
52+
requirement("pyqt-toast-notification"),
53+
],
54+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
from PyQt6.QtWidgets import QWidget
2+
from pyqttoast import Toast, ToastPreset
3+
4+
5+
def show_toast(
6+
parent: QWidget, title: str, text: str, timeout_ms: int, preset: ToastPreset
7+
) -> None:
8+
"""Display a toast message with the given properties
9+
:param parent: parent widget to inject toast message component
10+
:param title: title of toast message
11+
:param text: text of toast message
12+
:param timeout_ms: duration of the message (in ms)
13+
:param preset: preset of the message box
14+
"""
15+
toast = Toast(parent)
16+
toast.setDuration(timeout_ms)
17+
toast.setTitle(title)
18+
toast.setText(text)
19+
toast.applyPreset(preset)
20+
toast.setShowCloseButton(False)
21+
toast.setBorderRadius(10)
22+
toast.setShowDurationBar(False)
23+
toast.show()
24+
25+
26+
def success_toast(parent: QWidget, text: str, timeout_ms: int = 1000) -> None:
27+
"""Display a success toast message
28+
:param parent: parent widget to inject toast message component
29+
:param text: text of toast message
30+
:param timeout_ms: duration of the message (in ms)
31+
"""
32+
show_toast(parent, "", text, timeout_ms, ToastPreset.SUCCESS_DARK)
33+
34+
35+
def failure_toast(parent: QWidget, text: str, timeout_ms: int = 1000) -> None:
36+
"""Display a failure toast message
37+
:param parent: parent widget to inject toast message component
38+
:param text: text of toast message
39+
:param timeout_ms: duration of the message (in ms)
40+
"""
41+
show_toast(parent, "", text, timeout_ms, ToastPreset.ERROR_DARK)
42+
43+
44+
def info_toast(parent: QWidget, text: str, timeout_ms: int = 1000) -> None:
45+
"""Display an information toast message
46+
:param parent: parent widget to inject toast message component
47+
:param text: text of toast message
48+
:param timeout_ms: duration of the message (in ms)
49+
"""
50+
show_toast(parent, "", text, timeout_ms, ToastPreset.INFORMATION_DARK)
51+
52+
53+
def warning_toast(parent: QWidget, text: str, timeout_ms: int = 1000) -> None:
54+
"""Display a warning toast message
55+
:param parent: parent widget to inject toast message component
56+
:param text: text of toast message
57+
:param timeout_ms: duration of the message (in ms)
58+
"""
59+
show_toast(parent, "", text, timeout_ms, ToastPreset.WARNING_DARK)

src/software/thunderscope/constants.py

+1
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@ class EstopMode(IntEnum):
146146
<b><code>O:</code></b> Identify robots, toggle robot name visibility<br>
147147
<b><code>M:</code></b> Toggle measure mode<br>
148148
<b><code>S:</code></b> Toggle visibility of robot/ball speed visualization<br>
149+
<b><code>B:</code></b> Add a bookmark<br>
149150
<b><code>Ctrl + Space:</code></b> Stop AI vs AI simulation<br>
150151
<b><code>Ctrl + Up:</code></b> Increment simulation speed<br>
151152
<b><code>Ctrl + Down:</code></b> Decrement simulation speed<br>

src/software/thunderscope/gl/BUILD

+1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ py_library(
77
srcs = ["gl_widget.py"],
88
deps = [
99
"//software/thunderscope:proto_unix_io",
10+
"//software/thunderscope/common:toast_msg_helper",
1011
"//software/thunderscope/gl/helpers:extended_gl_view_widget",
1112
"//software/thunderscope/gl/layers:gl_layer",
1213
"//software/thunderscope/gl/layers:gl_measure_layer",

src/software/thunderscope/gl/gl_widget.py

+18
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import time
2+
13
import pyqtgraph as pg
24
from pyqtgraph.Qt import QtCore, QtGui
35
from pyqtgraph.Qt.QtCore import Qt
@@ -22,6 +24,10 @@
2224
)
2325
from software.thunderscope.thread_safe_buffer import ThreadSafeBuffer
2426
from proto.world_pb2 import SimulationState
27+
from proto.replay_bookmark_pb2 import ReplayBookmark
28+
from proto.tbots_timestamp_msg_pb2 import Timestamp
29+
30+
from software.thunderscope.common.toast_msg_helper import success_toast
2531

2632

2733
class GLWidget(QWidget):
@@ -95,6 +101,8 @@ def __init__(
95101
layers_menu=self.layers_menu,
96102
toolbars_menu=self.toolbars_menu,
97103
sandbox_mode=sandbox_mode,
104+
replay_mode=player is not None,
105+
on_add_bookmark=self.add_bookmark,
98106
)
99107

100108
# Setup gamecontroller toolbar
@@ -121,6 +129,7 @@ def __init__(
121129
self.layers = []
122130

123131
self.set_camera_view(CameraView.LANDSCAPE_HIGH_ANGLE)
132+
self.proto_unix_io = proto_unix_io
124133

125134
def get_sim_control_toolbar(self):
126135
"""Returns the simulation control toolbar"""
@@ -367,3 +376,12 @@ def __calc_orthographic_distance(self) -> float:
367376
distance *= half_x_length_with_buffer
368377

369378
return distance
379+
380+
def add_bookmark(self):
381+
"""Handler for clicking 'add bookmark' button"""
382+
timestamp = time.time()
383+
bookmark = ReplayBookmark(
384+
timestamp=Timestamp(epoch_timestamp_seconds=timestamp)
385+
)
386+
self.proto_unix_io.send_proto(ReplayBookmark, bookmark)
387+
success_toast(self.parentWidget(), "Added bookmark!")

src/software/thunderscope/gl/widgets/gl_field_toolbar.py

+18
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ def __init__(
2828
layers_menu: QMenu,
2929
toolbars_menu: QMenu,
3030
sandbox_mode: bool = False,
31+
replay_mode: bool = False,
32+
on_add_bookmark=Callable[[], None],
3133
):
3234
"""Set up the toolbar with these buttons:
3335
@@ -38,13 +40,16 @@ def __init__(
3840
- Help
3941
- Measure Mode Toggle
4042
- Camera View Select menu
43+
- Add bookmark
4144
4245
:param parent: the parent to overlay this toolbar over
4346
:param on_camera_view_change: the callback function for when the camera view is changed
4447
:param on_measure_mode: the callback function for when measure mode is toggled
4548
:param layers_menu: the QMenu for the layers menu selection
4649
:param toolbars_menu: the QMenu for the toolbars menu selection
4750
:param sandbox_mode: if sandbox mode should be enabled
51+
:param replay_mode: if replay mode is enabled
52+
:param on_add_bookmark: the callback function when adding a bookmark
4853
"""
4954
super(GLFieldToolbar, self).__init__(parent=parent)
5055

@@ -114,6 +119,15 @@ def __init__(
114119
self.toolbars_button.setMenu(toolbars_menu)
115120
self.toolbars_button.setStyleSheet(self.get_button_style())
116121

122+
if not replay_mode:
123+
self.bookmark_button = QPushButton()
124+
self.bookmark_button.setIcon(
125+
icons.get_bookmark_icon(self.BUTTON_ICON_COLOR)
126+
)
127+
self.bookmark_button.setShortcut("b")
128+
self.bookmark_button.setStyleSheet(self.get_button_style())
129+
self.bookmark_button.clicked.connect(on_add_bookmark)
130+
117131
# Setup simulation speed button and menu
118132
self.sim_speed_menu = QMenu()
119133
self.sim_speed_button = QPushButton()
@@ -161,10 +175,14 @@ def __init__(
161175
self.layout().addWidget(self.undo_button)
162176
self.layout().addWidget(self.pause_button)
163177
self.layout().addWidget(self.redo_button)
178+
164179
self.layout().addWidget(self.help_button)
165180
self.layout().addWidget(self.measure_button)
166181
self.layout().addWidget(self.camera_view_button)
167182

183+
if not replay_mode:
184+
self.layout().addWidget(self.bookmark_button)
185+
168186
def refresh(self) -> None:
169187
"""Refreshes the UI for all the toolbar icons and updates toolbar position"""
170188
# update the pause button state

src/software/thunderscope/gl/widgets/toolbar_icons/sandbox_mode/BUILD

+1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ py_library(
66
name = "icon_loader",
77
srcs = ["icon_loader.py"],
88
data = [
9+
":bookmark.svg",
910
":help.svg",
1011
":measure.svg",
1112
":pause.svg",
Loading

src/software/thunderscope/gl/widgets/toolbar_icons/sandbox_mode/icon_loader.py

+16
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ class GLFieldToolbarIconLoader:
1818
RESET_ICON = None
1919
VIEW_ICON = None
2020
MEASURE_ICON = None
21+
BOOKMARK_ICON = None
2122

2223

2324
def get_undo_icon(color: str) -> QtGui.QPixmap:
@@ -138,3 +139,18 @@ def get_measure_icon(color: str) -> QtGui.QPixmap:
138139
)
139140

140141
return GLFieldToolbarIconLoader.MEASURE_ICON
142+
143+
144+
def get_bookmark_icon(color: str) -> QtGui.QPixmap:
145+
"""Loads the Bookmark icon pixmap as a GLFieldToolbarIconLoader attribute
146+
147+
:param color: the color the icon should be initialized with if not already created
148+
:return: the icon pixmap
149+
"""
150+
if not GLFieldToolbarIconLoader.BOOKMARK_ICON:
151+
GLFieldToolbarIconLoader.BOOKMARK_ICON = get_icon(
152+
"software/thunderscope/gl/widgets/toolbar_icons/sandbox_mode/bookmark.svg",
153+
color,
154+
)
155+
156+
return GLFieldToolbarIconLoader.BOOKMARK_ICON

src/software/thunderscope/replay/BUILD

+8
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,18 @@ py_library(
2626
],
2727
deps = [
2828
requirement("pyqtgraph"),
29+
"bookmark_marker",
2930
":proto_player",
3031
],
3132
)
3233

34+
py_library(
35+
name = "bookmark_marker",
36+
srcs = [
37+
"bookmark_marker.py",
38+
],
39+
)
40+
3341
py_binary(
3442
name = "replay_file_debugging_script",
3543
srcs = [

0 commit comments

Comments
 (0)