Skip to content

Commit

Permalink
Save Proto Log (#3225)
Browse files Browse the repository at this point in the history
  • Loading branch information
Mr-Anyone authored Jul 10, 2024
1 parent fc912f9 commit dea10f8
Show file tree
Hide file tree
Showing 3 changed files with 206 additions and 17 deletions.
203 changes: 194 additions & 9 deletions src/software/thunderscope/common/proto_configuration_widget.py
Original file line number Diff line number Diff line change
@@ -1,32 +1,37 @@
from pyqtgraph.Qt import QtCore, QtGui
import os
from proto.import_all_protos import *
from software.py_constants import MILLISECONDS_PER_SECOND
from software.thunderscope.constants import ProtoConfigurationConstant
import logging
from pyqtgraph.Qt.QtCore import QTimer
from pyqtgraph.Qt.QtWidgets import *
from pyqtgraph import parametertree
from google.protobuf.json_format import MessageToDict
from thefuzz import fuzz
from proto.import_all_protos import *
from software.thunderscope.common import proto_parameter_tree_util
from typing import Any, Callable
from PyQt6.QtWidgets import *


class ProtoConfigurationWidget(QWidget):

"""Creates a searchable parameter widget that can take any protobuf,
and convert it into a pyqtgraph ParameterTree. This will allow users
to modify the values.
"""

def __init__(
self, proto_to_configure, on_change_callback, search_filter_threshold=60,
self,
on_change_callback: Callable[[Any, Any, ThunderbotsConfig], None],
is_yellow: bool,
search_filter_threshold: int = 60,
):
"""Create a parameter widget given a protobuf
NOTE: This class handles the ParameterRangeOptions
:param proto_to_configure: The protobuf we would like to generate
a parameter tree for. This should be
populated with the default values
:param on_change_callback: The callback to trigger on change
args: name, updated_value, updated_proto
:param is_yellow: whether we are editing yellow or blue team's configuration
:param search_filter_threshold: How close should the search query be?
100 is an exact match (not ideal), 0 lets everything through
Expand All @@ -36,13 +41,18 @@ def __init__(
self.setLayout(layout)

self.on_change_callback = on_change_callback
self.proto_to_configure = proto_to_configure

# Create search query bar
self.search_query = QLineEdit()
self.search_query.setPlaceholderText("Search Parameters")

self.search_query.textChanged.connect(self.__handle_search_query_changed)
self.search_filter_threshold = search_filter_threshold

self.is_yellow = is_yellow
self.path_to_file = ProtoConfigurationConstant.DEFAULT_SAVE_PATH
self.update_proto_from_file(self.path_to_file)

# Create ParameterGroup from Protobuf
self.param_group = parametertree.Parameter.create(
name="params",
Expand All @@ -58,9 +68,184 @@ def __init__(
self.param_group.sigTreeStateChanged.connect(self.__handle_parameter_changed)
self.param_tree.setAlternatingRowColors(False)

self.save_hbox_top, self.save_hbox_bottom = self.create_widget()

layout.addLayout(self.save_hbox_top)
layout.addLayout(self.save_hbox_bottom)

layout.addWidget(self.search_query)
layout.addWidget(self.param_tree)

self.run_onetime_async(3, self.send_proto_to_fullsystem)

def run_onetime_async(self, time_in_seconds: float, func: Callable):
"""
starting a timer that runs a given function after a given
amount of seconds one time asynchronously.
:time_in_seconds: the amount of time in seconds
:func: the function that is going to be ran
"""
self.timer = QTimer()
self.timer.setSingleShot(True)
self.timer.timeout.connect(func)
self.timer.start(time_in_seconds * MILLISECONDS_PER_SECOND)

def create_widget(self):
"""
Creating widgets that are used to load, save parameters
:return: the top of the layout, the bottom of the layout, and edit box widget
"""
save_button = QPushButton("Save Parameters")
load_proto_button = QPushButton("Load Parameters")
reset_button = QPushButton("Reset All Parameters")

reset_button.clicked.connect(self.reset_button_callback)
save_button.clicked.connect(self.save_proto_callback)
load_proto_button.clicked.connect(self.load_proto_with_file_explorer)

save_hbox_bottom = QHBoxLayout()
save_hbox_bottom.addWidget(load_proto_button)
save_hbox_bottom.addWidget(reset_button)
save_hbox_bottom.addWidget(save_button)

save_hbox_top = QHBoxLayout()

return save_hbox_top, save_hbox_bottom

def update_proto_from_file(self, path_to_file: str):
"""
load the protobuf from path_to_file to the variable self.proto_to_configure
:param path_to_file: the path to the ThunderbotsConfig proto
"""
if not os.path.isfile(path_to_file):
logging.info(
f"No previously saved ThunderbotsConfig found. Creating a new default one."
)
self.proto_to_configure = ThunderbotsConfig()
self.proto_to_configure.sensor_fusion_config.friendly_color_yellow = (
self.is_yellow
)

self.build_proto(self.proto_to_configure)
self.save_current_config_to_file(self.path_to_file)
return

logging.info("Loading protobuf from file: {}".format(path_to_file))
with open(path_to_file, "rb") as f:
self.proto_to_configure = ThunderbotsConfig()
self.proto_to_configure.ParseFromString(f.read())

self.proto_to_configure.sensor_fusion_config.friendly_color_yellow = (
self.is_yellow
)

def send_proto_to_fullsystem(self):
"""
Sending the default configuration protobufs to unix_full_system at startup.
Also updates the widget at the same time.
"""
self.update_proto_from_file(self.path_to_file)
self.on_change_callback(None, None, self.proto_to_configure)
self.update_widget()

def save_proto_callback(self):
"""
This is a callback for a button that save the current protobuf to disk!
"""
try:
save_to_path, should_save = QFileDialog.getSaveFileName(
self,
"Select Protobufs",
ProtoConfigurationConstant.DEFAULT_SAVE_PATH,
options=QFileDialog.Option.DontUseNativeDialog,
)

if not should_save:
return

self.save_current_config_to_file(save_to_path)
self.update_widget()
except Exception:
logging.warning("cannot save configuration to {}".format(save_to_path))

def save_current_config_to_file(self, path_to_file):
"""
This save the self.proto_to_configure to path_to_file
:param path_to_file: the path to file that we are saving to.
"""

logging.info("writing to file {}".format(path_to_file))
path_to_directory = os.path.dirname(path_to_file)
os.makedirs(path_to_directory, exist_ok=True)

with open(path_to_file, "wb") as f:
f.write(self.proto_to_configure.SerializeToString())

def load_proto_with_file_explorer(self):
"""
loading the current protobuf through file explorer
"""
try:
path_to_file, should_open = QFileDialog.getOpenFileName(
self,
"Select Protobufs",
ProtoConfigurationConstant.DEFAULT_SAVE_PATH,
options=QFileDialog.Option.DontUseNativeDialog,
)
if not should_open:
return

self.path_to_file = path_to_file
self.update_proto_from_file(path_to_file)

self.update_widget()
# this callback send the proto to unix full system
self.on_change_callback(None, None, self.proto_to_configure)
except Exception as e:
logging.warning(
"cannot load configuration from {}. Error: {} Are you sure it is a configuration proto?".format(
path_to_file, e
)
)

def update_widget(self):
"""
The following function updates the current parameters tree based on the
current protobuf that is being configured (i.e. self.proto_to_configure) widget.
"""

# refreshing widgets after the parameters is called
self.param_group = parametertree.Parameter.create(
name="params",
type="group",
children=self.config_proto_to_param_dict(self.proto_to_configure),
)

self.param_tree.setParameters(self.param_group, showTop=False)
self.param_group.sigTreeStateChanged.connect(self.__handle_parameter_changed)
self.param_tree.setAlternatingRowColors(False)

def reset_button_callback(self):
"""
resetting the protobufs when the reset button has been clicked!
"""

self.proto_to_configure = ThunderbotsConfig()
self.proto_to_configure.sensor_fusion_config.friendly_color_yellow = (
self.is_yellow
)

self.path_to_file = ProtoConfigurationConstant.DEFAULT_SAVE_PATH

self.build_proto(self.proto_to_configure)
self.update_widget()

self.on_change_callback(None, None, self.proto_to_configure)

def __handle_search_query_changed(self, search_term):
"""Given a new search term, reconfigure the parameter tree with parameters
that match the term.
Expand Down
5 changes: 5 additions & 0 deletions src/software/thunderscope/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,11 @@ class TrailValues:
DEFAULT_TRAIL_SAMPLING_RATE = 0


class ProtoConfigurationConstant:
DEFAULT_SAVE_DIRECTORY = "/opt/tbotspython/thunderbots_configuration_proto"
DEFAULT_SAVE_PATH = DEFAULT_SAVE_DIRECTORY + "/default_configuration.proto"


class CustomGLOptions:
"""
Custom OpenGL Rendering modes that could be used in addition to
Expand Down
15 changes: 7 additions & 8 deletions src/software/thunderscope/widget_setup_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -203,21 +203,20 @@ def setup_parameter_widget(
) -> ProtoConfigurationWidget:
"""Setup the parameter widget
:param proto_unix_io: The proto unix io object
:param friendly_colour_yellow:
:returns: The proto configuration widget
:param proto_unix_io: The proto unix io object
:param friendly_colour_yellow:
:returns: The proto configuration widget
"""

config = ThunderbotsConfig()
config.sensor_fusion_config.friendly_color_yellow = friendly_colour_yellow

def on_change_callback(
attr: Any, value: Any, updated_proto: ThunderbotsConfig
) -> None:
proto_unix_io.send_proto(ThunderbotsConfig, updated_proto)

return ProtoConfigurationWidget(config, on_change_callback)
return ProtoConfigurationWidget(
on_change_callback, is_yellow=friendly_colour_yellow
)


def setup_log_widget(proto_unix_io: ProtoUnixIO) -> g3logWidget:
Expand Down Expand Up @@ -274,7 +273,7 @@ def setup_play_info(proto_unix_io: ProtoUnixIO) -> PlayInfoWidget:


def setup_fps_widget(bufferswap_counter, refresh_func_counter):
""" setup fps widget
"""setup fps widget
:param bufferswap_counter: a counter at the bufferswap
:param refresh_func_counter: a counter at the refresh function
:returns: a FPS Widget
Expand Down

0 comments on commit dea10f8

Please sign in to comment.