Skip to content
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@

from typing import Union, List, Dict
from pymodaq.control_modules.move_utility_classes import (DAQ_Move_base, comon_parameters_fun,
main, DataActuatorType, DataActuator)

from pymodaq_utils.utils import ThreadCommand # object used to send info back to the main thread

from pymodaq_gui.parameter import Parameter

from pymeasure.instruments.redpitaya.redpitaya_scpi import RedPitayaScpi, AnalogOutputFastChannel

from pymodaq_data import Q_

from pymodaq_plugins_redpitaya.utils import Config

plugin_config = Config()

class DAQ_Move_RedpitayaAnalogSlow(DAQ_Move_base):
""" Instrument plugin class for Red Pitaya

This object inherits all functionalities to communicate with PyMoDAQ’s DAQ_Move module through
inheritance via DAQ_Move_base. It makes a bridge between the DAQ_Move module and the
Python wrapper of a particular instrument.

* Should be compatible with all redpitaya flavours using the SCPI communication protocol
* Tested with Red Pitaya 2.00-37 OS | STEMlab 125-14 Pro (Gen 2) version
* PyMoDAQ >= 5.0.5
* Tested with Linux Ubuntu 24.04.1 LTS
* Installation instruction:
These instructions are valid when using an Ethernet cable to communicate with the Red Pitaya.
The instrument is accessed using a TCP/IP Socket communication adapter, in the form: “TCPIP::x.y.z.k::port::SOCKET”
- x.y.z.k is the IP address of the SCPI server (that should be activated on the board)
- port is the TCP/IP port number, usually 5000
To activate the use of the Red Pitaya:
1. Connect the redpitaya to your computer/network with an ethernet cable.
2. Enter the url address written on the network plug (on the Red Pitaya)
It should be something like “RP-F0B462.LOCAL/”
3. Browse the menu, open the System application then the Network manager application
Look at the ip_address, and modify the config_template
4. Return to the menu, open the Development application and activate the SCPI server.
5. You can now run daq_move_RedpiatayaSCPI.py

Attributes:
-----------
controller: object
The particular object that allow the communication with the hardware, in general a python wrapper around the
hardware library.
"""

is_multiaxes = True
_axis_names: Union[List[str], Dict[str, int]] = ['voltage']
_controller_units: Union[str, List[str]] = ['V']
_epsilon: Union[float, List[float]] = [0.128] # Detailing 5mV and 1Hz. Resolution of reading is 5mV
# _epsilon: Union[float, List[float]] = 0.1 # Detailing 1mV and 1Hz #TODO replace this by a value that is correct depending on your controller
# TODO it could be a single float of a list of float (as much as the number of axes)
data_actuator_type = DataActuatorType.DataActuator

params = [ {'title': 'IP Address:', 'name': 'ip_address', 'type': 'str',
'value': plugin_config('ip_address')},
{'title': 'Port:', 'name': 'port', 'type': 'int', 'value': plugin_config('port')},
{'title': 'Board name:', 'name': 'bname', 'type': 'str', 'readonly': True},

{'title': 'Channel', 'name': 'channel', 'type': 'list', 'limits':{'0': 0, '1': 1, '2': 2, '3': 3},
'value': plugin_config('generator_slow', 'channel')},

] + comon_parameters_fun(is_multiaxes, axis_names=_axis_names, epsilon=_epsilon)
# _epsilon is the initial default value for the epsilon parameter allowing pymodaq to know if the controller reached
# the target value. It is the developer responsibility to put here a meaningful value

def ini_attributes(self):
self.controller: RedPitayaScpi = None
self.target_value = 0
# self.conv_factor = 50e-6
# self.scale = True
pass

def get_actuator_value(self):
"""Get the current value from the hardware with scaling conversion.

Returns
-------
float: The position obtained after scaling conversion.
"""

pos = DataActuator(data=getattr(self.aout_slow, self.axis_name),
units=self.axis_unit)
pos = self.get_position_with_scaling(pos)
return pos


def close(self):
"""Terminate the communication protocol"""
self.aout_slow.reset()


def commit_settings(self, param: Parameter):
"""Apply the consequences of a change of value in the detector settings

Parameters
----------
param: Parameter
A given parameter (within detector_settings) whose value has been changed by the user
"""
if param.name() == 'axis':
if param.value == 'voltage':
# self.settings.child('bounds', 'is_bounds').value()
self.settings.child('bounds', 'min_bound').setValue(0)
self.settings.child('bounds', 'max_bound').setValue(1.8)
self.settings.child('bounds', 'is_bounds').setValue(True)


# def is_enabled(self) -> bool:
# "It defines if the supply voltage is enabled on the output channel chosen"
# return self.settings['enable'] == True

@property
def aout_slow(self):
""" It defines what output channel the user chose"""
return self.controller.analog_out_slow[self.settings['channel']]

def ini_stage(self, controller=None):
"""Actuator communication initialization

Parameters
----------
controller: (object)
custom object of a PyMoDAQ plugin (Slave case). None if only one actuator by controller (Master case)

Returns
-------
info: str
initialized: bool
False if initialization failed otherwise True
"""

if self.is_master: # is needed when controller is master
self.controller = RedPitayaScpi(ip_address=self.settings['ip_address']) # arguments for instantiation!)
# self.controller = RedPitayaScpi(ip_address=plugin_config('ip_address')) # arguments for instantiation!)
else:
self.controller = controller

self.settings.child('bounds', 'min_bound').setValue(0)
self.settings.child('bounds', 'max_bound').setValue(1.8)
self.settings.child('bounds', 'is_bounds').value()
self.settings.child('bounds', 'is_bounds').setValue(True)
# self.settings.child('bounds', 'is_bounds').setOpts(readonly=True)

info = "Whatever info you want to log"
initialized = True
return info, initialized

def move_abs(self, value: DataActuator):
""" Move the actuator to the absolute target defined by value

Parameters
----------
value: (float) value of the absolute target positioning
"""
value = self.check_bound(value) #if user checked bounds, the defined bounds are applied here
self.target_value = value
print(value)
value = self.set_position_with_scaling(value) # TODO apply scaling if the user specified one
print(self.axis_unit)
setattr(self.aout_slow, self.axis_name, value.value(self.axis_unit))
# TODO change this run command to work with other trigger sources than INTernal trigger

def move_rel(self, value: DataActuator):
""" Move the actuator to the relative target actuator value defined by value

Parameters
----------
value: (float) value of the relative target positioning
"""
value = self.check_bound(self.current_position + value) - self.current_position
self.target_value = value + self.current_position
self.move_abs(self.target_value)

def move_home(self):
"""Call the reference method of the controller"""
pass

def stop_motion(self):
"""Stop the actuator and emits move_done signal"""
pass


if __name__ == '__main__':
main(__file__, init=False)
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ class DAQ_Move_RedpitayaSCPI(DAQ_Move_base):
Python wrapper of a particular instrument.

* Should be compatible with all redpitaya flavours using the SCPI communication protocol
* Tested with Red Pitaya 2.00-37 OS | STEMlab 125-14
* Tested with Red Pitaya 2.00-37 OS | STEMlab 125-14 Pro (Gen 2) version
* PyMoDAQ >= 5.0.5
* Tested with Linux Ubuntu 24.04.1 LTS
* Installation instruction:
Expand All @@ -50,14 +50,19 @@ class DAQ_Move_RedpitayaSCPI(DAQ_Move_base):
is_multiaxes = True
_axis_names: Union[List[str], Dict[str, int]] = ['amplitude', 'frequency']
_controller_units: Union[str, List[str]] = ['V','Hz']
_epsilon: Union[float, List[float]] = 0.1 # Detailing 1mV and 1Hz #TODO replace this by a value that is correct depending on your controller
_epsilon: Union[float, List[float]] = [0.005, 1] # Detailing 5mV and 1Hz. Resolution of reading is 5mV
# _epsilon: Union[float, List[float]] = 0.1 # Detailing 1mV and 1Hz #TODO replace this by a value that is correct depending on your controller
# TODO it could be a single float of a list of float (as much as the number of axes)
data_actuator_type = DataActuatorType.DataActuator

params = [ {'title': 'IP Address:', 'name': 'ip_address', 'type': 'str',
'value': plugin_config('ip_address')},
{'title': 'Port:', 'name': 'port', 'type': 'int', 'value': plugin_config('port')},
{'title': 'Board name:', 'name': 'bname', 'type': 'str', 'readonly': True},

{'title': 'Board Generation', 'name': 'gen', 'type': 'list', 'limits': {'Original': 1, 'Gen 2': 2},
'value': plugin_config('generation')},

{'title': 'Channel', 'name': 'channel', 'type': 'list', 'limits':{'1': 1, '2': 2},
'value': plugin_config('generator', 'channel')},

Expand Down Expand Up @@ -129,10 +134,14 @@ def commit_settings(self, param: Parameter):
self.settings.child('bounds', 'max_bound').setValue(50e6)
elif param.value == 'amplitude':
self.settings.child('bounds', 'min_bound').setValue(0)
self.settings.child('bounds', 'max_bound').setValue(1)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this has changed wrt the redpitaya version, one should add_them in the plugin preferences adding also the selection of the type of board gen1 or 2


self.settings.child('bounds', 'max_bound').setValue(self.settings['gen'])
self.settings.child('bounds', 'is_bounds').value()
self.settings.child('bounds', 'is_bounds').setValue(True)

elif param.name() == 'gen':
if self.axis_name == 'amplitude':
self.settings.child('bounds', 'min_bound').setValue(0)
self.settings.child('bounds', 'max_bound').setValue(param.value())
elif param.name() == 'enable':
self.aout.enable = param.value()
elif param.name() == 'shape':
Expand All @@ -155,6 +164,9 @@ def commit_settings(self, param: Parameter):
self.aout.sweep_state = param.value()
elif param.name() == 'sweep_direction':
self.aout.sweep_direction = param.value()
elif param.name() == 'gen_trigger':
self.aout.gen_trigger_source = param.value()
# TODO implement trigger source setting correctly

def is_enabled(self) -> bool:
"It defines if the supply voltage is enabled on the output channel chosen"
Expand All @@ -181,16 +193,24 @@ def ini_stage(self, controller=None):
"""

if self.is_master: # is needed when controller is master
self.controller = RedPitayaScpi(ip_address=plugin_config('ip_address')) # arguments for instantiation!)
self.controller = RedPitayaScpi(ip_address=self.settings['ip_address']) # arguments for instantiation!)
else:
self.controller = controller

self.aout.shape = self.settings['shape'] #plugin_config('generator', 'shape')

if self.axis_name =='frequency':
self.settings.child('bounds', 'min_bound').setValue(1e-6)
self.settings.child('bounds', 'max_bound').setValue(50e6)
elif self.axis_name == 'amplitude':
self.settings.child('bounds', 'min_bound').setValue(0)
self.settings.child('bounds', 'max_bound').setValue(self.settings['gen'])
self.settings.child('bounds', 'is_bounds').value()
self.settings.child('bounds', 'is_bounds').setValue(True)
self.settings.child('bounds', 'is_bounds').setOpts(readonly=True)

self.aout.enable = True
self.aout.run()
self.aout.run() # TODO change this function to work with other trigger sources than INTernal trigger

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we had quite a lot of issues with triggering with gen1 redpitaya, wonder if they worked on that


info = "Whatever info you want to log"
initialized = True
Expand All @@ -208,8 +228,9 @@ def move_abs(self, value: DataActuator):
value = self.check_bound(value) #if user checked bounds, the defined bounds are applied here
self.target_value = value
value = self.set_position_with_scaling(value) # apply scaling if the user specified one

setattr(self.controller.analog_out[self.settings['channel']], self.axis_name, value.value(self.axis_unit))
setattr(self.aout, self.axis_name, value.value(self.axis_unit))
self.aout.run() # regenerates trigger, otherwise it will continue to generate on the passed settings
# TODO change this function to work with other trigger sources than INTernal trigger

def move_rel(self, value: DataActuator):
""" Move the actuator to the relative target actuator value defined by value
Expand All @@ -232,4 +253,4 @@ def stop_motion(self):


if __name__ == '__main__':
main(__file__)
main(__file__, init=False)
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@
from pymodaq.utils.parameter import Parameter
from pymodaq_plugins_redpitaya.utils import Config

from pymeasure.instruments.redpitaya.redpitaya_scpi import RedPitayaScpi

from pymeasure.instruments.redpitaya.redpitaya_scpi import RedPitayaScpi, AnalogInputFastChannel

class DAQ_1DViewer_RedPitayaSCPI(DAQ_Viewer_base):
""" Instrument plugin class for a 1D viewer.
Expand All @@ -19,7 +18,7 @@ class DAQ_1DViewer_RedPitayaSCPI(DAQ_Viewer_base):
Python wrapper of a particular instrument.

* Should be compatible with all redpitaya flavour using the SCPI communication protocol
* Tested with the STEMlab 125-14 version
* Tested with the STEMlab 125-14 Pro (Gen 2) version
* PyMoDAQ >= 4.1.0

Attributes:
Expand Down Expand Up @@ -47,8 +46,12 @@ class DAQ_1DViewer_RedPitayaSCPI(DAQ_Viewer_base):
{'title': 'Buffer Length:', 'name': 'buffer_length', 'type': 'int', 'readonly': True},
{'title': 'Window Length:', 'name': 'window_length', 'type': 'float',
'value': 0, 'siPrefix': True, 'suffix': 's', 'readonly': True},
{'title': 'Gain1 (jumper CH1):', 'name': 'gain1', 'type': 'list',
'limits': RedPitayaScpi.GAINS, 'value': plugin_config('sampling', 'gain1')},
{'title': 'Gain2 (jumper CH2):', 'name': 'gain2', 'type': 'list',
'limits': RedPitayaScpi.GAINS, 'value': plugin_config('sampling', 'gain2')},

]},
]},
{'title': 'Triggering:', 'name': 'triggering', 'type': 'group', 'children': [
{'title': 'Source:', 'name': 'trigger_source', 'type': 'list',
'limits': RedPitayaScpi.TRIGGER_SOURCES, 'value': plugin_config('trigger', 'source')},
Expand Down Expand Up @@ -95,6 +98,11 @@ def commit_settings(self, param: Parameter):
elif param.name() == 'trigger_source':
self.controller.acq_trigger_source = param.value()

elif param.name() == 'gain1':
self.controller.acq_gain1 = param.value()
elif param.name() == 'gain2':
self.controller.acq_gain2 = param.value()

def _center_trigger(self):
if self.settings['triggering', 'center_trigger']:
self.controller.acq_trigger_delay_samples = \
Expand Down Expand Up @@ -134,8 +142,12 @@ def ini_detector(self, controller=None):
self.controller.acq_format = 'ASCII'
self.controller.acq_units = 'VOLTS'

self.controller.acq_gain1 = self.settings['sampling', 'gain1']
self.controller.acq_gain2 = self.settings['sampling', 'gain2']

self.controller.acq_trigger_level = self.settings['triggering', 'level']

self.controller.decimation = self.settings['sampling', 'decimation']
self.controller.decimation = self.settings['sampling', 'decimation']
self.controller.average_skipped_samples = self.settings['sampling', 'average']
self.settings.child('sampling', 'buffer_length').setValue(self.controller.buffer_length)
Expand All @@ -160,7 +172,7 @@ def grab_data(self, Naverage=1, **kwargs):
----------
Naverage: int
Number of hardware averaging (if hardware averaging is possible, self.hardware_averaging should be set to
True in class preamble and you should code this implementation)
True in class preamble, and you should code this implementation)
kwargs: dict
others optionals arguments
"""
Expand Down Expand Up @@ -196,7 +208,7 @@ def grab_data(self, Naverage=1, **kwargs):
size=nsamples)
self.dte_signal.emit(DataToExport('Redpitaya_dte',
data=[DataFromPlugins(name='RedPitaya', data=data_list,
dim='Data1D', labels=['AI0', 'AI1'],
dim='Data1D', labels=['IN1', 'IN2'],

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why changing the labels?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To agree with what is physically written on the Redpitaya's ports.

axes=[axis])]))

def stop(self):
Expand Down
Loading