From 189daae475a63794e92a1c0822d1e7f6589648fd Mon Sep 17 00:00:00 2001 From: Yannis Chatzikonstantinou Date: Wed, 27 Sep 2023 20:31:34 +0300 Subject: [PATCH 01/18] also compare without flux braking and negative velocity --- studio/Python/tests/test_board.py | 33 ++++++++++++++++++++----------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/studio/Python/tests/test_board.py b/studio/Python/tests/test_board.py index 2dd9c1bc..6316d2ca 100644 --- a/studio/Python/tests/test_board.py +++ b/studio/Python/tests/test_board.py @@ -367,19 +367,30 @@ def test_p_flux_braking(self): # Ensure we're idle self.check_state(0) self.try_calibrate() - - self.tm.controller.current.max_Ibrake = 10 + self.tm.controller.current.max_Ibrake = 0 self.tm.controller.velocity_mode() - self.tm.controller.velocity.setpoint = 200000 - time.sleep(0.4) - self.tm.controller.velocity.setpoint = 0 - I_brake_vals = [] - for _ in range(50): - I_brake_vals.append(self.tm.Ibus) - time.sleep(0.005) - time.sleep(0.5) + for v_set in [-250000, 250000]: + self.tm.controller.velocity.setpoint = v_set + time.sleep(0.4) + self.tm.controller.velocity.setpoint = 0 + I_brake_vals = [] + for _ in range(200): + I_brake_vals.append(self.tm.Ibus) + time.sleep(0.001) + time.sleep(0.2) + self.assertLess(min(I_brake_vals), -0.12 * A) + self.tm.controller.current.max_Ibrake = 10 + for v_set in [-250000, 250000]: + self.tm.controller.velocity.setpoint = v_set + time.sleep(0.4) + self.tm.controller.velocity.setpoint = 0 + I_brake_vals = [] + for _ in range(200): + I_brake_vals.append(self.tm.Ibus) + time.sleep(0.001) + time.sleep(0.2) + self.assertGreater(min(I_brake_vals), -0.12 * A) self.tm.controller.current.max_Ibrake = 0 - self.assertGreater(min(I_brake_vals), -1 * A) self.tm.controller.idle() time.sleep(0.4) From 6fb977c80a2ca8f83f5e71b67e35efe3308bee10 Mon Sep 17 00:00:00 2001 From: Yannis Chatzikonstantinou Date: Mon, 9 Oct 2023 03:18:27 +0300 Subject: [PATCH 02/18] all-around gui refactor and improvements --- studio/Python/tinymovr/gui/__init__.py | 7 +- studio/Python/tinymovr/gui/helpers.py | 79 ++++++++++--- studio/Python/tinymovr/gui/widgets.py | 151 +++++++++++++++++++++++-- studio/Python/tinymovr/gui/window.py | 80 ++++--------- 4 files changed, 231 insertions(+), 86 deletions(-) diff --git a/studio/Python/tinymovr/gui/__init__.py b/studio/Python/tinymovr/gui/__init__.py index 6bcc37c2..9b05a53a 100644 --- a/studio/Python/tinymovr/gui/__init__.py +++ b/studio/Python/tinymovr/gui/__init__.py @@ -7,14 +7,17 @@ display_file_open_dialog, display_file_save_dialog, magnitude_of, - hold_sema, TimedGetter, check_selected_items, get_dynamic_attrs, is_dark_mode ) from tinymovr.gui.widgets import ( - OurQTreeWidget, + NodeTreeWidgetItem, + AttrTreeWidgetItem, + FuncTreeWidgetItem, + OptionsTreeWidgetItem, + PlaceholderQTreeWidget, IconComboBoxWidget, ArgumentInputDialog ) diff --git a/studio/Python/tinymovr/gui/helpers.py b/studio/Python/tinymovr/gui/helpers.py index b06c5e8c..2097787a 100644 --- a/studio/Python/tinymovr/gui/helpers.py +++ b/studio/Python/tinymovr/gui/helpers.py @@ -33,7 +33,7 @@ QPushButton { background-color: #ededef; border-radius: 4px; - margin: 0 0 1px 0; + margin: 3px 10px 3px 0; } QPushButton:pressed { background-color: #cdcdcf; @@ -44,6 +44,22 @@ background-color: #eaeaec; } +/* --------------------------------------- QComboBox -----------------------------------*/ + + QComboBox { + margin: 0 10px 0 5px; + } + + QComboBox { + margin: 0 10px 0 0; + } + + QComboBox::drop-down { + border: none; + background-color: #ededef; + border-radius: 4px; + } + /* --------------------------------------- QScrollBar -----------------------------------*/ QScrollBar:horizontal @@ -57,7 +73,7 @@ QScrollBar::handle:horizontal { - background-color: #dfdfe1; /* #605F5F; */ + background-color: #dfdfe1; min-width: 5px; border-radius: 4px; } @@ -175,6 +191,11 @@ { background: none; } + + QAbstractScrollArea::corner { + background: #dfdfe1; + border: none; + } """ @@ -185,7 +206,7 @@ QPushButton { background-color: #363638; border-radius: 4px; - margin: 0 0 1px 0; + margin: 3px 10px 3px 0; } QPushButton:pressed { background-color: #767678; @@ -196,6 +217,37 @@ background-color: #464648; } +/* --------------------------------------- QComboBox -----------------------------------*/ + + QComboBox { + margin: 0 10px 0 5px; + } + + QComboBox::drop-down { + border: none; + background-color: #363638; + border-radius: 4px; + } + + QComboBox::down-arrow + { + border: 0px; + background-repeat: no-repeat; + background-position: center center; + border-image: url(:/qss_icons/rc/down_arrow.png); + height:20px; + width:20px; + } + +/* ----------------------------------- Headers (dark only) -------------------------------*/ + + QHeaderView::section { + border-right-color: #262628; + border-right-width: 1px; + border-style: solid; + margin: 0 4px; + } + /* --------------------------------------- QScrollBar -----------------------------------*/ QScrollBar:horizontal @@ -204,12 +256,12 @@ margin: 3px 15px 3px 15px; border: 1px transparent white; border-radius: 4px; - background-color: white; + background-color: #363638; } QScrollBar::handle:horizontal { - background-color: #dfdfe1; /* #605F5F; */ + background-color: #605F5F; min-width: 5px; border-radius: 4px; } @@ -266,7 +318,7 @@ QScrollBar:vertical { - background-color: white; + background-color: #363638; width: 15px; margin: 15px 3px 15px 3px; border: 1px transparent white; @@ -275,7 +327,7 @@ QScrollBar::handle:vertical { - background-color: #dfdfe1; + background-color: #605F5F; min-height: 5px; border-radius: 4px; } @@ -327,6 +379,11 @@ { background: none; } + + QAbstractScrollArea::corner { + background: #363638; + border: none; + } """ @@ -407,14 +464,6 @@ def magnitude_of(val): return val -def hold_sema(sema): - sema.acquire() - try: - yield - finally: - sema.release() - - class TimedGetter: """ An interface class that maintains timing diff --git a/studio/Python/tinymovr/gui/widgets.py b/studio/Python/tinymovr/gui/widgets.py index 05cee938..d4d097d5 100644 --- a/studio/Python/tinymovr/gui/widgets.py +++ b/studio/Python/tinymovr/gui/widgets.py @@ -1,8 +1,27 @@ -from PySide6 import QtGui +""" +Tinymovr Studio custom Qt widgets +Copyright Ioannis Chatzikonstantinou 2020-2023 + +Various customized widgets for the Tinymovr Studio app + +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 . +""" + +from functools import partial +from PySide6 import QtGui, QtCore from PySide6.QtGui import QPixmap from PySide6.QtWidgets import ( QWidget, QTreeWidget, + QTreeWidgetItem, QHBoxLayout, QLabel, QComboBox, @@ -12,12 +31,102 @@ QLineEdit, QPushButton, QFormLayout, + QHeaderView ) +from avlos import get_registry +from tinymovr.gui.helpers import load_icon, load_pixmap, format_value + + +class NodeTreeWidgetItem(QTreeWidgetItem): + def __init__(self, name, *args, **kwargs): + super().__init__([name, 0, ""], *args, **kwargs) + self._orig_flags = self.flags() + + def add_to_tree(self, tree_widget): + tree_widget.addTopLevelItem(self) + self._add_to_tree_cb() + + def _add_to_tree_cb(self): + for i in range(self.childCount()): + self.child(i)._add_to_tree_cb() + + +class AttrTreeWidgetItem(NodeTreeWidgetItem): + def __init__(self, name, node, *args, **kwargs): + super().__init__(name, *args, **kwargs) + self.setText( + 1, format_value(node.get_value()) + ) + self.setToolTip(0, node.summary) + self.setCheckState(0, QtCore.Qt.Unchecked) + self._tm_attribute = node + self._editing = False + self._checked = False + + +class FuncTreeWidgetItem(NodeTreeWidgetItem): + def __init__(self, name, node, *args, **kwargs): + super().__init__(name, *args, **kwargs) + self._tm_node = node + self.setToolTip(0, node.summary) + + def _add_to_tree_cb(self): + button = QPushButton("") + button.setIcon(load_icon("call.png")) + self.treeWidget().setItemWidget(self, 1, button) + button.clicked.connect(partial(self.f_call_clicked, self._tm_node)) + + @QtCore.Slot() + def f_call_clicked(self, f): + args = [] + + if f.arguments: + dialog = ArgumentInputDialog(f.arguments, self.treeWidget()) + if dialog.exec_() == QDialog.Accepted: + input_values = dialog.get_values() + args = [input_values[arg.name] for arg in f.arguments] + else: + return # User cancelled, stop the entire process + args = [get_registry()(arg) for arg in args] + + f(*args) + if "reload_data" in f.meta and f.meta["reload_data"]: + self.worker.reset() + + +class OptionsTreeWidgetItem(NodeTreeWidgetItem): + def __init__(self, name, node, *args, **kwargs): + super().__init__(name, *args, **kwargs) + self._tm_node = node + self.setToolTip(0, node.summary) + + def _add_to_tree_cb(self): + combo_box = IconComboBoxWidget("call.png", self._tm_node.options) + self.treeWidget().setItemWidget(self, 1, combo_box) + #combo_box.clicked.connect(partial(self.combo_item_selected, self._tm_node)) + + +class PlaceholderQTreeWidget(QTreeWidget): + """ + A custom QTreeWidget with support for displaying a placeholder image + when the widget is empty. + + Attributes: + - placeholder_image (QPixmap): An image displayed in the center of the + widget when there are no top-level items. + + Public Methods: + - paintEvent(event): Overrides the base class's paint event to paint the + placeholder image if the widget is empty. + + Usage: + - The placeholder image ("empty.png") is loaded upon initialization and is + displayed with 50% opacity in the center of the widget when there are no + top-level items. + - If there are items present in the tree widget, it behaves like a standard + QTreeWidget and displays the items without the placeholder. + """ -from tinymovr.gui.helpers import load_pixmap - - -class OurQTreeWidget(QTreeWidget): def __init__(self, parent=None): super().__init__(parent) self.placeholder_image = load_pixmap("empty.png") @@ -42,20 +151,40 @@ def paintEvent(self, event): class IconComboBoxWidget(QWidget): - def __init__(self, icon_path, parent=None): + """ + A custom QWidget that combines an icon (QLabel with QPixmap) and a + QComboBox into a single composite widget. + + Attributes: + - combo (QComboBox): The embedded combo box that is part of this widget. + + Public Methods: + - __init__(icon_path, enum_options, parent=None): Constructor to initialize the + composite widget with a specified icon, IntEnum options, and an optional parent widget. + + Usage: + - This widget can be used when a visual cue (icon) needs to be placed + directly beside a dropdown (QComboBox) in the UI. + - The icon is loaded from the provided `icon_path` and is placed to the + left of the combo box. + """ + + def __init__(self, icon_path, enum_options, parent=None): super(IconComboBoxWidget, self).__init__(parent) layout = QHBoxLayout(self) layout.setContentsMargins(0, 0, 0, 0) # Remove margins layout.setSpacing(2) # Small space between icon and combo box - # Icon (adjust the path to your icon) - icon_label = QLabel(self) - pixmap = QPixmap(icon_path) - icon_label.setPixmap(pixmap) - layout.addWidget(icon_label) + # Icon + # icon_label = QLabel(self) + # icon_label.setPixmap() + # layout.addWidget(icon_label) # ComboBox self.combo = QComboBox(self) + # Populate combo box with IntEnum options + for option in enum_options: + self.combo.addItem(option.name, option.value) layout.addWidget(self.combo) self.setLayout(layout) diff --git a/studio/Python/tinymovr/gui/window.py b/studio/Python/tinymovr/gui/window.py index 1e411c79..c6cd0dab 100644 --- a/studio/Python/tinymovr/gui/window.py +++ b/studio/Python/tinymovr/gui/window.py @@ -17,7 +17,6 @@ import time import pkg_resources -from functools import partial from contextlib import suppress import json from PySide6 import QtCore @@ -33,8 +32,6 @@ QVBoxLayout, QHeaderView, QLabel, - QTreeWidgetItem, - QPushButton, QMessageBox, ) from pint.errors import UndefinedUnitError @@ -46,12 +43,15 @@ from avlos import get_registry from avlos.json_codec import AvlosEncoder from tinymovr.gui import ( + NodeTreeWidgetItem, + AttrTreeWidgetItem, + FuncTreeWidgetItem, + OptionsTreeWidgetItem, Worker, - OurQTreeWidget, + PlaceholderQTreeWidget, IconComboBoxWidget, ArgumentInputDialog, format_value, - load_icon, display_file_open_dialog, display_file_save_dialog, magnitude_of, @@ -97,7 +97,7 @@ def __init__(self, app, arguments): self.setMenuBar(self.menu_bar) # Setup the tree widget - self.tree_widget = OurQTreeWidget() + self.tree_widget = PlaceholderQTreeWidget() self.tree_widget.itemChanged.connect(self.item_changed) self.tree_widget.itemDoubleClicked.connect(self.double_click) self.tree_widget.setHeaderLabels(["Attribute", "Value"]) @@ -112,8 +112,8 @@ def __init__(self, app, arguments): self.left_layout.setSpacing(0) self.left_layout.setContentsMargins(0, 0, 0, 0) self.left_frame.setLayout(self.left_layout) - self.left_frame.setMinimumWidth(340) - self.left_frame.setMaximumWidth(460) + self.left_frame.setMinimumWidth(320) + self.left_frame.setMaximumWidth(420) self.left_frame.setStyleSheet("border:0;") self.right_frame = QFrame(self) @@ -218,51 +218,32 @@ def regen_tree(self, devices_by_name): self.attr_widgets_by_id = {} self.tree_widget.clear() self.tree_widget.setEnabled(False) - all_items = [] for name, device in devices_by_name.items(): - widget, items_list = self.parse_node(device, name) - self.tree_widget.addTopLevelItem(widget) - all_items.extend(items_list) - for item in all_items: - if hasattr(item, "_tm_function"): - button = QPushButton("") - button.setIcon(load_icon("call.png")) - self.tree_widget.setItemWidget(item, 1, button) - button.clicked.connect(partial(self.f_call_clicked, item._tm_function)) - if hasattr(item, "_options_list"): - item_widget = IconComboBoxWidget() + widget = self.parse_node(device, name) + widget.add_to_tree(self.tree_widget) header = self.tree_widget.header() header.setSectionResizeMode(QHeaderView.ResizeToContents) - header.setStretchLastSection(False) + header.setStretchLastSection(True) self.tree_widget.setEnabled(True) def parse_node(self, node, name): - widget = QTreeWidgetItem([name, 0, ""]) - widget._orig_flags = widget.flags() - all_items = [] if hasattr(node, "remote_attributes"): + widget = NodeTreeWidgetItem(name) for attr_name, attr in node.remote_attributes.items(): - items, items_list = self.parse_node(attr, attr_name) - widget.addChild(items) - all_items.extend(items_list) - elif hasattr(node, "get_value"): - widget.setText(1, format_value(node.get_value())) - widget.setCheckState(0, QtCore.Qt.Unchecked) - widget._tm_attribute = node - widget._editing = False - widget._checked = False + 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, } - all_items.append(widget) - elif hasattr(node, "__call__"): - widget._tm_function = node - all_items.append(widget) - elif hasattr(node, "options"): - widget._options_list = [member.value for member in node.options] - all_items.append(widget) - return widget, all_items + return widget @QtCore.Slot() def item_changed(self, item): @@ -326,23 +307,6 @@ def timings_updated(self, timings_dict): ) ) - @QtCore.Slot() - def f_call_clicked(self, f): - args = [] - - if f.arguments: - dialog = ArgumentInputDialog(f.arguments, self) - if dialog.exec_() == QDialog.Accepted: - input_values = dialog.get_values() - args = [input_values[arg.name] for arg in f.arguments] - else: - return # User cancelled, stop the entire process - args = [get_registry()(arg) for arg in args] - - f(*args) - if "reload_data" in f.meta and f.meta["reload_data"]: - self.worker.reset() - @QtCore.Slot() def double_click(self, item, column): if ( From 9ad67355711f551bb22654af682ec73f46b5c536 Mon Sep 17 00:00:00 2001 From: Yannis Chatzikonstantinou Date: Mon, 9 Oct 2023 12:41:27 +0300 Subject: [PATCH 03/18] add combo box callback --- studio/Python/tinymovr/gui/widgets.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/studio/Python/tinymovr/gui/widgets.py b/studio/Python/tinymovr/gui/widgets.py index d4d097d5..2c37076c 100644 --- a/studio/Python/tinymovr/gui/widgets.py +++ b/studio/Python/tinymovr/gui/widgets.py @@ -30,8 +30,7 @@ QLabel, QLineEdit, QPushButton, - QFormLayout, - QHeaderView + QFormLayout ) from avlos import get_registry from tinymovr.gui.helpers import load_icon, load_pixmap, format_value @@ -74,10 +73,10 @@ def _add_to_tree_cb(self): button = QPushButton("") button.setIcon(load_icon("call.png")) self.treeWidget().setItemWidget(self, 1, button) - button.clicked.connect(partial(self.f_call_clicked, self._tm_node)) + button.clicked.connect(partial(self._on_f_call_clicked, self._tm_node)) @QtCore.Slot() - def f_call_clicked(self, f): + def _on_f_call_clicked(self, f): args = [] if f.arguments: @@ -102,8 +101,12 @@ def __init__(self, name, node, *args, **kwargs): def _add_to_tree_cb(self): combo_box = IconComboBoxWidget("call.png", self._tm_node.options) + combo_box.currentIndexChanged.connect(self._on_combobox_changed) self.treeWidget().setItemWidget(self, 1, combo_box) - #combo_box.clicked.connect(partial(self.combo_item_selected, self._tm_node)) + + @QtCore.Slot() + def _on_f_call_clicked(self, index): + self._tm_node.set_value(index) class PlaceholderQTreeWidget(QTreeWidget): From dff49e0d62687f9da8acbb57902130295f2d6853 Mon Sep 17 00:00:00 2001 From: Yannis Chatzikonstantinou Date: Mon, 9 Oct 2023 21:48:24 +0300 Subject: [PATCH 04/18] fix function implementations --- studio/Python/tinymovr/gui/widgets.py | 90 +++++++++++++++++++-------- studio/Python/tinymovr/gui/window.py | 47 +++----------- 2 files changed, 72 insertions(+), 65 deletions(-) diff --git a/studio/Python/tinymovr/gui/widgets.py b/studio/Python/tinymovr/gui/widgets.py index 2c37076c..fba1a722 100644 --- a/studio/Python/tinymovr/gui/widgets.py +++ b/studio/Python/tinymovr/gui/widgets.py @@ -30,8 +30,9 @@ QLabel, QLineEdit, QPushButton, - QFormLayout + QFormLayout, ) +from pint.errors import UndefinedUnitError from avlos import get_registry from tinymovr.gui.helpers import load_icon, load_pixmap, format_value @@ -50,34 +51,72 @@ def _add_to_tree_cb(self): self.child(i)._add_to_tree_cb() -class AttrTreeWidgetItem(NodeTreeWidgetItem): +class EdgeTreeWidgetItem(QTreeWidgetItem): def __init__(self, name, node, *args, **kwargs): - super().__init__(name, *args, **kwargs) - self.setText( - 1, format_value(node.get_value()) - ) + super().__init__([name, 0, ""], *args, **kwargs) + self._tm_node = node + self._orig_flags = self.flags() self.setToolTip(0, node.summary) + + def _add_to_tree_cb(self): + pass + + +class AttrTreeWidgetItem(EdgeTreeWidgetItem): + def __init__(self, name, node, *args, **kwargs): + super().__init__(name, node, *args, **kwargs) + self.setText(1, format_value(node.get_value())) self.setCheckState(0, QtCore.Qt.Unchecked) - self._tm_attribute = node self._editing = False self._checked = False + def _add_to_tree_cb(self): + self.treeWidget().itemDoubleClicked.connect(self._on_item_double_clicked) + + def _on_item_double_clicked(self, item, column): + if item == self and column == 1: + if ( + hasattr(self._tm_node, "setter_name") + and self._tm_node.setter_name != None + ): + self.setFlags(self.flags() | QtCore.Qt.ItemIsEditable) + self._editing = True + elif self._orig_flags != self.flags(): + self.setFlags(self._orig_flags) + self._editing = False + + def _on_editor_text_changed(self): + attr = self._tm_node + if self._editing: + self._editing = False + try: + attr.set_value(get_registry()(self.text(1))) + except UndefinedUnitError: + attr.set_value(self.text(1)) + if "reload_data" in attr.meta and attr.meta["reload_data"]: + self.worker.reset() + return + else: + self.setText(1, format_value(attr.get_value())) + else: + checked = self.checkState(0) == QtCore.Qt.Checked + if checked != self._checked: + self._checked = checked + return False + return True -class FuncTreeWidgetItem(NodeTreeWidgetItem): - def __init__(self, name, node, *args, **kwargs): - super().__init__(name, *args, **kwargs) - self._tm_node = node - self.setToolTip(0, node.summary) +class FuncTreeWidgetItem(EdgeTreeWidgetItem): def _add_to_tree_cb(self): button = QPushButton("") button.setIcon(load_icon("call.png")) self.treeWidget().setItemWidget(self, 1, button) - button.clicked.connect(partial(self._on_f_call_clicked, self._tm_node)) + button.clicked.connect(self._on_f_call_clicked) @QtCore.Slot() - def _on_f_call_clicked(self, f): + def _on_f_call_clicked(self): args = [] + f = self._tm_node if f.arguments: dialog = ArgumentInputDialog(f.arguments, self.treeWidget()) @@ -90,23 +129,22 @@ def _on_f_call_clicked(self, f): f(*args) if "reload_data" in f.meta and f.meta["reload_data"]: - self.worker.reset() + self.treeWidget().window().worker.reset() -class OptionsTreeWidgetItem(NodeTreeWidgetItem): - def __init__(self, name, node, *args, **kwargs): - super().__init__(name, *args, **kwargs) - self._tm_node = node - self.setToolTip(0, node.summary) - +class OptionsTreeWidgetItem(EdgeTreeWidgetItem): def _add_to_tree_cb(self): - combo_box = IconComboBoxWidget("call.png", self._tm_node.options) - combo_box.currentIndexChanged.connect(self._on_combobox_changed) - self.treeWidget().setItemWidget(self, 1, combo_box) + self.combo_box_container = IconComboBoxWidget("call.png", self._tm_node.options) + self.combo_box_container.combo.setCurrentIndex(self._tm_node.get_value()) + self.combo_box_container.combo.currentIndexChanged.connect( + self._on_combobox_changed + ) + self.treeWidget().setItemWidget(self, 1, self.combo_box_container) @QtCore.Slot() - def _on_f_call_clicked(self, index): + def _on_combobox_changed(self, index): self._tm_node.set_value(index) + self.combo_box_container.combo.setCurrentIndex(self._tm_node.get_value()) class PlaceholderQTreeWidget(QTreeWidget): @@ -162,7 +200,7 @@ class IconComboBoxWidget(QWidget): - combo (QComboBox): The embedded combo box that is part of this widget. Public Methods: - - __init__(icon_path, enum_options, parent=None): Constructor to initialize the + - __init__(icon_path, enum_options, parent=None): Constructor to initialize the composite widget with a specified icon, IntEnum options, and an optional parent widget. Usage: diff --git a/studio/Python/tinymovr/gui/window.py b/studio/Python/tinymovr/gui/window.py index c6cd0dab..834c1296 100644 --- a/studio/Python/tinymovr/gui/window.py +++ b/studio/Python/tinymovr/gui/window.py @@ -99,7 +99,6 @@ def __init__(self, app, arguments): # Setup the tree widget self.tree_widget = PlaceholderQTreeWidget() self.tree_widget.itemChanged.connect(self.item_changed) - self.tree_widget.itemDoubleClicked.connect(self.double_click) self.tree_widget.setHeaderLabels(["Attribute", "Value"]) self.status_label = QLabel() @@ -247,32 +246,15 @@ def parse_node(self, node, name): @QtCore.Slot() def item_changed(self, item): - # Value changed - if item._editing: - item._editing = False - attr = item._tm_attribute - try: - attr.set_value(get_registry()(item.text(1))) - except UndefinedUnitError: - attr.set_value(item.text(1)) - if "reload_data" in attr.meta and attr.meta["reload_data"]: - self.worker.reset() - return - else: - item.setText(1, format_value(attr.get_value())) - - # Checkbox changed - if hasattr(item, "_tm_attribute"): - attr = item._tm_attribute + if not item._on_editor_text_changed(): + attr = item._tm_node attr_name = attr.full_name - checked = item.checkState(0) == QtCore.Qt.Checked - if checked != item._checked: - item._checked = 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) + 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): @@ -307,19 +289,6 @@ def timings_updated(self, timings_dict): ) ) - @QtCore.Slot() - def double_click(self, item, column): - if ( - column == 1 - and hasattr(item, "_tm_attribute") - and hasattr(item._tm_attribute, "setter_name") - and item._tm_attribute.setter_name != None - ): - item.setFlags(item.flags() | QtCore.Qt.ItemIsEditable) - item._editing = True - elif item._orig_flags != item.flags(): - item.setFlags(item._orig_flags) - def on_export(self): selected_items = self.tree_widget.selectedItems() if check_selected_items(selected_items): From d962c1b1574043c892ef9be45c5cf7a6a2b3b9f6 Mon Sep 17 00:00:00 2001 From: Yannis Chatzikonstantinou Date: Mon, 9 Oct 2023 21:49:03 +0300 Subject: [PATCH 05/18] formatting --- studio/Python/tinymovr/gui/worker.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/studio/Python/tinymovr/gui/worker.py b/studio/Python/tinymovr/gui/worker.py index 29b0e3b3..3932d9a6 100644 --- a/studio/Python/tinymovr/gui/worker.py +++ b/studio/Python/tinymovr/gui/worker.py @@ -116,7 +116,11 @@ def _update(self): if len(last_updated) > 0: self.updateAttrsSignal.emit(last_updated) self.updateTimingsSignal.emit( - {"meas_freq": 1/self.dt_update, "load": self.dt_load/self.dt_update, "getter_dt": self.timed_getter.dt} + { + "meas_freq": 1 / self.dt_update, + "load": self.dt_load / self.dt_update, + "getter_dt": self.timed_getter.dt, + } ) self.mutx.unlock() From b682a32e6dbc44299678aa932f337ff62ff2f1d6 Mon Sep 17 00:00:00 2001 From: Yannis Chatzikonstantinou Date: Tue, 10 Oct 2023 01:49:21 +0300 Subject: [PATCH 06/18] joggable text fields --- studio/Python/tinymovr/gui/widgets.py | 123 +++++++++++++++++++------- studio/Python/tinymovr/gui/window.py | 6 +- 2 files changed, 92 insertions(+), 37 deletions(-) diff --git a/studio/Python/tinymovr/gui/widgets.py b/studio/Python/tinymovr/gui/widgets.py index fba1a722..f4f9b4e2 100644 --- a/studio/Python/tinymovr/gui/widgets.py +++ b/studio/Python/tinymovr/gui/widgets.py @@ -15,8 +15,8 @@ this program. If not, see . """ -from functools import partial from PySide6 import QtGui, QtCore +from PySide6.QtCore import Signal, QTimer from PySide6.QtGui import QPixmap from PySide6.QtWidgets import ( QWidget, @@ -65,45 +65,43 @@ def _add_to_tree_cb(self): class AttrTreeWidgetItem(EdgeTreeWidgetItem): def __init__(self, name, node, *args, **kwargs): super().__init__(name, node, *args, **kwargs) - self.setText(1, format_value(node.get_value())) - self.setCheckState(0, QtCore.Qt.Unchecked) - self._editing = False + editable = hasattr(self._tm_node, "setter_name") and self._tm_node.setter_name != None + if editable: + self.text_editor = JoggableLineEdit(format_value(node.get_value()), editable, editable) + self.text_editor.ValueChangedByJog.connect(self._on_editor_text_changed) + self.text_editor.editingFinished.connect(self._on_editor_text_changed) + else: + self.text_editor = QLineEdit(format_value(node.get_value())) self._checked = False def _add_to_tree_cb(self): - self.treeWidget().itemDoubleClicked.connect(self._on_item_double_clicked) - - def _on_item_double_clicked(self, item, column): - if item == self and column == 1: - if ( - hasattr(self._tm_node, "setter_name") - and self._tm_node.setter_name != None - ): - self.setFlags(self.flags() | QtCore.Qt.ItemIsEditable) - self._editing = True - elif self._orig_flags != self.flags(): - self.setFlags(self._orig_flags) - self._editing = False + self.treeWidget().setItemWidget(self, 1, self.text_editor) + self.setCheckState(0, QtCore.Qt.Unchecked) + def set_text(self, text): + self.text_editor.setText(text) + + @QtCore.Slot() def _on_editor_text_changed(self): attr = self._tm_node - if self._editing: - self._editing = False - try: - attr.set_value(get_registry()(self.text(1))) - except UndefinedUnitError: - attr.set_value(self.text(1)) - if "reload_data" in attr.meta and attr.meta["reload_data"]: - self.worker.reset() - return - else: - self.setText(1, format_value(attr.get_value())) + text = self.text_editor.text() + try: + attr.set_value(get_registry()(text)) + except UndefinedUnitError: + attr.set_value(text) + if "reload_data" in attr.meta and attr.meta["reload_data"]: + self.worker.reset() + return else: - checked = self.checkState(0) == QtCore.Qt.Checked - if checked != self._checked: - self._checked = checked - return False - return True + self.text_editor.setText(format_value(attr.get_value())) + + @QtCore.Slot() + def _on_checkbox_changed(self): + checked = self.checkState(0) == QtCore.Qt.Checked + if checked != self._checked: + self._checked = checked + return True + return False class FuncTreeWidgetItem(EdgeTreeWidgetItem): @@ -231,6 +229,65 @@ def __init__(self, icon_path, enum_options, parent=None): self.setLayout(layout) +class JoggableLineEdit(QLineEdit): + + ValueChangedByJog = Signal() + + def __init__(self, initial_text="0", editable=True, joggable=True, *args, **kwargs): + super().__init__(*args, **kwargs) + self.editable = editable + self.joggable = joggable + self.jogging = False + self.last_x = 0 + self.setText(initial_text) + self.setReadOnly(not editable) + self.normal_cursor = self.cursor() + + self.jog_start_timer = QTimer(self) + self.jog_start_timer.setSingleShot(True) + self.jog_start_timer.timeout.connect(self.start_jog) + + def mousePressEvent(self, event): + if self.joggable: + self.jog_start_timer.start(500) + self.last_x = event.x() + super().mousePressEvent(event) + + def start_jog(self): + self.setReadOnly(True) + self.setCursor(QtGui.QCursor(QtGui.Qt.ClosedHandCursor)) + self.jogging = True + + def mouseReleaseEvent(self, event): + if self.joggable: + self.jog_start_timer.stop() + + self.setReadOnly(not self.editable) + self.setCursor(self.normal_cursor) + self.jogging = False + super().mouseReleaseEvent(event) + + def mouseMoveEvent(self, event): + if self.jogging: + diff = event.x() - self.last_x + text = self.text() + try: + try: + value = get_registry()(text) + except UndefinedUnitError: + value = float(text) + if value != 0: + value += value * diff * 0.02 + self.setText(str(value)) + self.ValueChangedByJog.emit() + except ValueError: + pass + self.last_x = event.x() + else: + self.jog_start_timer.stop() + super().mouseMoveEvent(event) + + class ArgumentInputDialog(QDialog): def __init__(self, arguments, parent=None): super(ArgumentInputDialog, self).__init__(parent) diff --git a/studio/Python/tinymovr/gui/window.py b/studio/Python/tinymovr/gui/window.py index 834c1296..2fe8f3ff 100644 --- a/studio/Python/tinymovr/gui/window.py +++ b/studio/Python/tinymovr/gui/window.py @@ -246,7 +246,7 @@ def parse_node(self, node, name): @QtCore.Slot() def item_changed(self, item): - if not item._on_editor_text_changed(): + if item._on_checkbox_changed(): attr = item._tm_node attr_name = attr.full_name checked = item._checked @@ -260,9 +260,7 @@ def item_changed(self, item): def attrs_updated(self, data): for attr_name, val in data.items(): try: - self.attr_widgets_by_id[attr_name]["widget"].setText( - 1, format_value(val) - ) + 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: From a0fb244718b494e5a1fc8b05da78d5cc69eaff52 Mon Sep 17 00:00:00 2001 From: Yannis Chatzikonstantinou Date: Tue, 10 Oct 2023 03:09:49 +0300 Subject: [PATCH 07/18] cleanup imports --- studio/Python/tinymovr/gui/window.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/studio/Python/tinymovr/gui/window.py b/studio/Python/tinymovr/gui/window.py index 2fe8f3ff..6d7b2659 100644 --- a/studio/Python/tinymovr/gui/window.py +++ b/studio/Python/tinymovr/gui/window.py @@ -23,7 +23,6 @@ from PySide6.QtCore import Signal, QTimer from PySide6.QtWidgets import ( QMainWindow, - QDialog, QMenu, QMenuBar, QWidget, @@ -34,7 +33,6 @@ QLabel, QMessageBox, ) -from pint.errors import UndefinedUnitError from PySide6.QtGui import QAction import pyqtgraph as pg from tinymovr.constants import app_name @@ -49,8 +47,6 @@ OptionsTreeWidgetItem, Worker, PlaceholderQTreeWidget, - IconComboBoxWidget, - ArgumentInputDialog, format_value, display_file_open_dialog, display_file_save_dialog, From 697dfa6b0c60d8077729c8e8643392d44228f7ba Mon Sep 17 00:00:00 2001 From: Yannis Chatzikonstantinou Date: Tue, 10 Oct 2023 18:37:27 +0300 Subject: [PATCH 08/18] improve jogging performance --- studio/Python/tinymovr/gui/widgets.py | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/studio/Python/tinymovr/gui/widgets.py b/studio/Python/tinymovr/gui/widgets.py index f4f9b4e2..9ce634e2 100644 --- a/studio/Python/tinymovr/gui/widgets.py +++ b/studio/Python/tinymovr/gui/widgets.py @@ -233,12 +233,14 @@ class JoggableLineEdit(QLineEdit): ValueChangedByJog = Signal() - def __init__(self, initial_text="0", editable=True, joggable=True, *args, **kwargs): + def __init__(self, initial_text="0", editable=True, joggable=True, jog_step=None, *args, **kwargs): super().__init__(*args, **kwargs) self.editable = editable self.joggable = joggable self.jogging = False self.last_x = 0 + self.jog_step = jog_step + self.current_jog_step = 0 self.setText(initial_text) self.setReadOnly(not editable) self.normal_cursor = self.cursor() @@ -256,6 +258,15 @@ def mousePressEvent(self, event): def start_jog(self): self.setReadOnly(True) self.setCursor(QtGui.QCursor(QtGui.Qt.ClosedHandCursor)) + if self.jog_step: + self.current_jog_step = self.jog_step + else: + text = self.text() + try: + value = float(text) + except ValueError: + value = get_registry()(text).magnitude + self.current_jog_step = max(abs(value) * 0.01, 1e-6) self.jogging = True def mouseReleaseEvent(self, event): @@ -273,15 +284,14 @@ def mouseMoveEvent(self, event): text = self.text() try: try: - value = get_registry()(text) - except UndefinedUnitError: value = float(text) - if value != 0: - value += value * diff * 0.02 - self.setText(str(value)) - self.ValueChangedByJog.emit() + except ValueError: + value = get_registry()(text).magnitude + value += self.current_jog_step * diff + self.setText(str(value)) + self.ValueChangedByJog.emit() except ValueError: - pass + print("valueerror") self.last_x = event.x() else: self.jog_start_timer.stop() From 47a1ab9a025af0174f99212d4949c12829ede49d Mon Sep 17 00:00:00 2001 From: Yannis Chatzikonstantinou Date: Tue, 10 Oct 2023 18:38:02 +0300 Subject: [PATCH 09/18] conditionally use joggable and plottable fields --- studio/Python/tinymovr/gui/widgets.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/studio/Python/tinymovr/gui/widgets.py b/studio/Python/tinymovr/gui/widgets.py index 9ce634e2..b766e963 100644 --- a/studio/Python/tinymovr/gui/widgets.py +++ b/studio/Python/tinymovr/gui/widgets.py @@ -34,6 +34,7 @@ ) from pint.errors import UndefinedUnitError from avlos import get_registry +from avlos.datatypes import DataType from tinymovr.gui.helpers import load_icon, load_pixmap, format_value @@ -66,17 +67,20 @@ class AttrTreeWidgetItem(EdgeTreeWidgetItem): def __init__(self, name, node, *args, **kwargs): super().__init__(name, node, *args, **kwargs) editable = hasattr(self._tm_node, "setter_name") and self._tm_node.setter_name != None - if editable: + if editable and node.dtype == DataType.FLOAT: self.text_editor = JoggableLineEdit(format_value(node.get_value()), editable, editable) self.text_editor.ValueChangedByJog.connect(self._on_editor_text_changed) self.text_editor.editingFinished.connect(self._on_editor_text_changed) else: self.text_editor = QLineEdit(format_value(node.get_value())) + if not editable: + self.text_editor.setReadOnly(True) self._checked = False def _add_to_tree_cb(self): self.treeWidget().setItemWidget(self, 1, self.text_editor) - self.setCheckState(0, QtCore.Qt.Unchecked) + if self._tm_node.dtype == DataType.FLOAT: + self.setCheckState(0, QtCore.Qt.Unchecked) def set_text(self, text): self.text_editor.setText(text) From 5ec77b0f5681bdbfdfde845a117902b0df801e80 Mon Sep 17 00:00:00 2001 From: Yannis Chatzikonstantinou Date: Tue, 10 Oct 2023 18:50:39 +0300 Subject: [PATCH 10/18] use optional fixed jog step --- firmware/src/can/can_endpoints.c | 2 +- studio/Python/tinymovr/gui/widgets.py | 2 +- .../Python/tinymovr/specs/tinymovr_1_7_x.yaml | 526 ++++++++++++++++++ 3 files changed, 528 insertions(+), 2 deletions(-) create mode 100644 studio/Python/tinymovr/specs/tinymovr_1_7_x.yaml diff --git a/firmware/src/can/can_endpoints.c b/firmware/src/can/can_endpoints.c index 58e478dc..5280c9fd 100644 --- a/firmware/src/can/can_endpoints.c +++ b/firmware/src/can/can_endpoints.c @@ -19,7 +19,7 @@ uint8_t (*avlos_endpoints[79])(uint8_t * buffer, uint8_t * buffer_len, Avlos_Command cmd) = {&avlos_protocol_hash, &avlos_uid, &avlos_fw_version, &avlos_hw_revision, &avlos_Vbus, &avlos_Ibus, &avlos_power, &avlos_temp, &avlos_calibrated, &avlos_errors, &avlos_save_config, &avlos_erase_config, &avlos_reset, &avlos_enter_dfu, &avlos_scheduler_errors, &avlos_controller_state, &avlos_controller_mode, &avlos_controller_warnings, &avlos_controller_errors, &avlos_controller_position_setpoint, &avlos_controller_position_p_gain, &avlos_controller_velocity_setpoint, &avlos_controller_velocity_limit, &avlos_controller_velocity_p_gain, &avlos_controller_velocity_i_gain, &avlos_controller_velocity_deadband, &avlos_controller_velocity_increment, &avlos_controller_current_Iq_setpoint, &avlos_controller_current_Id_setpoint, &avlos_controller_current_Iq_limit, &avlos_controller_current_Iq_estimate, &avlos_controller_current_bandwidth, &avlos_controller_current_Iq_p_gain, &avlos_controller_current_max_Ibus_regen, &avlos_controller_current_max_Ibrake, &avlos_controller_voltage_Vq_setpoint, &avlos_controller_calibrate, &avlos_controller_idle, &avlos_controller_position_mode, &avlos_controller_velocity_mode, &avlos_controller_current_mode, &avlos_controller_set_pos_vel_setpoints, &avlos_comms_can_rate, &avlos_comms_can_id, &avlos_motor_R, &avlos_motor_L, &avlos_motor_pole_pairs, &avlos_motor_type, &avlos_motor_offset, &avlos_motor_direction, &avlos_motor_calibrated, &avlos_motor_I_cal, &avlos_motor_errors, &avlos_encoder_position_estimate, &avlos_encoder_velocity_estimate, &avlos_encoder_type, &avlos_encoder_bandwidth, &avlos_encoder_calibrated, &avlos_encoder_errors, &avlos_traj_planner_max_accel, &avlos_traj_planner_max_decel, &avlos_traj_planner_max_vel, &avlos_traj_planner_t_accel, &avlos_traj_planner_t_decel, &avlos_traj_planner_t_total, &avlos_traj_planner_move_to, &avlos_traj_planner_move_to_tlimit, &avlos_traj_planner_errors, &avlos_homing_velocity, &avlos_homing_max_homing_t, &avlos_homing_retract_dist, &avlos_homing_warnings, &avlos_homing_stall_detect_velocity, &avlos_homing_stall_detect_delta_pos, &avlos_homing_stall_detect_t, &avlos_homing_home, &avlos_watchdog_enabled, &avlos_watchdog_triggered, &avlos_watchdog_timeout }; -uint32_t avlos_proto_hash = 4118115615; +uint32_t avlos_proto_hash = 3526126264; uint32_t _avlos_get_proto_hash(void) { diff --git a/studio/Python/tinymovr/gui/widgets.py b/studio/Python/tinymovr/gui/widgets.py index b766e963..62061482 100644 --- a/studio/Python/tinymovr/gui/widgets.py +++ b/studio/Python/tinymovr/gui/widgets.py @@ -68,7 +68,7 @@ def __init__(self, name, node, *args, **kwargs): super().__init__(name, node, *args, **kwargs) editable = hasattr(self._tm_node, "setter_name") and self._tm_node.setter_name != None if editable and node.dtype == DataType.FLOAT: - self.text_editor = JoggableLineEdit(format_value(node.get_value()), editable, editable) + self.text_editor = JoggableLineEdit(format_value(node.get_value()), editable, editable, node.meta.get("jog_step")) self.text_editor.ValueChangedByJog.connect(self._on_editor_text_changed) self.text_editor.editingFinished.connect(self._on_editor_text_changed) else: diff --git a/studio/Python/tinymovr/specs/tinymovr_1_7_x.yaml b/studio/Python/tinymovr/specs/tinymovr_1_7_x.yaml new file mode 100644 index 00000000..30578409 --- /dev/null +++ b/studio/Python/tinymovr/specs/tinymovr_1_7_x.yaml @@ -0,0 +1,526 @@ + +name: tm +remote_attributes: + - name: protocol_hash + dtype: uint32 + getter_name: _avlos_get_proto_hash + summary: The Avlos protocol hash. + - name: uid + dtype: uint32 + getter_name: system_get_uid + summary: The unique device ID, unique to each PAC55xx chip produced. + - name: fw_version + dtype: string + getter_name: system_get_fw_version_string + summary: The firmware version. + - name: hw_revision + dtype: uint32 + getter_name: system_get_hw_revision + summary: The hardware revision. + - name: Vbus + dtype: float + unit: volt + meta: {dynamic: True} + getter_name: system_get_Vbus + summary: The measured bus voltage. + - name: Ibus + dtype: float + unit: ampere + meta: {dynamic: True} + getter_name: controller_get_Ibus_est + summary: The estimated bus current. Only estimates current drawn by motor. + - name: power + dtype: float + unit: watt + meta: {dynamic: True} + getter_name: controller_get_power_est + summary: The estimated power. Only estimates power drawn by motor. + - name: temp + dtype: float + unit: degC + meta: {dynamic: True} + getter_name: adc_get_mcu_temp + summary: The internal temperature of the PAC55xx MCU. + - name: calibrated + dtype: bool + meta: {dynamic: True} + getter_name: system_get_calibrated + summary: Whether the system has been calibrated. + - name: errors + flags: [UNDERVOLTAGE, DRIVER_FAULT, CHARGE_PUMP_FAULT_STAT, CHARGE_PUMP_FAULT, DRV10_DISABLE, DRV32_DISABLE, DRV54_DISABLE] + meta: {dynamic: True} + getter_name: system_get_errors + summary: Any system errors, as a bitmask + - name: save_config + summary: Save configuration to non-volatile memory. + caller_name: nvm_save_config + dtype: void + arguments: [] + - name: erase_config + summary: Erase the config stored in non-volatile memory and reset the device. + caller_name: nvm_erase + dtype: void + arguments: [] + meta: {reload_data: True} + - name: reset + summary: Reset the device. + caller_name: system_reset + dtype: void + arguments: [] + meta: {reload_data: True} + - name: enter_dfu + summary: Enter DFU mode. + caller_name: system_enter_dfu + dtype: void + arguments: [] + meta: {reload_data: True} + - name: scheduler + remote_attributes: + - name: errors + flags: [CONTROL_BLOCK_REENTERED] + meta: {dynamic: True} + getter_name: scheduler_get_errors + summary: Any scheduler errors, as a bitmask + - name: controller + remote_attributes: + - name: state + dtype: uint8 + meta: {dynamic: True} + getter_name: controller_get_state + setter_name: controller_set_state + summary: The state of the controller. + - name: mode + dtype: uint8 + meta: {dynamic: True} + getter_name: controller_get_mode + setter_name: controller_set_mode + summary: The control mode of the controller. + - name: warnings + meta: {dynamic: True} + flags: [VELOCITY_LIMITED, CURRENT_LIMITED, MODULATION_LIMITED] + getter_name: controller_get_warnings + summary: Any controller warnings, as a bitmask + - name: errors + meta: {dynamic: True} + flags: [CURRENT_LIMIT_EXCEEDED] + getter_name: controller_get_errors + summary: Any controller errors, as a bitmask + - name: position + remote_attributes: + - name: setpoint + dtype: float + unit: tick + meta: {jog_step: 100} + getter_name: controller_get_pos_setpoint_user_frame + setter_name: controller_set_pos_setpoint_user_frame + summary: The position setpoint. + - name: p_gain + dtype: float + meta: {export: True} + getter_name: controller_get_pos_gain + setter_name: controller_set_pos_gain + summary: The proportional gain of the position controller. + - name: velocity + remote_attributes: + - name: setpoint + dtype: float + unit: tick/sec + meta: {jog_step: 200} + getter_name: controller_get_vel_setpoint_user_frame + setter_name: controller_set_vel_setpoint_user_frame + summary: The velocity setpoint. + - name: limit + dtype: float + unit: tick/sec + meta: {export: True} + getter_name: controller_get_vel_limit + setter_name: controller_set_vel_limit + summary: The velocity limit. + - name: p_gain + dtype: float + meta: {export: True} + getter_name: controller_get_vel_gain + setter_name: controller_set_vel_gain + summary: The proportional gain of the velocity controller. + - name: i_gain + dtype: float + meta: {export: True} + getter_name: controller_get_vel_integrator_gain + setter_name: controller_set_vel_integrator_gain + summary: The integral gain of the velocity controller. + - name: deadband + dtype: float + unit: tick + meta: {export: True} + getter_name: controller_get_vel_integrator_deadband + setter_name: controller_set_vel_integrator_deadband + rst_target: integrator-deadband + summary: The deadband of the velocity integrator. A region around the position setpoint where the velocity integrator is not updated. + - name: increment + dtype: float + meta: {export: True} + getter_name: controller_get_vel_increment + setter_name: controller_set_vel_increment + summary: Max velocity setpoint increment (ramping) rate. Set to 0 to disable. + - name: current + remote_attributes: + - name: Iq_setpoint + dtype: float + unit: ampere + meta: {jog_step: 0.005} + getter_name: controller_get_Iq_setpoint_user_frame + setter_name: controller_set_Iq_setpoint_user_frame + summary: The Iq setpoint. + - name: Id_setpoint + dtype: float + unit: ampere + meta: {dynamic: True} + getter_name: controller_get_Id_setpoint_user_frame + summary: The Id setpoint. + - name: Iq_limit + dtype: float + unit: ampere + getter_name: controller_get_Iq_limit + setter_name: controller_set_Iq_limit + summary: The Iq limit. + - name: Iq_estimate + dtype: float + unit: ampere + meta: {dynamic: True} + getter_name: controller_get_Iq_estimate_user_frame + summary: The Iq estimate. + - name: bandwidth + dtype: float + unit: Hz + meta: {export: True} + getter_name: controller_get_I_bw + setter_name: controller_set_I_bw + summary: The current controller bandwidth. + - name: Iq_p_gain + dtype: float + getter_name: controller_get_Iq_gain + summary: The current controller proportional gain. + - name: max_Ibus_regen + dtype: float + unit: ampere + getter_name: controller_get_max_Ibus_regen + setter_name: controller_set_max_Ibus_regen + summary: The max current allowed to be fed back to the power source before flux braking activates. + - name: max_Ibrake + dtype: float + unit: ampere + meta: {export: True} + getter_name: controller_get_max_Ibrake + setter_name: controller_set_max_Ibrake + summary: The max current allowed to be dumped to the motor windings during flux braking. Set to zero to deactivate flux braking. + - name: voltage + remote_attributes: + - name: Vq_setpoint + dtype: float + unit: volt + meta: {dynamic: True} + getter_name: controller_get_Vq_setpoint_user_frame + summary: The Vq setpoint. + - name: calibrate + summary: Calibrate the device. + caller_name: controller_calibrate + dtype: void + arguments: [] + - name: idle + summary: Set idle mode, disabling the driver. + caller_name: controller_idle + dtype: void + arguments: [] + - name: position_mode + summary: Set position control mode. + caller_name: controller_position_mode + dtype: void + arguments: [] + - name: velocity_mode + summary: Set velocity control mode. + caller_name: controller_velocity_mode + dtype: void + arguments: [] + - name: current_mode + summary: Set current control mode. + caller_name: controller_current_mode + dtype: void + arguments: [] + - name: set_pos_vel_setpoints + summary: Set the position and velocity setpoints in one go, and retrieve the position estimate + caller_name: controller_set_pos_vel_setpoints + dtype: float + arguments: + - name: pos_setpoint + dtype: float + unit: tick + - name: vel_setpoint + dtype: float + unit: tick + - name: comms + remote_attributes: + - name: can + remote_attributes: + - name: rate + dtype: uint32 + meta: {export: True} + getter_name: CAN_get_kbit_rate + setter_name: CAN_set_kbit_rate + rst_target: api-can-rate + summary: The baud rate of the CAN interface. + - name: id + dtype: uint32 + meta: {export: True, reload_data: True} + getter_name: CAN_get_ID + setter_name: CAN_set_ID + summary: The ID of the CAN interface. + - name: motor + remote_attributes: + - name: R + dtype: float + unit: ohm + meta: {dynamic: True, export: True} + getter_name: motor_get_phase_resistance + setter_name: motor_set_phase_resistance + summary: The motor Resistance value. + - name: L + dtype: float + unit: henry + meta: {dynamic: True, export: True} + getter_name: motor_get_phase_inductance + setter_name: motor_set_phase_inductance + summary: The motor Inductance value. + - name: pole_pairs + dtype: uint8 + meta: {dynamic: True, export: True} + getter_name: motor_get_pole_pairs + setter_name: motor_set_pole_pairs + summary: The motor pole pair count. + - name: type + options: [HIGH_CURRENT, GIMBAL] + meta: {export: True} + getter_name: motor_get_is_gimbal + setter_name: motor_set_is_gimbal + summary: The type of the motor. Either high current or gimbal. + - name: offset + dtype: float + meta: {export: True} + getter_name: motor_get_user_offset + setter_name: motor_set_user_offset + summary: User-defined offset of the motor. + - name: direction + dtype: int8 + meta: {export: True} + getter_name: motor_get_user_direction + setter_name: motor_set_user_direction + summary: User-defined direction of the motor. + - name: calibrated + dtype: bool + meta: {dynamic: True} + getter_name: motor_get_calibrated + summary: Whether the motor has been calibrated. + - name: I_cal + dtype: float + unit: ampere + meta: {export: True} + getter_name: motor_get_I_cal + setter_name: motor_set_I_cal + summary: The calibration current. + - name: errors + flags: [PHASE_RESISTANCE_OUT_OF_RANGE, PHASE_INDUCTANCE_OUT_OF_RANGE,INVALID_POLE_PAIRS] + meta: {dynamic: True} + getter_name: motor_get_errors + summary: Any motor/calibration errors, as a bitmask + # - name: phase_currents + # remote_attributes: + # - name: U + # dtype: float + # unit: ampere + # getter_name: motor_get_IU + # summary: Measured current in phase U. + # - name: V + # dtype: float + # unit: ampere + # getter_name: motor_get_IV + # summary: Measured current in phase V. + # - name: W + # dtype: float + # unit: ampere + # getter_name: motor_get_IW + # summary: Measured current in phase W. + - name: encoder + remote_attributes: + - name: position_estimate + dtype: float + unit: ticks + meta: {dynamic: True} + getter_name: observer_get_pos_estimate_user_frame + summary: The filtered encoder position estimate. + - name: velocity_estimate + dtype: float + unit: ticks/second + meta: {dynamic: True} + getter_name: observer_get_vel_estimate_user_frame + summary: The filtered encoder velocity estimate. + - name: type + options: [INTERNAL, HALL] + meta: {export: True} + getter_name: encoder_get_type + setter_name: encoder_set_type + summary: The encoder type. Either INTERNAL or HALL. + - name: bandwidth + dtype: float + unit: Hz + meta: {export: True} + getter_name: observer_get_bw + setter_name: observer_set_bw + summary: The encoder observer bandwidth. + - name: calibrated + dtype: bool + meta: {dynamic: True} + getter_name: encoder_get_calibrated + summary: Whether the encoder has been calibrated. + - name: errors + flags: [CALIBRATION_FAILED, READING_UNSTABLE] + meta: {dynamic: True} + getter_name: encoder_get_errors + summary: Any encoder errors, as a bitmask + - name: traj_planner + remote_attributes: + - name: max_accel + dtype: float + unit: ticks/s + meta: {export: True} + getter_name: planner_get_max_accel + setter_name: planner_set_max_accel + summary: The max allowed acceleration of the generated trajectory. + - name: max_decel + dtype: float + unit: ticks/second/second + meta: {export: True} + getter_name: planner_get_max_decel + setter_name: planner_set_max_decel + summary: The max allowed deceleration of the generated trajectory. + - name: max_vel + dtype: float + unit: ticks/second + meta: {export: True} + getter_name: planner_get_max_vel + setter_name: planner_set_max_vel + summary: The max allowed cruise velocity of the generated trajectory. + - name: t_accel + dtype: float + unit: second + meta: {export: True} + getter_name: planner_get_deltat_accel + setter_name: planner_set_deltat_accel + summary: In time mode, the acceleration time of the generated trajectory. + - name: t_decel + dtype: float + unit: second + meta: {export: True} + getter_name: planner_get_deltat_decel + setter_name: planner_set_deltat_decel + summary: In time mode, the deceleration time of the generated trajectory. + - name: t_total + dtype: float + unit: second + meta: {export: True} + getter_name: planner_get_deltat_total + setter_name: planner_set_deltat_total + summary: In time mode, the total time of the generated trajectory. + - name: move_to + summary: Move to target position respecting velocity and acceleration limits. + caller_name: planner_move_to_vlimit + dtype: void + arguments: + - name: pos_setpoint + dtype: float + unit: tick + - name: move_to_tlimit + summary: Move to target position respecting time limits for each sector. + caller_name: planner_move_to_tlimit + dtype: void + arguments: + - name: pos_setpoint + dtype: float + unit: tick + - name: errors + flags: [INVALID_INPUT, VCRUISE_OVER_LIMIT] + getter_name: planner_get_errors + summary: Any errors in the trajectory planner, as a bitmask + - name: homing + remote_attributes: + - name: velocity + dtype: float + unit: ticks/s + meta: {export: True} + getter_name: homing_planner_get_homing_velocity + setter_name: homing_planner_set_homing_velocity + summary: The velocity at which the motor performs homing. + - name: max_homing_t + dtype: float + unit: s + meta: {export: True} + getter_name: homing_planner_get_max_homing_t + setter_name: homing_planner_set_max_homing_t + summary: The maximum time the motor is allowed to travel before homing times out and aborts. + - name: retract_dist + dtype: float + unit: ticks + meta: {export: True} + getter_name: homing_planner_get_retract_distance + setter_name: homing_planner_set_retract_distance + summary: The retraction distance the motor travels after the endstop has been found. + - name: warnings + meta: {dynamic: True} + flags: [HOMING_TIMEOUT] + getter_name: homing_planner_get_warnings + summary: Any homing warnings, as a bitmask + - name: stall_detect + remote_attributes: + - name: velocity + dtype: float + unit: ticks/s + meta: {export: True} + getter_name: homing_planner_get_max_stall_vel + setter_name: homing_planner_set_max_stall_vel + summary: The velocity below which (and together with `stall_detect.delta_pos`) stall detection mode is triggered. + - name: delta_pos + dtype: float + unit: ticks + meta: {export: True} + getter_name: homing_planner_get_max_stall_delta_pos + setter_name: homing_planner_set_max_stall_delta_pos + summary: The velocity below which (and together with `stall_detect.delta_pos`) stall detection mode is triggered. + - name: t + dtype: float + unit: s + meta: {export: True} + getter_name: homing_planner_get_max_stall_t + setter_name: homing_planner_set_max_stall_t + summary: The time to remain in stall detection mode before the motor is considered stalled. + - name: home + summary: Perform the homing operation. + caller_name: homing_planner_home + dtype: void + arguments: [] + - name: watchdog + remote_attributes: + - name: enabled + dtype: bool + getter_name: Watchdog_get_enabled + setter_name: Watchdog_set_enabled + summary: Whether the watchdog is enabled or not. + - name: triggered + dtype: bool + meta: {dynamic: True} + getter_name: Watchdog_triggered + summary: Whether the watchdog has been triggered or not. + - name: timeout + dtype: float + unit: s + meta: {export: True} + getter_name: Watchdog_get_timeout_seconds + setter_name: Watchdog_set_timeout_seconds + summary: The watchdog timeout period. From 4c3406be8f9efc4b00e81e4bab30fc00f50169cc Mon Sep 17 00:00:00 2001 From: Yannis Chatzikonstantinou Date: Tue, 10 Oct 2023 20:16:10 +0300 Subject: [PATCH 11/18] remove unused attribute --- studio/Python/tinymovr/gui/widgets.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/studio/Python/tinymovr/gui/widgets.py b/studio/Python/tinymovr/gui/widgets.py index 62061482..a457a3d0 100644 --- a/studio/Python/tinymovr/gui/widgets.py +++ b/studio/Python/tinymovr/gui/widgets.py @@ -39,9 +39,6 @@ class NodeTreeWidgetItem(QTreeWidgetItem): - def __init__(self, name, *args, **kwargs): - super().__init__([name, 0, ""], *args, **kwargs) - self._orig_flags = self.flags() def add_to_tree(self, tree_widget): tree_widget.addTopLevelItem(self) @@ -56,7 +53,6 @@ class EdgeTreeWidgetItem(QTreeWidgetItem): def __init__(self, name, node, *args, **kwargs): super().__init__([name, 0, ""], *args, **kwargs) self._tm_node = node - self._orig_flags = self.flags() self.setToolTip(0, node.summary) def _add_to_tree_cb(self): From 889243ff70684ce13ba22cdb223b1ac93bf58c9b Mon Sep 17 00:00:00 2001 From: Yannis Chatzikonstantinou Date: Tue, 10 Oct 2023 20:16:35 +0300 Subject: [PATCH 12/18] formatting --- studio/Python/tinymovr/gui/widgets.py | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/studio/Python/tinymovr/gui/widgets.py b/studio/Python/tinymovr/gui/widgets.py index a457a3d0..84349fa7 100644 --- a/studio/Python/tinymovr/gui/widgets.py +++ b/studio/Python/tinymovr/gui/widgets.py @@ -34,7 +34,7 @@ ) from pint.errors import UndefinedUnitError from avlos import get_registry -from avlos.datatypes import DataType +from avlos.datatypes import DataType from tinymovr.gui.helpers import load_icon, load_pixmap, format_value @@ -62,9 +62,16 @@ def _add_to_tree_cb(self): class AttrTreeWidgetItem(EdgeTreeWidgetItem): def __init__(self, name, node, *args, **kwargs): super().__init__(name, node, *args, **kwargs) - editable = hasattr(self._tm_node, "setter_name") and self._tm_node.setter_name != None + editable = ( + hasattr(self._tm_node, "setter_name") and self._tm_node.setter_name != None + ) if editable and node.dtype == DataType.FLOAT: - self.text_editor = JoggableLineEdit(format_value(node.get_value()), editable, editable, node.meta.get("jog_step")) + self.text_editor = JoggableLineEdit( + format_value(node.get_value()), + editable, + editable, + node.meta.get("jog_step"), + ) self.text_editor.ValueChangedByJog.connect(self._on_editor_text_changed) self.text_editor.editingFinished.connect(self._on_editor_text_changed) else: @@ -94,7 +101,7 @@ def _on_editor_text_changed(self): return else: self.text_editor.setText(format_value(attr.get_value())) - + @QtCore.Slot() def _on_checkbox_changed(self): checked = self.checkState(0) == QtCore.Qt.Checked @@ -244,7 +251,7 @@ def __init__(self, initial_text="0", editable=True, joggable=True, jog_step=None self.setText(initial_text) self.setReadOnly(not editable) self.normal_cursor = self.cursor() - + self.jog_start_timer = QTimer(self) self.jog_start_timer.setSingleShot(True) self.jog_start_timer.timeout.connect(self.start_jog) @@ -266,13 +273,13 @@ def start_jog(self): value = float(text) except ValueError: value = get_registry()(text).magnitude - self.current_jog_step = max(abs(value) * 0.01, 1e-6) + self.current_jog_step = max(abs(value) * 0.01, 1e-6) self.jogging = True def mouseReleaseEvent(self, event): if self.joggable: self.jog_start_timer.stop() - + self.setReadOnly(not self.editable) self.setCursor(self.normal_cursor) self.jogging = False @@ -287,7 +294,7 @@ def mouseMoveEvent(self, event): value = float(text) except ValueError: value = get_registry()(text).magnitude - value += self.current_jog_step * diff + value += self.current_jog_step * diff self.setText(str(value)) self.ValueChangedByJog.emit() except ValueError: From f05c66f8c01fa2b0ee5fa1bec7974df2aec164da Mon Sep 17 00:00:00 2001 From: Yannis Chatzikonstantinou Date: Tue, 10 Oct 2023 20:27:52 +0300 Subject: [PATCH 13/18] add class descriptions --- studio/Python/tinymovr/gui/widgets.py | 150 +++++++++++++++++++++++++- 1 file changed, 149 insertions(+), 1 deletion(-) diff --git a/studio/Python/tinymovr/gui/widgets.py b/studio/Python/tinymovr/gui/widgets.py index 84349fa7..a123a6bb 100644 --- a/studio/Python/tinymovr/gui/widgets.py +++ b/studio/Python/tinymovr/gui/widgets.py @@ -39,6 +39,19 @@ class NodeTreeWidgetItem(QTreeWidgetItem): + """ + NodeTreeWidgetItem: A specialized tree widget item class for managing hierarchical node structures. + + Designed to facilitate the addition of child nodes to a given tree widget, it ensures that the underlying node + structure is accurately reflected within the tree widget interface. + + Inheritance: + - Inherits from QTreeWidgetItem. + + Methods: + - add_to_tree(tree_widget): Adds the current tree widget item to the provided tree widget and initializes its children. + - _add_to_tree_cb(): Iteratively calls the '_add_to_tree_cb' method for each child node, enabling a recursive representation of the node hierarchy. + """ def add_to_tree(self, tree_widget): tree_widget.addTopLevelItem(self) @@ -50,6 +63,24 @@ def _add_to_tree_cb(self): class EdgeTreeWidgetItem(QTreeWidgetItem): + """ + EdgeTreeWidgetItem: A base tree widget item subclass for representing and managing nodes. + + Designed as a base class for other specialized tree widget items, it assists in presenting nodes + within a QTreeWidget. Each node has a name and a summary, with the summary being set as a tooltip + for the tree item. + + Inheritance: + - Inherits from QTreeWidgetItem. + + Attributes: + - _tm_node (Node): The node associated with this tree widget item. + + Methods: + - __init__(name, node, *args, **kwargs): Initializes the tree widget item with the provided name and node. + - _add_to_tree_cb(): A callback method meant to be overridden by subclasses for adding custom components to the tree. + """ + def __init__(self, name, node, *args, **kwargs): super().__init__([name, 0, ""], *args, **kwargs) self._tm_node = node @@ -60,6 +91,28 @@ def _add_to_tree_cb(self): class AttrTreeWidgetItem(EdgeTreeWidgetItem): + """ + AttrTreeWidgetItem: A tree widget item subclass designed for managing and presenting attributes. + + This widget item specializes in showing attributes and provides an interface for editing them, + especially when they are of type `FLOAT`. It integrates with the `JoggableLineEdit` for + floating-point attributes that support in-line jogging. For attributes that don't support + jogging or aren't of type `FLOAT`, a standard QLineEdit is used. + + Inheritance: + - Inherits from EdgeTreeWidgetItem. + + Attributes: + - text_editor (QLineEdit or JoggableLineEdit): Editor for the attribute's value. + - _checked (bool): A private attribute that maintains the checkbox state (for FLOAT types). + + Methods: + - _add_to_tree_cb(): Adds the text editor to the tree widget. If the attribute is of type FLOAT, a checkbox is also added. + - set_text(text): Sets the provided text to the text editor. + - _on_editor_text_changed(): Slot to handle changes in the text editor. This method manages the process of setting the attribute value and triggers data reload if necessary. + - _on_checkbox_changed(): Slot to handle checkbox state changes (used for FLOAT types). + """ + def __init__(self, name, node, *args, **kwargs): super().__init__(name, node, *args, **kwargs) editable = ( @@ -112,6 +165,24 @@ def _on_checkbox_changed(self): class FuncTreeWidgetItem(EdgeTreeWidgetItem): + """ + FuncTreeWidgetItem: A tree widget item subclass for managing and triggering functions. + + This widget item is specialized to present functions that can be invoked via a button. + Once the button is clicked, the function associated with the widget item is executed. + If the function requires arguments, an input dialog is presented to the user to collect them. + + Inheritance: + - Inherits from EdgeTreeWidgetItem. + + Attributes: + - None directly in this class. Inherits attributes from the superclass. + + Methods: + - _add_to_tree_cb(): Adds a button with an icon to the tree widget. The button serves as the trigger to invoke the associated function. + - _on_f_call_clicked(): Slot to handle button click events. This method manages the process of collecting function arguments and invoking the function. If the function has associated meta information indicating the need to reload data, a reset operation on the tree widget's worker is triggered. + """ + def _add_to_tree_cb(self): button = QPushButton("") button.setIcon(load_icon("call.png")) @@ -138,6 +209,24 @@ def _on_f_call_clicked(self): class OptionsTreeWidgetItem(EdgeTreeWidgetItem): + """ + OptionsTreeWidgetItem: A tree widget item subclass for managing selectable options. + + This widget item is specialized to handle options in the form of a combo box. + The combo box is populated with the provided options and integrated within the tree widget. + The current selection index of the combo box is synchronized with the underlying node's value. + + Attributes: + - combo_box_container (IconComboBoxWidget): A custom combo box widget containing an icon and a combo box. + + Inheritance: + - Inherits from EdgeTreeWidgetItem. + + Methods: + - _add_to_tree_cb(): Sets up and adds the combo box to the tree widget. + - _on_combobox_changed(int): Slot to handle combo box index changes, synchronizes the new index with the underlying node's value. + """ + def _add_to_tree_cb(self): self.combo_box_container = IconComboBoxWidget("call.png", self._tm_node.options) self.combo_box_container.combo.setCurrentIndex(self._tm_node.get_value()) @@ -237,10 +326,41 @@ def __init__(self, icon_path, enum_options, parent=None): class JoggableLineEdit(QLineEdit): + """ + A QLineEdit subclass that supports "jogging" (incremental adjustments) of its value + using mouse movement. + + Features: + - User can edit the text as a regular QLineEdit. + - By holding the mouse button down for a short delay, the control enters jogging mode. + Moving the mouse horizontally adjusts the value. + - The increment step for jogging can be preset or will be determined based on the current value. + + Signals: + - ValueChangedByJog: Emitted when the value is changed via jogging. + + Attributes: + - editable (bool): Determines if the QLineEdit is editable when not jogging. + - joggable (bool): Determines if the control supports jogging. + - jogging (bool): Indicates if the control is currently in jogging mode. + - jog_step (float or None): The increment step for jogging. If None, it's determined based on the value. + + Usage: + editor = JoggableLineEdit(initial_text="0", editable=True, joggable=True) + editor.ValueChangedByJog.connect(some_function) + """ ValueChangedByJog = Signal() - def __init__(self, initial_text="0", editable=True, joggable=True, jog_step=None, *args, **kwargs): + def __init__( + self, + initial_text="0", + editable=True, + joggable=True, + jog_step=None, + *args, + **kwargs, + ): super().__init__(*args, **kwargs) self.editable = editable self.joggable = joggable @@ -306,6 +426,34 @@ def mouseMoveEvent(self, event): class ArgumentInputDialog(QDialog): + """ + A QDialog subclass that provides a dynamic form for user input based on a list of arguments. + + The dialog populates a QFormLayout with a QLabel and QLineEdit for each provided argument. + It also appends Ok and Cancel buttons to finalize or dismiss the input. + + Features: + - Each argument provided will be represented as a row with its name and data type. + - User input can be retrieved as a dictionary using the `get_values` method. + + Attributes: + - arguments (list): A list of objects with 'name' and 'dtype' attributes to represent each argument. + - inputs (dict): A dictionary mapping argument names to their QLineEdit instances. + + Usage: + dialog = ArgumentInputDialog(arguments) + if dialog.exec_() == QDialog.Accepted: + values = dialog.get_values() + + Args: + - arguments (list): A list of objects where each object should have a 'name' and 'dtype' attribute. + - parent (QWidget, optional): Parent widget for this dialog. + + Methods: + - get_values(): Returns a dictionary mapping argument names to user inputs. + + """ + def __init__(self, arguments, parent=None): super(ArgumentInputDialog, self).__init__(parent) self.arguments = arguments From 4544979e7e6f4c0e4001b4b79680946858908a7d Mon Sep 17 00:00:00 2001 From: Yannis Chatzikonstantinou Date: Wed, 11 Oct 2023 14:54:58 +0300 Subject: [PATCH 14/18] static assert no integer remainder in setting custom pwm frequency --- firmware/src/adc/adc.c | 2 +- firmware/src/common.h | 5 ++++- firmware/src/gatedriver/gatedriver.h | 6 +++--- firmware/src/timer/timer.c | 2 +- 4 files changed, 9 insertions(+), 6 deletions(-) diff --git a/firmware/src/adc/adc.c b/firmware/src/adc/adc.c index a4be73c5..179fd7fb 100644 --- a/firmware/src/adc/adc.c +++ b/firmware/src/adc/adc.c @@ -221,7 +221,7 @@ void ADC_DTSE_Init(void) PAC55XX_ADC->DTSETRIGENT0TO3.TRIG1CFGIDX = 12; // DTSE Trigger 1 Sequence Configuration Entry Index PAC55XX_ADC->DTSETRIGENT0TO3.TRIG1EDGE = ADCDTSE_TRIGEDGE_RISING; // PWMA0 rising edge - pac5xxx_timer_a_ccctr1_value_set((timer_freq_hz / 2 / PWM_FREQ_HZ) - 2); + pac5xxx_timer_a_ccctr1_value_set((TIMER_FREQ_HZ/(2*PWM_FREQ_HZ)) - 2); //===== Setup DTSE Sequence B (sense current) - Starts at Entry 12 ===== pac5xxx_dtse_seq_config(12, ADC0, EMUX_AIO10, 0, 0); diff --git a/firmware/src/common.h b/firmware/src/common.h index 91cbfc67..d64398d9 100644 --- a/firmware/src/common.h +++ b/firmware/src/common.h @@ -129,16 +129,19 @@ #define BOARD_REV_IDX 21 #endif +#define TIMER_FREQ_HZ (ACLK_FREQ_HZ >> TXCTL_PS_DIV) + static const float one_by_sqrt3 = 0.57735026919f; static const float two_by_sqrt3 = 1.15470053838f; static const float threehalfpi = 4.7123889f; static const float pi = PI; static const float halfpi = PI * 0.5f; static const float quarterpi = PI * 0.25f; -static const int32_t timer_freq_hz = ACLK_FREQ_HZ >> TXCTL_PS_DIV; static const float twopi_by_enc_ticks = TWOPI / ENCODER_TICKS; static const float twopi_by_hall_sectors = TWOPI / HALL_SECTORS; +_Static_assert (TIMER_FREQ_HZ % (2*PWM_FREQ_HZ) == 0, "Timer frequency not an integer multiple of PWM frequency"); + typedef struct { float A; diff --git a/firmware/src/gatedriver/gatedriver.h b/firmware/src/gatedriver/gatedriver.h index 388f73ee..ae09bcfc 100644 --- a/firmware/src/gatedriver/gatedriver.h +++ b/firmware/src/gatedriver/gatedriver.h @@ -30,17 +30,17 @@ void gate_driver_set_duty_cycle(const FloatTriplet *dc); //============================================= static inline void m1_u_set_duty(const float duty) { - uint16_t val = ((uint16_t)(duty * (timer_freq_hz/PWM_FREQ_HZ) )) >>1; + uint16_t val = ((uint16_t)(duty * (TIMER_FREQ_HZ/PWM_FREQ_HZ) )) >>1; PAC55XX_TIMERA->CCTR4.CTR = val; } static inline void m1_v_set_duty(const float duty) { - uint16_t val = ((uint16_t)(duty * (timer_freq_hz/PWM_FREQ_HZ) )) >>1; + uint16_t val = ((uint16_t)(duty * (TIMER_FREQ_HZ/PWM_FREQ_HZ) )) >>1; PAC55XX_TIMERA->CCTR5.CTR = val; } static inline void m1_w_set_duty(const float duty) { - uint16_t val = ((uint16_t)(duty * (timer_freq_hz/PWM_FREQ_HZ) )) >>1; + uint16_t val = ((uint16_t)(duty * (TIMER_FREQ_HZ/PWM_FREQ_HZ) )) >>1; PAC55XX_TIMERA->CCTR6.CTR = val; } diff --git a/firmware/src/timer/timer.c b/firmware/src/timer/timer.c index 103d9119..78d84361 100644 --- a/firmware/src/timer/timer.c +++ b/firmware/src/timer/timer.c @@ -22,7 +22,7 @@ void Timer_Init(void) { // Configure Timer A Controls pac5xxx_timer_clock_config(TimerA, TXCTL_CS_ACLK, TXCTL_PS_DIV); // Configure timer clock input for ACLK, divider - pac5xxx_timer_base_config(TimerA, (timer_freq_hz/2/PWM_FREQ_HZ), AUTO_RELOAD, + pac5xxx_timer_base_config(TimerA, (TIMER_FREQ_HZ/(2*PWM_FREQ_HZ)), AUTO_RELOAD, TxCTL_MODE_UPDOWN, TIMER_SLAVE_SYNC_DISABLE); // Configure timer frequency and count mode // Configure Dead time generators From 3864bb1b1b0f2a27cda2a2e89e382085406689de Mon Sep 17 00:00:00 2001 From: Yannis Chatzikonstantinou Date: Wed, 11 Oct 2023 14:56:06 +0300 Subject: [PATCH 15/18] fix names disappearing --- studio/Python/tinymovr/gui/widgets.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/studio/Python/tinymovr/gui/widgets.py b/studio/Python/tinymovr/gui/widgets.py index a123a6bb..6dacf5a9 100644 --- a/studio/Python/tinymovr/gui/widgets.py +++ b/studio/Python/tinymovr/gui/widgets.py @@ -52,6 +52,8 @@ class NodeTreeWidgetItem(QTreeWidgetItem): - add_to_tree(tree_widget): Adds the current tree widget item to the provided tree widget and initializes its children. - _add_to_tree_cb(): Iteratively calls the '_add_to_tree_cb' method for each child node, enabling a recursive representation of the node hierarchy. """ + def __init__(self, name, *args, **kwargs): + super().__init__([name, 0, ""], *args, **kwargs) def add_to_tree(self, tree_widget): tree_widget.addTopLevelItem(self) From bf80c1f51e449a6576ca539bd1cfc5a604ccb505 Mon Sep 17 00:00:00 2001 From: Yannis Chatzikonstantinou Date: Wed, 11 Oct 2023 14:56:21 +0300 Subject: [PATCH 16/18] only update fields not in focus (editing) --- studio/Python/tinymovr/gui/widgets.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/studio/Python/tinymovr/gui/widgets.py b/studio/Python/tinymovr/gui/widgets.py index 6dacf5a9..6914b827 100644 --- a/studio/Python/tinymovr/gui/widgets.py +++ b/studio/Python/tinymovr/gui/widgets.py @@ -141,7 +141,8 @@ def _add_to_tree_cb(self): self.setCheckState(0, QtCore.Qt.Unchecked) def set_text(self, text): - self.text_editor.setText(text) + if not self.text_editor.hasFocus(): + self.text_editor.setText(text) @QtCore.Slot() def _on_editor_text_changed(self): From 2504b0b801b7716606f1aaa87f68c16ab9151ca8 Mon Sep 17 00:00:00 2001 From: Yannis Chatzikonstantinou Date: Sat, 6 Jan 2024 20:12:00 +0200 Subject: [PATCH 17/18] change test limits --- studio/Python/tests/test_base_function.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/studio/Python/tests/test_base_function.py b/studio/Python/tests/test_base_function.py index c204587d..a56c6837 100644 --- a/studio/Python/tests/test_base_function.py +++ b/studio/Python/tests/test_base_function.py @@ -22,15 +22,17 @@ def test_position_control(self): Test position control """ self.check_state(0) + self.tm.motor.I_cal = 5 + self.tm.controller.current.Iq_limit = 5 self.try_calibrate() self.tm.controller.position_mode() self.check_state(2) for i in range(5): - self.tm.controller.position.setpoint = i * 10000 * ticks + self.tm.controller.position.setpoint = i * 3000 * ticks time.sleep(0.25) self.assertAlmostEqual( - i * 10000 * ticks, self.tm.encoder.position_estimate, delta=1000 * ticks + i * 3000 * ticks, self.tm.encoder.position_estimate, delta=1000 * ticks ) From e95bacb0313625b0f07f1832ae9b453d9207c57d Mon Sep 17 00:00:00 2001 From: Yannis Chatzikonstantinou Date: Sat, 6 Jan 2024 20:13:00 +0200 Subject: [PATCH 18/18] allow adding custom spec files as cli parameters --- studio/Python/tests/test_dfu.py | 3 +- studio/Python/tinymovr/cli.py | 12 +++- studio/Python/tinymovr/config/__init__.py | 3 +- studio/Python/tinymovr/config/config.py | 82 +++++++++++------------ studio/Python/tinymovr/discovery.py | 4 +- studio/Python/tinymovr/gui/gui.py | 16 ++++- studio/Python/tinymovr/gui/window.py | 10 ++- 7 files changed, 73 insertions(+), 57 deletions(-) diff --git a/studio/Python/tests/test_dfu.py b/studio/Python/tests/test_dfu.py index 344e6a54..c560b576 100644 --- a/studio/Python/tests/test_dfu.py +++ b/studio/Python/tests/test_dfu.py @@ -22,8 +22,7 @@ from tinymovr import init_tee, destroy_tee from tinymovr.config import ( get_bus_config, - create_device, - definitions + create_device ) import unittest diff --git a/studio/Python/tinymovr/cli.py b/studio/Python/tinymovr/cli.py index 3c70a9d7..03a7ea16 100644 --- a/studio/Python/tinymovr/cli.py +++ b/studio/Python/tinymovr/cli.py @@ -1,16 +1,18 @@ """Tinymovr Studio CLI Usage: - tinymovr_cli [--bus=] [--chan=] [--bitrate=] + tinymovr_cli [--bus=] [--chan=] [--spec=] [--bitrate=] tinymovr_cli -h | --help tinymovr_cli --version Options: --bus= One or more interfaces to use, first available is used [default: canine,slcan_disco]. --chan= The bus device "channel". + --spec= A custom device spec to be added to the list of discoverable spec. --bitrate= CAN bitrate [default: 1000000]. """ +import yaml import can import pkg_resources import IPython @@ -20,7 +22,7 @@ from tinymovr import init_tee, destroy_tee from tinymovr.discovery import Discovery from tinymovr.constants import app_name -from tinymovr.config import get_bus_config, configure_logging +from tinymovr.config import get_bus_config, configure_logging, add_spec """ Tinymovr CLI Module @@ -49,6 +51,12 @@ def spawn(): logger = configure_logging() + spec_file = arguments["--spec"] + if spec_file: + with open(spec_file, 'r') as file: + spec_data = yaml.safe_load(file) + add_spec(spec_data, logger) + buses = arguments["--bus"].rsplit(sep=",") channel = arguments["--chan"] bitrate = int(arguments["--bitrate"]) diff --git a/studio/Python/tinymovr/config/__init__.py b/studio/Python/tinymovr/config/__init__.py index f48375b6..87c5bf66 100644 --- a/studio/Python/tinymovr/config/__init__.py +++ b/studio/Python/tinymovr/config/__init__.py @@ -1,8 +1,7 @@ from tinymovr.config.config import ( get_bus_config, configure_logging, - definitions, create_device, create_device_with_hash_msg, - ProtocolVersionError, + add_spec, ) diff --git a/studio/Python/tinymovr/config/config.py b/studio/Python/tinymovr/config/config.py index 91dc61d0..28ab4698 100644 --- a/studio/Python/tinymovr/config/config.py +++ b/studio/Python/tinymovr/config/config.py @@ -25,29 +25,30 @@ from tinymovr.codec import DataType from tinymovr.channel import CANChannel -definitions = {"hash_uint32": {}, "name": {}} - -for yaml_file in Path(files("tinymovr").joinpath("specs/")).glob("*.yaml"): - with open(str(yaml_file)) as def_raw: - definition = yaml.safe_load(def_raw) - tmp_node = deserialize(definition) - definitions["hash_uint32"][tmp_node.hash_uint32] = definition - definitions["name"][definition["name"]] = definition - - -class ProtocolVersionError(Exception): - def __init__(self, dev_id, version_str, *args, **kwargs): - self.dev_id = dev_id - self.version_str = cleanup_incomplete_version(version_str) - msg = ( - "Incompatible protocol versions (hash mismatch) for device {}. " - "Firmware is compatible with Studio version {}.\n\n" - "Either upgrade studio and firmware, or install a compatible Studio version like so:\n\n" - "pip3 uninstall tinymovr\npip3 install tinymovr=={}".format( - self.dev_id, self.version_str, self.version_str - ) - ) - super().__init__(msg, *args, **kwargs) +specs = {"hash_uint32": {}} + + +def init_specs_dict(): + global specs + for yaml_file in Path(files("tinymovr").joinpath("specs/")).glob("*.yaml"): + with open(str(yaml_file)) as def_raw: + spec = yaml.safe_load(def_raw) + add_spec(spec) + + +def add_spec(spec, logger=None): + if logger is None: + logger = logging.getLogger("tinymovr") + + tmp_node = deserialize(spec) + hash_value = tmp_node.hash_uint32 + if hash_value in specs["hash_uint32"]: + logger.warning("Provided spec with hash {} already exists in hash/name dictionary".format(hash_value)) + else: + specs["hash_uint32"][hash_value] = spec + + +init_specs_dict() def get_bus_config(suggested_types=None): @@ -70,24 +71,22 @@ def create_device(node_id): """ chan = CANChannel(node_id) - # Temporarily using a default definition to get the protocol_hash - # This assumes that `protocol_hash` is standard across different definitions - # Get the first definition as a temp - tmp_definition = list(definitions["hash_uint32"].values())[0] - node = deserialize(tmp_definition) + # Temporarily using a default spec to get the protocol_hash + # This assumes that `protocol_hash` is standard across different specs + # Get the first spec as a temp + tmp_spec = list(specs["hash_uint32"].values())[0] + node = deserialize(tmp_spec) node._channel = chan - # Check for the correct definition using the remote hash + # Check for the correct spec using the remote hash protocol_hash = node.protocol_hash - device_definition = definitions["hash_uint32"].get(protocol_hash) + device_spec = specs["hash_uint32"].get(protocol_hash) - if not device_definition: - raise ValueError(f"No device definition found for hash {protocol_hash}.") + if not device_spec: + raise ValueError(f"No device spec found for hash {protocol_hash}.") - node = deserialize(device_definition) + node = deserialize(device_spec) node._channel = chan - if node.hash_uint32 != protocol_hash: - raise ProtocolVersionError(node_id, "") return node @@ -101,17 +100,12 @@ def create_device_with_hash_msg(heartbeat_msg): chan = CANChannel(node_id) hash, *_ = chan.serializer.deserialize(heartbeat_msg.data[:4], DataType.UINT32) - device_definition = definitions["hash_uint32"].get(hash) + device_spec = specs["hash_uint32"].get(hash) - if not device_definition: - raise ValueError(f"No device definition found for hash {hash}.") + if not device_spec: + raise ValueError(f"No device spec found for hash {hash}.") - node = deserialize(device_definition) - if node.hash_uint32 != hash: - version_str = "".join([chr(n) for n in heartbeat_msg.data[4:]]) - if not version_str.strip(): - version_str = "1.3.1" - raise ProtocolVersionError(node_id, version_str) + node = deserialize(device_spec) node._channel = chan return node diff --git a/studio/Python/tinymovr/discovery.py b/studio/Python/tinymovr/discovery.py index 4a643354..f4f946f0 100644 --- a/studio/Python/tinymovr/discovery.py +++ b/studio/Python/tinymovr/discovery.py @@ -21,7 +21,7 @@ from tinymovr.channel import ResponseError from tinymovr.tee import get_tee from tinymovr.constants import HEARTBEAT_BASE -from tinymovr.config import create_device_with_hash_msg, ProtocolVersionError +from tinymovr.config import create_device_with_hash_msg class Discovery: @@ -72,7 +72,7 @@ def _recv_cb(self, frame): self._append_to_queue((node, node_id)) except ResponseError as e: self.logger.error(e) - except ProtocolVersionError as e: + except ValueError as e: self.logger.error(e) self.incompatible_nodes.add(node_id) self.pending_nodes.remove(node_id) diff --git a/studio/Python/tinymovr/gui/gui.py b/studio/Python/tinymovr/gui/gui.py index 0d1902ca..99539b00 100644 --- a/studio/Python/tinymovr/gui/gui.py +++ b/studio/Python/tinymovr/gui/gui.py @@ -1,23 +1,26 @@ """Tinymovr Studio GUI Usage: - tinymovr [--bus=] [--chan=] [--bitrate=] [--max-timeouts=] + tinymovr [--bus=] [--chan=] [--spec=] [--bitrate=] [--max-timeouts=] tinymovr -h | --help tinymovr --version Options: --bus= One or more interfaces to use, first available is used [default: canine,slcan_disco]. --chan= The bus device "channel". + --spec= A custom device spec to be added to the list of discoverable specs. --bitrate= CAN bitrate [default: 1000000]. --max-timeouts= Max timeouts before nodes are rescanned [default: 5]. """ import sys +import yaml import pkg_resources from docopt import docopt from PySide6.QtWidgets import QApplication from tinymovr.gui import MainWindow, app_stylesheet, app_stylesheet_dark, is_dark_mode from tinymovr.constants import app_name +from tinymovr.config import configure_logging, add_spec """ @@ -41,11 +44,20 @@ def spawn(): version = pkg_resources.require("tinymovr")[0].version arguments = docopt(__doc__, version=app_name + " " + str(version)) + + logger = configure_logging() + + spec_file = arguments["--spec"] + if spec_file: + with open(spec_file, 'r') as file: + spec_data = yaml.safe_load(file) + add_spec(spec_data, logger) + app = QApplication(sys.argv) if is_dark_mode(): app.setStyleSheet(app_stylesheet_dark) else: app.setStyleSheet(app_stylesheet) - w = MainWindow(app, arguments) + w = MainWindow(app, arguments, logger) w.show() sys.exit(app.exec_()) diff --git a/studio/Python/tinymovr/gui/window.py b/studio/Python/tinymovr/gui/window.py index 1e411c79..00d50752 100644 --- a/studio/Python/tinymovr/gui/window.py +++ b/studio/Python/tinymovr/gui/window.py @@ -16,6 +16,7 @@ """ import time +import logging import pkg_resources from functools import partial from contextlib import suppress @@ -42,7 +43,7 @@ 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 tinymovr.config import get_bus_config from avlos import get_registry from avlos.json_codec import AvlosEncoder from tinymovr.gui import ( @@ -62,14 +63,17 @@ class MainWindow(QMainWindow): TreeItemCheckedSignal = Signal(dict) - def __init__(self, app, arguments): + def __init__(self, app, arguments, logger): super(MainWindow, self).__init__() # set units default format get_registry().default_format = ".6f~" self.start_time = time.time() - self.logger = configure_logging() + if logger is None: + self.logger = logging.getLogger("tinymovr") + else: + self.logger = logger self.attr_widgets_by_id = {} self.graphs_by_id = {}