diff --git a/.github/workflows/python_package.yml b/.github/workflows/python_package.yml new file mode 100644 index 00000000..d052c21f --- /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", "develop"] + + # 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__)" diff --git a/CODEOWNERS b/CODEOWNERS index 7a28a7fa..4b563075 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -4,6 +4,10 @@ # 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 + # PIERROOOTT owns the Opencv cameras /src/crappy/camera/opencv_* @WeisLeDocto @PIERROOOTT 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/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/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/crappy_docs/modifiers.rst b/docs/source/crappy_docs/modifiers.rst index 7986b245..e923b970 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 2a0391af..5352ba51 100644 --- a/docs/source/features.rst +++ b/docs/source/features.rst @@ -561,6 +561,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 `_ 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 +.. sectionauthor:: Pierre Margotin - :ref:`Demux` @@ -828,6 +850,12 @@ On-the-fly data modification (Modifiers) Calculates the time derivative of a given label. +- :ref:`DownSampler` + + 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` Integrates a given label over time. diff --git a/examples/modifiers/downsampler.py b/examples/modifiers/downsampler.py new file mode 100644 index 00000000..2d84ebd3 --- /dev/null +++ b/examples/modifiers/downsampler.py @@ -0,0 +1,80 @@ +# 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 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 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. +""" + +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 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', + '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 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 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 + # 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_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 + modifier=crappy.modifier.DownSampler(15)) + + # Mandatory line for starting the test, this call is blocking + crappy.start() 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' 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..eadc156a --- /dev/null +++ b/src/crappy/actuator/phidgets_stepper4a.py @@ -0,0 +1,316 @@ +# coding: utf-8 + +import logging +import numpy as np +from pathlib import Path +from platform import system +from typing import Optional, Tuple, Union + +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.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') + + +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, + absolute_mode: bool = False, + reference_pos: float = 0, + 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. + + 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²`. + 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 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. Only takes effect if ``save_last_pos`` is :obj:`True`. + """ + + 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 + 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 + 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: + 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 + 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() + + # Setting up the switches + 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) + 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") + 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) + for switch in self._switches: + switch.setOnStateChangeHandler(self._on_end) + + # 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 !") + + # Opening the connection to the switches + 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._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. + + 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)) + + # 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: + self._motor.setTargetPosition(position) + else: + self._motor.setTargetPosition(position - self._ref_pos) + + 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.""" + + if not self._absolute_mode: + 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: + """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: + 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: + switch.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 + + def _on_end(self, _: DigitalInput, state) -> None: + """Callback when a switch is hit.""" + + if not bool(state): + self.stop() + raise ValueError(f"A switch has been hit or disconnected !") diff --git a/src/crappy/inout/__init__.py b/src/crappy/inout/__init__.py index b7752727..5f800c8e 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 .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/phidgets_wheatstone_bridge.py b/src/crappy/inout/phidgets_wheatstone_bridge.py new file mode 100644 index 00000000..ff9f81cf --- /dev/null +++ b/src/crappy/inout/phidgets_wheatstone_bridge.py @@ -0,0 +1,149 @@ +# 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 : + :math:`returned = gain * acquired + offset` + offset: An offset to apply to the acquired value, following the formula : + :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 + + 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() + 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) + + # 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}") 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..a57ce7ef --- /dev/null +++ b/src/crappy/modifier/downsampler.py @@ -0,0 +1,49 @@ +# 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 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: One value will be sent to the downstream Block only once + every ``n_points`` received values. + """ + + super().__init__() + self._n_points: int = n_points + 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 + threshold, returns the data. + + If the counter doesn't match the threshold, doesn't return anything and + increments the counter. + """ + + self.log(logging.DEBUG, f"Received {data}") + + if self._count == self._n_points - 1: + self._count = 0 + self.log(logging.DEBUG, f"Sending {data}") + return data + + else: + self._count += 1 + self.log(logging.DEBUG, "Not returning any data")