Loading files
ece376249This file is pinned to the selected release. Download the original to open it in your design tool.
"""
Tinymovr GUI Window
Copyright Ioannis Chatzikonstantinou 2020-2023
The GUI Window class that subclasses QMainWindow
This program is free software: you can redistribute it and/or modify it under
the terms of the GNU General Public License as published by the Free Software
Foundation, either version 3 of the License, or (at your option) any later
version.
This program is distributed in the hope that it will be useful, but WITHOUT
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with
this program. If not, see <http://www.gnu.org/licenses/>.
"""
import time
import pkg_resources
from contextlib import suppress
import json
from PySide6 import QtCore
from PySide6.QtCore import Signal, QTimer
from PySide6.QtWidgets import (
QMainWindow,
QMenu,
QMenuBar,
QWidget,
QFrame,
QHBoxLayout,
QVBoxLayout,
QHeaderView,
QLabel,
QMessageBox,
)
from PySide6.QtGui import QAction
import pyqtgraph as pg
from tinymovr.constants import app_name
from tinymovr.channel import ResponseError as ChannelResponseError
from tinymovr.config import get_bus_config, configure_logging
from avlos import get_registry
from avlos.json_codec import AvlosEncoder
from tinymovr.gui import (
NodeTreeWidgetItem,
AttrTreeWidgetItem,
FuncTreeWidgetItem,
OptionsTreeWidgetItem,
Worker,
PlaceholderQTreeWidget,
format_value,
display_file_open_dialog,
display_file_save_dialog,
magnitude_of,
check_selected_items,
)
class MainWindow(QMainWindow):
TreeItemCheckedSignal = Signal(dict)
def __init__(self, app, arguments):
super(MainWindow, self).__init__()
# set units default format
get_registry().default_format = ".6f~"
self.start_time = time.time()
self.logger = configure_logging()
self.attr_widgets_by_id = {}
self.graphs_by_id = {}
self.setWindowTitle(app_name)
# Create a menu bar, menus and actions
self.menu_bar = QMenuBar()
self.file_menu = QMenu("File")
self.help_menu = QMenu("Help")
self.export_action = QAction("Export Config...", self)
self.import_action = QAction("Import Config", self)
self.about_action = QAction("About", self)
self.export_action.triggered.connect(self.on_export)
self.import_action.triggered.connect(self.on_import)
self.about_action.triggered.connect(self.show_about_box)
self.file_menu.addAction(self.export_action)
self.file_menu.addAction(self.import_action)
self.help_menu.addAction(self.about_action)
self.menu_bar.addMenu(self.file_menu)
self.menu_bar.addMenu(self.help_menu)
self.setMenuBar(self.menu_bar)
# Setup the tree widget
self.tree_widget = PlaceholderQTreeWidget()
self.tree_widget.itemChanged.connect(self.item_changed)
self.tree_widget.setHeaderLabels(["Attribute", "Value"])
self.status_label = QLabel()
self.status_label.setStyleSheet("margin: 5px;")
self.left_frame = QFrame(self)
self.left_layout = QVBoxLayout()
self.left_layout.addWidget(self.tree_widget)
self.left_layout.addWidget(self.status_label)
self.left_layout.setSpacing(0)
self.left_layout.setContentsMargins(0, 0, 0, 0)
self.left_frame.setLayout(self.left_layout)
self.left_frame.setMinimumWidth(320)
self.left_frame.setMaximumWidth(420)
self.left_frame.setStyleSheet("border:0;")
self.right_frame = QFrame(self)
self.right_layout = QVBoxLayout()
self.right_layout.setSpacing(0)
self.right_layout.setContentsMargins(0, 0, 0, 0)
self.right_frame.setLayout(self.right_layout)
main_layout = QHBoxLayout()
main_layout.addWidget(self.left_frame)
main_layout.addWidget(self.right_frame)
main_layout.setSpacing(0)
main_layout.setContentsMargins(0, 0, 0, 0)
main_widget = QWidget()
main_widget.setLayout(main_layout)
main_widget.setMinimumHeight(600)
self.setCentralWidget(main_widget)
self.timeout_count = 0
buses = arguments["--bus"].rsplit(sep=",")
channel = arguments["--chan"]
bitrate = int(arguments["--bitrate"])
self.max_timeouts = int(arguments["--max-timeouts"])
if channel == None:
params = get_bus_config(buses)
params["bitrate"] = bitrate
else:
params = {"bustype": buses[0], "channel": channel, "bitrate": bitrate}
self.worker = Worker(params, self.logger)
self.TreeItemCheckedSignal.connect(self.worker.update_active_attrs)
self.worker.handleErrorSignal.connect(
self.handle_worker_error, QtCore.Qt.QueuedConnection
)
self.worker.regenSignal.connect(self.regen_tree, QtCore.Qt.QueuedConnection)
self.worker.updateAttrsSignal.connect(
self.attrs_updated, QtCore.Qt.QueuedConnection
)
self.worker.updateTimingsSignal.connect(
self.timings_updated, QtCore.Qt.QueuedConnection
)
app.aboutToQuit.connect(self.about_to_quit)
self.timer = QTimer(self)
self.timer.timeout.connect(self.worker._update)
self.timer.start(40)
@QtCore.Slot()
def about_to_quit(self):
self.timer.stop()
self.worker.stop()
@QtCore.Slot()
def handle_worker_error(self, e):
if isinstance(e, ChannelResponseError):
if self.timeout_count > self.max_timeouts:
self.timeout_count = 0
self.logger.warn("Max timeouts exceeded. Triggering rescan...")
self.worker.reset()
else:
self.logger.warn("Timeout while getting value.")
self.timeout_count += 1
else:
raise e
def add_graph_for_attr(self, attr):
"""
Create a new pyqtgraph object, for the passed
attribute. Add a dictionary containing the
object and related data container to the local index.
"""
graph_widget = pg.PlotWidget(title=attr.full_name)
graph_widget.setMinimumWidth(800)
pi = graph_widget.getPlotItem()
pi.skipFiniteCheck = True
if attr.unit:
pi.setLabel(axis="left", text=attr.name, units=f"{attr.unit}")
else:
pi.setLabel(axis="left", text=attr.name)
pi.setLabel(axis="bottom", text="time", units="sec")
data = {"x": [], "y": []}
data_line = pg.PlotCurveItem(data["x"], data["y"], pen=pg.mkPen(width=1.00))
graph_widget.addItem(data_line)
self.graphs_by_id[attr.full_name] = {
"widget": graph_widget,
"data": data,
"data_line": data_line,
}
self.right_layout.addWidget(graph_widget)
@QtCore.Slot()
def regen_tree(self, devices_by_name):
"""
Regenerate the attribute tree
"""
self.delete_graphs()
self.attr_widgets_by_id = {}
self.tree_widget.clear()
self.tree_widget.setEnabled(False)
for name, device in devices_by_name.items():
widget = self.parse_node(device, name)
widget.add_to_tree(self.tree_widget)
header = self.tree_widget.header()
header.setSectionResizeMode(QHeaderView.ResizeToContents)
header.setStretchLastSection(True)
self.tree_widget.setEnabled(True)
def parse_node(self, node, name):
if hasattr(node, "remote_attributes"):
widget = NodeTreeWidgetItem(name)
for attr_name, attr in node.remote_attributes.items():
attr_widgets_node_widget = self.parse_node(attr, attr_name)
widget.addChild(attr_widgets_node_widget)
elif hasattr(node, "__call__"):
widget = FuncTreeWidgetItem(name, node)
else:
if hasattr(node, "options"):
widget = OptionsTreeWidgetItem(name, node)
elif hasattr(node, "get_value"):
widget = AttrTreeWidgetItem(name, node)
self.attr_widgets_by_id[node.full_name] = {
"node": node,
"widget": widget,
}
return widget
@QtCore.Slot()
def item_changed(self, item):
if item._on_checkbox_changed():
attr = item._tm_node
attr_name = attr.full_name
checked = item._checked
self.TreeItemCheckedSignal.emit({"attr": attr, "checked": checked})
if checked and attr_name not in self.graphs_by_id:
self.add_graph_for_attr(attr)
elif not checked and attr_name in self.graphs_by_id:
self.delete_graph_by_attr_name(attr_name)
@QtCore.Slot()
def attrs_updated(self, data):
for attr_name, val in data.items():
try:
self.attr_widgets_by_id[attr_name]["widget"].set_text(format_value(val))
except RuntimeError:
self.logger.warn("Attribute widget disappeared while updating")
if attr_name in self.graphs_by_id:
graph_info = self.graphs_by_id[attr_name]
x = graph_info["data"]["x"]
y = graph_info["data"]["y"]
if len(x) >= 200:
x.pop(0)
y.pop(0)
x.append(time.time() - self.start_time)
y.append(magnitude_of(val))
graph_info["data_line"].setData(x, y)
graph_info["widget"].update()
@QtCore.Slot()
def timings_updated(self, timings_dict):
meas_freq = timings_dict["meas_freq"]
meas_freq_str = "-" if meas_freq == 0 else "{:.1f}Hz".format(meas_freq)
self.status_label.setText(
"{}\t Load:{:.0f}%\t RT:{:.1f}ms".format(
meas_freq_str,
timings_dict["load"] * 100,
timings_dict["getter_dt"] * 1000,
)
)
def on_export(self):
selected_items = self.tree_widget.selectedItems()
if check_selected_items(selected_items):
root_node = selected_items[0]._tm_attribute.root
values_object = root_node.export_values()
json_data = json.dumps(values_object, cls=AvlosEncoder)
file_path = display_file_save_dialog()
with suppress(FileNotFoundError), open(file_path, "w") as file:
file.write(json_data)
def on_import(self):
selected_items = self.tree_widget.selectedItems()
if check_selected_items(selected_items):
root_node = selected_items[0]._tm_attribute.root
file_path = display_file_open_dialog()
with suppress(FileNotFoundError), open(file_path, "r") as file:
values_object = json.load(file)
root_node.import_values(values_object)
time.sleep(0.1)
self.worker.force_regen()
def delete_graph_by_attr_name(self, attr_name):
self.graphs_by_id[attr_name]["widget"].deleteLater()
del self.graphs_by_id[attr_name]
def delete_graphs(self):
for attr_name in list(self.graphs_by_id.keys()):
self.delete_graph_by_attr_name(attr_name)
def show_about_box(self):
version_str = pkg_resources.require("tinymovr")[0].version
app_str = "{} {}".format(app_name, version_str)
QMessageBox.about(
self,
"About Tinymovr",
"{}\nhttps://tinymovr.com\n\nCat Sleeping Icon by Denis Sazhin from Noun Project".format(
app_str
),
)