From cde6f9bd21c9b06b4cc1e8b6d726ab597c77a4b7 Mon Sep 17 00:00:00 2001 From: PIERROOOTT Date: Tue, 28 Nov 2023 17:11:16 +0100 Subject: [PATCH 01/35] feat: add the Phidget actuator stepper 4A --- src/crappy/actuator/__init__.py | 1 + src/crappy/actuator/phidgets_stepper4a.py | 222 ++++++++++++++++++++++ 2 files changed, 223 insertions(+) create mode 100644 src/crappy/actuator/phidgets_stepper4a.py diff --git a/src/crappy/actuator/__init__.py b/src/crappy/actuator/__init__.py index be1b5ab8..cddf4745 100644 --- a/src/crappy/actuator/__init__.py +++ b/src/crappy/actuator/__init__.py @@ -9,6 +9,7 @@ from .kollmorgen_servostar_300 import ServoStar300 from .newport_tra6ppd import NewportTRA6PPD from .oriental_ard_k import OrientalARDK +from .phidgets_stepper4a import Phidget4AStepper from .pololu_tic import PololuTic from .schneider_mdrive_23 import SchneiderMDrive23 diff --git a/src/crappy/actuator/phidgets_stepper4a.py b/src/crappy/actuator/phidgets_stepper4a.py new file mode 100644 index 00000000..2a2e4681 --- /dev/null +++ b/src/crappy/actuator/phidgets_stepper4a.py @@ -0,0 +1,222 @@ +# coding: utf-8 + +import logging +from typing import Optional + +from .meta_actuator import Actuator +from .._global import OptionalModule + +try: + from Phidget22.Net import Net, PhidgetServerType + from Phidget22.Devices.Stepper import Stepper, StepperControlMode + from Phidget22.PhidgetException import PhidgetException +except (ImportError, ModuleNotFoundError): + Net = OptionalModule('Phidget22') + PhidgetServerType = OptionalModule('Phidget22') + Stepper = OptionalModule('Phidget22') + StepperControlMode = OptionalModule('Phidget22') + PhidgetException = OptionalModule('Phidget22') + + +class Phidget4AStepper(Actuator): + """This class can drive Phidget's 4A Stepper module in speed or in position. + + It relies on the :mod:`Phidget22` module to communicate with the motor + driver. The driver can deliver up to 4A to the motor, and uses 16 microsteps + by default. Its acquisition rate is set to 10 values per second in this + class. + + The distance unit is the `mm` and the time unit is the `s`, so speeds are in + `mm/s` and accelerations in `mm/s²`. + """ + + def __init__(self, + steps_per_mm: float, + current_limit: float, + max_acceleration: Optional[float] = None, + remote: bool = False) -> None: + """Sets the args and initializes the parent class. + + Args: + steps_per_mm: The number of steps necessary to move by `1 mm`. This value + is used to set a conversion factor on the driver, so that it can be + driven in `mm` and `s` units. + current_limit: The maximum current the driver is allowed to deliver to + the motor, in `A` . + max_acceleration: If given, sets the maximum acceleration the motor is + allowed to reach in `mm/s²`. + """ + + self._motor: Optional[Stepper] = None + + super().__init__() + + self._steps_per_mm = steps_per_mm + self._current_limit = current_limit + self._max_acceleration = max_acceleration + self.remote = remote + + # These buffers store the last known position and speed + self._last_velocity: Optional[float] = None + self._last_position: Optional[float] = None + + def open(self) -> None: + """Sets up the connection to the motor driver as well as the various + callbacks, and waits for the motor driver to attach.""" + + # Setting up the motor driver + self.log(logging.DEBUG, "Enabling server discovery") + Net.enableServerDiscovery(PhidgetServerType.PHIDGETSERVER_DEVICEREMOTE) + self._motor = Stepper() + if self.remote is True: + self._motor.setIsLocal(False) + self._motor.setIsRemote(True) + else: + self._motor.setIsLocal(True) + self._motor.setIsRemote(False) + self._motor.setIsRemote(True) + + # Setting up the callbacks + self.log(logging.DEBUG, "Setting the callbacks") + self._motor.setOnAttachHandler(self._on_attach) + self._motor.setOnErrorHandler(self._on_error) + self._motor.setOnVelocityChangeHandler(self._on_velocity_change) + self._motor.setOnPositionChangeHandler(self._on_position_change) + + # Opening the connection to the motor driver + try: + self.log(logging.DEBUG, "Trying to attach the motor") + self._motor.openWaitForAttachment(10000) + except PhidgetException: + raise TimeoutError("Waited too long for the motor to attach !") + + # Energizing the motor + self._motor.setEngaged(True) + + def set_speed(self, speed: float) -> None: + """Sets the requested speed for the motor. + + Switches to the correct driving mode if needed. + + Args: + speed: The speed to reach, in `mm/s`. + """ + + # Switching the control mode if needed + if not self._motor.getControlMode() == StepperControlMode.CONTROL_MODE_RUN: + self.log(logging.DEBUG, "Setting the control mode to run") + self._motor.setControlMode(StepperControlMode.CONTROL_MODE_RUN) + + # Setting the desired velocity + if abs(speed) > self._motor.getMaxVelocityLimit(): + raise ValueError(f"Cannot set a velocity greater than " + f"{self._motor.getMaxVelocityLimit()} mm/s !") + else: + self._motor.setVelocityLimit(speed) + + def set_position(self, + position: float, + speed: Optional[float] = None) -> None: + """Sets the requested position for the motor. + + Switches to the correct driving mode if needed. + + Args: + position: The position to reach, in `mm`. + speed: If not :obj:`None`, the speed to use for moving to the desired + position. + """ + + # Switching the control mode if needed + if not (self._motor.getControlMode() == + StepperControlMode.CONTROL_MODE_STEP): + self.log(logging.DEBUG, "Setting the control mode to step") + self._motor.setControlMode(StepperControlMode.CONTROL_MODE_STEP) + + # Setting the desired velocity if required + if speed is not None: + if abs(speed) > self._motor.getMaxVelocityLimit(): + raise ValueError(f"Cannot set a velocity greater than " + f"{self._motor.getMaxVelocityLimit()} mm/s !") + else: + self._motor.setVelocityLimit(abs(speed)) + + # Setting the requested position + min_pos = self._motor.getMinPosition() + max_pos = self._motor.getMaxPosition() + if not min_pos <= position <= max_pos: + raise ValueError(f"The position value must be between {min_pos} and " + f"{max_pos}, got {position} !") + else: + self._motor.setTargetPosition(position) + + def get_speed(self) -> Optional[float]: + """Returns the last known speed of the motor.""" + + return self._last_velocity + + def get_position(self) -> Optional[float]: + """Returns the last known position of the motor.""" + + return self._last_position + + def stop(self) -> None: + """Deenergizes the motor.""" + + if self._motor is not None: + self._motor.setEngaged(False) + + def close(self) -> None: + """Closes the connection to the motor.""" + + if self._motor is not None: + self._motor.close() + + def _on_attach(self, _: Stepper) -> None: + """Callback called when the motor driver attaches to the program. + + It sets the current limit, scale factor, data rate and maximum + acceleration. + """ + + self.log(logging.INFO, "Motor successfully attached") + + # Setting the current limit for the motor + min_current = self._motor.getMinCurrentLimit() + max_current = self._motor.getMaxCurrentLimit() + if not min_current <= self._current_limit <= max_current: + raise ValueError(f"The current limit should be between {min_current} $" + f"and {max_current} A !") + else: + self._motor.setCurrentLimit(self._current_limit) + + # Setting the scale factor and the data rate + self._motor.setRescaleFactor(1 / 16 / self._steps_per_mm) + self._motor.setDataInterval(100) + + # Setting the maximum acceleration + if self._max_acceleration is not None: + min_accel = self._motor.getMinAcceleration() + max_accel = self._motor.getMaxAcceleration() + if not min_accel <= self._max_acceleration <= max_accel: + raise ValueError(f"The maximum acceleration should be between " + f"{min_accel} and {max_accel} m/s² !") + else: + self._motor.setAcceleration(self._max_acceleration) + + def _on_error(self, _: Stepper, error_code: int, error: str) -> None: + """Callback called when the motor driver returns an error.""" + + raise RuntimeError(f"Got error with error code {error_code}: {error}") + + def _on_velocity_change(self, _: Stepper, velocity: float) -> None: + """Callback called when the motor velocity changes.""" + + self.log(logging.DEBUG, f"Velocity changed to {velocity}") + self._last_velocity = velocity + + def _on_position_change(self, _: Stepper, position: float) -> None: + """Callback called when the motor position changes.""" + + self.log(logging.DEBUG, f"Position changed to {position}") + self._last_position = position From a2d7737ff6ebaf044df500a3a3797df1e73e7041 Mon Sep 17 00:00:00 2001 From: PIERROOOTT Date: Tue, 28 Nov 2023 17:12:26 +0100 Subject: [PATCH 02/35] feat: add the Phidget InOut Wheatstone Bridge --- src/crappy/inout/__init__.py | 1 + .../inout/pjidgets_wheatstone_bridge.py | 150 ++++++++++++++++++ 2 files changed, 151 insertions(+) create mode 100644 src/crappy/inout/pjidgets_wheatstone_bridge.py diff --git a/src/crappy/inout/__init__.py b/src/crappy/inout/__init__.py index b7752727..44c3262d 100644 --- a/src/crappy/inout/__init__.py +++ b/src/crappy/inout/__init__.py @@ -19,6 +19,7 @@ from .ni_daqmx import NIDAQmx from .opsens_handysens import HandySens from .pijuice_hat import PiJuice +from .pjidgets_wheatstone_bridge import PhidgetWheatstoneBridge from .sim868 import Sim868 from .spectrum_m2i4711 import SpectrumM2I4711 from .waveshare_ad_da import WaveshareADDA diff --git a/src/crappy/inout/pjidgets_wheatstone_bridge.py b/src/crappy/inout/pjidgets_wheatstone_bridge.py new file mode 100644 index 00000000..0a0b1f0e --- /dev/null +++ b/src/crappy/inout/pjidgets_wheatstone_bridge.py @@ -0,0 +1,150 @@ +# coding: utf-8 + +from time import time +from typing import Optional, List +import logging +from math import log2 + +from .meta_inout import InOut +from .._global import OptionalModule + +try: + from Phidget22.Net import Net, PhidgetServerType + from Phidget22.Devices.VoltageRatioInput import VoltageRatioInput + from Phidget22.PhidgetException import PhidgetException +except (ImportError, ModuleNotFoundError): + Net = OptionalModule('Phidget22') + PhidgetServerType = OptionalModule('Phidget22') + VoltageRatioInput = OptionalModule('Phidget22') + PhidgetException = OptionalModule('Phidget22') + + +class PhidgetWheatstoneBridge(InOut): + """This class can read voltage ratio values from a Phidget Wheatstone Bridge. + + It relies on the :mod:`Phidget22` module to communicate with the load cell + conditioner. It can acquire values up to `50Hz` with possible gain values + from `1` to `128`. + """ + + def __init__(self, + channel: int = 0, + hardware_gain: int = 1, + data_rate: float = 50, + gain: float = 1, + offset: float = 0, + remote: bool = False) -> None: + """Sets the args and initializes the parent class. + + Args: + channel: The index of the channel from which to acquire data, as an + :obj:`int`. Should be either `0` or `1`. + hardware_gain: The gain used by the conditioner for data acquisition. The + higher the gain, the better the resolution but the narrower the range. + Should be a power of `2` between `1` and `128`. + data_rate: The number of samples to acquire per second, as an :obj:`int`. + Should be between `0.017` (one sample per minute) and `50`. + gain: A gain to apply to the acquired value, following the formula : + :: + + returned = gain * acquired + offset + offset: An offset to apply to the acquired value, following the formula : + :: + + returned = gain * acquired + offset + remote: True when the channel is attached via a Phidget network server. + """ + + self._load_cell: Optional[VoltageRatioInput] = None + + super().__init__() + + if channel not in (0, 1): + raise ValueError("The channel should be 0 or 1 !") + self._channel = channel + + if hardware_gain not in (2 ** i for i in range(8)): + raise ValueError("The hardware gain should be either 1, 2, 4, 8, 16, " + "32, 64 or 128 !") + self._hardware_gain = hardware_gain + + self._data_rate = data_rate + self._gain = gain + self._offset = offset + self.remote = remote + + self._last_ratio: Optional[float] = None + + def open(self) -> None: + """Sets up the connection to the load cell conditioner as well as the + various callbacks, and waits for the load cell conditioner to attach.""" + + # Setting up the load cell conditioner + self.log(logging.DEBUG, "Enabling server discovery") + Net.enableServerDiscovery(PhidgetServerType.PHIDGETSERVER_DEVICEREMOTE) + self._load_cell = VoltageRatioInput() + if self.remote is True: + self._load_cell.setIsLocal(False) + self._load_cell.setIsRemote(True) + else: + self._load_cell.setIsLocal(True) + self._load_cell.setIsRemote(False) + self._load_cell.setChannel(self._channel) + + # Setting up the callbacks + self._load_cell.setOnAttachHandler(self._on_attach) + self._load_cell.setOnVoltageRatioChangeHandler(self._on_ratio_change) + + # Opening the connection to the load cell conditioner + try: + self.log(logging.DEBUG, "Trying to attach the load cell conditioner") + self._load_cell.openWaitForAttachment(10000) + except PhidgetException: + raise TimeoutError("Waited too long for the motor to attach !") + + def get_data(self) -> Optional[List[float]]: + """Returns the last known voltage ratio value, adjusted with the gain and + the offset.""" + + if self._last_ratio is not None: + return [time(), self._gain * self._last_ratio + self._offset] + + def close(self) -> None: + """Closes the connection to the load cell conditioner.""" + + if self._load_cell is not None: + self._load_cell.close() + + def _on_attach(self, _: VoltageRatioInput) -> None: + """Callback called when the load cell conditioner attaches to the program. + + Sets the data rate and the hardware gain of the conditioner. + """ + + self.log(logging.INFO, "Load cell conditioner successfully attached") + + # Setting the hardware gain + self._load_cell.setBridgeGain(int(round(log2(self._hardware_gain), 0) + 1)) + + # Setting the data rate + min_rate = self._load_cell.getMinDataRate() + max_rate = self._load_cell.getMaxDataRate() + if not min_rate <= self._data_rate <= max_rate: + raise ValueError(f"The data rate should be between {min_rate} and " + f"{max_rate}, got {self._data_rate} !") + else: + self._load_cell.setDataRate(self._data_rate) + + def _on_ratio_change(self, _: VoltageRatioInput, ratio: float) -> None: + """Callback called when the voltage ratio changes.""" + + self.log(logging.DEBUG, f"Voltage ratio changed to {ratio}") + self._last_ratio = ratio + + def _on_error(self, + _: VoltageRatioInput, + error_code: int, + error: str) -> None: + """Callback called when the load cell conditioner returns an error.""" + + raise RuntimeError(f"Got error with error code {error_code}: {error}") From dca04bb12c5d44548013fa96e15b1e585f4bdf26 Mon Sep 17 00:00:00 2001 From: PIERROOOTT Date: Tue, 28 Nov 2023 17:16:12 +0100 Subject: [PATCH 03/35] docs: add the Phidget actuator stepper 4A in the documentation --- docs/source/crappy_docs/actuators.rst | 7 +++++++ docs/source/features.rst | 11 +++++++++++ 2 files changed, 18 insertions(+) diff --git a/docs/source/crappy_docs/actuators.rst b/docs/source/crappy_docs/actuators.rst index b1356185..ce23d7c4 100644 --- a/docs/source/crappy_docs/actuators.rst +++ b/docs/source/crappy_docs/actuators.rst @@ -49,6 +49,13 @@ Oriental ARD-K :members: open, get_position, set_speed, set_position, stop, close :special-members: __init__ +Phidget Stepper4A ++++++++++++++++++ +.. autoclass:: crappy.actuator.Phidget4AStepper + :members: open, set_speed, set_position, get_speed, get_position, stop, + close + :special-members: __init__ + Pololu Tic ++++++++++ .. autoclass:: crappy.actuator.PololuTic diff --git a/docs/source/features.rst b/docs/source/features.rst index caa3c8a1..b6a79feb 100644 --- a/docs/source/features.rst +++ b/docs/source/features.rst @@ -555,6 +555,17 @@ Supported Actuators This object hasn't been maintained nor tested for a while, it is not sure that it still works as expected ! +- :ref:`Phidget Stepper4A` + + Drives 4A bipolar stepper motors using Phidget's `Stepper4A `_ in speed or in position, by using several + Phidget libraries. + + .. Important:: + This Actuator must be connected to Phidget's VINT Hub to work. See the + following link ``_ + to connect properly to the Hub. + - :ref:`Pololu Tic` Drives Pololu's `Tic Date: Tue, 28 Nov 2023 17:16:59 +0100 Subject: [PATCH 04/35] docs: add the Phidget InOut Wheatstone Bridge in the documentation --- docs/source/crappy_docs/inouts.rst | 6 ++++++ docs/source/features.rst | 30 ++++++++++++++++++++---------- 2 files changed, 26 insertions(+), 10 deletions(-) diff --git a/docs/source/crappy_docs/inouts.rst b/docs/source/crappy_docs/inouts.rst index dd204b7c..97b8a07e 100644 --- a/docs/source/crappy_docs/inouts.rst +++ b/docs/source/crappy_docs/inouts.rst @@ -104,6 +104,12 @@ OpSens HandySens :members: open, get_data, close :special-members: __init__ +Phidget Wheatstone Bridge ++++++++++++++++++++++++++ +.. autoclass:: crappy.inout.PhidgetWheatstoneBridge + :members: open, get_data, close + :special-members: __init__ + PiJuice +++++++ .. autoclass:: crappy.inout.PiJuice diff --git a/docs/source/features.rst b/docs/source/features.rst index b6a79feb..b46b16b2 100644 --- a/docs/source/features.rst +++ b/docs/source/features.rst @@ -680,16 +680,6 @@ Sensors Reads voltages from Sparfun's `'Qwiic Scale' NAU7802 `_ load cell conditioner. Communicates over I2C. -- :ref:`PiJuice` - - Reads the charging status and battery level of Kubii's `PiJuice `_ Raspberry Pi power supply. - - .. Important:: - This InOut was written for a specific application, so it may not be - usable as-is in the general case. - - :ref:`OpSens HandySens` Reads data from OpSens' `single channel signal conditioner `_ load cell conditioner, by using several Phidget libraries. + + .. Important:: + This InOut must be connected to Phidget's VINT Hub to work. See the + following link ``_ to + connect properly to the Hub. + +- :ref:`PiJuice` + + Reads the charging status and battery level of Kubii's `PiJuice `_ Raspberry Pi power supply. + + .. Important:: + This InOut was written for a specific application, so it may not be + usable as-is in the general case. + - :ref:`Spectrum M2I 4711` Reads voltages from Spectrum's `M2i 4711 EXP Date: Tue, 5 Dec 2023 22:24:13 +0100 Subject: [PATCH 05/35] refactor: various small adjustments to the Phidget objects for PR #88 --- src/crappy/actuator/phidgets_stepper4a.py | 9 ++++++--- src/crappy/inout/__init__.py | 2 +- ...ridge.py => phidgets_wheatstone_bridge.py} | 19 +++++++++---------- 3 files changed, 16 insertions(+), 14 deletions(-) rename src/crappy/inout/{pjidgets_wheatstone_bridge.py => phidgets_wheatstone_bridge.py} (93%) diff --git a/src/crappy/actuator/phidgets_stepper4a.py b/src/crappy/actuator/phidgets_stepper4a.py index 2a2e4681..e82f37f6 100644 --- a/src/crappy/actuator/phidgets_stepper4a.py +++ b/src/crappy/actuator/phidgets_stepper4a.py @@ -45,6 +45,8 @@ def __init__(self, the motor, in `A` . max_acceleration: If given, sets the maximum acceleration the motor is allowed to reach in `mm/s²`. + remote: Set to :obj:`True` to drive the stepper via a network VINT Hub, + or to :obj:`False` to drive it via a USB VINT Hub. """ self._motor: Optional[Stepper] = None @@ -54,7 +56,7 @@ def __init__(self, self._steps_per_mm = steps_per_mm self._current_limit = current_limit self._max_acceleration = max_acceleration - self.remote = remote + self._remote = remote # These buffers store the last known position and speed self._last_velocity: Optional[float] = None @@ -68,13 +70,14 @@ def open(self) -> None: self.log(logging.DEBUG, "Enabling server discovery") Net.enableServerDiscovery(PhidgetServerType.PHIDGETSERVER_DEVICEREMOTE) self._motor = Stepper() - if self.remote is True: + + # Setting the remote or local status + if self._remote is True: self._motor.setIsLocal(False) self._motor.setIsRemote(True) else: self._motor.setIsLocal(True) self._motor.setIsRemote(False) - self._motor.setIsRemote(True) # Setting up the callbacks self.log(logging.DEBUG, "Setting the callbacks") diff --git a/src/crappy/inout/__init__.py b/src/crappy/inout/__init__.py index 44c3262d..5f800c8e 100644 --- a/src/crappy/inout/__init__.py +++ b/src/crappy/inout/__init__.py @@ -19,7 +19,7 @@ from .ni_daqmx import NIDAQmx from .opsens_handysens import HandySens from .pijuice_hat import PiJuice -from .pjidgets_wheatstone_bridge import PhidgetWheatstoneBridge +from .phidgets_wheatstone_bridge import PhidgetWheatstoneBridge from .sim868 import Sim868 from .spectrum_m2i4711 import SpectrumM2I4711 from .waveshare_ad_da import WaveshareADDA diff --git a/src/crappy/inout/pjidgets_wheatstone_bridge.py b/src/crappy/inout/phidgets_wheatstone_bridge.py similarity index 93% rename from src/crappy/inout/pjidgets_wheatstone_bridge.py rename to src/crappy/inout/phidgets_wheatstone_bridge.py index 0a0b1f0e..ff9f81cf 100644 --- a/src/crappy/inout/pjidgets_wheatstone_bridge.py +++ b/src/crappy/inout/phidgets_wheatstone_bridge.py @@ -45,14 +45,11 @@ def __init__(self, data_rate: The number of samples to acquire per second, as an :obj:`int`. Should be between `0.017` (one sample per minute) and `50`. gain: A gain to apply to the acquired value, following the formula : - :: - - returned = gain * acquired + offset + :math:`returned = gain * acquired + offset` offset: An offset to apply to the acquired value, following the formula : - :: - - returned = gain * acquired + offset - remote: True when the channel is attached via a Phidget network server. + :math:`returned = gain * acquired + offset` + remote: Set to :obj:`True` to drive the bridge via a network VINT Hub, + or to :obj:`False` to drive it via a USB VINT Hub. """ self._load_cell: Optional[VoltageRatioInput] = None @@ -71,7 +68,7 @@ def __init__(self, self._data_rate = data_rate self._gain = gain self._offset = offset - self.remote = remote + self._remote = remote self._last_ratio: Optional[float] = None @@ -83,13 +80,15 @@ def open(self) -> None: self.log(logging.DEBUG, "Enabling server discovery") Net.enableServerDiscovery(PhidgetServerType.PHIDGETSERVER_DEVICEREMOTE) self._load_cell = VoltageRatioInput() - if self.remote is True: + self._load_cell.setChannel(self._channel) + + # Setting the remote or local status + if self._remote is True: self._load_cell.setIsLocal(False) self._load_cell.setIsRemote(True) else: self._load_cell.setIsLocal(True) self._load_cell.setIsRemote(False) - self._load_cell.setChannel(self._channel) # Setting up the callbacks self._load_cell.setOnAttachHandler(self._on_attach) From 8460c862cee163ceab22732cba69011d61b138f1 Mon Sep 17 00:00:00 2001 From: Antoine Weisrock Date: Tue, 5 Dec 2023 22:27:02 +0100 Subject: [PATCH 06/35] refactor: @PIERROOOTT is now a code owner for the Phidget hardware --- CODEOWNERS | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CODEOWNERS b/CODEOWNERS index ad9a3588..f6dabf93 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -3,3 +3,7 @@ # PIERROOOTT owns the Gstreamer cameras /src/crappy/camera/gstreamer_* @WeisLeDocto @PIERROOOTT + +# PIERROOOTT owns the Phidget hardware +/src/crappy/actuator/phidgets_stepper4a.py @WeisLeDocto @PIERROOOTT +/src/crappy/inout/phidgets_wheatstone_bridge.py @WeisLeDocto @PIERROOOTT From 0a82d6c2decd8dfc4e63b76508ff77648b8545d0 Mon Sep 17 00:00:00 2001 From: Antoine Weisrock Date: Tue, 12 Dec 2023 22:36:31 +0100 Subject: [PATCH 07/35] test: add workflow checking the installation and import on supported OS --- .github/workflows/python_package.yml | 38 ++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 .github/workflows/python_package.yml diff --git a/.github/workflows/python_package.yml b/.github/workflows/python_package.yml new file mode 100644 index 00000000..895d7442 --- /dev/null +++ b/.github/workflows/python_package.yml @@ -0,0 +1,38 @@ +# Installs the Python dependencies, installs Crappy, and checks that it imports +name: Python Package + +on: + # Runs on pull requests targeting the default branch + pull_request: + types: [opened, edited, reopened, synchronize] + branches: ["master"] + + # May also be started manually + workflow_dispatch: + + # Runs automatically every first day of the month + schedule: + - cron: '0 12 1 * *' + +jobs: + build: + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"] + os: [ubuntu-latest, windows-latest, macos-latest] + + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Set up Python ${{ matrix.python-version }} on ${{ matrix.os }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: python -m pip install --upgrade pip wheel build setuptools + - name: Install Crappy + run: python -m pip install . + - name: Import Crappy + run: python -c "import crappy; print(crappy.__version__)" From 538a88588f253a248d00efb1d85c6251628ce511 Mon Sep 17 00:00:00 2001 From: Antoine Weisrock Date: Tue, 12 Dec 2023 22:38:34 +0100 Subject: [PATCH 08/35] test: also run the package workflow on PR to the develop branch --- .github/workflows/python_package.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/python_package.yml b/.github/workflows/python_package.yml index 895d7442..d052c21f 100644 --- a/.github/workflows/python_package.yml +++ b/.github/workflows/python_package.yml @@ -5,7 +5,7 @@ on: # Runs on pull requests targeting the default branch pull_request: types: [opened, edited, reopened, synchronize] - branches: ["master"] + branches: ["master", "develop"] # May also be started manually workflow_dispatch: From c48f677b5d5cdf556f2cdb0ab8764bee56269995 Mon Sep 17 00:00:00 2001 From: PIERROOOTT Date: Wed, 13 Dec 2023 15:08:50 +0100 Subject: [PATCH 09/35] feat: add a downsampling modifier It returns the last value received each n values. --- src/crappy/modifier/__init__.py | 1 + src/crappy/modifier/downsampler.py | 59 ++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+) create mode 100644 src/crappy/modifier/downsampler.py diff --git a/src/crappy/modifier/__init__.py b/src/crappy/modifier/__init__.py index ec46e051..7e990683 100644 --- a/src/crappy/modifier/__init__.py +++ b/src/crappy/modifier/__init__.py @@ -6,6 +6,7 @@ from .demux import Demux from .differentiate import Diff +from .downsampler import DownSampler from .integrate import Integrate from .mean import Mean from .median import Median diff --git a/src/crappy/modifier/downsampler.py b/src/crappy/modifier/downsampler.py new file mode 100644 index 00000000..075a625f --- /dev/null +++ b/src/crappy/modifier/downsampler.py @@ -0,0 +1,59 @@ +# coding: utf-8 + +from typing import Dict, Any, Optional +import logging + +from .meta_modifier import Modifier + + +class DownSampler(Modifier): + """Modifier waiting for a given number of data points to be received, then + returning the last point, and starting all over again. + + like :class:`~crappy.modifier.Mean`, it only returns a value once + every ``n_points`` points. + .. versionadded:: 2.0.3 + + """ + + def __init__(self, n_points: int = 10) -> None: + """Sets the args and initializes the parent class. + + Args: + n_points: The number of points on which to compute the average. + """ + + super().__init__() + self._n_points = n_points + self._buf = None + + def __call__(self, data: Dict[str, Any]) -> Optional[Dict[str, Any]]: + """Receives data from the upstream Block, once the right number of points + have been received, empties the buffer and returns the last point. + + If there are not enough points, doesn't return anything. + """ + + self.log(logging.DEBUG, f"Received {data}") + + # Initializing the buffer + if self._buf is None: + self._buf = {key: [value] for key, value in data.items()} + + ret = {} + for label in data: + # Updating the buffer with the newest data + self._buf[label].append(data[label]) + + # Once there's enough data in the buffer, calculating the average value + if len(self._buf[label]) == self._n_points: + ret[label] = self._buf[label][-1] + + # Resetting the buffer + self._buf[label].clear() + + if ret: + self.log(logging.DEBUG, f"Sending {ret}") + return ret + + self.log(logging.DEBUG, "Not returning any data") From 3130246499bdd7dcce0d93a23374fc338105871d Mon Sep 17 00:00:00 2001 From: PIERROOOTT Date: Wed, 13 Dec 2023 15:33:36 +0100 Subject: [PATCH 10/35] docs: add the downsampler modifier documentation --- docs/source/crappy_docs/modifiers.rst | 5 +++++ docs/source/features.rst | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/docs/source/crappy_docs/modifiers.rst b/docs/source/crappy_docs/modifiers.rst index 7986b245..65742547 100644 --- a/docs/source/crappy_docs/modifiers.rst +++ b/docs/source/crappy_docs/modifiers.rst @@ -15,6 +15,11 @@ Differentiate .. autoclass:: crappy.modifier.Diff :special-members: __init__, __call__ +Downsampler +++++ +.. autoclass:: crappy.modifier.Downsampler + :special-members: __init__, __call__ + Integrate +++++++++ .. autoclass:: crappy.modifier.Integrate diff --git a/docs/source/features.rst b/docs/source/features.rst index 0c32131f..5e79d8f9 100644 --- a/docs/source/features.rst +++ b/docs/source/features.rst @@ -849,6 +849,11 @@ On-the-fly data modification (Modifiers) Calculates the time derivative of a given label. +- :ref:`Downsampler` + + Returns the last value of a label out of a given number of points. Only + returns a value once every number of points. + - :ref:`Integrate` Integrates a given label over time. From f18bb6e7ad2fe8c102c5b09741cf3da5a1b5f995 Mon Sep 17 00:00:00 2001 From: PIERROOOTT Date: Tue, 16 Jan 2024 16:15:53 +0100 Subject: [PATCH 11/35] style: adjust typo --- docs/source/crappy_docs/modifiers.rst | 4 ++-- docs/source/features.rst | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/source/crappy_docs/modifiers.rst b/docs/source/crappy_docs/modifiers.rst index 65742547..ea23efe9 100644 --- a/docs/source/crappy_docs/modifiers.rst +++ b/docs/source/crappy_docs/modifiers.rst @@ -15,9 +15,9 @@ Differentiate .. autoclass:: crappy.modifier.Diff :special-members: __init__, __call__ -Downsampler +DownSampler ++++ -.. autoclass:: crappy.modifier.Downsampler +.. autoclass:: crappy.modifier.DownSampler :special-members: __init__, __call__ Integrate diff --git a/docs/source/features.rst b/docs/source/features.rst index 5e79d8f9..1d8df6db 100644 --- a/docs/source/features.rst +++ b/docs/source/features.rst @@ -849,7 +849,7 @@ On-the-fly data modification (Modifiers) Calculates the time derivative of a given label. -- :ref:`Downsampler` +- :ref:`DownSampler` Returns the last value of a label out of a given number of points. Only returns a value once every number of points. From f2028641d4845bf3940594ae4fe0243e90b99dee Mon Sep 17 00:00:00 2001 From: PIERROOOTT Date: Wed, 17 Jan 2024 13:21:33 +0100 Subject: [PATCH 12/35] docs: correct the DownSampler documentation --- docs/source/crappy_docs/modifiers.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/crappy_docs/modifiers.rst b/docs/source/crappy_docs/modifiers.rst index ea23efe9..e923b970 100644 --- a/docs/source/crappy_docs/modifiers.rst +++ b/docs/source/crappy_docs/modifiers.rst @@ -16,7 +16,7 @@ Differentiate :special-members: __init__, __call__ DownSampler -++++ ++++++++++++ .. autoclass:: crappy.modifier.DownSampler :special-members: __init__, __call__ From 228d3472c90e8e8675cb705de93edcc1feaaa2bc Mon Sep 17 00:00:00 2001 From: PIERROOOTT Date: Wed, 17 Jan 2024 14:46:09 +0100 Subject: [PATCH 13/35] feat: add switch and associated callbacks to the Phidget4AStepper --- src/crappy/actuator/phidgets_stepper4a.py | 50 +++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/src/crappy/actuator/phidgets_stepper4a.py b/src/crappy/actuator/phidgets_stepper4a.py index e82f37f6..e062bc59 100644 --- a/src/crappy/actuator/phidgets_stepper4a.py +++ b/src/crappy/actuator/phidgets_stepper4a.py @@ -9,12 +9,14 @@ try: from Phidget22.Net import Net, PhidgetServerType from Phidget22.Devices.Stepper import Stepper, StepperControlMode + from Phidget22.Devices.DigitalInput import DigitalInput from Phidget22.PhidgetException import PhidgetException except (ImportError, ModuleNotFoundError): Net = OptionalModule('Phidget22') PhidgetServerType = OptionalModule('Phidget22') Stepper = OptionalModule('Phidget22') StepperControlMode = OptionalModule('Phidget22') + DigitalInput = OptionalModule('Phidget22') PhidgetException = OptionalModule('Phidget22') @@ -35,6 +37,7 @@ def __init__(self, current_limit: float, max_acceleration: Optional[float] = None, remote: bool = False) -> None: + switch_ports: Optional[Tuple[int, ...]] = None, """Sets the args and initializes the parent class. Args: @@ -47,6 +50,8 @@ def __init__(self, allowed to reach in `mm/s²`. remote: Set to :obj:`True` to drive the stepper via a network VINT Hub, or to :obj:`False` to drive it via a USB VINT Hub. + switch_ports: The port numbers of the VINT Hub where the switches are + connected. """ self._motor: Optional[Stepper] = None @@ -57,6 +62,9 @@ def __init__(self, self._current_limit = current_limit self._max_acceleration = max_acceleration self._remote = remote + self._switch_ports = switch_ports + if self._switch_ports is not None: + self._switches = [] # These buffers store the last known position and speed self._last_velocity: Optional[float] = None @@ -71,13 +79,29 @@ def open(self) -> None: Net.enableServerDiscovery(PhidgetServerType.PHIDGETSERVER_DEVICEREMOTE) self._motor = Stepper() + # Setting up the switches + if self._switch_ports is not None: + for port in self._switch_ports: + switch = DigitalInput() + switch.setIsHubPortDevice(True) + switch.setHubPort(port) + self._switches.append(switch) + # Setting the remote or local status if self._remote is True: self._motor.setIsLocal(False) self._motor.setIsRemote(True) + if self._switch_ports is not None: + for switch in self._switches: + switch.setIsLocal(False) + switch.setIsRemote(True) else: self._motor.setIsLocal(True) self._motor.setIsRemote(False) + if self._switch_ports is not None: + for switch in self._switches: + switch.setIsLocal(True) + switch.setIsRemote(False) # Setting up the callbacks self.log(logging.DEBUG, "Setting the callbacks") @@ -85,6 +109,9 @@ def open(self) -> None: self._motor.setOnErrorHandler(self._on_error) self._motor.setOnVelocityChangeHandler(self._on_velocity_change) self._motor.setOnPositionChangeHandler(self._on_position_change) + if self._switch_ports is not None: + for switch in self._switches: + switch.setOnStateChangeHandler(self._on_end) # Opening the connection to the motor driver try: @@ -93,9 +120,24 @@ def open(self) -> None: except PhidgetException: raise TimeoutError("Waited too long for the motor to attach !") + # Opening the connection to the switches + if self._switch_ports is not None: + for switch in self._switches: + try: + self.log(logging.DEBUG, "Trying to attach the switch") + switch.openWaitForAttachment(10000) + except PhidgetException: + raise TimeoutError("Waited too long for the switch to attach !") + # Energizing the motor self._motor.setEngaged(True) + # Check the state of the switches + if self._switch_ports is not None: + for switch in self._switches: + if switch.getState() is False: + raise ValueError(f"The switch is already hit or disconnected") + def set_speed(self, speed: float) -> None: """Sets the requested speed for the motor. @@ -175,6 +217,10 @@ def close(self) -> None: if self._motor is not None: self._motor.close() + if self._switch_ports is not None: + for switch in self._switches: + switch.close() + def _on_attach(self, _: Stepper) -> None: """Callback called when the motor driver attaches to the program. @@ -223,3 +269,7 @@ def _on_position_change(self, _: Stepper, position: float) -> None: self.log(logging.DEBUG, f"Position changed to {position}") self._last_position = position + + def _on_end(self, _: DigitalInput, state) -> None: + """Callback when a switch is hit.""" + self.stop() From 3052e75896372606724e60f837e11e5bbcc6e752 Mon Sep 17 00:00:00 2001 From: PIERROOOTT Date: Wed, 17 Jan 2024 15:45:20 +0100 Subject: [PATCH 14/35] feat: add an absolute mode parameter to the Phidget4AStepper --- src/crappy/actuator/phidgets_stepper4a.py | 35 +++++++++++++++++------ 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/src/crappy/actuator/phidgets_stepper4a.py b/src/crappy/actuator/phidgets_stepper4a.py index e062bc59..46fd9277 100644 --- a/src/crappy/actuator/phidgets_stepper4a.py +++ b/src/crappy/actuator/phidgets_stepper4a.py @@ -37,6 +37,9 @@ def __init__(self, current_limit: float, max_acceleration: Optional[float] = None, remote: bool = False) -> None: + remote: bool = False, + absolute_mode: Optional[bool] = False, + reference_pos: Optional[float] = 0, switch_ports: Optional[Tuple[int, ...]] = None, """Sets the args and initializes the parent class. @@ -50,6 +53,11 @@ def __init__(self, allowed to reach in `mm/s²`. remote: Set to :obj:`True` to drive the stepper via a network VINT Hub, or to :obj:`False` to drive it via a USB VINT Hub. + absolute_mode: If :obj:`True`, the target position of the motor will be + calculated from a reference position. If :obj:`False`, the target + position of the motor will be calculated from its actual position. + reference_pos: The position considered as the reference position for the + absolute mode at the beginning of the test. switch_ports: The port numbers of the VINT Hub where the switches are connected. """ @@ -65,6 +73,9 @@ def __init__(self, self._switch_ports = switch_ports if self._switch_ports is not None: self._switches = [] + self._absolute_mode = absolute_mode + if self._absolute_mode is True: + self._ref_pos = reference_pos # These buffers store the last known position and speed self._last_velocity: Optional[float] = None @@ -186,14 +197,17 @@ def set_position(self, else: self._motor.setVelocityLimit(abs(speed)) - # Setting the requested position - min_pos = self._motor.getMinPosition() - max_pos = self._motor.getMaxPosition() - if not min_pos <= position <= max_pos: - raise ValueError(f"The position value must be between {min_pos} and " - f"{max_pos}, got {position} !") + if self._absolute_mode is not True: + # Setting the requested position + min_pos = self._motor.getMinPosition() + max_pos = self._motor.getMaxPosition() + if not min_pos <= position <= max_pos: + raise ValueError(f"The position value must be between {min_pos} and " + f"{max_pos}, got {position} !") + else: + self._motor.setTargetPosition(position) else: - self._motor.setTargetPosition(position) + self._motor.setTargetPosition(position-self._ref_pos) def get_speed(self) -> Optional[float]: """Returns the last known speed of the motor.""" @@ -203,7 +217,12 @@ def get_speed(self) -> Optional[float]: def get_position(self) -> Optional[float]: """Returns the last known position of the motor.""" - return self._last_position + if self._absolute_mode is not True: + return self._last_position + else: + if self._last_position is None: + return self._last_position + return self._last_position + self._ref_pos def stop(self) -> None: """Deenergizes the motor.""" From e32147faf25b5adbc00fc2f12835ccb934e32868 Mon Sep 17 00:00:00 2001 From: PIERROOOTT Date: Wed, 17 Jan 2024 15:47:51 +0100 Subject: [PATCH 15/35] feat: add a parameter for saving last position to the Phidget4AStepper --- src/crappy/actuator/phidgets_stepper4a.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/crappy/actuator/phidgets_stepper4a.py b/src/crappy/actuator/phidgets_stepper4a.py index 46fd9277..1352afcb 100644 --- a/src/crappy/actuator/phidgets_stepper4a.py +++ b/src/crappy/actuator/phidgets_stepper4a.py @@ -1,7 +1,8 @@ # coding: utf-8 import logging -from typing import Optional +import numpy as np +from typing import Optional, Tuple from .meta_actuator import Actuator from .._global import OptionalModule @@ -36,11 +37,12 @@ def __init__(self, steps_per_mm: float, current_limit: float, max_acceleration: Optional[float] = None, - remote: bool = False) -> None: remote: bool = False, absolute_mode: Optional[bool] = False, reference_pos: Optional[float] = 0, switch_ports: Optional[Tuple[int, ...]] = None, + save_last_pos: Optional[bool] = False, + save_pos_folder: Optional[str] = './') -> None: """Sets the args and initializes the parent class. Args: @@ -60,6 +62,10 @@ def __init__(self, absolute mode at the beginning of the test. switch_ports: The port numbers of the VINT Hub where the switches are connected. + save_last_pos: If :obj:`True`, the last position of the actuator will be + saved in a .npy file. + save_pos_folder: The path to the folder where to save the last position + of the motor. """ self._motor: Optional[Stepper] = None @@ -76,6 +82,11 @@ def __init__(self, self._absolute_mode = absolute_mode if self._absolute_mode is True: self._ref_pos = reference_pos + self._save_last_pos = save_last_pos + if self._save_last_pos is True: + self._save_folder = save_pos_folder + if self._save_folder[-1] != '/': + self._save_folder += '/' # These buffers store the last known position and speed self._last_velocity: Optional[float] = None @@ -234,6 +245,8 @@ def close(self) -> None: """Closes the connection to the motor.""" if self._motor is not None: + if self._save_last_pos is True: + np.save(self._save_folder + 'last_pos', self.get_position()) self._motor.close() if self._switch_ports is not None: From fd19ed25ae5aecba63c281671b12a43376a3592b Mon Sep 17 00:00:00 2001 From: Antoine Weisrock Date: Wed, 17 Jan 2024 16:36:15 +0100 Subject: [PATCH 16/35] docs: minor adjustment of DownSampler description Signed-off-by: Antoine Weisrock --- docs/source/features.rst | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/source/features.rst b/docs/source/features.rst index 1d8df6db..5352ba51 100644 --- a/docs/source/features.rst +++ b/docs/source/features.rst @@ -838,6 +838,7 @@ On-the-fly data modification (Modifiers) ---------------------------------------- .. sectionauthor:: Antoine Weisrock +.. sectionauthor:: Pierre Margotin - :ref:`Demux` @@ -851,8 +852,9 @@ On-the-fly data modification (Modifiers) - :ref:`DownSampler` - Returns the last value of a label out of a given number of points. Only - returns a value once every number of points. + Transmits the values to downstream Blocks only once every given number of + points. The values that are not sent are discarded. The values are directly + sent without being altered. - :ref:`Integrate` From 6394716075fa603fd22794a77972a51ca2a46da8 Mon Sep 17 00:00:00 2001 From: Antoine Weisrock Date: Wed, 17 Jan 2024 16:51:20 +0100 Subject: [PATCH 17/35] refactor: optimize __call__ method in DownSampler Modifier The unsent values were unnecessarily stored in a buffer. Instead, it was replaced with a counter system. Signed-off-by: Antoine Weisrock --- src/crappy/modifier/downsampler.py | 52 ++++++++++++------------------ 1 file changed, 21 insertions(+), 31 deletions(-) diff --git a/src/crappy/modifier/downsampler.py b/src/crappy/modifier/downsampler.py index 075a625f..0d58cbb4 100644 --- a/src/crappy/modifier/downsampler.py +++ b/src/crappy/modifier/downsampler.py @@ -8,52 +8,42 @@ class DownSampler(Modifier): """Modifier waiting for a given number of data points to be received, then - returning the last point, and starting all over again. - - like :class:`~crappy.modifier.Mean`, it only returns a value once - every ``n_points`` points. - .. versionadded:: 2.0.3 + returning only the last received point. + Similar to :class:`~crappy.modifier.Mean`, except it discards the values + that are not transmitted instead of averaging them. Useful for reducing + the amount of data sent to a Block. + + .. versionadded:: 2.0.4 """ def __init__(self, n_points: int = 10) -> None: """Sets the args and initializes the parent class. Args: - n_points: The number of points on which to compute the average. + n_points: One value will be sent to the downstream Block only once + every ``n_points`` received values. """ super().__init__() - self._n_points = n_points - self._buf = None + self._n_points: int = n_points + self._count: int = 0 def __call__(self, data: Dict[str, Any]) -> Optional[Dict[str, Any]]: - """Receives data from the upstream Block, once the right number of points - have been received, empties the buffer and returns the last point. + """Receives data from the upstream Block, and if the counter matches the + threshold, returns the data. - If there are not enough points, doesn't return anything. + If the counter doesn't match the threshold, doesn't return anything and + increments the counter. """ self.log(logging.DEBUG, f"Received {data}") - # Initializing the buffer - if self._buf is None: - self._buf = {key: [value] for key, value in data.items()} - - ret = {} - for label in data: - # Updating the buffer with the newest data - self._buf[label].append(data[label]) - - # Once there's enough data in the buffer, calculating the average value - if len(self._buf[label]) == self._n_points: - ret[label] = self._buf[label][-1] - - # Resetting the buffer - self._buf[label].clear() - - if ret: + if self._count == self._n_points - 1: + self._count = 0 self.log(logging.DEBUG, f"Sending {ret}") - return ret - - self.log(logging.DEBUG, "Not returning any data") + return data + + else: + self._count += 1 + self.log(logging.DEBUG, "Not returning any data") From 3fecefd5caac45cbf7decd4a129d298f1b78b47b Mon Sep 17 00:00:00 2001 From: PIERROOOTT Date: Thu, 18 Jan 2024 11:05:37 +0100 Subject: [PATCH 18/35] refactor: minor adjustment of the DownSampler --- src/crappy/modifier/downsampler.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/crappy/modifier/downsampler.py b/src/crappy/modifier/downsampler.py index 0d58cbb4..a57ce7ef 100644 --- a/src/crappy/modifier/downsampler.py +++ b/src/crappy/modifier/downsampler.py @@ -27,7 +27,7 @@ def __init__(self, n_points: int = 10) -> None: super().__init__() self._n_points: int = n_points - self._count: int = 0 + self._count: int = n_points - 1 def __call__(self, data: Dict[str, Any]) -> Optional[Dict[str, Any]]: """Receives data from the upstream Block, and if the counter matches the @@ -41,7 +41,7 @@ def __call__(self, data: Dict[str, Any]) -> Optional[Dict[str, Any]]: if self._count == self._n_points - 1: self._count = 0 - self.log(logging.DEBUG, f"Sending {ret}") + self.log(logging.DEBUG, f"Sending {data}") return data else: From d72245dd1786af33fe78b88308d9aa778e97955f Mon Sep 17 00:00:00 2001 From: PIERROOOTT Date: Thu, 18 Jan 2024 11:12:56 +0100 Subject: [PATCH 19/35] feat: add a use example of the DownSampler --- examples/modifiers/downsampler.py | 81 +++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 examples/modifiers/downsampler.py diff --git a/examples/modifiers/downsampler.py b/examples/modifiers/downsampler.py new file mode 100644 index 00000000..b584ea35 --- /dev/null +++ b/examples/modifiers/downsampler.py @@ -0,0 +1,81 @@ +# coding: utf-8 + +""" +This example demonstrates the use of the DownSampler Modifier. It does not +require any specific hardware to run, but necessitates the matplotlib Python +module to be installed. + +The DownSampler Modifier returns only the last received point every n data it +receives from its upstream Block. It therefore reduces the data rate on a given +Link. This Modifier is useful for reducing the data rate in a given Link. + +Here, a sine wave is generated by a Generator Block and sent to two Grapher +Blocks for display. One Grapher displays it as it is generated, and the other +displays it downsampled by a DownSampler Modifier. Because the frequency of the +sine wave is 1 Hz and the frequency of the Generator Block is twice as much the +number of points of downsampling, the value as returned by the DownSampler +Modifier is always close to 1 or -1, the peak of the sine wave. + +After starting this script, just watch how the raw signal is transformed by the +DownSampler Modifier and alternates between 1 and -1. Also notice how the +initial data rate of the signal is divided when passing through the DownSampler + Modifier. This demo ends after 22s. You can also hit CTRL+C to stop it $ +earlier, but it is not a clean way to stop Crappy. +""" + +import crappy +import numpy as np + +if __name__ == '__main__': + + # This Generator Block generates a sine wave for the Graphers to display. It + # sends it to one Grapher that displays it as is, and to another Grapher that + # receives it averaged by the Mean Modifier + gen = crappy.blocks.Generator( + # Generating a sine wave of amplitude 2 and frequency 1 and phase pi/2 + ({'type': 'Sine', + 'condition': 'delay=20', + 'amplitude': 2, + 'freq': 1, + 'phase': np.pi/2},), + freq=30, # Lowering the default frequency because it's just a demo + cmd_label='sine', # The label carrying the generated signal + + # Sticking to default for the other arguments + ) + + # This Grapher Block displays the raw sine wave it receives from the + # Generator. As the Generator runs at 30Hz, 30 data points are received each + # second for display + graph = crappy.blocks.Grapher( + ('t(s)', 'sine'), # The names of the labels to plot on the graph + interp=False, # Not linking the displayed spots, to better see the + # frequency of the input + length=100, # Only displaying the data for the last 100 points + + # Sticking to default for the other arguments + ) + + # This Grapher Block displays the averaged sine wave it receives from the + # Generator. Because the frequency of the signal is the same as that of the + # averaging, only values close to 0 are received. A point is received once + # every second, because the Mean Modifier only outputs data once every 30 + # received points + graph_avg = crappy.blocks.Grapher( + ('t(s)', 'sine'), # The names of the labels to plot on the graph + interp=False, # Not linking the displayed spots, to better see the + # frequency of the input + + # Sticking to default for the other arguments + ) + + # Linking the Block so that the information is correctly sent and received + crappy.link(gen, graph) + crappy.link(gen, graph_avg, + # Adding a DownSampler Modifier for downsampling the sine + # wave before sending to the Grapher. A data point is sent for + # display once every 15 received points + modifier=crappy.modifier.DownSampler(15)) + + # Mandatory line for starting the test, this call is blocking + crappy.start() From 0aacc3d8c4132b739f6633d1595ee6747fb0ff94 Mon Sep 17 00:00:00 2001 From: PIERROOOTT Date: Thu, 18 Jan 2024 11:23:17 +0100 Subject: [PATCH 20/35] style: add line break --- src/crappy/actuator/phidgets_stepper4a.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/crappy/actuator/phidgets_stepper4a.py b/src/crappy/actuator/phidgets_stepper4a.py index 1352afcb..0393df2e 100644 --- a/src/crappy/actuator/phidgets_stepper4a.py +++ b/src/crappy/actuator/phidgets_stepper4a.py @@ -304,4 +304,5 @@ def _on_position_change(self, _: Stepper, position: float) -> None: def _on_end(self, _: DigitalInput, state) -> None: """Callback when a switch is hit.""" + self.stop() From b4db8b65c5701cc51c69a1202a5496812425c5ca Mon Sep 17 00:00:00 2001 From: PIERROOOTT Date: Thu, 18 Jan 2024 14:33:28 +0100 Subject: [PATCH 21/35] docs: minor adjustments in DownSampler example --- examples/modifiers/downsampler.py | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/examples/modifiers/downsampler.py b/examples/modifiers/downsampler.py index b584ea35..d91fe3e5 100644 --- a/examples/modifiers/downsampler.py +++ b/examples/modifiers/downsampler.py @@ -5,22 +5,22 @@ require any specific hardware to run, but necessitates the matplotlib Python module to be installed. -The DownSampler Modifier returns only the last received point every n data it -receives from its upstream Block. It therefore reduces the data rate on a given -Link. This Modifier is useful for reducing the data rate in a given Link. +The DownSampler Modifier returns only the last received data point every n data +points it receives from its upstream Block. This Modifier is useful for +reducing the data rate in a given Link. Here, a sine wave is generated by a Generator Block and sent to two Grapher Blocks for display. One Grapher displays it as it is generated, and the other displays it downsampled by a DownSampler Modifier. Because the frequency of the sine wave is 1 Hz and the frequency of the Generator Block is twice as much the number of points of downsampling, the value as returned by the DownSampler -Modifier is always close to 1 or -1, the peak of the sine wave. +Modifier is always close to 1 or -1, the peaks of the sine wave. After starting this script, just watch how the raw signal is transformed by the DownSampler Modifier and alternates between 1 and -1. Also notice how the initial data rate of the signal is divided when passing through the DownSampler - Modifier. This demo ends after 22s. You can also hit CTRL+C to stop it $ -earlier, but it is not a clean way to stop Crappy. +Modifier. This demo ends after 22s. You can also hit CTRL+C to stop it earlier, +but it is not a clean way to stop Crappy. """ import crappy @@ -30,7 +30,7 @@ # This Generator Block generates a sine wave for the Graphers to display. It # sends it to one Grapher that displays it as is, and to another Grapher that - # receives it averaged by the Mean Modifier + # receives it downsampled by the DownSampler Modifier gen = crappy.blocks.Generator( # Generating a sine wave of amplitude 2 and frequency 1 and phase pi/2 ({'type': 'Sine', @@ -56,12 +56,11 @@ # Sticking to default for the other arguments ) - # This Grapher Block displays the averaged sine wave it receives from the - # Generator. Because the frequency of the signal is the same as that of the - # averaging, only values close to 0 are received. A point is received once - # every second, because the Mean Modifier only outputs data once every 30 - # received points - graph_avg = crappy.blocks.Grapher( + # This Grapher Block displays the downsampled sine wave it receives from the + # Generator. Because the frequency of the sine wave is 1 Hz and the frequency + # of the Generator Block is twice as much the number of points of + # downsampling, only values close to 1 and -1 are received. + graph_down = crappy.blocks.Grapher( ('t(s)', 'sine'), # The names of the labels to plot on the graph interp=False, # Not linking the displayed spots, to better see the # frequency of the input @@ -71,7 +70,7 @@ # Linking the Block so that the information is correctly sent and received crappy.link(gen, graph) - crappy.link(gen, graph_avg, + crappy.link(gen, graph_down, # Adding a DownSampler Modifier for downsampling the sine # wave before sending to the Grapher. A data point is sent for # display once every 15 received points From c03ec5231d4bba6b739d7c4b11415061115462f2 Mon Sep 17 00:00:00 2001 From: PIERROOOTT Date: Thu, 18 Jan 2024 14:59:54 +0100 Subject: [PATCH 22/35] refactor: optimization of the switches implementation --- src/crappy/actuator/phidgets_stepper4a.py | 47 ++++++++++------------- 1 file changed, 20 insertions(+), 27 deletions(-) diff --git a/src/crappy/actuator/phidgets_stepper4a.py b/src/crappy/actuator/phidgets_stepper4a.py index 0393df2e..338a8b6a 100644 --- a/src/crappy/actuator/phidgets_stepper4a.py +++ b/src/crappy/actuator/phidgets_stepper4a.py @@ -77,8 +77,7 @@ def __init__(self, self._max_acceleration = max_acceleration self._remote = remote self._switch_ports = switch_ports - if self._switch_ports is not None: - self._switches = [] + self._switches = [] self._absolute_mode = absolute_mode if self._absolute_mode is True: self._ref_pos = reference_pos @@ -113,17 +112,15 @@ def open(self) -> None: if self._remote is True: self._motor.setIsLocal(False) self._motor.setIsRemote(True) - if self._switch_ports is not None: - for switch in self._switches: - switch.setIsLocal(False) - switch.setIsRemote(True) + for switch in self._switches: + switch.setIsLocal(False) + switch.setIsRemote(True) else: self._motor.setIsLocal(True) self._motor.setIsRemote(False) - if self._switch_ports is not None: - for switch in self._switches: - switch.setIsLocal(True) - switch.setIsRemote(False) + for switch in self._switches: + switch.setIsLocal(True) + switch.setIsRemote(False) # Setting up the callbacks self.log(logging.DEBUG, "Setting the callbacks") @@ -131,9 +128,8 @@ def open(self) -> None: self._motor.setOnErrorHandler(self._on_error) self._motor.setOnVelocityChangeHandler(self._on_velocity_change) self._motor.setOnPositionChangeHandler(self._on_position_change) - if self._switch_ports is not None: - for switch in self._switches: - switch.setOnStateChangeHandler(self._on_end) + for switch in self._switches: + switch.setOnStateChangeHandler(self._on_end) # Opening the connection to the motor driver try: @@ -143,22 +139,20 @@ def open(self) -> None: raise TimeoutError("Waited too long for the motor to attach !") # Opening the connection to the switches - if self._switch_ports is not None: - for switch in self._switches: - try: - self.log(logging.DEBUG, "Trying to attach the switch") - switch.openWaitForAttachment(10000) - except PhidgetException: - raise TimeoutError("Waited too long for the switch to attach !") + for switch in self._switches: + try: + self.log(logging.DEBUG, "Trying to attach the switch") + switch.openWaitForAttachment(10000) + except PhidgetException: + raise TimeoutError("Waited too long for the switch to attach !") # Energizing the motor self._motor.setEngaged(True) # Check the state of the switches - if self._switch_ports is not None: - for switch in self._switches: - if switch.getState() is False: - raise ValueError(f"The switch is already hit or disconnected") + for switch in self._switches: + if switch.getState() is False: + raise ValueError(f"The switch is already hit or disconnected") def set_speed(self, speed: float) -> None: """Sets the requested speed for the motor. @@ -249,9 +243,8 @@ def close(self) -> None: np.save(self._save_folder + 'last_pos', self.get_position()) self._motor.close() - if self._switch_ports is not None: - for switch in self._switches: - switch.close() + for switch in self._switches: + switch.close() def _on_attach(self, _: Stepper) -> None: """Callback called when the motor driver attaches to the program. From 238f6cabf4c1cbb6f0f726235b28a5f01ea52fb6 Mon Sep 17 00:00:00 2001 From: PIERROOOTT Date: Thu, 18 Jan 2024 15:01:45 +0100 Subject: [PATCH 23/35] refactor: type hint adjustments --- src/crappy/actuator/phidgets_stepper4a.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/crappy/actuator/phidgets_stepper4a.py b/src/crappy/actuator/phidgets_stepper4a.py index 338a8b6a..01292d53 100644 --- a/src/crappy/actuator/phidgets_stepper4a.py +++ b/src/crappy/actuator/phidgets_stepper4a.py @@ -38,10 +38,10 @@ def __init__(self, current_limit: float, max_acceleration: Optional[float] = None, remote: bool = False, - absolute_mode: Optional[bool] = False, + absolute_mode: bool = False, reference_pos: Optional[float] = 0, switch_ports: Optional[Tuple[int, ...]] = None, - save_last_pos: Optional[bool] = False, + save_last_pos: bool = False, save_pos_folder: Optional[str] = './') -> None: """Sets the args and initializes the parent class. From 6bedddb2c070639418e86a698380c3af03fcfe78 Mon Sep 17 00:00:00 2001 From: PIERROOOTT Date: Wed, 24 Jan 2024 17:31:05 +0100 Subject: [PATCH 24/35] style: optimize some parts of the code --- src/crappy/actuator/phidgets_stepper4a.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/crappy/actuator/phidgets_stepper4a.py b/src/crappy/actuator/phidgets_stepper4a.py index 01292d53..4e7b844c 100644 --- a/src/crappy/actuator/phidgets_stepper4a.py +++ b/src/crappy/actuator/phidgets_stepper4a.py @@ -77,9 +77,10 @@ def __init__(self, self._max_acceleration = max_acceleration self._remote = remote self._switch_ports = switch_ports - self._switches = [] + self._switches = list() + self._absolute_mode = absolute_mode - if self._absolute_mode is True: + if self._absolute_mode: self._ref_pos = reference_pos self._save_last_pos = save_last_pos if self._save_last_pos is True: @@ -109,7 +110,7 @@ def open(self) -> None: self._switches.append(switch) # Setting the remote or local status - if self._remote is True: + if self._remote: self._motor.setIsLocal(False) self._motor.setIsRemote(True) for switch in self._switches: @@ -202,7 +203,7 @@ def set_position(self, else: self._motor.setVelocityLimit(abs(speed)) - if self._absolute_mode is not True: + if not self._absolute_mode: # Setting the requested position min_pos = self._motor.getMinPosition() max_pos = self._motor.getMaxPosition() @@ -222,7 +223,7 @@ def get_speed(self) -> Optional[float]: def get_position(self) -> Optional[float]: """Returns the last known position of the motor.""" - if self._absolute_mode is not True: + if not self._absolute_mode: return self._last_position else: if self._last_position is None: @@ -295,7 +296,7 @@ def _on_position_change(self, _: Stepper, position: float) -> None: self.log(logging.DEBUG, f"Position changed to {position}") self._last_position = position - def _on_end(self, _: DigitalInput, state) -> None: + def _on_end(self, _: DigitalInput, __) -> None: """Callback when a switch is hit.""" self.stop() From 347fff0caf8e3b92278d8672afbe01938fa23d10 Mon Sep 17 00:00:00 2001 From: PIERROOOTT Date: Wed, 24 Jan 2024 17:32:48 +0100 Subject: [PATCH 25/35] refactor: type hint adjustment --- src/crappy/actuator/phidgets_stepper4a.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/crappy/actuator/phidgets_stepper4a.py b/src/crappy/actuator/phidgets_stepper4a.py index 4e7b844c..1e6f4536 100644 --- a/src/crappy/actuator/phidgets_stepper4a.py +++ b/src/crappy/actuator/phidgets_stepper4a.py @@ -2,7 +2,9 @@ import logging import numpy as np -from typing import Optional, Tuple +from pathlib import Path +from platform import system +from typing import Optional, Tuple, Union from .meta_actuator import Actuator from .._global import OptionalModule @@ -39,7 +41,7 @@ def __init__(self, max_acceleration: Optional[float] = None, remote: bool = False, absolute_mode: bool = False, - reference_pos: Optional[float] = 0, + reference_pos: float = 0, switch_ports: Optional[Tuple[int, ...]] = None, save_last_pos: bool = False, save_pos_folder: Optional[str] = './') -> None: From 56651d3cca03cb26e66f7d7bb78aeaca51fc2a39 Mon Sep 17 00:00:00 2001 From: PIERROOOTT Date: Wed, 24 Jan 2024 17:33:38 +0100 Subject: [PATCH 26/35] refactor: refactor how to save the last position with pathlib --- src/crappy/actuator/phidgets_stepper4a.py | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/src/crappy/actuator/phidgets_stepper4a.py b/src/crappy/actuator/phidgets_stepper4a.py index 1e6f4536..1f5ff4a4 100644 --- a/src/crappy/actuator/phidgets_stepper4a.py +++ b/src/crappy/actuator/phidgets_stepper4a.py @@ -44,7 +44,7 @@ def __init__(self, reference_pos: float = 0, switch_ports: Optional[Tuple[int, ...]] = None, save_last_pos: bool = False, - save_pos_folder: Optional[str] = './') -> None: + save_pos_folder: Optional[Union[str, Path]] = None) -> None: """Sets the args and initializes the parent class. Args: @@ -84,11 +84,17 @@ def __init__(self, self._absolute_mode = absolute_mode if self._absolute_mode: self._ref_pos = reference_pos - self._save_last_pos = save_last_pos - if self._save_last_pos is True: - self._save_folder = save_pos_folder - if self._save_folder[-1] != '/': - self._save_folder += '/' + + self._path: Optional[Path] = None + if save_last_pos: + if save_pos_folder is not None: + self._path = Path(save_pos_folder) + elif system() in ('Linux', 'Darwin'): + self._path = Path.home() / '.machine1000N' + elif system() == 'Windows': + self._path = Path.home() / 'AppData' / 'Local' / 'machine1000N' + else: + self._save_last_pos = False # These buffers store the last known position and speed self._last_velocity: Optional[float] = None @@ -242,8 +248,9 @@ def close(self) -> None: """Closes the connection to the motor.""" if self._motor is not None: - if self._save_last_pos is True: - np.save(self._save_folder + 'last_pos', self.get_position()) + if self._path is not None: + self._path.mkdir(parents=False, exist_ok=True) + np.save(self._path / 'last_pos.npy', self.get_position()) self._motor.close() for switch in self._switches: From 2961db95fa298424abe1b3a0f670fc2b133044c8 Mon Sep 17 00:00:00 2001 From: Antoine Weisrock Date: Thu, 25 Jan 2024 16:42:37 +0100 Subject: [PATCH 27/35] refactor: minor syntax improvements in comments Signed-off-by: Antoine Weisrock --- examples/modifiers/downsampler.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/examples/modifiers/downsampler.py b/examples/modifiers/downsampler.py index d91fe3e5..2d84ebd3 100644 --- a/examples/modifiers/downsampler.py +++ b/examples/modifiers/downsampler.py @@ -5,22 +5,22 @@ require any specific hardware to run, but necessitates the matplotlib Python module to be installed. -The DownSampler Modifier returns only the last received data point every n data -points it receives from its upstream Block. This Modifier is useful for -reducing the data rate in a given Link. +The DownSampler Modifier transmits only one data point every n received points +received from its upstream Block. It is therefore useful for reducing the data +rate in a given Link. Here, a sine wave is generated by a Generator Block and sent to two Grapher Blocks for display. One Grapher displays it as it is generated, and the other displays it downsampled by a DownSampler Modifier. Because the frequency of the -sine wave is 1 Hz and the frequency of the Generator Block is twice as much the -number of points of downsampling, the value as returned by the DownSampler -Modifier is always close to 1 or -1, the peaks of the sine wave. +sine wave is 1 Hz and the frequency of the Generator Block is twice that of the +DownSampler, the value passed to the second Grapher are always close to 1 or +-1, the peak of the sine wave. After starting this script, just watch how the raw signal is transformed by the DownSampler Modifier and alternates between 1 and -1. Also notice how the initial data rate of the signal is divided when passing through the DownSampler -Modifier. This demo ends after 22s. You can also hit CTRL+C to stop it earlier, -but it is not a clean way to stop Crappy. +Modifier. This demo ends after 22s. You can also hit CTRL+C to stop it +earlier, but it is not a clean way to stop Crappy. """ import crappy @@ -58,8 +58,8 @@ # This Grapher Block displays the downsampled sine wave it receives from the # Generator. Because the frequency of the sine wave is 1 Hz and the frequency - # of the Generator Block is twice as much the number of points of - # downsampling, only values close to 1 and -1 are received. + # of the Generator Block is twice that of the DownSampler, only values close + # to 1 and -1 are received. graph_down = crappy.blocks.Grapher( ('t(s)', 'sine'), # The names of the labels to plot on the graph interp=False, # Not linking the displayed spots, to better see the From 1df4d0954076c02b59aa3312a00ba3300c215226 Mon Sep 17 00:00:00 2001 From: Antoine Weisrock Date: Thu, 25 Jan 2024 17:36:10 +0100 Subject: [PATCH 28/35] fix: not checking for position boundaries in absolute mode --- src/crappy/actuator/phidgets_stepper4a.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/crappy/actuator/phidgets_stepper4a.py b/src/crappy/actuator/phidgets_stepper4a.py index 1f5ff4a4..c7f5bbee 100644 --- a/src/crappy/actuator/phidgets_stepper4a.py +++ b/src/crappy/actuator/phidgets_stepper4a.py @@ -211,17 +211,18 @@ def set_position(self, else: self._motor.setVelocityLimit(abs(speed)) + # Ensuring that the requested position is valid + min_pos = self._motor.getMinPosition() + max_pos = self._motor.getMaxPosition() + if not min_pos <= position <= max_pos: + raise ValueError(f"The position value must be between {min_pos} and " + f"{max_pos}, got {position} !") + + # Setting the position depending on the driving mode if not self._absolute_mode: - # Setting the requested position - min_pos = self._motor.getMinPosition() - max_pos = self._motor.getMaxPosition() - if not min_pos <= position <= max_pos: - raise ValueError(f"The position value must be between {min_pos} and " - f"{max_pos}, got {position} !") - else: - self._motor.setTargetPosition(position) + self._motor.setTargetPosition(position) else: - self._motor.setTargetPosition(position-self._ref_pos) + self._motor.setTargetPosition(position - self._ref_pos) def get_speed(self) -> Optional[float]: """Returns the last known speed of the motor.""" From 8574b551f2270ead6414cd667266ab697978bb8b Mon Sep 17 00:00:00 2001 From: Antoine Weisrock Date: Thu, 25 Jan 2024 17:36:47 +0100 Subject: [PATCH 29/35] refactor: simplify overly complex code --- src/crappy/actuator/phidgets_stepper4a.py | 23 ++++++++--------------- 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/src/crappy/actuator/phidgets_stepper4a.py b/src/crappy/actuator/phidgets_stepper4a.py index c7f5bbee..3765eea8 100644 --- a/src/crappy/actuator/phidgets_stepper4a.py +++ b/src/crappy/actuator/phidgets_stepper4a.py @@ -118,18 +118,11 @@ def open(self) -> None: self._switches.append(switch) # Setting the remote or local status - if self._remote: - self._motor.setIsLocal(False) - self._motor.setIsRemote(True) - for switch in self._switches: - switch.setIsLocal(False) - switch.setIsRemote(True) - else: - self._motor.setIsLocal(True) - self._motor.setIsRemote(False) - for switch in self._switches: - switch.setIsLocal(True) - switch.setIsRemote(False) + self._motor.setIsLocal(not self._remote) + self._motor.setIsRemote(self._remote) + for switch in self._switches: + switch.setIsLocal(not self._remote) + switch.setIsRemote(self._remote) # Setting up the callbacks self.log(logging.DEBUG, "Setting the callbacks") @@ -234,9 +227,9 @@ def get_position(self) -> Optional[float]: if not self._absolute_mode: return self._last_position - else: - if self._last_position is None: - return self._last_position + + # Cannot perform addition if no known last position + elif self._last_position is not None: return self._last_position + self._ref_pos def stop(self) -> None: From 8d8d5bfa1bc4dae5a64ecbda03d76280403a485a Mon Sep 17 00:00:00 2001 From: Antoine Weisrock Date: Thu, 25 Jan 2024 17:37:36 +0100 Subject: [PATCH 30/35] refactor: change default of switch_ports argument for more simplicity --- src/crappy/actuator/phidgets_stepper4a.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/crappy/actuator/phidgets_stepper4a.py b/src/crappy/actuator/phidgets_stepper4a.py index 3765eea8..64724f48 100644 --- a/src/crappy/actuator/phidgets_stepper4a.py +++ b/src/crappy/actuator/phidgets_stepper4a.py @@ -42,7 +42,7 @@ def __init__(self, remote: bool = False, absolute_mode: bool = False, reference_pos: float = 0, - switch_ports: Optional[Tuple[int, ...]] = None, + switch_ports: Tuple[int, ...] = tuple(), save_last_pos: bool = False, save_pos_folder: Optional[Union[str, Path]] = None) -> None: """Sets the args and initializes the parent class. @@ -110,12 +110,11 @@ def open(self) -> None: self._motor = Stepper() # Setting up the switches - if self._switch_ports is not None: - for port in self._switch_ports: - switch = DigitalInput() - switch.setIsHubPortDevice(True) - switch.setHubPort(port) - self._switches.append(switch) + for port in self._switch_ports: + switch = DigitalInput() + switch.setIsHubPortDevice(True) + switch.setHubPort(port) + self._switches.append(switch) # Setting the remote or local status self._motor.setIsLocal(not self._remote) From 2423f14e1d59b92e269575f85c0191b91665367a Mon Sep 17 00:00:00 2001 From: Antoine Weisrock Date: Thu, 25 Jan 2024 17:37:55 +0100 Subject: [PATCH 31/35] docs: minor changes in comments and docstrings --- src/crappy/actuator/phidgets_stepper4a.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/crappy/actuator/phidgets_stepper4a.py b/src/crappy/actuator/phidgets_stepper4a.py index 64724f48..4b9bfc14 100644 --- a/src/crappy/actuator/phidgets_stepper4a.py +++ b/src/crappy/actuator/phidgets_stepper4a.py @@ -59,15 +59,16 @@ def __init__(self, or to :obj:`False` to drive it via a USB VINT Hub. absolute_mode: If :obj:`True`, the target position of the motor will be calculated from a reference position. If :obj:`False`, the target - position of the motor will be calculated from its actual position. - reference_pos: The position considered as the reference position for the - absolute mode at the beginning of the test. - switch_ports: The port numbers of the VINT Hub where the switches are + position of the motor will be calculated from its current position. + reference_pos: The position considered as the reference position at the + beginning of the test. Only takes effect if ``absolute_mode`` is + :obj:`True`. + switch_ports: The indexes of the VINT Hub ports where the switches are connected. save_last_pos: If :obj:`True`, the last position of the actuator will be saved in a .npy file. save_pos_folder: The path to the folder where to save the last position - of the motor. + of the motor. Only takes effect if ``save_last_pos`` is :obj:`True`. """ self._motor: Optional[Stepper] = None @@ -85,6 +86,8 @@ def __init__(self, if self._absolute_mode: self._ref_pos = reference_pos + # Determining the path where to save the last position + # It depends on the current operating system self._path: Optional[Path] = None if save_last_pos: if save_pos_folder is not None: From 6beedcc77e3ed64866150258036feba59be44b66 Mon Sep 17 00:00:00 2001 From: PIERROOOTT Date: Fri, 2 Feb 2024 17:12:42 +0100 Subject: [PATCH 32/35] feat: add a class attribute to check switches state in the open method --- src/crappy/actuator/phidgets_stepper4a.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/crappy/actuator/phidgets_stepper4a.py b/src/crappy/actuator/phidgets_stepper4a.py index 4b9bfc14..0571747a 100644 --- a/src/crappy/actuator/phidgets_stepper4a.py +++ b/src/crappy/actuator/phidgets_stepper4a.py @@ -81,6 +81,7 @@ def __init__(self, self._remote = remote self._switch_ports = switch_ports self._switches = list() + self._check_switch = True self._absolute_mode = absolute_mode if self._absolute_mode: @@ -154,9 +155,10 @@ def open(self) -> None: self._motor.setEngaged(True) # Check the state of the switches - for switch in self._switches: - if switch.getState() is False: - raise ValueError(f"The switch is already hit or disconnected") + if self._check_switch: + for switch in self._switches: + if switch.getState() is False: + raise ValueError(f"A switch is already hit or disconnected") def set_speed(self, speed: float) -> None: """Sets the requested speed for the motor. @@ -304,4 +306,7 @@ def _on_position_change(self, _: Stepper, position: float) -> None: def _on_end(self, _: DigitalInput, __) -> None: """Callback when a switch is hit.""" - self.stop() + for switch in self._switches: + if switch.getState() is False: + self.stop() + raise ValueError(f"A switch has been hit or disconnected") From c5d145302680ae08dab57f40efefb755f59b8abd Mon Sep 17 00:00:00 2001 From: PIERROOOTT Date: Mon, 5 Feb 2024 15:34:55 +0100 Subject: [PATCH 33/35] refactor: minor optimization of the detection of switch state changes --- src/crappy/actuator/phidgets_stepper4a.py | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/src/crappy/actuator/phidgets_stepper4a.py b/src/crappy/actuator/phidgets_stepper4a.py index 0571747a..4c07bb36 100644 --- a/src/crappy/actuator/phidgets_stepper4a.py +++ b/src/crappy/actuator/phidgets_stepper4a.py @@ -81,6 +81,12 @@ def __init__(self, self._remote = remote self._switch_ports = switch_ports self._switches = list() + + # The following attribute is set to True to automatically check the state + # of the switches in the open method to keep the motor to move if a switch + # has been disconnected or hit. + # Nevertheless, this check can be bypassed if the motor is used outside + # from a Crappy loop by setting the attribute to False. self._check_switch = True self._absolute_mode = absolute_mode @@ -155,10 +161,9 @@ def open(self) -> None: self._motor.setEngaged(True) # Check the state of the switches - if self._check_switch: - for switch in self._switches: - if switch.getState() is False: - raise ValueError(f"A switch is already hit or disconnected") + if self._check_switch and not all( + switch.getState() for switch in self._switches): + raise ValueError(f"A switch is already hit or disconnected !") def set_speed(self, speed: float) -> None: """Sets the requested speed for the motor. @@ -303,10 +308,9 @@ def _on_position_change(self, _: Stepper, position: float) -> None: self.log(logging.DEBUG, f"Position changed to {position}") self._last_position = position - def _on_end(self, _: DigitalInput, __) -> None: + def _on_end(self, _: DigitalInput, state) -> None: """Callback when a switch is hit.""" - for switch in self._switches: - if switch.getState() is False: - self.stop() - raise ValueError(f"A switch has been hit or disconnected") + if bool(state) is False: + self.stop() + raise ValueError(f"A switch has been hit or disconnected !") From f00e3e83a659a73bd7747ac00e138c7e5c46ae91 Mon Sep 17 00:00:00 2001 From: PIERROOOTT Date: Tue, 6 Feb 2024 13:59:43 +0100 Subject: [PATCH 34/35] style: minor optimization --- src/crappy/actuator/phidgets_stepper4a.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/crappy/actuator/phidgets_stepper4a.py b/src/crappy/actuator/phidgets_stepper4a.py index 4c07bb36..eadc156a 100644 --- a/src/crappy/actuator/phidgets_stepper4a.py +++ b/src/crappy/actuator/phidgets_stepper4a.py @@ -311,6 +311,6 @@ def _on_position_change(self, _: Stepper, position: float) -> None: def _on_end(self, _: DigitalInput, state) -> None: """Callback when a switch is hit.""" - if bool(state) is False: + if not bool(state): self.stop() raise ValueError(f"A switch has been hit or disconnected !") From b7daa6ba3ef80527b75b6b604ece1802cb1d25b7 Mon Sep 17 00:00:00 2001 From: Antoine Weisrock Date: Wed, 7 Feb 2024 10:24:43 +0100 Subject: [PATCH 35/35] build: update project version from 2.0.3 to 2.0.4 --- docs/source/conf.py | 2 +- pyproject.toml | 2 +- src/crappy/__version__.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 6aebf3b2..5e09dfe7 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -16,7 +16,7 @@ from time import gmtime, strftime from re import match -__version__ = '2.0.3' +__version__ = '2.0.4' # -- Project information ----------------------------------------------------- diff --git a/pyproject.toml b/pyproject.toml index 93f67b36..64ad7452 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta" [project] name = "crappy" dynamic = ["readme"] -version = "2.0.3" +version = "2.0.4" description = "Command and Real-time Acquisition in Parallelized Python" license = {file = "LICENSE"} keywords = ["control", "command", "acquisition", "multiprocessing"] diff --git a/src/crappy/__version__.py b/src/crappy/__version__.py index 572682a9..dcdef668 100644 --- a/src/crappy/__version__.py +++ b/src/crappy/__version__.py @@ -1,3 +1,3 @@ # coding: utf-8 -__version__ = '2.0.3' +__version__ = '2.0.4'