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/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/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 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 ) 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) 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/__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/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/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..6914b827 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 PySide6 import QtGui, QtCore +from PySide6.QtCore import Signal, QTimer from PySide6.QtGui import QPixmap from PySide6.QtWidgets import ( QWidget, QTreeWidget, + QTreeWidgetItem, QHBoxLayout, QLabel, QComboBox, @@ -13,11 +32,239 @@ QPushButton, QFormLayout, ) +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 + + +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 __init__(self, name, *args, **kwargs): + super().__init__([name, 0, ""], *args, **kwargs) + + 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 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 + self.setToolTip(0, node.summary) + + def _add_to_tree_cb(self): + pass + + +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 = ( + 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.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) + if self._tm_node.dtype == DataType.FLOAT: + self.setCheckState(0, QtCore.Qt.Unchecked) + + def set_text(self, text): + if not self.text_editor.hasFocus(): + self.text_editor.setText(text) + + @QtCore.Slot() + def _on_editor_text_changed(self): + attr = self._tm_node + 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: + 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): + """ + 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")) + self.treeWidget().setItemWidget(self, 1, button) + button.clicked.connect(self._on_f_call_clicked) + + @QtCore.Slot() + def _on_f_call_clicked(self): + args = [] + f = self._tm_node + + 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] -from tinymovr.gui.helpers import load_pixmap + f(*args) + if "reload_data" in f.meta and f.meta["reload_data"]: + self.treeWidget().window().worker.reset() -class OurQTreeWidget(QTreeWidget): +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()) + self.combo_box_container.combo.currentIndexChanged.connect( + self._on_combobox_changed + ) + self.treeWidget().setItemWidget(self, 1, self.combo_box_container) + + @QtCore.Slot() + 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): + """ + 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. + """ + def __init__(self, parent=None): super().__init__(parent) self.placeholder_image = load_pixmap("empty.png") @@ -42,26 +289,174 @@ 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) +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, + ): + 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() + + 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)) + 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): + 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 = float(text) + except ValueError: + value = get_registry()(text).magnitude + value += self.current_jog_step * diff + self.setText(str(value)) + self.ValueChangedByJog.emit() + except ValueError: + print("valueerror") + self.last_x = event.x() + else: + self.jog_start_timer.stop() + super().mouseMoveEvent(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 diff --git a/studio/Python/tinymovr/gui/window.py b/studio/Python/tinymovr/gui/window.py index 1e411c79..bee0cf0e 100644 --- a/studio/Python/tinymovr/gui/window.py +++ b/studio/Python/tinymovr/gui/window.py @@ -16,15 +16,14 @@ """ import time +import logging import pkg_resources -from functools import partial from contextlib import suppress import json from PySide6 import QtCore from PySide6.QtCore import Signal, QTimer from PySide6.QtWidgets import ( QMainWindow, - QDialog, QMenu, QMenuBar, QWidget, @@ -33,25 +32,23 @@ QVBoxLayout, QHeaderView, QLabel, - QTreeWidgetItem, - QPushButton, QMessageBox, ) -from pint.errors import UndefinedUnitError 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 tinymovr.config import get_bus_config from avlos import get_registry from avlos.json_codec import AvlosEncoder from tinymovr.gui import ( + NodeTreeWidgetItem, + AttrTreeWidgetItem, + FuncTreeWidgetItem, + OptionsTreeWidgetItem, Worker, - OurQTreeWidget, - IconComboBoxWidget, - ArgumentInputDialog, + PlaceholderQTreeWidget, format_value, - load_icon, display_file_open_dialog, display_file_save_dialog, magnitude_of, @@ -62,14 +59,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 = {} @@ -97,9 +97,8 @@ 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"]) self.status_label = QLabel() @@ -112,8 +111,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,88 +217,50 @@ 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): - # 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 item._on_checkbox_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): 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: @@ -326,36 +287,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 ( - 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): 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() 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.