From 0f854b1c77aa0298e1fa15db2c5d74d523ea4e42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Margueritat?= Date: Fri, 17 Nov 2023 17:12:56 +0100 Subject: [PATCH 001/140] new simple counter of all type --- .../plugins_0D/daq_0Dviewer_DAQmx_counter.py | 141 ++++++++++++++++++ 1 file changed, 141 insertions(+) create mode 100644 src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_DAQmx_counter.py diff --git a/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_DAQmx_counter.py b/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_DAQmx_counter.py new file mode 100644 index 0000000..eb345f3 --- /dev/null +++ b/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_DAQmx_counter.py @@ -0,0 +1,141 @@ +import numpy as np +from pymodaq.utils.daq_utils import ThreadCommand +from pymodaq.utils.data import DataWithAxes, DataToExport, DataSource +from pymodaq.control_modules.viewer_utility_classes import DAQ_Viewer_base, comon_parameters, main +from pymodaq.utils.parameter import Parameter + +from pymodaq_plugins_daqmx.hardware.national_instruments.daqmx import DAQmx, \ + Edge, ClockSettings, Counter, ClockCounter, TriggerSettings + +from PyDAQmx import DAQmx_Val_ContSamps + +class DAQ_0DViewer_DAQmx_counter(DAQ_Viewer_base): + """ + Plugin for a 0D PL counter, based on a NI card. + """ + params = comon_parameters+[ + {"title": "Counting channel:", "name": "counter_channel", + "type": "list", "limits": DAQmx.get_NIDAQ_channels(source_type="Counter")}, + {"title": "Acq. Time (s):", "name": "acq_time", + "type": "float", "value": 1., "default": 1.}, + {'title': 'Clock channel:', 'name': 'clock_channel', 'type': 'list', + 'limits': DAQmx.get_NIDAQ_channels(source_type='Counter')} + ] + + def ini_attributes(self): + self.controller = None + self.clock_channel = None + self.counter_channel = None + self.live = False # True during a continuous grab + self.counting_time = 0.1 + + 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() == "acq_time": + self.counting_time = param.value() + else: + self.stop() + self.update_tasks() + + def ini_detector(self, controller=None): + """Detector communication initialization + + Parameters + ---------- + controller: (object) + custom object of a PyMoDAQ plugin (Slave case). None if only one actuator/detector by controller + (Master case) + + Returns + ------- + info: str + initialized: bool + False if initialization failed otherwise True + """ + self.controller = {"clock": DAQmx(), "counter": DAQmx()} + try: + self.update_tasks() + initialized = True + info = "NI card based counter" + self.counting_time = self.settings.child("acq_time").value() + except Exception as e: + print(e) + initialized = False + info = "Error" + + self.dte_signal_temp.emit(DataToExport(name='PL', + data=[DataWithAxes(name='Counts', data=[np.array([0])], + source=DataSource['raw'], + dim='Data0D', labels=['counts (Hz)'])])) + + return info, initialized + + def close(self): + """Terminate the communication protocol""" + self.controller["clock"].close() + self.controller["counter"].close() + + def grab_data(self, Naverage=1, **kwargs): + """Start a grab from the detector + + Parameters + ---------- + Naverage: int + Number of hardware averaging not relevant here. + kwargs: dict + others optionals arguments + """ + update = True # to decide if we do the initial set up or not + + if 'live' in kwargs: + if kwargs['live'] == self.live and self.live: + update = False # we are already live + self.live = kwargs['live'] + + if update: + self.update_tasks() + self.controller["clock"].start() + + read_data = self.controller["counter"].readCounter(1, counting_time=self.counting_time) + data_pl = read_data/self.counting_time # convert to kcts/s + self.dte_signal.emit(DataToExport(name='PL', + data=[DataWithAxes(name='PL', data=[data_pl], + source=DataSource['raw'], + dim='Data0D', labels=['PL (kcts/s)'])])) + + def stop(self): + """Stop the current grab hardware wise if necessary""" + self.close() + self.emit_status(ThreadCommand('Update_Status', ['Acquisition stopped.'])) + return '' + + def update_tasks(self): + """Set up the counting tasks in the NI card.""" + # Create channels + self.clock_channel = ClockCounter(1/self.settings.child("acq_time").value(), + name=self.settings.child("clock_channel").value(), + source="Counter") + self.counter_channel = Counter(name=self.settings.child("counter_channel").value(), + source="Counter", edge=Edge.names()[0]) + + self.controller["clock"].update_task(channels=[self.clock_channel], + clock_settings=ClockSettings(), + trigger_settings=TriggerSettings()) + self.controller["clock"].task.CfgImplicitTiming(DAQmx_Val_ContSamps, 1) + + self.controller["counter"].update_task(channels=[self.counter_channel], + clock_settings=ClockSettings(), + trigger_settings=TriggerSettings()) + + # connect the clock to the counter + self.controller["counter"].task.SetSampClkSrc("/" + self.clock_channel.name + "InternalOutput") + + +if __name__ == '__main__': + main(__file__) From 5d37aa4252316f56317da840d7a221cafcd279e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Margueritat?= Date: Sat, 18 Nov 2023 18:17:42 +0100 Subject: [PATCH 002/140] Update daq_0Dviewer_DAQmx_counter.py --- .../plugins_0D/daq_0Dviewer_DAQmx_counter.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_DAQmx_counter.py b/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_DAQmx_counter.py index eb345f3..3c00b4d 100644 --- a/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_DAQmx_counter.py +++ b/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_DAQmx_counter.py @@ -103,11 +103,11 @@ def grab_data(self, Naverage=1, **kwargs): self.controller["clock"].start() read_data = self.controller["counter"].readCounter(1, counting_time=self.counting_time) - data_pl = read_data/self.counting_time # convert to kcts/s - self.dte_signal.emit(DataToExport(name='PL', - data=[DataWithAxes(name='PL', data=[data_pl], + data = read_data/self.counting_time # convert to cts/s + self.dte_signal.emit(DataToExport(name='Counts', + data=[DataWithAxes(name='Counts', data=[data], source=DataSource['raw'], - dim='Data0D', labels=['PL (kcts/s)'])])) + dim='Data0D', labels=['Counts (Hz)'])])) def stop(self): """Stop the current grab hardware wise if necessary""" From 730c40d75fb8cd58a3c2b04c8747dbdb81d42de6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Margueritat?= Date: Tue, 21 Nov 2023 16:06:55 +0100 Subject: [PATCH 003/140] using nidaqmx --- .../plugins_0D/daq_0Dviewer_DAQmx_counter.py | 36 +- .../hardware/national_instruments/_nidaqmx.py | 20 + .../national_instruments/counter_test.py | 19 + .../hardware/national_instruments/daqmxNI.py | 781 ++++++++++++++++++ 4 files changed, 842 insertions(+), 14 deletions(-) create mode 100644 src/pymodaq_plugins_daqmx/hardware/national_instruments/_nidaqmx.py create mode 100644 src/pymodaq_plugins_daqmx/hardware/national_instruments/counter_test.py create mode 100644 src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxNI.py diff --git a/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_DAQmx_counter.py b/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_DAQmx_counter.py index 3c00b4d..22082c6 100644 --- a/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_DAQmx_counter.py +++ b/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_DAQmx_counter.py @@ -19,7 +19,9 @@ class DAQ_0DViewer_DAQmx_counter(DAQ_Viewer_base): {"title": "Acq. Time (s):", "name": "acq_time", "type": "float", "value": 1., "default": 1.}, {'title': 'Clock channel:', 'name': 'clock_channel', 'type': 'list', - 'limits': DAQmx.get_NIDAQ_channels(source_type='Counter')} + 'limits': DAQmx.get_NIDAQ_channels(source_type='Counter')}, + {"title": "Clk Trig:", "name": "trig", + "type": "list", "limits": DAQmx.getTriggeringSources()} ] def ini_attributes(self): @@ -39,9 +41,7 @@ def commit_settings(self, param: Parameter): """ if param.name() == "acq_time": self.counting_time = param.value() - else: - self.stop() - self.update_tasks() + self.update_tasks() def ini_detector(self, controller=None): """Detector communication initialization @@ -69,17 +69,18 @@ def ini_detector(self, controller=None): initialized = False info = "Error" - self.dte_signal_temp.emit(DataToExport(name='PL', - data=[DataWithAxes(name='Counts', data=[np.array([0])], - source=DataSource['raw'], - dim='Data0D', labels=['counts (Hz)'])])) + #self.dte_signal_temp.emit(DataToExport(name='Counts', + # data=[DataWithAxes(name='Counts', data=[np.array([0])], + # source=DataSource['raw'], + # dim='Data0D', labels=['counts (Hz)'])])) return info, initialized def close(self): """Terminate the communication protocol""" - self.controller["clock"].close() - self.controller["counter"].close() + pass + #self.controller["clock"].close() + #self.controller["counter"].close() def grab_data(self, Naverage=1, **kwargs): """Start a grab from the detector @@ -91,13 +92,20 @@ def grab_data(self, Naverage=1, **kwargs): kwargs: dict others optionals arguments """ - update = True # to decide if we do the initial set up or not + update = False # to decide if we do the initial set up or not + if 'live' in kwargs: - if kwargs['live'] == self.live and self.live: + if kwargs['live'] != self.live: + update = True + self.live = kwargs['live'] + """ + if 'live' in kwargs: + if kwargs['live'] == self.live: update = False # we are already live self.live = kwargs['live'] - + """ + if update: self.update_tasks() self.controller["clock"].start() @@ -119,7 +127,7 @@ def update_tasks(self): """Set up the counting tasks in the NI card.""" # Create channels self.clock_channel = ClockCounter(1/self.settings.child("acq_time").value(), - name=self.settings.child("clock_channel").value(), + name=self.settings.child("trig").value(), source="Counter") self.counter_channel = Counter(name=self.settings.child("counter_channel").value(), source="Counter", edge=Edge.names()[0]) diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/_nidaqmx.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/_nidaqmx.py new file mode 100644 index 0000000..e1f1ec6 --- /dev/null +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/_nidaqmx.py @@ -0,0 +1,20 @@ +import nidaqmx +from nidaqmx.constants import AcquisitionType,Edge +import time +import matplotlib.pyplot as plt + +task=nidaqmx.Task() +task.ci_channels.add_ci_count_edges_chan("Dev1/ctr0") + + +task.start() +data=task.read(512) +#for i in range(512): +# mesure0 = task.read() +# data.append(task.read()-mesure0) +task.stop() + +print(data) +task.close() +plt.plot(data) +plt.show() \ No newline at end of file diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/counter_test.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/counter_test.py new file mode 100644 index 0000000..11e474a --- /dev/null +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/counter_test.py @@ -0,0 +1,19 @@ +import PyDAQmx +import ctypes +import numpy as np + + +counter_input = PyDAQmx.Task() +read = PyDAQmx.int32() +data = np.zeros((1000,),dtype=np.float64) + +#DAQmx Configure Code +counter_input.CreateCICountEdgesChan("Dev1/ctr0","",PyDAQmx.DAQmx_Val_Rising,0,PyDAQmx.DAQmx_Val_CountUp) + +#DAQmx Start Code +counter_input.StartTask() + +#DAQmx Read Code +counter_input.ReadCounterScalarU32(10.0,PyDAQmx.byref(data),None) + +print(read.value) \ No newline at end of file diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxNI.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxNI.py new file mode 100644 index 0000000..7ce5fe1 --- /dev/null +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxNI.py @@ -0,0 +1,781 @@ +import PyDAQmx +import ctypes +from enum import IntEnum +import numpy as np +from pymodaq.utils.logger import set_logger, get_module_name + +logger = set_logger(get_module_name(__file__)) + + +class DAQ_NIDAQ_source(IntEnum): + """ + Enum class of NIDAQ_source + + =============== ========== + **Attributes** **Type** + *Analog_Input* int + *Counter* int + =============== ========== + """ + Analog_Input = 0 + Counter = 1 + Analog_Output = 2 + Digital_Output = 3 + Digital_Input = 4 + Terminals = 5 + + @classmethod + def names(cls): + return [name for name, member in cls.__members__.items()] + + +class DAQ_analog_types(IntEnum): + """ + Enum class of Ai types + + =============== ========== + **Attributes** **Type** + =============== ========== + """ + Voltage = PyDAQmx.DAQmx_Val_Voltage + Current = PyDAQmx.DAQmx_Val_Current + Thermocouple = PyDAQmx.DAQmx_Val_Temp_TC + + @classmethod + def names(cls): + return [name for name, member in cls.__members__.items()] + + @classmethod + def values(cls): + return [cls[name].value for name, member in cls.__members__.items()] + + +class DAQ_thermocouples(IntEnum): + """ + Enum class of thermocouples type + + =============== ========== + **Attributes** **Type** + =============== ========== + """ + J = PyDAQmx.DAQmx_Val_J_Type_TC + K = PyDAQmx.DAQmx_Val_K_Type_TC + N = PyDAQmx.DAQmx_Val_N_Type_TC + R = PyDAQmx.DAQmx_Val_R_Type_TC + S = PyDAQmx.DAQmx_Val_S_Type_TC + T = PyDAQmx.DAQmx_Val_T_Type_TC + B = PyDAQmx.DAQmx_Val_B_Type_TC + E = PyDAQmx.DAQmx_Val_E_Type_TC + + @classmethod + def names(cls): + return [name for name, member in cls.__members__.items()] + + +class DAQ_termination(IntEnum): + """ + Enum class of thermocouples type + + =============== ========== + **Attributes** **Type** + =============== ========== + """ + Auto = PyDAQmx.DAQmx_Val_Cfg_Default + RSE = PyDAQmx.DAQmx_Val_RSE + NRSE = PyDAQmx.DAQmx_Val_NRSE + Diff = PyDAQmx.DAQmx_Val_Diff + Pseudodiff = PyDAQmx.DAQmx_Val_PseudoDiff + + @classmethod + def names(cls): + return [name for name, member in cls.__members__.items()] + + +class Edge(IntEnum): + """ + """ + Rising = PyDAQmx.DAQmx_Val_Rising + Falling = PyDAQmx.DAQmx_Val_Falling + + @classmethod + def names(cls): + return [name for name, member in cls.__members__.items()] + + +class ClockMode(IntEnum): + """ + """ + Finite = PyDAQmx.DAQmx_Val_Rising + Continuous = PyDAQmx.DAQmx_Val_Falling + + @classmethod + def names(cls): + return [name for name, member in cls.__members__.items()] + + +class ClockSettingsBase: + def __init__(self, Nsamples=1000, repetition=False): + + self.Nsamples = Nsamples + self.repetition = repetition + + +class ClockSettings(ClockSettingsBase): + def __init__(self, source=None, frequency=1000, Nsamples=1000, edge=Edge.names()[0], repetition=False): + super().__init__(Nsamples, repetition) + self.source = source + assert edge in Edge.names() + self.frequency = frequency + self.edge = edge + + +class ChangeDetectionSettings(ClockSettingsBase): + def __init__(self, Nsamples=1000, rising_channel='', falling_channel='', + repetition=False): + super().__init__(Nsamples, repetition) + self.rising_channel = rising_channel + self.falling_channel = falling_channel + + +class TriggerSettings: + def __init__(self, trig_source='', enable=False, edge=Edge.names()[0], level=0.1): + assert edge in Edge.names() + self.trig_source = trig_source + self.enable = enable + self.edge = edge + self.level = level + + +class Channel: + def __init__(self, name='', source=DAQ_NIDAQ_source.names()[0]): + """ + Parameters + ---------- + + """ + self.name = name + assert source in DAQ_NIDAQ_source.names() + self.source = source + + +class AChannel(Channel): + def __init__(self, analog_type=DAQ_analog_types.names()[0], value_min=-10., value_max=+10., **kwargs): + """ + Parameters + ---------- + min: (float) minimum value for the configured input channel (could be voltage, amps, temperature...) + max: (float) maximum value for the configured input channel + """ + super().__init__(**kwargs) + self.value_min = value_min + self.value_max = value_max + self.analog_type = analog_type + + +class AIChannel(AChannel): + def __init__(self, termination=DAQ_termination.names()[0], **kwargs): + super().__init__(**kwargs) + assert termination in DAQ_termination.names() + self.termination = termination + + +class AIThermoChannel(AIChannel): + def __init__(self, thermo_type=DAQ_thermocouples.names()[0], **kwargs): + super().__init__(**kwargs) + assert thermo_type in DAQ_thermocouples.names() + self.thermo_type = thermo_type + + +class AOChannel(AChannel): + def __init__(self, **kwargs): + super().__init__(**kwargs) + + +class Counter(Channel): + def __init__(self, edge=Edge.names()[0], **kwargs): + assert edge in Edge.names() + super().__init__(**kwargs) + self.edge = edge + self.counter_type = "Edge Counter" + + +class ClockCounter(Counter): + def __init__(self, clock_frequency, **kwargs): + super().__init__(**kwargs) + self.clock_frequency = clock_frequency + self.counter_type = "Clock Output" + + +class SemiPeriodCounter(Counter): + def __init__(self, value_max, **kwargs): + super().__init__(**kwargs) + self.value_max = value_max + self.counter_type = "SemiPeriod Input" + + +class DigitalChannel(Channel): + def __init__(self, **kwargs): + super().__init__(**kwargs) + + +class DOChannel(DigitalChannel): + def __init__(self, **kwargs): + super().__init__(**kwargs) + + +class DIChannel(DigitalChannel): + def __init__(self, **kwargs): + super().__init__(**kwargs) + + +def try_string_buffer(fun, *args): + """ + generic function to read string from a PyDAQmx function making sure the chosen buffer is large enough + Parameters + ---------- + fun: (PyDAQmx function pointer) e.g. PyDAQmx.DAQmxGetSysDevNames + kwargs + + Returns + ------- + + """ + buff_size = 1024 + while True: + buff = PyDAQmx.create_string_buffer(buff_size) + try: + if not not len(args): + fun(args[0], buff, buff_size) + else: + fun(buff, buff_size) + break + + except Exception as e: + if isinstance(e, PyDAQmx.DAQmxFunctions.DAQException): + if e.error == -200228: # BufferTooSmallForStringError + buff_size = 2 * buff_size + else: + raise e + else: + raise e + return buff.value.decode() + + +class DAQmx: + """Wrapper around the PyDAQmx package giving an easy to use object to instantiate channels and tasks""" + def __init__(self): + self.devices = [] + self.channels = [] + self._device = None + self._task = None + self.update_NIDAQ_devices() + self.update_NIDAQ_channels() + self.c_callback = None + self.callback_data = None + self.is_scalar = True + self.write_buffer = np.array([0.]) + + @property + def task(self): + return self._task + + @property + def device(self): + return self._device + + @device.setter + def device(self, device): + if device not in self.devices: + raise IOError(f'your device: {device} is not known or connected') + self._device = device + + def update_NIDAQ_devices(self): + self.devices = self.get_NIDAQ_devices() + + @classmethod + def get_NIDAQ_devices(cls): + """Get list of NI connected devices + + Returns + ------- + list + list of devices as strings to be used in subsequent commands + """ + try: + string = try_string_buffer(PyDAQmx.DAQmxGetSysDevNames) + devices = string.split(', ') + if devices == ['']: + devices = [] + return devices + except: + return [] + + def update_NIDAQ_channels(self, source_type=None): + self.channels = self.get_NIDAQ_channels(self.devices, source_type=source_type) + + @classmethod + def get_NIDAQ_channels(cls, devices=None, source_type=None): + """Get the list of available channels for all NiDAq connected devices + + Parameters + ---------- + devices: list + list of strings, each one being a connected device + source_type: str + One of the entries of DAQ_NIDAQ_source enum + + Returns + ------- + List of str containing device and channel names + + """ + if devices is None: + devices = cls.get_NIDAQ_devices() + + if source_type is None: + source_type = DAQ_NIDAQ_source.names() + if not isinstance(source_type, list): + source_type = [source_type] + channels_tot = [] + if not not devices: + for device in devices: + for source in source_type: + if source == DAQ_NIDAQ_source['Analog_Input'].name: # analog input + string = try_string_buffer(PyDAQmx.DAQmxGetDevAIPhysicalChans, device) + elif source == DAQ_NIDAQ_source['Counter'].name: # counter + string = try_string_buffer(PyDAQmx.DAQmxGetDevCIPhysicalChans, device) + elif source == DAQ_NIDAQ_source['Analog_Output'].name: # analog output + string = try_string_buffer(PyDAQmx.DAQmxGetDevAOPhysicalChans, device) + elif source == DAQ_NIDAQ_source['Digital_Output'].name: # digital output + string = try_string_buffer(PyDAQmx.DAQmxGetDevDOLines, device) + elif source == DAQ_NIDAQ_source['Digital_Input'].name: # digital output + string = try_string_buffer(PyDAQmx.DAQmxGetDevDILines, device) + elif source == DAQ_NIDAQ_source['Terminals'].name: # digital output + string = try_string_buffer(PyDAQmx.DAQmxGetDevTerminals, device) + + channels = string.split(', ') + if channels != ['']: + channels_tot.extend(channels) + + return channels_tot + + @classmethod + def getAOMaxRate(cls, device): + data = PyDAQmx.c_double() + PyDAQmx.DAQmxGetDevAOMaxRate(device, PyDAQmx.byref(data)) + return data.value + + @classmethod + def getAIMaxRate(cls, device): + data = PyDAQmx.c_double() + PyDAQmx.DAQmxGetDevAIMaxSingleChanRate(device, PyDAQmx.byref(data)) + return data.value + + @classmethod + def isAnalogTriggeringSupported(cls, device): + data = PyDAQmx.c_uint32() + PyDAQmx.DAQmxGetDevAnlgTrigSupported(device, PyDAQmx.byref(data)) + return bool(data.value) + + @classmethod + def isDigitalTriggeringSupported(cls, device): + data = PyDAQmx.c_uint32() + PyDAQmx.DAQmxGetDevDigTrigSupported(device, PyDAQmx.byref(data)) + return bool(data.value) + + @classmethod + def getTriggeringSources(cls, devices=None): + sources = [] + if devices is None: + devices = cls.get_NIDAQ_devices() + + for device in devices: + if cls.isDigitalTriggeringSupported(device): + string = try_string_buffer(PyDAQmx.DAQmxGetDevTerminals, device) + channels = [chan for chan in string.split(', ') if 'PFI' in chan] + if channels != ['']: + sources.extend(channels) + if cls.isAnalogTriggeringSupported(device): + channels = cls.get_NIDAQ_channels(devices=[device], source_type='Analog_Input') + if channels != ['']: + sources.extend(channels) + return sources + + def update_task(self, channels=[], clock_settings=ClockSettings(), trigger_settings=TriggerSettings()): + + try: + if self._task is not None: + if isinstance(self._task, PyDAQmx.Task): + self._task.ClearTask() + + self._task = None + self.c_callback = None + + self._task = PyDAQmx.Task() + + ## create all channels one task for one type of channels + for channel in channels: + if channel.source == 'Analog_Input': #analog input + if channel.analog_type == "Voltage": + err_code = self._task.CreateAIVoltageChan(channel.name, "analog voltage task", + DAQ_termination[channel.termination].value, + channel.value_min, + channel.value_max, + PyDAQmx.DAQmx_Val_Volts, None) + + elif channel.analog_type == "Current": + err_code = self._task.CreateAICurrentChan(channel.name, "", + DAQ_termination[channel.termination].value, + channel.value_min, + channel.value_max, + PyDAQmx.DAQmx_Val_Amps, PyDAQmx.DAQmx_Val_Internal, + 0., None) + + elif channel.analog_type == "Thermocouple": + err_code = self._task.CreateAIThrmcplChan(channel.name, "", + channel.value_min, + channel.value_max, + PyDAQmx.DAQmx_Val_DegC, + DAQ_termination[channel.thermo_type].value, + PyDAQmx.DAQmx_Val_BuiltIn, 0., "") + + elif channel.source == 'Counter': #counter + if channel.counter_type == "Edge Counter": + err_code = self._task.CreateCICountEdgesChan(channel.name, "", + Edge[channel.edge].value, 0, + PyDAQmx.DAQmx_Val_CountUp) + elif channel.counter_type == "Clock Output": + err_code = self._task.CreateCOPulseChanFreq(channel.name, "clock task", + # units, Hertz in our case + PyDAQmx.DAQmx_Val_Hz, + # idle state + PyDAQmx.DAQmx_Val_Low, + # initial delay + 0, + # pulse frequency + channel.clock_frequency, + # duty cycle of pulses, 0.5 such that + # high and low duration are both + # equal to count_interval + 0.5) + elif channel.counter_type == "SemiPeriod Input": + err_code = self._task.CreateCISemiPeriodChan(channel.name, "counter task", + 0, # expected min + channel.value_max, # expected max + PyDAQmx.DAQmx_Val_Ticks, "") + + + if not not err_code: + status = self.DAQmxGetErrorString(err_code) + raise IOError(status) + + elif channel.source == 'Analog_Output': # Analog_Output + if channel.analog_type == "Voltage": + err_code = self._task.CreateAOVoltageChan(channel.name, "", + channel.value_min, + channel.value_max, + PyDAQmx.DAQmx_Val_Volts, None) + + if channel.analog_type == "Current": + err_code = self._task.CreateAOCurrentChan(channel.name, "", + channel.value_min, + channel.value_max, + PyDAQmx.DAQmx_Val_Amps, None) + + elif channel.source == 'Digital_Output': #Digital_Output + err_code = self._task.CreateDOChan(channel.name, "", PyDAQmx.DAQmx_Val_ChanPerLine) + if not not err_code: + status = self.DAQmxGetErrorString(err_code) + raise IOError(status) + + elif channel.source == 'Digital_Input': #Digital_Input + err_code = self._task.CreateDIChan(channel.name, "", PyDAQmx.DAQmx_Val_ChanPerLine) + if not not err_code: + status = self.DAQmxGetErrorString(err_code) + raise IOError(status) + + ## configure the timing + if clock_settings.repetition: + mode = PyDAQmx.DAQmx_Val_ContSamps + else: + mode = PyDAQmx.DAQmx_Val_FiniteSamps + if clock_settings.Nsamples > 1 and err_code == 0: + if isinstance(clock_settings, ClockSettings): + err_code =\ + self._task.CfgSampClkTiming(clock_settings.source, clock_settings.frequency, + Edge[clock_settings.edge].value, + mode, + clock_settings.Nsamples) + elif isinstance(clock_settings, ChangeDetectionSettings): + err_code =\ + self._task.CfgChangeDetectionTiming(clock_settings.rising_channel, + clock_settings.falling_channel, + mode, + clock_settings.Nsamples) + + if not not err_code: + status = self.DAQmxGetErrorString(err_code) + raise IOError(status) + + # if channel.source == 'Analog_Input': # analog input + # if isinstance(clock_settings, ClockSettings): + # err_code =\ + # self._task.CfgSampClkTiming(clock_settings.source, clock_settings.frequency, + # Edge[clock_settings.edge].value, + # mode, + # clock_settings.Nsamples) + # elif isinstance(clock_settings, ChangeDetectionSettings): + # err_code =\ + # self._task.CfgChangeDetectionTiming(clock_settings.source, + # clock_settings.rising_channel, + # clock_settings.falling_channel, + # mode, + # clock_settings.Nsamples) + # + # if not not err_code: + # status = self.DAQmxGetErrorString(err_code) + # raise IOError(status) + # + # elif channel.source == 'Counter': # counter + # pass + # + # elif channel.source == 'Analog_Output': # Analog_Output + # if clock_settings.Nsamples > 1 and err_code == 0: + # + # if isinstance(clock_settings, ClockSettings): + # err_code = self._task.CfgSampClkTiming(clock_settings.source, + # clock_settings.frequency, + # Edge[clock_settings.edge].value, + # mode, + # clock_settings.Nsamples) + # elif isinstance(clock_settings, ChangeDetectionSettings): + # err_code = \ + # self._task.CfgChangeDetectionTiming(clock_settings.source, + # clock_settings.rising_channel, + # clock_settings.falling_channel, + # mode, + # clock_settings.Nsamples) + # + # if not not err_code: + # status = self.DAQmxGetErrorString(err_code) + # raise IOError(status) + # + # else: + # pass + + ##configure the triggering, except for counters + if not trigger_settings.enable: + if channel.source == 'Counter': + pass + else: + err = self._task.DisableStartTrig() + if err != 0: + raise IOError(self.DAQmxGetErrorString(err)) + else: + if 'PF' in trigger_settings.trig_source: + self._task.CfgDigEdgeStartTrig(trigger_settings.trig_source, + Edge[trigger_settings.edge].value) + elif 'ai' in trigger_settings.trig_source: + self._task.CfgAnlgEdgeStartTrig(trigger_settings.trig_source, + Edge[trigger_settings.edge].value, + PyDAQmx.c_double(trigger_settings.level)) + else: + raise IOError('Unsupported Trigger source') + + except Exception as e: + print(e) + + def register_callback(self, callback, event='done', nsamples=1): + + if event == 'done': + self.c_callback = PyDAQmx.DAQmxDoneEventCallbackPtr(callback) + self._task.RegisterDoneEvent(0, self.c_callback, None) + elif event == 'sample': + self.c_callback = PyDAQmx.DAQmxSignalEventCallbackPtr(callback) + self._task.RegisterSignalEvent(PyDAQmx.DAQmx_Val_SampleCompleteEvent, 0, self.c_callback, None) + elif event == 'Nsamples': + self.c_callback = PyDAQmx.DAQmxEveryNSamplesEventCallbackPtr(callback) + self._task.RegisterEveryNSamplesEvent(PyDAQmx.DAQmx_Val_Acquired_Into_Buffer, nsamples, + 0, self.c_callback, None) + + def get_last_write_index(self): + if self.task is not None: + index_buffer = PyDAQmx.c_uint64() + ret = self._task.GetWriteCurrWritePos(PyDAQmx.byref(index_buffer)) + if not not ret: + raise IOError(self.DAQmxGetErrorString(ret)) + else: + return index_buffer.value + else: + return -1 + + def get_last_write(self): + if self.is_scalar: + return self.write_buffer[-1] + else: + index = self.get_last_write_index() + if index != -1: + return self.write_buffer[index % len(self.write_buffer)] + + else: + return 0. + + def writeAnalog(self, Nsamples, Nchannels, values, autostart=False): + """ + Write Nsamples on N analog output channels + Parameters + ---------- + Nsamples: (int) numver of samples to write on each channel + Nchannels: (int) number of AO channels defined in the task + values: (ndarray) 2D array (or flattened array) of size Nsamples * Nchannels + + Returns + ------- + + """ + if np.prod(values.shape) != Nsamples * Nchannels: + raise ValueError(f'The shape of analog outputs values is incorrect, should be {Nsamples} x {Nchannels}') + + if len(values.shape) != 1: + values = values.reshape((Nchannels * Nsamples)) + self.write_buffer = values + + timeout = -1 + if values.size == 1: + self._task.WriteAnalogScalarF64(autostart, timeout, values[0], None) + self.is_scalar = True + + else: + self.is_scalar = False + read = PyDAQmx.int32() + self._task.WriteAnalogF64(Nsamples, autostart, timeout, PyDAQmx.DAQmx_Val_GroupByChannel, values, + PyDAQmx.byref(read), None) + if read.value != Nsamples: + raise IOError(f'Insufficient number of samples have been written:{read.value}/{Nsamples}') + + def readAnalog(self, Nchannels, clock_settings): + read = PyDAQmx.int32() + N = clock_settings.Nsamples + data = np.zeros(N * Nchannels, dtype=np.float64) + timeout = N * Nchannels * 1 / clock_settings.frequency * 2 # set to twice the time it should take to acquire the data + + self._task.ReadAnalogF64(N, timeout, PyDAQmx.DAQmx_Val_GroupByChannel, data, len(data), + PyDAQmx.byref(read), None) + if read.value == N: + return data + else: + raise IOError(f'Insufficient number of samples have been read:{read.value}/{N}') + + def readCounter(self, Nchannels, counting_time=10., read_function="Ex"): + + data_counter = np.zeros(Nchannels, dtype='uint32') + read = PyDAQmx.int32() + if read_function == "Ex": + self._task.ReadCounterU32Ex(PyDAQmx.DAQmx_Val_Auto, 2*counting_time, + PyDAQmx.DAQmx_Val_GroupByChannel, + data_counter, + Nchannels, PyDAQmx.byref(read), None) + else: + self._task.ReadCounterU32(PyDAQmx.DAQmx_Val_Auto, 2*counting_time, + data_counter, Nchannels, PyDAQmx.byref(read), None) + + self._task.StopTask() + + if read.value == Nchannels: + return data_counter + else: + raise IOError(f'Insufficient number of samples have been read:{read}/{Nchannels}') + + def readDigital(self, Nchannels): + read = PyDAQmx.int32() + bytes_sample = PyDAQmx.int32() + data = np.zeros(Nchannels, dtype='uint8') + self._task.ReadDigitalLines(Nchannels, 0, PyDAQmx.DAQmx_Val_GroupByChannel, + data, Nchannels, PyDAQmx.byref(read), PyDAQmx.byref(bytes_sample), None); + + if read.value == Nchannels: + return data + else: + raise IOError(f'Insufficient number of samples have been read:{read}/{Nchannels}') + + def writeDigital(self, Nchannels, values, autostart=False): + if np.prod(values.shape) != Nchannels: + raise ValueError(f'The shape of digital outputs values is incorrect, should be {Nchannels}') + values.astype(np.uint8) + written = PyDAQmx.int32() + self._task.WriteDigitalLines(Nchannels, autostart, 0, PyDAQmx.DAQmx_Val_GroupByChannel, + values, PyDAQmx.byref(written), None) + if written.value != Nchannels: + raise IOError(f'Insufficient number of samples have been written:{written}/{Nchannels}') + + @classmethod + def getAIVoltageRange(cls, device='Dev1'): + buff_size = 100 + ranges = ctypes.pointer((buff_size*ctypes.c_double)()) + ret = PyDAQmx.DAQmxGetDevAIVoltageRngs(device, ranges[0], buff_size) + if ret == 0: + return [tuple(ranges.contents[2*ind:2*(ind+1)]) for ind in range(int(buff_size/2-2)) + if np.abs(ranges.contents[2*ind]) > 1e-12] + return [(-10., 10.)] + + @classmethod + def getAOVoltageRange(cls, device='Dev1'): + buff_size = 100 + ranges = ctypes.pointer((buff_size*ctypes.c_double)()) + ret = PyDAQmx.DAQmxGetDevAOVoltageRngs(device, ranges[0], buff_size) + if ret == 0: + return [tuple(ranges.contents[2*ind:2*(ind+1)]) for ind in range(int(buff_size/2-2)) + if np.abs(ranges.contents[2*ind]) > 1e-12] + return [(-10., 10.)] + + def stop(self): + if self._task is not None: + self._task.StopTask() + + def start(self): + if self._task is not None: + self._task.StartTask() + + def close(self): + """ + close the current task. + """ + if self._task is not None: + self._task.StopTask() + self._task.ClearTask() + self._task = None + + @classmethod + def DAQmxGetErrorString(cls, error_code): + if error_code is None: + return '' + else: + buffer = PyDAQmx.create_string_buffer(1024) + PyDAQmx.DAQmxGetErrorString(error_code, buffer, len(buffer)) + return buffer.value.decode() + + def isTaskDone(self): + done = PyDAQmx.bool32(False) + self._task.GetTaskComplete(PyDAQmx.byref(done)) + return bool(done.value) + + def waitTaskDone(self, timeout=10.): + ret = self._task.WaitUntilTaskDone(timeout) + if ret != 0: + logger.info(self.DAQmxGetErrorString(ret)) + + def refresh_hardware(self): + """ + Refresh the NIDAQ hardware from settings values. + + See Also + -------- + update_NIDAQ_devices, update_NIDAQ_channels + """ + devices = self.update_NIDAQ_devices() + self.update_NIDAQ_channels(devices) + + +if __name__ == '__main__': + print(DAQmx.get_NIDAQ_channels()) + pass From b3b899139d810149b03a491d6e9430ba1f302086 Mon Sep 17 00:00:00 2001 From: jerlfan Date: Thu, 7 Dec 2023 17:31:28 +0100 Subject: [PATCH 004/140] adding the use of the nidaqmx module --- plugin_info.toml | 2 +- .../plugins_0D/daq_0Dviewer_DAQmx_counter.py | 2 +- .../hardware/national_instruments/_nidaqmx.py | 23 ++- .../national_instruments/counter_test.py | 19 -- .../{daqmxNI.py => daqmx-ni.py} | 170 +++++++++--------- 5 files changed, 105 insertions(+), 111 deletions(-) delete mode 100644 src/pymodaq_plugins_daqmx/hardware/national_instruments/counter_test.py rename src/pymodaq_plugins_daqmx/hardware/national_instruments/{daqmxNI.py => daqmx-ni.py} (83%) diff --git a/plugin_info.toml b/plugin_info.toml index d5abec0..166e6c0 100644 --- a/plugin_info.toml +++ b/plugin_info.toml @@ -11,7 +11,7 @@ license = 'MIT' [plugin-install] #packages required for your plugin: -packages-required = ['pydaqmx', 'pymodaq>4.0.1'] +packages-required = ['pydaqmx','nidaqmx', 'pymodaq>4.0.1'] ## [features] # defines the plugin features contained into this plugin diff --git a/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_DAQmx_counter.py b/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_DAQmx_counter.py index 22082c6..5236f83 100644 --- a/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_DAQmx_counter.py +++ b/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_DAQmx_counter.py @@ -3,7 +3,7 @@ from pymodaq.utils.data import DataWithAxes, DataToExport, DataSource from pymodaq.control_modules.viewer_utility_classes import DAQ_Viewer_base, comon_parameters, main from pymodaq.utils.parameter import Parameter - +import nidaqmx from pymodaq_plugins_daqmx.hardware.national_instruments.daqmx import DAQmx, \ Edge, ClockSettings, Counter, ClockCounter, TriggerSettings diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/_nidaqmx.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/_nidaqmx.py index e1f1ec6..eac34f0 100644 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/_nidaqmx.py +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/_nidaqmx.py @@ -2,19 +2,28 @@ from nidaqmx.constants import AcquisitionType,Edge import time import matplotlib.pyplot as plt +import time +import numpy as np task=nidaqmx.Task() -task.ci_channels.add_ci_count_edges_chan("Dev1/ctr0") +CI_channel = task.ci_channels.add_ci_count_edges_chan("Dev1/ctr0") + +print(task.ci_channels.channel_names) +nidaqmx.CtrTime(0.2,0.1) +a=0 +b=0 +data=[] task.start() -data=task.read(512) -#for i in range(512): -# mesure0 = task.read() -# data.append(task.read()-mesure0) -task.stop() +for i in range(512): + a=task.read() + data.append(a) + print(a) -print(data) + + +task.stop() task.close() plt.plot(data) plt.show() \ No newline at end of file diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/counter_test.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/counter_test.py deleted file mode 100644 index 11e474a..0000000 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/counter_test.py +++ /dev/null @@ -1,19 +0,0 @@ -import PyDAQmx -import ctypes -import numpy as np - - -counter_input = PyDAQmx.Task() -read = PyDAQmx.int32() -data = np.zeros((1000,),dtype=np.float64) - -#DAQmx Configure Code -counter_input.CreateCICountEdgesChan("Dev1/ctr0","",PyDAQmx.DAQmx_Val_Rising,0,PyDAQmx.DAQmx_Val_CountUp) - -#DAQmx Start Code -counter_input.StartTask() - -#DAQmx Read Code -counter_input.ReadCounterScalarU32(10.0,PyDAQmx.byref(data),None) - -print(read.value) \ No newline at end of file diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxNI.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmx-ni.py similarity index 83% rename from src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxNI.py rename to src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmx-ni.py index 7ce5fe1..e20cc87 100644 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxNI.py +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmx-ni.py @@ -1,4 +1,4 @@ -import PyDAQmx +import nidaqmx import ctypes from enum import IntEnum import numpy as np @@ -37,9 +37,9 @@ class DAQ_analog_types(IntEnum): **Attributes** **Type** =============== ========== """ - Voltage = PyDAQmx.DAQmx_Val_Voltage - Current = PyDAQmx.DAQmx_Val_Current - Thermocouple = PyDAQmx.DAQmx_Val_Temp_TC + Voltage = nidaqmx.constants.UsageTypeAI.VOLTAGE.value + Current = nidaqmx.constants.UsageTypeAI.CURRENT.value + Thermocouple = nidaqmx.constants.UsageTypeAI.TEMPERATURE_THERMOCOUPLE.value @classmethod def names(cls): @@ -58,14 +58,14 @@ class DAQ_thermocouples(IntEnum): **Attributes** **Type** =============== ========== """ - J = PyDAQmx.DAQmx_Val_J_Type_TC - K = PyDAQmx.DAQmx_Val_K_Type_TC - N = PyDAQmx.DAQmx_Val_N_Type_TC - R = PyDAQmx.DAQmx_Val_R_Type_TC - S = PyDAQmx.DAQmx_Val_S_Type_TC - T = PyDAQmx.DAQmx_Val_T_Type_TC - B = PyDAQmx.DAQmx_Val_B_Type_TC - E = PyDAQmx.DAQmx_Val_E_Type_TC + J = nidaqmx.constants.ThermocoupleType.J.value + K = nidaqmx.constants.ThermocoupleType.K.value + N = nidaqmx.constants.ThermocoupleType.N.value + R = nidaqmx.constants.ThermocoupleType.R.value + S = nidaqmx.constants.ThermocoupleType.S.value + T = nidaqmx.constants.ThermocoupleType.T.value + B = nidaqmx.constants.ThermocoupleType.B.value + E = nidaqmx.constants.ThermocoupleType.E.value @classmethod def names(cls): @@ -80,11 +80,11 @@ class DAQ_termination(IntEnum): **Attributes** **Type** =============== ========== """ - Auto = PyDAQmx.DAQmx_Val_Cfg_Default - RSE = PyDAQmx.DAQmx_Val_RSE - NRSE = PyDAQmx.DAQmx_Val_NRSE - Diff = PyDAQmx.DAQmx_Val_Diff - Pseudodiff = PyDAQmx.DAQmx_Val_PseudoDiff + Auto = nidaqmx.constants.TerminalConfiguration.DEFAULT.value + RSE = nidaqmx.constants.TerminalConfiguration.RSE.value + NRSE = nidaqmx.constants.TerminalConfiguration.NRSE.value + Diff = nidaqmx.constants.TerminalConfiguration.DIFF.value + Pseudodiff = nidaqmx.constants.TerminalConfiguration.PSEUDO_DIFF.value @classmethod def names(cls): @@ -94,8 +94,8 @@ def names(cls): class Edge(IntEnum): """ """ - Rising = PyDAQmx.DAQmx_Val_Rising - Falling = PyDAQmx.DAQmx_Val_Falling + Rising = nidaqmx.constants.Edge.RISING.value + Falling = nidaqmx.constants.Edge.FALLING.value @classmethod def names(cls): @@ -105,8 +105,10 @@ def names(cls): class ClockMode(IntEnum): """ """ - Finite = PyDAQmx.DAQmx_Val_Rising - Continuous = PyDAQmx.DAQmx_Val_Falling + #Finite = PyDAQmx.DAQmx_Val_Rising // These values were not correct? + #Continuous = PyDAQmx.DAQmx_Val_Falling + Finite = nidaqmx.constants.AcquisitionType.FINITE.value + Continuous = nidaqmx.constants.AcquisitionType.CONTINUOUS.value @classmethod def names(cls): @@ -302,8 +304,8 @@ def get_NIDAQ_devices(cls): list of devices as strings to be used in subsequent commands """ try: - string = try_string_buffer(PyDAQmx.DAQmxGetSysDevNames) - devices = string.split(', ') + #string = try_string_buffer(PyDAQmx.DAQmxGetSysDevNames) + devices = nidaqmx.system.System.local().devices.device_names#string.split(', ') if devices == ['']: devices = [] return devices @@ -341,19 +343,19 @@ def get_NIDAQ_channels(cls, devices=None, source_type=None): for device in devices: for source in source_type: if source == DAQ_NIDAQ_source['Analog_Input'].name: # analog input - string = try_string_buffer(PyDAQmx.DAQmxGetDevAIPhysicalChans, device) + string = nidaqmx.system.System.local().devices[device].ai_physical_chans.channel_names elif source == DAQ_NIDAQ_source['Counter'].name: # counter - string = try_string_buffer(PyDAQmx.DAQmxGetDevCIPhysicalChans, device) + string = nidaqmx.system.System.local().devices[device].ci_physical_chans.channel_names elif source == DAQ_NIDAQ_source['Analog_Output'].name: # analog output - string = try_string_buffer(PyDAQmx.DAQmxGetDevAOPhysicalChans, device) + string = nidaqmx.system.System.local().devices[device].ao_physical_chans.channel_names elif source == DAQ_NIDAQ_source['Digital_Output'].name: # digital output - string = try_string_buffer(PyDAQmx.DAQmxGetDevDOLines, device) - elif source == DAQ_NIDAQ_source['Digital_Input'].name: # digital output - string = try_string_buffer(PyDAQmx.DAQmxGetDevDILines, device) - elif source == DAQ_NIDAQ_source['Terminals'].name: # digital output - string = try_string_buffer(PyDAQmx.DAQmxGetDevTerminals, device) + string = nidaqmx.system.System.local().devices[device].do_lines.channel_names + elif source == DAQ_NIDAQ_source['Digital_Input'].name: # digital iutput + string = nidaqmx.system.System.local().devices[device].di_lines.channel_names + elif source == DAQ_NIDAQ_source['Terminals'].name: # terminals + string = nidaqmx.system.System.local().devices[device].terminals - channels = string.split(', ') + channels = string if channels != ['']: channels_tot.extend(channels) @@ -361,27 +363,27 @@ def get_NIDAQ_channels(cls, devices=None, source_type=None): @classmethod def getAOMaxRate(cls, device): - data = PyDAQmx.c_double() - PyDAQmx.DAQmxGetDevAOMaxRate(device, PyDAQmx.byref(data)) - return data.value + #data = PyDAQmx.c_double() + #PyDAQmx.DAQmxGetDevAOMaxRate(device, PyDAQmx.byref(data)) + return nidaqmx.system.device.Device(device).ao_max_rate @classmethod def getAIMaxRate(cls, device): - data = PyDAQmx.c_double() - PyDAQmx.DAQmxGetDevAIMaxSingleChanRate(device, PyDAQmx.byref(data)) - return data.value + #data = PyDAQmx.c_double() + #PyDAQmx.DAQmxGetDevAIMaxSingleChanRate(device, PyDAQmx.byref(data)) + return nidaqmx.system.device.Device(device).ai_max_single_chan_rate @classmethod def isAnalogTriggeringSupported(cls, device): - data = PyDAQmx.c_uint32() - PyDAQmx.DAQmxGetDevAnlgTrigSupported(device, PyDAQmx.byref(data)) - return bool(data.value) + #data = PyDAQmx.c_uint32() + #PyDAQmx.DAQmxGetDevAnlgTrigSupported(device, PyDAQmx.byref(data)) + return nidaqmx.system.device.Device(device).anlg_trig_supported @classmethod def isDigitalTriggeringSupported(cls, device): - data = PyDAQmx.c_uint32() - PyDAQmx.DAQmxGetDevDigTrigSupported(device, PyDAQmx.byref(data)) - return bool(data.value) + #data = PyDAQmx.c_uint32() + #PyDAQmx.DAQmxGetDevDigTrigSupported(device, PyDAQmx.byref(data)) + return nidaqmx.system.device.Device(device).dig_trig_supported @classmethod def getTriggeringSources(cls, devices=None): @@ -391,12 +393,12 @@ def getTriggeringSources(cls, devices=None): for device in devices: if cls.isDigitalTriggeringSupported(device): - string = try_string_buffer(PyDAQmx.DAQmxGetDevTerminals, device) - channels = [chan for chan in string.split(', ') if 'PFI' in chan] + string = nidaqmx.system.device.Device(device).terminals + channels = [chan for chan in string if 'PFI' in chan] if channels != ['']: sources.extend(channels) if cls.isAnalogTriggeringSupported(device): - channels = cls.get_NIDAQ_channels(devices=[device], source_type='Analog_Input') + channels = nidaqmx.system.device.Device(device).ai_physical_chans.channel_names if channels != ['']: sources.extend(channels) return sources @@ -405,51 +407,52 @@ def update_task(self, channels=[], clock_settings=ClockSettings(), trigger_setti try: if self._task is not None: - if isinstance(self._task, PyDAQmx.Task): + if isinstance(self._task, nidaqmx.Task): self._task.ClearTask() self._task = None self.c_callback = None - self._task = PyDAQmx.Task() + self._task = nidaqmx.Task() ## create all channels one task for one type of channels for channel in channels: if channel.source == 'Analog_Input': #analog input if channel.analog_type == "Voltage": - err_code = self._task.CreateAIVoltageChan(channel.name, "analog voltage task", + err_code = self._task.ai_channels.add_ai_voltage_chan(channel.name, "", DAQ_termination[channel.termination].value, channel.value_min, channel.value_max, - PyDAQmx.DAQmx_Val_Volts, None) + nidaqmx.constants.VoltageUnits.VOLTS, None) elif channel.analog_type == "Current": - err_code = self._task.CreateAICurrentChan(channel.name, "", + err_code = self._task.ai_channels.add_ai_current_chan(channel.name, "", DAQ_termination[channel.termination].value, channel.value_min, channel.value_max, - PyDAQmx.DAQmx_Val_Amps, PyDAQmx.DAQmx_Val_Internal, + nidaqmx.constants.CurrentUnits.AMPS, + nidaqmx.constants.CurrentShuntResistorLocation.INTERNAL, 0., None) elif channel.analog_type == "Thermocouple": - err_code = self._task.CreateAIThrmcplChan(channel.name, "", + err_code = self._task.ai_channels.add_ai_thrmcpl_chan(channel.name, "", channel.value_min, channel.value_max, - PyDAQmx.DAQmx_Val_DegC, + nidaqmx.constants.TemperatureUnits.DEG_C, DAQ_termination[channel.thermo_type].value, - PyDAQmx.DAQmx_Val_BuiltIn, 0., "") + nidaqmx.constants.CJCSource.BUILT_IN, 0., "") elif channel.source == 'Counter': #counter if channel.counter_type == "Edge Counter": - err_code = self._task.CreateCICountEdgesChan(channel.name, "", + err_code = self._task.ci_channels.add_ci_count_edges_chan(channel.name, "", Edge[channel.edge].value, 0, - PyDAQmx.DAQmx_Val_CountUp) + nidaqmx.constants.CountDirection.COUNT_UP) elif channel.counter_type == "Clock Output": - err_code = self._task.CreateCOPulseChanFreq(channel.name, "clock task", + err_code = self._task.co_channels.add_co_pulse_chan_freq(channel.name, "clock task", # units, Hertz in our case - PyDAQmx.DAQmx_Val_Hz, + nidaqmx.constants.FrequencyUnits.HZ, # idle state - PyDAQmx.DAQmx_Val_Low, + nidaqmx.constants.Level.LOW, # initial delay 0, # pulse frequency @@ -459,10 +462,10 @@ def update_task(self, channels=[], clock_settings=ClockSettings(), trigger_setti # equal to count_interval 0.5) elif channel.counter_type == "SemiPeriod Input": - err_code = self._task.CreateCISemiPeriodChan(channel.name, "counter task", + err_code = self._task.ci_channels.add_ci_semi_period_chan(channel.name, "counter task", 0, # expected min channel.value_max, # expected max - PyDAQmx.DAQmx_Val_Ticks, "") + nidaqmx.constants.TimeUnits.TICKS, "") if not not err_code: @@ -720,29 +723,30 @@ def getAIVoltageRange(cls, device='Dev1'): @classmethod def getAOVoltageRange(cls, device='Dev1'): - buff_size = 100 - ranges = ctypes.pointer((buff_size*ctypes.c_double)()) - ret = PyDAQmx.DAQmxGetDevAOVoltageRngs(device, ranges[0], buff_size) - if ret == 0: - return [tuple(ranges.contents[2*ind:2*(ind+1)]) for ind in range(int(buff_size/2-2)) - if np.abs(ranges.contents[2*ind]) > 1e-12] - return [(-10., 10.)] + #buff_size = 100 + #ranges = ctypes.pointer((buff_size*ctypes.c_double)()) + #ret = PyDAQmx.DAQmxGetDevAOVoltageRngs(device, ranges[0], buff_size) + ret = nidaqmx.system.System.local().devices[device].ao_voltage_rngs + #if ret == 0: + # return [tuple(ranges.contents[2*ind:2*(ind+1)]) for ind in range(int(buff_size/2-2)) + # if np.abs(ranges.contents[2*ind]) > 1e-12] + return #[(-10., 10.)] Why this format is needed def stop(self): if self._task is not None: - self._task.StopTask() + self._task.stop() def start(self): if self._task is not None: - self._task.StartTask() + self._task.start() def close(self): """ close the current task. """ if self._task is not None: - self._task.StopTask() - self._task.ClearTask() + self._task.stop() + self._task.close() self._task = None @classmethod @@ -750,17 +754,17 @@ def DAQmxGetErrorString(cls, error_code): if error_code is None: return '' else: - buffer = PyDAQmx.create_string_buffer(1024) - PyDAQmx.DAQmxGetErrorString(error_code, buffer, len(buffer)) - return buffer.value.decode() - + #buffer = PyDAQmx.create_string_buffer(1024) + #PyDAQmx.DAQmxGetErrorString(error_code, buffer, len(buffer)) + #return buffer.value.decode() + return nidaqmx.errors.check_for_error(error_code) def isTaskDone(self): - done = PyDAQmx.bool32(False) - self._task.GetTaskComplete(PyDAQmx.byref(done)) - return bool(done.value) - + #done = PyDAQmx.bool32(False) + #self._task.GetTaskComplete(PyDAQmx.byref(done)) + #return bool(done.value) + return task.is_task_done() def waitTaskDone(self, timeout=10.): - ret = self._task.WaitUntilTaskDone(timeout) + ret = self._task.wait_until_done(timeout) if ret != 0: logger.info(self.DAQmxGetErrorString(ret)) From a34f000d813b83663eeb57a641d7f043baeecbbc Mon Sep 17 00:00:00 2001 From: jerlfan Date: Fri, 8 Dec 2023 09:20:47 +0100 Subject: [PATCH 005/140] replacing PyDAQmx by nidaqmx in daqmx-ni --- .../hardware/national_instruments/daqmx-ni.py | 135 ++++-------------- 1 file changed, 31 insertions(+), 104 deletions(-) diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmx-ni.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmx-ni.py index e20cc87..ea8e5d5 100644 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmx-ni.py +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmx-ni.py @@ -230,39 +230,6 @@ def __init__(self, **kwargs): super().__init__(**kwargs) -def try_string_buffer(fun, *args): - """ - generic function to read string from a PyDAQmx function making sure the chosen buffer is large enough - Parameters - ---------- - fun: (PyDAQmx function pointer) e.g. PyDAQmx.DAQmxGetSysDevNames - kwargs - - Returns - ------- - - """ - buff_size = 1024 - while True: - buff = PyDAQmx.create_string_buffer(buff_size) - try: - if not not len(args): - fun(args[0], buff, buff_size) - else: - fun(buff, buff_size) - break - - except Exception as e: - if isinstance(e, PyDAQmx.DAQmxFunctions.DAQException): - if e.error == -200228: # BufferTooSmallForStringError - buff_size = 2 * buff_size - else: - raise e - else: - raise e - return buff.value.decode() - - class DAQmx: """Wrapper around the PyDAQmx package giving an easy to use object to instantiate channels and tasks""" def __init__(self): @@ -474,44 +441,46 @@ def update_task(self, channels=[], clock_settings=ClockSettings(), trigger_setti elif channel.source == 'Analog_Output': # Analog_Output if channel.analog_type == "Voltage": - err_code = self._task.CreateAOVoltageChan(channel.name, "", + err_code = self._task.ao_channels.add_ao_voltage_chan(channel.name, "", channel.value_min, channel.value_max, - PyDAQmx.DAQmx_Val_Volts, None) + nidaqmx.constants.VoltageUnits.VOLTS, None) if channel.analog_type == "Current": - err_code = self._task.CreateAOCurrentChan(channel.name, "", + err_code = self._task.ao_channels.add_ao_current_chan(channel.name, "", channel.value_min, channel.value_max, - PyDAQmx.DAQmx_Val_Amps, None) + nidaqmx.constants.VoltageUnits.VOLTS, None) elif channel.source == 'Digital_Output': #Digital_Output - err_code = self._task.CreateDOChan(channel.name, "", PyDAQmx.DAQmx_Val_ChanPerLine) + err_code = self._task.do_channels.add_do_chan(channel.name, "", + nidaqmx.constants.LineGrouping.CHAN_PER_LINE) if not not err_code: status = self.DAQmxGetErrorString(err_code) raise IOError(status) elif channel.source == 'Digital_Input': #Digital_Input - err_code = self._task.CreateDIChan(channel.name, "", PyDAQmx.DAQmx_Val_ChanPerLine) + err_code = self._task.di_channels.add_di_chan(channel.name, "", + nidaqmx.constants.LineGrouping.CHAN_PER_LINE) if not not err_code: status = self.DAQmxGetErrorString(err_code) raise IOError(status) ## configure the timing if clock_settings.repetition: - mode = PyDAQmx.DAQmx_Val_ContSamps + mode = nidaqmx.constants.AcquisitionType.CONTINUOUS else: - mode = PyDAQmx.DAQmx_Val_FiniteSamps + mode = nidaqmx.constants.AcquisitionType.FINITE if clock_settings.Nsamples > 1 and err_code == 0: if isinstance(clock_settings, ClockSettings): err_code =\ - self._task.CfgSampClkTiming(clock_settings.source, clock_settings.frequency, + self._task.timing.cfg_samp_clk_timing(clock_settings.source, clock_settings.frequency, Edge[clock_settings.edge].value, mode, clock_settings.Nsamples) elif isinstance(clock_settings, ChangeDetectionSettings): err_code =\ - self._task.CfgChangeDetectionTiming(clock_settings.rising_channel, + self._task.timing.cfg_change_detection_timing(clock_settings.rising_channel, clock_settings.falling_channel, mode, clock_settings.Nsamples) @@ -520,53 +489,6 @@ def update_task(self, channels=[], clock_settings=ClockSettings(), trigger_setti status = self.DAQmxGetErrorString(err_code) raise IOError(status) - # if channel.source == 'Analog_Input': # analog input - # if isinstance(clock_settings, ClockSettings): - # err_code =\ - # self._task.CfgSampClkTiming(clock_settings.source, clock_settings.frequency, - # Edge[clock_settings.edge].value, - # mode, - # clock_settings.Nsamples) - # elif isinstance(clock_settings, ChangeDetectionSettings): - # err_code =\ - # self._task.CfgChangeDetectionTiming(clock_settings.source, - # clock_settings.rising_channel, - # clock_settings.falling_channel, - # mode, - # clock_settings.Nsamples) - # - # if not not err_code: - # status = self.DAQmxGetErrorString(err_code) - # raise IOError(status) - # - # elif channel.source == 'Counter': # counter - # pass - # - # elif channel.source == 'Analog_Output': # Analog_Output - # if clock_settings.Nsamples > 1 and err_code == 0: - # - # if isinstance(clock_settings, ClockSettings): - # err_code = self._task.CfgSampClkTiming(clock_settings.source, - # clock_settings.frequency, - # Edge[clock_settings.edge].value, - # mode, - # clock_settings.Nsamples) - # elif isinstance(clock_settings, ChangeDetectionSettings): - # err_code = \ - # self._task.CfgChangeDetectionTiming(clock_settings.source, - # clock_settings.rising_channel, - # clock_settings.falling_channel, - # mode, - # clock_settings.Nsamples) - # - # if not not err_code: - # status = self.DAQmxGetErrorString(err_code) - # raise IOError(status) - # - # else: - # pass - - ##configure the triggering, except for counters if not trigger_settings.enable: if channel.source == 'Counter': pass @@ -576,12 +498,12 @@ def update_task(self, channels=[], clock_settings=ClockSettings(), trigger_setti raise IOError(self.DAQmxGetErrorString(err)) else: if 'PF' in trigger_settings.trig_source: - self._task.CfgDigEdgeStartTrig(trigger_settings.trig_source, + self._task.triggers.start_trigger.disable_start_trig(trigger_settings.trig_source, Edge[trigger_settings.edge].value) elif 'ai' in trigger_settings.trig_source: - self._task.CfgAnlgEdgeStartTrig(trigger_settings.trig_source, + self._task.triggers.start_trigger.cfg_anlg_edge_start_trig(trigger_settings.trig_source, Edge[trigger_settings.edge].value, - PyDAQmx.c_double(trigger_settings.level)) + trigger_settings.level) else: raise IOError('Unsupported Trigger source') @@ -591,15 +513,19 @@ def update_task(self, channels=[], clock_settings=ClockSettings(), trigger_setti def register_callback(self, callback, event='done', nsamples=1): if event == 'done': - self.c_callback = PyDAQmx.DAQmxDoneEventCallbackPtr(callback) - self._task.RegisterDoneEvent(0, self.c_callback, None) + #self.c_callback = PyDAQmx.DAQmxDoneEventCallbackPtr(callback) + #self._task.register_done_event(0, self.c_callback, None) + self._task.register_done_event(nidaqmx.constants.Signal.SAMPLE_COMPLETE, callback) + #NOT SURE HERE elif event == 'sample': - self.c_callback = PyDAQmx.DAQmxSignalEventCallbackPtr(callback) - self._task.RegisterSignalEvent(PyDAQmx.DAQmx_Val_SampleCompleteEvent, 0, self.c_callback, None) + #self.c_callback = PyDAQmx.DAQmxSignalEventCallbackPtr(callback) + #self._task.register_signal_event(PyDAQmx.DAQmx_Val_SampleCompleteEvent, 0, self.c_callback, None) + self._task.register_every_n_samples_acquired_into_buffer_event(1, + callback) elif event == 'Nsamples': - self.c_callback = PyDAQmx.DAQmxEveryNSamplesEventCallbackPtr(callback) - self._task.RegisterEveryNSamplesEvent(PyDAQmx.DAQmx_Val_Acquired_Into_Buffer, nsamples, - 0, self.c_callback, None) + #self.c_callback = PyDAQmx.DAQmxEveryNSamplesEventCallbackPtr(callback) + self._task.register_every_n_samples_acquired_into_buffer_event(nsamples, + callback) def get_last_write_index(self): if self.task is not None: @@ -713,9 +639,10 @@ def writeDigital(self, Nchannels, values, autostart=False): @classmethod def getAIVoltageRange(cls, device='Dev1'): - buff_size = 100 - ranges = ctypes.pointer((buff_size*ctypes.c_double)()) - ret = PyDAQmx.DAQmxGetDevAIVoltageRngs(device, ranges[0], buff_size) + #buff_size = 100 + #ranges = ctypes.pointer((buff_size*ctypes.c_double)()) + #ret = PyDAQmx.DAQmxGetDevAIVoltageRngs(device, ranges[0], buff_size) + ret = nidaqmx.system.System.local().devices[device].ai_voltage_rngs if ret == 0: return [tuple(ranges.contents[2*ind:2*(ind+1)]) for ind in range(int(buff_size/2-2)) if np.abs(ranges.contents[2*ind]) > 1e-12] @@ -730,7 +657,7 @@ def getAOVoltageRange(cls, device='Dev1'): #if ret == 0: # return [tuple(ranges.contents[2*ind:2*(ind+1)]) for ind in range(int(buff_size/2-2)) # if np.abs(ranges.contents[2*ind]) > 1e-12] - return #[(-10., 10.)] Why this format is needed + return [tuple(ret)] #[(-10., 10.)] Why this format is needed def stop(self): if self._task is not None: From feb7235fa2dc1d45ebbc598328b6d2b3d5662978 Mon Sep 17 00:00:00 2001 From: jerlfan Date: Thu, 14 Dec 2023 23:09:23 +0100 Subject: [PATCH 006/140] working version of wrapper daqmxni Wrapper daqmxni as been implemented from nidaqmx library. Simple counter works. Other channels must be tested. Must add a "timer" to the counter. --- plugin_info.toml | 2 +- .../daq_0Dviewer_DAQmx_PLcounter.py | 4 - .../plugins_0D/daq_0Dviewer_DAQmx_counter.py | 91 +-- .../hardware/national_instruments/_nidaqmx.py | 29 - .../counter_clock_nidaqmx.py | 42 ++ .../counter_simple_nidaqmx.py | 41 + .../hardware/national_instruments/daqmx-ni.py | 712 ------------------ .../hardware/national_instruments/daqmx.py | 5 +- .../hardware/national_instruments/daqmxni.py | 592 +++++++++++++++ 9 files changed, 718 insertions(+), 800 deletions(-) delete mode 100644 src/pymodaq_plugins_daqmx/hardware/national_instruments/_nidaqmx.py create mode 100644 src/pymodaq_plugins_daqmx/hardware/national_instruments/counter_clock_nidaqmx.py create mode 100644 src/pymodaq_plugins_daqmx/hardware/national_instruments/counter_simple_nidaqmx.py delete mode 100644 src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmx-ni.py create mode 100644 src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py diff --git a/plugin_info.toml b/plugin_info.toml index 166e6c0..946fe9e 100644 --- a/plugin_info.toml +++ b/plugin_info.toml @@ -11,7 +11,7 @@ license = 'MIT' [plugin-install] #packages required for your plugin: -packages-required = ['pydaqmx','nidaqmx', 'pymodaq>4.0.1'] +packages-required = ['pydaqmx','nidaqmx','pymodaq>4.0.1'] ## [features] # defines the plugin features contained into this plugin diff --git a/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_DAQmx_PLcounter.py b/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_DAQmx_PLcounter.py index b1afc2b..154df24 100755 --- a/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_DAQmx_PLcounter.py +++ b/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_DAQmx_PLcounter.py @@ -8,10 +8,6 @@ Edge, ClockSettings, Counter, ClockCounter, TriggerSettings from PyDAQmx import DAQmx_Val_ContSamps -# DAQmx_Val_DoNotInvertPolarity, DAQmxConnectTerms, -# DAQmx_Val_FiniteSamps, DAQmx_Val_CurrReadPos, \ -# DAQmx_Val_DoNotOverwriteUnreadSamps - class DAQ_0DViewer_DAQmx_PLcounter(DAQ_Viewer_base): """ diff --git a/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_DAQmx_counter.py b/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_DAQmx_counter.py index 5236f83..b66aa6a 100644 --- a/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_DAQmx_counter.py +++ b/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_DAQmx_counter.py @@ -4,11 +4,8 @@ from pymodaq.control_modules.viewer_utility_classes import DAQ_Viewer_base, comon_parameters, main from pymodaq.utils.parameter import Parameter import nidaqmx -from pymodaq_plugins_daqmx.hardware.national_instruments.daqmx import DAQmx, \ - Edge, ClockSettings, Counter, ClockCounter, TriggerSettings - -from PyDAQmx import DAQmx_Val_ContSamps +from pymodaq_plugins_daqmx.hardware.national_instruments.daqmxni import DAQmx, Edge, Counter class DAQ_0DViewer_DAQmx_counter(DAQ_Viewer_base): """ Plugin for a 0D PL counter, based on a NI card. @@ -18,10 +15,6 @@ class DAQ_0DViewer_DAQmx_counter(DAQ_Viewer_base): "type": "list", "limits": DAQmx.get_NIDAQ_channels(source_type="Counter")}, {"title": "Acq. Time (s):", "name": "acq_time", "type": "float", "value": 1., "default": 1.}, - {'title': 'Clock channel:', 'name': 'clock_channel', 'type': 'list', - 'limits': DAQmx.get_NIDAQ_channels(source_type='Counter')}, - {"title": "Clk Trig:", "name": "trig", - "type": "list", "limits": DAQmx.getTriggeringSources()} ] def ini_attributes(self): @@ -58,7 +51,7 @@ def ini_detector(self, controller=None): initialized: bool False if initialization failed otherwise True """ - self.controller = {"clock": DAQmx(), "counter": DAQmx()} + self.controller = DAQmx() #{"clock": DAQmx(), "counter": DAQmx()} try: self.update_tasks() initialized = True @@ -68,20 +61,15 @@ def ini_detector(self, controller=None): print(e) initialized = False info = "Error" - - #self.dte_signal_temp.emit(DataToExport(name='Counts', - # data=[DataWithAxes(name='Counts', data=[np.array([0])], - # source=DataSource['raw'], - # dim='Data0D', labels=['counts (Hz)'])])) - + return info, initialized - + def close(self): """Terminate the communication protocol""" pass #self.controller["clock"].close() #self.controller["counter"].close() - + def grab_data(self, Naverage=1, **kwargs): """Start a grab from the detector @@ -92,33 +80,34 @@ def grab_data(self, Naverage=1, **kwargs): kwargs: dict others optionals arguments """ - update = False # to decide if we do the initial set up or not - - - if 'live' in kwargs: - if kwargs['live'] != self.live: - update = True - self.live = kwargs['live'] - """ - if 'live' in kwargs: - if kwargs['live'] == self.live: - update = False # we are already live - self.live = kwargs['live'] - """ - - if update: - self.update_tasks() - self.controller["clock"].start() - - read_data = self.controller["counter"].readCounter(1, counting_time=self.counting_time) - data = read_data/self.counting_time # convert to cts/s + #update = False # to decide if we do the initial set up or not + + + #if 'live' in kwargs: + # if kwargs['live'] != self.live: + # update = True + # self.live = kwargs['live'] + # """ + #if 'live' in kwargs: + # if kwargs['live'] == self.live: + # update = False # we are already live + # self.live = kwargs['live'] + # """ + + #if update: + # self.update_tasks() + # self.controller["clock"].start() + + read_data = 32#self.controller.readCounter()#, counting_time=self.counting_time) + data = read_data #/self.counting_time # convert to cts/s self.dte_signal.emit(DataToExport(name='Counts', - data=[DataWithAxes(name='Counts', data=[data], + data=[DataWithAxes(name='Counts', data=[np.array([data])], source=DataSource['raw'], dim='Data0D', labels=['Counts (Hz)'])])) def stop(self): """Stop the current grab hardware wise if necessary""" + self.task.stop() self.close() self.emit_status(ThreadCommand('Update_Status', ['Acquisition stopped.'])) return '' @@ -126,24 +115,22 @@ def stop(self): def update_tasks(self): """Set up the counting tasks in the NI card.""" # Create channels - self.clock_channel = ClockCounter(1/self.settings.child("acq_time").value(), - name=self.settings.child("trig").value(), - source="Counter") + self.counter_channel = Counter(name=self.settings.child("counter_channel").value(), source="Counter", edge=Edge.names()[0]) - - self.controller["clock"].update_task(channels=[self.clock_channel], - clock_settings=ClockSettings(), - trigger_settings=TriggerSettings()) - self.controller["clock"].task.CfgImplicitTiming(DAQmx_Val_ContSamps, 1) - - self.controller["counter"].update_task(channels=[self.counter_channel], - clock_settings=ClockSettings(), - trigger_settings=TriggerSettings()) + # + #self.controller["clock"].update_task(channels=[self.clock_channel], + # clock_settings=ClockSettings(), + # trigger_settings=TriggerSettings()) + #self.controller["clock"].task.CfgImplicitTiming(DAQmx_Val_ContSamps, 1) + # + #self.controller["counter"].update_task(channels=[self.counter_channel], + # clock_settings=ClockSettings(), + # trigger_settings=TriggerSettings()) # connect the clock to the counter - self.controller["counter"].task.SetSampClkSrc("/" + self.clock_channel.name + "InternalOutput") + #self.controller["counter"].task.SetSampClkSrc("/" + self.clock_channel.name + "InternalOutput") + - if __name__ == '__main__': main(__file__) diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/_nidaqmx.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/_nidaqmx.py deleted file mode 100644 index eac34f0..0000000 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/_nidaqmx.py +++ /dev/null @@ -1,29 +0,0 @@ -import nidaqmx -from nidaqmx.constants import AcquisitionType,Edge -import time -import matplotlib.pyplot as plt -import time -import numpy as np - -task=nidaqmx.Task() -CI_channel = task.ci_channels.add_ci_count_edges_chan("Dev1/ctr0") - - -print(task.ci_channels.channel_names) -nidaqmx.CtrTime(0.2,0.1) - -a=0 -b=0 -data=[] -task.start() -for i in range(512): - a=task.read() - data.append(a) - print(a) - - - -task.stop() -task.close() -plt.plot(data) -plt.show() \ No newline at end of file diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/counter_clock_nidaqmx.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/counter_clock_nidaqmx.py new file mode 100644 index 0000000..2f5577f --- /dev/null +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/counter_clock_nidaqmx.py @@ -0,0 +1,42 @@ +import nidaqmx +import numpy as np +from nidaqmx.constants import Edge, AcquisitionType, FillMode, LineGrouping, CountDirection +from nidaqmx.stream_readers import CounterReader +from ctypes import c_uint32 + +def main(): + error = 0 + task_handle = nidaqmx.Task() + data = np.zeros(0, dtype=np.uint32) + read = np.uint32() + + try: + # DAQmx Configure Code + task_handle.ci_channels.add_ci_count_edges_chan("Dev1/ctr0", edge=Edge.RISING, initial_count=0, count_direction=CountDirection.COUNT_UP) + task_handle.timing.cfg_samp_clk_timing(source="/Dev1/Ctr1InternalOutput", rate=1000.0, active_edge=Edge.RISING, sample_mode=AcquisitionType.CONTINUOUS, samps_per_chan=1000) + + counter_reader=CounterReader(task_handle.in_stream) + # DAQmx Start Code + task_handle.start() + + print("En cours de lecture en continu. Appuyez sur Ctrl+C pour interrompre\n") + while True: + # DAQmx Read Code + #data=task_handle.read(number_of_samples_per_channel=1000, timeout=10.0) + #task_handle.in_stream.read_all_avail_samp + read=counter_reader.read_many_sample_uint32(data) + print("\rAcquis {} échantillons".format(read), end='', flush=True) + + except Exception as e: + print("\n") + if task_handle.is_task_done() == 0: + task_handle.stop() + task_handle.close() + print("Erreur DAQmx : {}".format(str(e))) + + finally: + print("\nFin du programme. Appuyez sur la touche Entrée pour quitter\n") + input() + +if __name__ == "__main__": + main() diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/counter_simple_nidaqmx.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/counter_simple_nidaqmx.py new file mode 100644 index 0000000..d6ceedc --- /dev/null +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/counter_simple_nidaqmx.py @@ -0,0 +1,41 @@ +import nidaqmx +import numpy as np +import time + +# Constantes équivalentes aux macros en C +DAQmx_Val_Rising = nidaqmx.constants.Edge.RISING +DAQmx_Val_CountUp = nidaqmx.constants.CountDirection.COUNT_UP + +def main(): + error = 0 + task_handle = nidaqmx.Task() + data = np.zeros(1, dtype=np.uint32) + + try: + # DAQmx Configure Code + task_handle.ci_channels.add_ci_count_edges_chan("Dev1/ctr0", edge=DAQmx_Val_Rising, initial_count=0, count_direction=DAQmx_Val_CountUp) + + # DAQmx Start Code + task_handle.start() + + print("En cours de lecture en continu. Appuyez sur Ctrl+C pour interrompre\n") + while True: + # DAQmx Read Code + data=task_handle.read(number_of_samples_per_channel=1) + + print("\rCompteur : {}".format(data[0]), end='', flush=True) + time.sleep(0.1) # Intervalle pour éviter une utilisation excessive du processeur + + except Exception as e: + print("\n") + if task_handle.is_task_done() == 0: + task_handle.stop() + task_handle.close() + print("Erreur DAQmx : {}".format(str(e))) + + finally: + print("\nFin du programme. Appuyez sur la touche Entrée pour quitter\n") + input() + +if __name__ == "__main__": + main() diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmx-ni.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmx-ni.py deleted file mode 100644 index ea8e5d5..0000000 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmx-ni.py +++ /dev/null @@ -1,712 +0,0 @@ -import nidaqmx -import ctypes -from enum import IntEnum -import numpy as np -from pymodaq.utils.logger import set_logger, get_module_name - -logger = set_logger(get_module_name(__file__)) - - -class DAQ_NIDAQ_source(IntEnum): - """ - Enum class of NIDAQ_source - - =============== ========== - **Attributes** **Type** - *Analog_Input* int - *Counter* int - =============== ========== - """ - Analog_Input = 0 - Counter = 1 - Analog_Output = 2 - Digital_Output = 3 - Digital_Input = 4 - Terminals = 5 - - @classmethod - def names(cls): - return [name for name, member in cls.__members__.items()] - - -class DAQ_analog_types(IntEnum): - """ - Enum class of Ai types - - =============== ========== - **Attributes** **Type** - =============== ========== - """ - Voltage = nidaqmx.constants.UsageTypeAI.VOLTAGE.value - Current = nidaqmx.constants.UsageTypeAI.CURRENT.value - Thermocouple = nidaqmx.constants.UsageTypeAI.TEMPERATURE_THERMOCOUPLE.value - - @classmethod - def names(cls): - return [name for name, member in cls.__members__.items()] - - @classmethod - def values(cls): - return [cls[name].value for name, member in cls.__members__.items()] - - -class DAQ_thermocouples(IntEnum): - """ - Enum class of thermocouples type - - =============== ========== - **Attributes** **Type** - =============== ========== - """ - J = nidaqmx.constants.ThermocoupleType.J.value - K = nidaqmx.constants.ThermocoupleType.K.value - N = nidaqmx.constants.ThermocoupleType.N.value - R = nidaqmx.constants.ThermocoupleType.R.value - S = nidaqmx.constants.ThermocoupleType.S.value - T = nidaqmx.constants.ThermocoupleType.T.value - B = nidaqmx.constants.ThermocoupleType.B.value - E = nidaqmx.constants.ThermocoupleType.E.value - - @classmethod - def names(cls): - return [name for name, member in cls.__members__.items()] - - -class DAQ_termination(IntEnum): - """ - Enum class of thermocouples type - - =============== ========== - **Attributes** **Type** - =============== ========== - """ - Auto = nidaqmx.constants.TerminalConfiguration.DEFAULT.value - RSE = nidaqmx.constants.TerminalConfiguration.RSE.value - NRSE = nidaqmx.constants.TerminalConfiguration.NRSE.value - Diff = nidaqmx.constants.TerminalConfiguration.DIFF.value - Pseudodiff = nidaqmx.constants.TerminalConfiguration.PSEUDO_DIFF.value - - @classmethod - def names(cls): - return [name for name, member in cls.__members__.items()] - - -class Edge(IntEnum): - """ - """ - Rising = nidaqmx.constants.Edge.RISING.value - Falling = nidaqmx.constants.Edge.FALLING.value - - @classmethod - def names(cls): - return [name for name, member in cls.__members__.items()] - - -class ClockMode(IntEnum): - """ - """ - #Finite = PyDAQmx.DAQmx_Val_Rising // These values were not correct? - #Continuous = PyDAQmx.DAQmx_Val_Falling - Finite = nidaqmx.constants.AcquisitionType.FINITE.value - Continuous = nidaqmx.constants.AcquisitionType.CONTINUOUS.value - - @classmethod - def names(cls): - return [name for name, member in cls.__members__.items()] - - -class ClockSettingsBase: - def __init__(self, Nsamples=1000, repetition=False): - - self.Nsamples = Nsamples - self.repetition = repetition - - -class ClockSettings(ClockSettingsBase): - def __init__(self, source=None, frequency=1000, Nsamples=1000, edge=Edge.names()[0], repetition=False): - super().__init__(Nsamples, repetition) - self.source = source - assert edge in Edge.names() - self.frequency = frequency - self.edge = edge - - -class ChangeDetectionSettings(ClockSettingsBase): - def __init__(self, Nsamples=1000, rising_channel='', falling_channel='', - repetition=False): - super().__init__(Nsamples, repetition) - self.rising_channel = rising_channel - self.falling_channel = falling_channel - - -class TriggerSettings: - def __init__(self, trig_source='', enable=False, edge=Edge.names()[0], level=0.1): - assert edge in Edge.names() - self.trig_source = trig_source - self.enable = enable - self.edge = edge - self.level = level - - -class Channel: - def __init__(self, name='', source=DAQ_NIDAQ_source.names()[0]): - """ - Parameters - ---------- - - """ - self.name = name - assert source in DAQ_NIDAQ_source.names() - self.source = source - - -class AChannel(Channel): - def __init__(self, analog_type=DAQ_analog_types.names()[0], value_min=-10., value_max=+10., **kwargs): - """ - Parameters - ---------- - min: (float) minimum value for the configured input channel (could be voltage, amps, temperature...) - max: (float) maximum value for the configured input channel - """ - super().__init__(**kwargs) - self.value_min = value_min - self.value_max = value_max - self.analog_type = analog_type - - -class AIChannel(AChannel): - def __init__(self, termination=DAQ_termination.names()[0], **kwargs): - super().__init__(**kwargs) - assert termination in DAQ_termination.names() - self.termination = termination - - -class AIThermoChannel(AIChannel): - def __init__(self, thermo_type=DAQ_thermocouples.names()[0], **kwargs): - super().__init__(**kwargs) - assert thermo_type in DAQ_thermocouples.names() - self.thermo_type = thermo_type - - -class AOChannel(AChannel): - def __init__(self, **kwargs): - super().__init__(**kwargs) - - -class Counter(Channel): - def __init__(self, edge=Edge.names()[0], **kwargs): - assert edge in Edge.names() - super().__init__(**kwargs) - self.edge = edge - self.counter_type = "Edge Counter" - - -class ClockCounter(Counter): - def __init__(self, clock_frequency, **kwargs): - super().__init__(**kwargs) - self.clock_frequency = clock_frequency - self.counter_type = "Clock Output" - - -class SemiPeriodCounter(Counter): - def __init__(self, value_max, **kwargs): - super().__init__(**kwargs) - self.value_max = value_max - self.counter_type = "SemiPeriod Input" - - -class DigitalChannel(Channel): - def __init__(self, **kwargs): - super().__init__(**kwargs) - - -class DOChannel(DigitalChannel): - def __init__(self, **kwargs): - super().__init__(**kwargs) - - -class DIChannel(DigitalChannel): - def __init__(self, **kwargs): - super().__init__(**kwargs) - - -class DAQmx: - """Wrapper around the PyDAQmx package giving an easy to use object to instantiate channels and tasks""" - def __init__(self): - self.devices = [] - self.channels = [] - self._device = None - self._task = None - self.update_NIDAQ_devices() - self.update_NIDAQ_channels() - self.c_callback = None - self.callback_data = None - self.is_scalar = True - self.write_buffer = np.array([0.]) - - @property - def task(self): - return self._task - - @property - def device(self): - return self._device - - @device.setter - def device(self, device): - if device not in self.devices: - raise IOError(f'your device: {device} is not known or connected') - self._device = device - - def update_NIDAQ_devices(self): - self.devices = self.get_NIDAQ_devices() - - @classmethod - def get_NIDAQ_devices(cls): - """Get list of NI connected devices - - Returns - ------- - list - list of devices as strings to be used in subsequent commands - """ - try: - #string = try_string_buffer(PyDAQmx.DAQmxGetSysDevNames) - devices = nidaqmx.system.System.local().devices.device_names#string.split(', ') - if devices == ['']: - devices = [] - return devices - except: - return [] - - def update_NIDAQ_channels(self, source_type=None): - self.channels = self.get_NIDAQ_channels(self.devices, source_type=source_type) - - @classmethod - def get_NIDAQ_channels(cls, devices=None, source_type=None): - """Get the list of available channels for all NiDAq connected devices - - Parameters - ---------- - devices: list - list of strings, each one being a connected device - source_type: str - One of the entries of DAQ_NIDAQ_source enum - - Returns - ------- - List of str containing device and channel names - - """ - if devices is None: - devices = cls.get_NIDAQ_devices() - - if source_type is None: - source_type = DAQ_NIDAQ_source.names() - if not isinstance(source_type, list): - source_type = [source_type] - channels_tot = [] - if not not devices: - for device in devices: - for source in source_type: - if source == DAQ_NIDAQ_source['Analog_Input'].name: # analog input - string = nidaqmx.system.System.local().devices[device].ai_physical_chans.channel_names - elif source == DAQ_NIDAQ_source['Counter'].name: # counter - string = nidaqmx.system.System.local().devices[device].ci_physical_chans.channel_names - elif source == DAQ_NIDAQ_source['Analog_Output'].name: # analog output - string = nidaqmx.system.System.local().devices[device].ao_physical_chans.channel_names - elif source == DAQ_NIDAQ_source['Digital_Output'].name: # digital output - string = nidaqmx.system.System.local().devices[device].do_lines.channel_names - elif source == DAQ_NIDAQ_source['Digital_Input'].name: # digital iutput - string = nidaqmx.system.System.local().devices[device].di_lines.channel_names - elif source == DAQ_NIDAQ_source['Terminals'].name: # terminals - string = nidaqmx.system.System.local().devices[device].terminals - - channels = string - if channels != ['']: - channels_tot.extend(channels) - - return channels_tot - - @classmethod - def getAOMaxRate(cls, device): - #data = PyDAQmx.c_double() - #PyDAQmx.DAQmxGetDevAOMaxRate(device, PyDAQmx.byref(data)) - return nidaqmx.system.device.Device(device).ao_max_rate - - @classmethod - def getAIMaxRate(cls, device): - #data = PyDAQmx.c_double() - #PyDAQmx.DAQmxGetDevAIMaxSingleChanRate(device, PyDAQmx.byref(data)) - return nidaqmx.system.device.Device(device).ai_max_single_chan_rate - - @classmethod - def isAnalogTriggeringSupported(cls, device): - #data = PyDAQmx.c_uint32() - #PyDAQmx.DAQmxGetDevAnlgTrigSupported(device, PyDAQmx.byref(data)) - return nidaqmx.system.device.Device(device).anlg_trig_supported - - @classmethod - def isDigitalTriggeringSupported(cls, device): - #data = PyDAQmx.c_uint32() - #PyDAQmx.DAQmxGetDevDigTrigSupported(device, PyDAQmx.byref(data)) - return nidaqmx.system.device.Device(device).dig_trig_supported - - @classmethod - def getTriggeringSources(cls, devices=None): - sources = [] - if devices is None: - devices = cls.get_NIDAQ_devices() - - for device in devices: - if cls.isDigitalTriggeringSupported(device): - string = nidaqmx.system.device.Device(device).terminals - channels = [chan for chan in string if 'PFI' in chan] - if channels != ['']: - sources.extend(channels) - if cls.isAnalogTriggeringSupported(device): - channels = nidaqmx.system.device.Device(device).ai_physical_chans.channel_names - if channels != ['']: - sources.extend(channels) - return sources - - def update_task(self, channels=[], clock_settings=ClockSettings(), trigger_settings=TriggerSettings()): - - try: - if self._task is not None: - if isinstance(self._task, nidaqmx.Task): - self._task.ClearTask() - - self._task = None - self.c_callback = None - - self._task = nidaqmx.Task() - - ## create all channels one task for one type of channels - for channel in channels: - if channel.source == 'Analog_Input': #analog input - if channel.analog_type == "Voltage": - err_code = self._task.ai_channels.add_ai_voltage_chan(channel.name, "", - DAQ_termination[channel.termination].value, - channel.value_min, - channel.value_max, - nidaqmx.constants.VoltageUnits.VOLTS, None) - - elif channel.analog_type == "Current": - err_code = self._task.ai_channels.add_ai_current_chan(channel.name, "", - DAQ_termination[channel.termination].value, - channel.value_min, - channel.value_max, - nidaqmx.constants.CurrentUnits.AMPS, - nidaqmx.constants.CurrentShuntResistorLocation.INTERNAL, - 0., None) - - elif channel.analog_type == "Thermocouple": - err_code = self._task.ai_channels.add_ai_thrmcpl_chan(channel.name, "", - channel.value_min, - channel.value_max, - nidaqmx.constants.TemperatureUnits.DEG_C, - DAQ_termination[channel.thermo_type].value, - nidaqmx.constants.CJCSource.BUILT_IN, 0., "") - - elif channel.source == 'Counter': #counter - if channel.counter_type == "Edge Counter": - err_code = self._task.ci_channels.add_ci_count_edges_chan(channel.name, "", - Edge[channel.edge].value, 0, - nidaqmx.constants.CountDirection.COUNT_UP) - elif channel.counter_type == "Clock Output": - err_code = self._task.co_channels.add_co_pulse_chan_freq(channel.name, "clock task", - # units, Hertz in our case - nidaqmx.constants.FrequencyUnits.HZ, - # idle state - nidaqmx.constants.Level.LOW, - # initial delay - 0, - # pulse frequency - channel.clock_frequency, - # duty cycle of pulses, 0.5 such that - # high and low duration are both - # equal to count_interval - 0.5) - elif channel.counter_type == "SemiPeriod Input": - err_code = self._task.ci_channels.add_ci_semi_period_chan(channel.name, "counter task", - 0, # expected min - channel.value_max, # expected max - nidaqmx.constants.TimeUnits.TICKS, "") - - - if not not err_code: - status = self.DAQmxGetErrorString(err_code) - raise IOError(status) - - elif channel.source == 'Analog_Output': # Analog_Output - if channel.analog_type == "Voltage": - err_code = self._task.ao_channels.add_ao_voltage_chan(channel.name, "", - channel.value_min, - channel.value_max, - nidaqmx.constants.VoltageUnits.VOLTS, None) - - if channel.analog_type == "Current": - err_code = self._task.ao_channels.add_ao_current_chan(channel.name, "", - channel.value_min, - channel.value_max, - nidaqmx.constants.VoltageUnits.VOLTS, None) - - elif channel.source == 'Digital_Output': #Digital_Output - err_code = self._task.do_channels.add_do_chan(channel.name, "", - nidaqmx.constants.LineGrouping.CHAN_PER_LINE) - if not not err_code: - status = self.DAQmxGetErrorString(err_code) - raise IOError(status) - - elif channel.source == 'Digital_Input': #Digital_Input - err_code = self._task.di_channels.add_di_chan(channel.name, "", - nidaqmx.constants.LineGrouping.CHAN_PER_LINE) - if not not err_code: - status = self.DAQmxGetErrorString(err_code) - raise IOError(status) - - ## configure the timing - if clock_settings.repetition: - mode = nidaqmx.constants.AcquisitionType.CONTINUOUS - else: - mode = nidaqmx.constants.AcquisitionType.FINITE - if clock_settings.Nsamples > 1 and err_code == 0: - if isinstance(clock_settings, ClockSettings): - err_code =\ - self._task.timing.cfg_samp_clk_timing(clock_settings.source, clock_settings.frequency, - Edge[clock_settings.edge].value, - mode, - clock_settings.Nsamples) - elif isinstance(clock_settings, ChangeDetectionSettings): - err_code =\ - self._task.timing.cfg_change_detection_timing(clock_settings.rising_channel, - clock_settings.falling_channel, - mode, - clock_settings.Nsamples) - - if not not err_code: - status = self.DAQmxGetErrorString(err_code) - raise IOError(status) - - if not trigger_settings.enable: - if channel.source == 'Counter': - pass - else: - err = self._task.DisableStartTrig() - if err != 0: - raise IOError(self.DAQmxGetErrorString(err)) - else: - if 'PF' in trigger_settings.trig_source: - self._task.triggers.start_trigger.disable_start_trig(trigger_settings.trig_source, - Edge[trigger_settings.edge].value) - elif 'ai' in trigger_settings.trig_source: - self._task.triggers.start_trigger.cfg_anlg_edge_start_trig(trigger_settings.trig_source, - Edge[trigger_settings.edge].value, - trigger_settings.level) - else: - raise IOError('Unsupported Trigger source') - - except Exception as e: - print(e) - - def register_callback(self, callback, event='done', nsamples=1): - - if event == 'done': - #self.c_callback = PyDAQmx.DAQmxDoneEventCallbackPtr(callback) - #self._task.register_done_event(0, self.c_callback, None) - self._task.register_done_event(nidaqmx.constants.Signal.SAMPLE_COMPLETE, callback) - #NOT SURE HERE - elif event == 'sample': - #self.c_callback = PyDAQmx.DAQmxSignalEventCallbackPtr(callback) - #self._task.register_signal_event(PyDAQmx.DAQmx_Val_SampleCompleteEvent, 0, self.c_callback, None) - self._task.register_every_n_samples_acquired_into_buffer_event(1, - callback) - elif event == 'Nsamples': - #self.c_callback = PyDAQmx.DAQmxEveryNSamplesEventCallbackPtr(callback) - self._task.register_every_n_samples_acquired_into_buffer_event(nsamples, - callback) - - def get_last_write_index(self): - if self.task is not None: - index_buffer = PyDAQmx.c_uint64() - ret = self._task.GetWriteCurrWritePos(PyDAQmx.byref(index_buffer)) - if not not ret: - raise IOError(self.DAQmxGetErrorString(ret)) - else: - return index_buffer.value - else: - return -1 - - def get_last_write(self): - if self.is_scalar: - return self.write_buffer[-1] - else: - index = self.get_last_write_index() - if index != -1: - return self.write_buffer[index % len(self.write_buffer)] - - else: - return 0. - - def writeAnalog(self, Nsamples, Nchannels, values, autostart=False): - """ - Write Nsamples on N analog output channels - Parameters - ---------- - Nsamples: (int) numver of samples to write on each channel - Nchannels: (int) number of AO channels defined in the task - values: (ndarray) 2D array (or flattened array) of size Nsamples * Nchannels - - Returns - ------- - - """ - if np.prod(values.shape) != Nsamples * Nchannels: - raise ValueError(f'The shape of analog outputs values is incorrect, should be {Nsamples} x {Nchannels}') - - if len(values.shape) != 1: - values = values.reshape((Nchannels * Nsamples)) - self.write_buffer = values - - timeout = -1 - if values.size == 1: - self._task.WriteAnalogScalarF64(autostart, timeout, values[0], None) - self.is_scalar = True - - else: - self.is_scalar = False - read = PyDAQmx.int32() - self._task.WriteAnalogF64(Nsamples, autostart, timeout, PyDAQmx.DAQmx_Val_GroupByChannel, values, - PyDAQmx.byref(read), None) - if read.value != Nsamples: - raise IOError(f'Insufficient number of samples have been written:{read.value}/{Nsamples}') - - def readAnalog(self, Nchannels, clock_settings): - read = PyDAQmx.int32() - N = clock_settings.Nsamples - data = np.zeros(N * Nchannels, dtype=np.float64) - timeout = N * Nchannels * 1 / clock_settings.frequency * 2 # set to twice the time it should take to acquire the data - - self._task.ReadAnalogF64(N, timeout, PyDAQmx.DAQmx_Val_GroupByChannel, data, len(data), - PyDAQmx.byref(read), None) - if read.value == N: - return data - else: - raise IOError(f'Insufficient number of samples have been read:{read.value}/{N}') - - def readCounter(self, Nchannels, counting_time=10., read_function="Ex"): - - data_counter = np.zeros(Nchannels, dtype='uint32') - read = PyDAQmx.int32() - if read_function == "Ex": - self._task.ReadCounterU32Ex(PyDAQmx.DAQmx_Val_Auto, 2*counting_time, - PyDAQmx.DAQmx_Val_GroupByChannel, - data_counter, - Nchannels, PyDAQmx.byref(read), None) - else: - self._task.ReadCounterU32(PyDAQmx.DAQmx_Val_Auto, 2*counting_time, - data_counter, Nchannels, PyDAQmx.byref(read), None) - - self._task.StopTask() - - if read.value == Nchannels: - return data_counter - else: - raise IOError(f'Insufficient number of samples have been read:{read}/{Nchannels}') - - def readDigital(self, Nchannels): - read = PyDAQmx.int32() - bytes_sample = PyDAQmx.int32() - data = np.zeros(Nchannels, dtype='uint8') - self._task.ReadDigitalLines(Nchannels, 0, PyDAQmx.DAQmx_Val_GroupByChannel, - data, Nchannels, PyDAQmx.byref(read), PyDAQmx.byref(bytes_sample), None); - - if read.value == Nchannels: - return data - else: - raise IOError(f'Insufficient number of samples have been read:{read}/{Nchannels}') - - def writeDigital(self, Nchannels, values, autostart=False): - if np.prod(values.shape) != Nchannels: - raise ValueError(f'The shape of digital outputs values is incorrect, should be {Nchannels}') - values.astype(np.uint8) - written = PyDAQmx.int32() - self._task.WriteDigitalLines(Nchannels, autostart, 0, PyDAQmx.DAQmx_Val_GroupByChannel, - values, PyDAQmx.byref(written), None) - if written.value != Nchannels: - raise IOError(f'Insufficient number of samples have been written:{written}/{Nchannels}') - - @classmethod - def getAIVoltageRange(cls, device='Dev1'): - #buff_size = 100 - #ranges = ctypes.pointer((buff_size*ctypes.c_double)()) - #ret = PyDAQmx.DAQmxGetDevAIVoltageRngs(device, ranges[0], buff_size) - ret = nidaqmx.system.System.local().devices[device].ai_voltage_rngs - if ret == 0: - return [tuple(ranges.contents[2*ind:2*(ind+1)]) for ind in range(int(buff_size/2-2)) - if np.abs(ranges.contents[2*ind]) > 1e-12] - return [(-10., 10.)] - - @classmethod - def getAOVoltageRange(cls, device='Dev1'): - #buff_size = 100 - #ranges = ctypes.pointer((buff_size*ctypes.c_double)()) - #ret = PyDAQmx.DAQmxGetDevAOVoltageRngs(device, ranges[0], buff_size) - ret = nidaqmx.system.System.local().devices[device].ao_voltage_rngs - #if ret == 0: - # return [tuple(ranges.contents[2*ind:2*(ind+1)]) for ind in range(int(buff_size/2-2)) - # if np.abs(ranges.contents[2*ind]) > 1e-12] - return [tuple(ret)] #[(-10., 10.)] Why this format is needed - - def stop(self): - if self._task is not None: - self._task.stop() - - def start(self): - if self._task is not None: - self._task.start() - - def close(self): - """ - close the current task. - """ - if self._task is not None: - self._task.stop() - self._task.close() - self._task = None - - @classmethod - def DAQmxGetErrorString(cls, error_code): - if error_code is None: - return '' - else: - #buffer = PyDAQmx.create_string_buffer(1024) - #PyDAQmx.DAQmxGetErrorString(error_code, buffer, len(buffer)) - #return buffer.value.decode() - return nidaqmx.errors.check_for_error(error_code) - def isTaskDone(self): - #done = PyDAQmx.bool32(False) - #self._task.GetTaskComplete(PyDAQmx.byref(done)) - #return bool(done.value) - return task.is_task_done() - def waitTaskDone(self, timeout=10.): - ret = self._task.wait_until_done(timeout) - if ret != 0: - logger.info(self.DAQmxGetErrorString(ret)) - - def refresh_hardware(self): - """ - Refresh the NIDAQ hardware from settings values. - - See Also - -------- - update_NIDAQ_devices, update_NIDAQ_channels - """ - devices = self.update_NIDAQ_devices() - self.update_NIDAQ_channels(devices) - - -if __name__ == '__main__': - print(DAQmx.get_NIDAQ_channels()) - pass diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmx.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmx.py index 7ce5fe1..81c972a 100644 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmx.py +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmx.py @@ -105,8 +105,9 @@ def names(cls): class ClockMode(IntEnum): """ """ - Finite = PyDAQmx.DAQmx_Val_Rising - Continuous = PyDAQmx.DAQmx_Val_Falling + Finite = PyDAQmx.DAQmx_Val_FiniteSamps + Continuous = PyDAQmx.DAQmx_Val_ContSamps + @classmethod def names(cls): diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py new file mode 100644 index 0000000..5d1d7cb --- /dev/null +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py @@ -0,0 +1,592 @@ +import time + + +from enum import IntEnum +import numpy as np +from pymodaq.utils.logger import set_logger, get_module_name +# from threading import Timer +import nidaqmx +from nidaqmx.constants import * +from nidaqmx.system.device import Device +from nidaqmx.errors import DaqError, DAQmxErrors + + +logger = set_logger(get_module_name(__file__)) + + +class DAQ_NIDAQ_source(IntEnum): + """ + Enum class of NIDAQ_source + + =============== ========== + **Attributes** **Type** + *Analog_Input* int + *Counter* int + =============== ========== + """ + Analog_Input = 0 + Counter = 1 + Analog_Output = 2 + Digital_Output = 3 + Digital_Input = 4 + Terminals = 5 + + @classmethod + def names(cls): + return [name for name, member in cls.__members__.items()] + + +class DAQ_analog_types(IntEnum): + """ + Enum class of Ai types + + =============== ========== + **Attributes** **Type** + =============== ========== + """ + Voltage = UsageTypeAI.VOLTAGE.value + Current = UsageTypeAI.CURRENT.value + Thermocouple = UsageTypeAI.TEMPERATURE_THERMOCOUPLE.value + + @classmethod + def names(cls): + return [name for name, member in cls.__members__.items()] + + @classmethod + def values(cls): + return [cls[name].value for name, member in cls.__members__.items()] + + +class DAQ_thermocouples(IntEnum): + """ + Enum class of thermocouples type + + =============== ========== + **Attributes** **Type** + =============== ========== + """ + J = ThermocoupleType.J.value + K = ThermocoupleType.K.value + N = ThermocoupleType.N.value + R = ThermocoupleType.R.value + S = ThermocoupleType.S.value + T = ThermocoupleType.T.value + B = ThermocoupleType.B.value + E = ThermocoupleType.E.value + + @classmethod + def names(cls): + return [name for name, member in cls.__members__.items()] + + +class DAQ_termination(IntEnum): + """ + Enum class of thermocouples type + + =============== ========== + **Attributes** **Type** + =============== ========== + """ + Auto = TerminalConfiguration.DEFAULT.value + RSE = TerminalConfiguration.RSE.value + NRSE = TerminalConfiguration.NRSE.value + Diff = TerminalConfiguration.DIFF.value + Pseudodiff = TerminalConfiguration.PSEUDO_DIFF.value + + @classmethod + def names(cls): + return [name for name, member in cls.__members__.items()] + + +class Edge(IntEnum): + """ + """ + Rising = Edge.RISING.value + Falling = Edge.FALLING.value + + @classmethod + def names(cls): + return [name for name, member in cls.__members__.items()] + + +class ClockMode(IntEnum): + """ + """ + Finite = AcquisitionType.FINITE.value + Continuous = AcquisitionType.CONTINUOUS.value + + @classmethod + def names(cls): + return [name for name, member in cls.__members__.items()] + + +class ClockSettingsBase: + def __init__(self, Nsamples=1000, repetition=False): + + self.Nsamples = Nsamples + self.repetition = repetition + + +class ClockSettings(ClockSettingsBase): + def __init__(self, source=None, frequency=1000, Nsamples=1000, edge=Edge.names()[0], repetition=False): + super().__init__(Nsamples, repetition) + self.source = source + assert edge in Edge.names() + self.frequency = frequency + self.edge = edge + + +class ChangeDetectionSettings(ClockSettingsBase): + def __init__(self, Nsamples=1000, rising_channel='', falling_channel='', + repetition=False): + super().__init__(Nsamples, repetition) + self.rising_channel = rising_channel + self.falling_channel = falling_channel + + +class TriggerSettings: + def __init__(self, trig_source='', enable=False, edge=Edge.names()[0], level=0.1): + assert edge in Edge.names() + self.trig_source = trig_source + self.enable = enable + self.edge = edge + self.level = level + + +class Channel: + def __init__(self, name='', source=DAQ_NIDAQ_source.names()[0]): + """ + Parameters + ---------- + + """ + self.name = name + assert source in DAQ_NIDAQ_source.names() + self.source = source + + +class AChannel(Channel): + def __init__(self, analog_type=DAQ_analog_types.names()[0], value_min=-10., value_max=+10., **kwargs): + """ + Parameters + ---------- + min: (float) minimum value for the configured input channel (could be voltage, amps, temperature...) + max: (float) maximum value for the configured input channel + """ + super().__init__(**kwargs) + self.value_min = value_min + self.value_max = value_max + self.analog_type = analog_type + + +class AIChannel(AChannel): + def __init__(self, termination=DAQ_termination.names()[0], **kwargs): + super().__init__(**kwargs) + assert termination in DAQ_termination.names() + self.termination = termination + + +class AIThermoChannel(AIChannel): + def __init__(self, thermo_type=DAQ_thermocouples.names()[0], **kwargs): + super().__init__(**kwargs) + assert thermo_type in DAQ_thermocouples.names() + self.thermo_type = thermo_type + + +class Counter(Channel): + def __init__(self, edge=Edge.names()[0], **kwargs): + assert edge in Edge.names() + super().__init__(**kwargs) + self.edge = edge + self.counter_type = "Edge Counter" + + +class ClockCounter(Counter): + def __init__(self, clock_frequency=100, **kwargs): + super().__init__(**kwargs) + self.clock_frequency = clock_frequency + self.counter_type = "Clock Output" + + +class DAQmx: + """Wrapper around the PyDAQmx package giving an easy to use object to instantiate channels and tasks""" + def __init__(self): + self.devices = [] + self.channels = [] + self._device = None + self._task = None + self.update_NIDAQ_devices() + self.update_NIDAQ_channels() + self.c_callback = None # A qui servent ces callback ?? + self.callback_data = None + self.is_scalar = True + self.write_buffer = np.array([0.]) # ou est utilisé ce buffer?? + + @property + def task(self): + return self._task + + @property + def device(self): + return self._device + + @device.setter + def device(self, device): + if device not in self.devices: + raise IOError(f'your device: {device} is not known or connected') + self._device = device + + def update_NIDAQ_devices(self): + self.devices = self.get_NIDAQ_devices() + + @classmethod + def get_NIDAQ_devices(cls): + """Get list of NI connected devices + + Returns + ------- + list + list of devices as strings to be used in subsequent commands + """ + try: + devices = nidaqmx.system.System.local().devices.device_names + if devices == ['']: + devices = [] + return devices + except DaqError as e: + return e.error_code + + def update_NIDAQ_channels(self, source_type=None): + self.channels = self.get_NIDAQ_channels(self.devices, source_type=source_type) + + @classmethod + def get_NIDAQ_channels(cls, devices=None, source_type=None): + """Get the list of available channels for all NiDAq connected devices + + Parameters + ---------- + devices: list + list of strings, each one being a connected device + source_type: str + One of the entries of DAQ_NIDAQ_source enum + + Returns + ------- + List of str containing device and channel names + + """ + if devices is None: + devices = cls.get_NIDAQ_devices() + + if source_type is None: + source_type = DAQ_NIDAQ_source.names() + if not isinstance(source_type, list): + source_type = [source_type] + channels_tot = [] + channels = [] + if not not devices: + for device in devices: + for source in source_type: + if source == DAQ_NIDAQ_source['Analog_Input'].name: # analog input + channels = Device(device).ai_physical_chans.channel_names + elif source == DAQ_NIDAQ_source['Counter'].name: # counter + channels = Device(device).ci_physical_chans.channel_names + elif source == DAQ_NIDAQ_source['Analog_Output'].name: # analog output + channels = Device(device).ao_physical_chans.channel_names + elif source == DAQ_NIDAQ_source['Digital_Output'].name: # digital output + channels = Device(device).do_lines.channel_names + elif source == DAQ_NIDAQ_source['Digital_Input'].name: # digital iutput + channels = Device(device).di_lines.channel_names + elif source == DAQ_NIDAQ_source['Terminals'].name: # terminals + channels = Device(device).terminals + + if channels != ['']: + channels_tot.extend(channels) + + return channels_tot + + @classmethod + def getAOMaxRate(cls, device): + return Device(device).ao_max_rate + + @classmethod + def getAIMaxRate(cls, device): + return Device(device).ai_max_single_chan_rate + + @classmethod + def isAnalogTriggeringSupported(cls, device): + return Device(device).anlg_trig_supported + + @classmethod + def isDigitalTriggeringSupported(cls, device): + return Device(device).dig_trig_supported + + @classmethod + def getTriggeringSources(cls, devices=None): + sources = [] + if devices is None: + devices = cls.get_NIDAQ_devices() + + for device in devices: + if cls.isDigitalTriggeringSupported(device): + string = Device(device).terminals + channels = [chan for chan in string if 'PFI' in chan] + if channels != ['']: + sources.extend(channels) + if cls.isAnalogTriggeringSupported(device): + channels = Device(device).ai_physical_chans.channel_names + if channels != ['']: + sources.extend(channels) + return sources + + def update_task(self, channels=[], clock_settings=ClockSettings(), trigger_settings=TriggerSettings()): + + try: + if self._task is not None: + if isinstance(self._task, nidaqmx.Task): + self._task.close() + + self._task = None + self.c_callback = None + + self._task = nidaqmx.Task() + err_code = None + + # create all channels one task for one type of channels + for channel in channels: + if channel.source == 'Analog_Input': # analog input + try: + if channel.analog_type == "Voltage": + self._task.ai_channels.add_ai_voltage_chan(channel.name, "", + DAQ_termination[channel.termination], + channel.value_min, + channel.value_max, + VoltageUnits.VOLTS, None) + + elif channel.analog_type == "Current": + self._task.ai_channels.add_ai_current_chan(channel.name, "", + DAQ_termination[channel.termination], + channel.value_min, + channel.value_max, + CurrentUnits.AMPS, + CurrentShuntResistorLocation.INTERNAL, + 0., None) + + elif channel.analog_type == "Thermocouple": + self._task.ai_channels.add_ai_thrmcpl_chan(channel.name, "", + channel.value_min, + channel.value_max, + TemperatureUnits.DEG_C, + DAQ_termination[channel.thermo_type], + CJCSource.BUILT_IN, 0., "") + except DaqError as e: + err_code = e.error_code + if not not err_code: + status = self.DAQmxGetErrorString(err_code) + raise IOError(status) + elif channel.source == 'Counter': # counter + try: + if channel.counter_type == "Edge Counter": + self._task.ci_channels.add_ci_count_edges_chan(channel.name, "", + Edge[channel.edge], 0, + CountDirection.COUNT_UP) + + elif channel.counter_type == "Clock Output": + self._task.co_channels.add_co_pulse_chan_freq(channel.name, "clock task", + FrequencyUnits.HZ, + Level.LOW, + 0, + channel.clock_frequency, + 0.5) + + elif channel.counter_type == "SemiPeriod Input": + self._task.ci_channels.add_ci_semi_period_chan(channel.name, "counter task", + 0, # expected min + channel.value_max, # expected max + TimeUnits.TICKS, "") + + except DaqError as e: + err_code = e.error_code + + if not not err_code: + status = self.DAQmxGetErrorString(err_code) + raise IOError(status) + + if not not err_code: + status = self.DAQmxGetErrorString(err_code) + raise IOError(status) + elif channel.source == 'Analog_Output': # Analog_Output + try: + if channel.analog_type == "Voltage": + self._task.ao_channels.add_ao_voltage_chan(channel.name, "", + channel.value_min, + channel.value_max, + VoltageUnits.VOLTS, None) + + elif channel.analog_type == "Current": + self._task.ao_channels.add_ao_current_chan(channel.name, "", + channel.value_min, + channel.value_max, + VoltageUnits.VOLTS, None) + except DaqError as e: + err_code = e.error_code + if not not err_code: + status = self.DAQmxGetErrorString(err_code) + raise IOError(status) + elif channel.source == 'Digital_Output': + try: + self._task.do_channels.add_do_chan(channel.name, "", + LineGrouping.CHAN_PER_LINE) + except DaqError as e: + err_code = e.error_code + if not not err_code: + status = self.DAQmxGetErrorString(err_code) + raise IOError(status) + elif channel.source == 'Digital_Input': # Digital_Input + try: + self._task.di_channels.add_di_chan(channel.name, "", + LineGrouping.CHAN_PER_LINE) + except DaqError as e: + err_code = e.error_code + if not not err_code: + status = self.DAQmxGetErrorString(err_code) + raise IOError(status) + + # configure the timing + if clock_settings.repetition: + mode = AcquisitionType.CONTINUOUS + else: + mode = AcquisitionType.FINITE + if clock_settings.Nsamples > 1 and err_code == 0: + try: + if isinstance(clock_settings, ClockSettings): + self._task.timing.cfg_samp_clk_timing(clock_settings.source, clock_settings.frequency, + Edge[clock_settings.edge].value, + mode, + clock_settings.Nsamples) + + elif isinstance(clock_settings, ChangeDetectionSettings): + self._task.timing.cfg_change_detection_timing(clock_settings.rising_channel, + clock_settings.falling_channel, + mode, + clock_settings.Nsamples) + except DaqError as e: + err_code = e.error_code + if not not err_code: + status = self.DAQmxGetErrorString(err_code) + raise IOError(status) + + for channel in channels: + if not trigger_settings.enable: + if channel.source == 'Counter': + pass # Maybe here adding the configuration fastCTr0 with Ctr1 etc...? + else: + err = self._task.triggers.start_trigger.disable_start_trig() + if err != 0: + raise IOError(self.DAQmxGetErrorString(err)) + else: + if 'PF' in trigger_settings.trig_source: + self._task.triggers.start_trigger.disable_start_trig() + elif 'ai' in trigger_settings.trig_source: + self._task.triggers.start_trigger.cfg_anlg_edge_start_trig( + trigger_settings.trig_source, + Edge[trigger_settings.edge], + trigger_settings.level) + else: + raise IOError('Unsupported Trigger source') + + except Exception as e: + print(e) + + def register_callback(self, callback, event='done', nsamples=1): + + if event == 'done': + self._task.register_done_event(Signal.SAMPLE_COMPLETE, callback) + # NOT SURE HERE + elif event == 'sample': + self._task.register_every_n_samples_acquired_into_buffer_event(1, + callback) + elif event == 'Nsamples': + self._task.register_every_n_samples_acquired_into_buffer_event(nsamples, + callback) + + def readCounter(self): + # return 25 + + time.sleep(1) + self.value = self._task.read() + + # t = Timer(counting_time,self.read) + # self._task.start() + # t.start() + # time.sleep(2*counting_time) + # print(self.value) + return self.value + + @classmethod + def getAIVoltageRange(cls, device='Dev1'): + ret = nidaqmx.system.System.local().devices[device].ai_voltage_rngs + return [tuple(ret[6:8])] + + @classmethod + def getAOVoltageRange(cls, device='Dev1'): + ret = nidaqmx.system.System.local().devices[device].ao_voltage_rngs + return [tuple(ret)] # [(-10., 10.)] Why this format is needed?? + + def stop(self): + if self._task is not None: + self._task.stop() + + def start(self): + if self._task is not None: + self._task.start() + + def close(self): + """ + close the current task. + """ + if self._task is not None: + self._task.stop() + self._task.close() + self._task = None + + @classmethod + def DAQmxGetErrorString(cls, error_code): + if error_code is None: + return '' + else: + return DAQmxErrors(error_code).name + + def isTaskDone(self): + return self._task.is_task_done() + + def waitTaskDone(self, timeout=10.): + ret = self._task.wait_until_done(timeout) + if ret != 0: + logger.info(self.DAQmxGetErrorString(ret)) + + def refresh_hardware(self): + """ + Refresh the NIDAQ hardware from settings values. + + See Also + -------- + update_NIDAQ_devices, update_NIDAQ_channels + """ + devices = self.update_NIDAQ_devices() + self.update_NIDAQ_channels(devices) + + +if __name__ == '__main__': + print(DAQmx.get_NIDAQ_channels()) + controller = DAQmx() + ch_counter = Counter(name='Dev1/ctr0', + source='Counter', + edge=Edge.names()[0]) + controller.update_task([ch_counter]) + controller.start() + print(controller.readCounter()) + controller.stop() + controller.close() + + pass From 9919deee6ebe9b9c0facf8529d7a02d2fd5a74dd Mon Sep 17 00:00:00 2001 From: Sebastien Date: Tue, 17 Sep 2024 15:26:09 +0200 Subject: [PATCH 007/140] NIDAQmx structure --- src/pymodaq_plugins_daqmx/daq_move_plugins/daq_move_NIDAQmx.py | 0 .../daq_viewer_plugins/plugins_0D/daq_0Dviewer_NIDAQmx.py | 0 .../hardware/national_instruments/daqmxni.py | 2 +- 3 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 src/pymodaq_plugins_daqmx/daq_move_plugins/daq_move_NIDAQmx.py create mode 100644 src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_NIDAQmx.py diff --git a/src/pymodaq_plugins_daqmx/daq_move_plugins/daq_move_NIDAQmx.py b/src/pymodaq_plugins_daqmx/daq_move_plugins/daq_move_NIDAQmx.py new file mode 100644 index 0000000..e69de29 diff --git a/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_NIDAQmx.py b/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_NIDAQmx.py new file mode 100644 index 0000000..e69de29 diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py index 5d1d7cb..84fc60a 100644 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py @@ -209,7 +209,7 @@ def __init__(self, clock_frequency=100, **kwargs): class DAQmx: - """Wrapper around the PyDAQmx package giving an easy to use object to instantiate channels and tasks""" + """Wrapper around the NIDAQmx package giving an easy to use object to instantiate channels and tasks""" def __init__(self): self.devices = [] self.channels = [] From ab58aea0c986e1e2affbc4ae729ef6f2318b8e08 Mon Sep 17 00:00:00 2001 From: Sebastien Date: Thu, 19 Sep 2024 15:33:54 +0200 Subject: [PATCH 008/140] New daq_viewer_NIDAQmx + wrapper modifs --- .../plugins_0D/daq_0Dviewer_NIDAQmx.py | 253 ++++++++++++++++++ .../hardware/national_instruments/daqmxni.py | 34 ++- 2 files changed, 286 insertions(+), 1 deletion(-) diff --git a/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_NIDAQmx.py b/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_NIDAQmx.py index e69de29..2c2fb5b 100644 --- a/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_NIDAQmx.py +++ b/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_NIDAQmx.py @@ -0,0 +1,253 @@ +import numpy as np +from pymodaq.utils.daq_utils import ThreadCommand +from pymodaq.utils.data import DataWithAxes, DataToExport, DataSource, DataFromPlugins +from pymodaq.control_modules.viewer_utility_classes import DAQ_Viewer_base, comon_parameters, main +from pymodaq.utils.parameter import Parameter +from pymodaq_plugins_daqmx.hardware.national_instruments.daqmxni import * +from pymodaq.utils.logger import set_logger, get_module_name +logger = set_logger(get_module_name(__file__)) + + +class DAQ_0DViewer_NIDAQmx(DAQ_Viewer_base): + """ + Plugin for a 0D data visualization & acquisition with various NI modules plugged in a NI cDAQ. + """ + + params = comon_parameters+[ + {'title': 'Display type:', 'name': 'display', 'type': 'list', 'limits': ['0D', '1D']}, + {'title': 'Module ref. :', 'name': 'module', 'type': 'list', 'limits': DAQmx.get_NIDAQ_devices(), + 'value': DAQmx.get_NIDAQ_devices()[0] + }, + {'title': 'Devices', 'name': 'devices', 'type': 'text', 'value': ''}, + {'title': 'Channels', 'name': 'channels', 'type': 'text', 'value': ''}, + {'title': 'Analog channel :', 'name': 'ai_channel', 'type': 'list', + 'limits': DAQmx.get_NIDAQ_channels(source_type='Analog_Input'), + 'value': DAQmx.get_NIDAQ_channels(source_type='Analog_Input')[0] + }, + {'title': 'Analog Source :', 'name': 'ai_srce', 'type': 'list', + 'limits': DAQ_NIDAQ_source.names(), + 'value': DAQ_NIDAQ_source.names()[0] + }, + {'title': 'Analog Type :', 'name': 'ai_type', 'type': 'list', + 'limits': DAQ_analog_types.names(), + 'value': DAQ_analog_types.names()[2] + }, + {'title': 'Min. value:', 'name': 'ai_min', 'type': 'float', 'value': -80e-3, 'min': -1e4}, + {'title': 'Max. value:', 'name': 'ai_max', 'type': 'float', 'value': 80e-3, 'max': 1e4}, + {'title': 'Frequency Acq.:', 'name': 'frequency', 'type': 'int', 'value': 4, 'min': 1}, + {'title': 'Nsamples:', 'name': 'Nsamples', 'type': 'int', 'value': 2, 'default': 100, 'min': 1}, + {"title": "Counting channel:", "name": "counter_channel", + "type": "list", "limits": DAQmx.get_NIDAQ_channels(source_type="Counter"), + # 'value': DAQmx.get_NIDAQ_channels(source_type='Counter')[0] + }, + ] + + def __init__(self, parent=None, params_state=None): + super().__init__(parent, params_state) + + def ini_attributes(self): + self.controller: DAQmx = None + self.channels_ai = None + self.task = None + self.live = False # True during a continuous grab + + + 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 + """ + self.update_tasks() + + def ini_detector(self, controller=None): + """Detector communication initialization + + Parameters + ---------- + controller: (object) + custom object of a PyMoDAQ plugin (Slave case). None if only one actuator/detector by controller + (Master case) + + Returns + ------- + info: str + initialized: bool + False if initialization failed otherwise True + """ + logger.info("Detector 0D initialized") + + try: + self.controller = self.ini_detector_init(controller, DAQmx()) + self.update_tasks() + initialized = True + info = "DAQ_0D initialized" + self.controller.update_NIDAQ_devices() + self.controller.update_NIDAQ_channels() + devices = "" + channels = "" + for device in self.controller.devices: + devices += device + " // " + for channel in self.controller.channels: + channels += channel + " // " + self.settings.child('devices').setValue(devices[:-4]) + self.settings.child('channels').setValue(channels[:-4]) + except Exception as e: + print(e) + initialized = False + info = "Error" + + return info, initialized + + def close(self): + """Terminate the communication protocol""" + self.task.close() + self.controller.close() + pass + + def grab_data(self, Naverage=1, **kwargs): + """Start a grab from the detector + + Parameters + ---------- + Naverage: int + Number of hardware averaging not relevant here. + kwargs: dict + others optionals arguments + """ + self.update_tasks() + self.emit_data(self.read_data()) + self.task.close() + + def read_data(self): + + get_data = self.task.read() + + return get_data + + def emit_data(self, data): + print("DATA = ", data) + print("type = ", type(data)) + print("DATA array = ", np.array([data])) + dte = DataToExport(name='NIDAQmx', + data=[DataFromPlugins(name='NI Analog Input', + data=np.array([data]), + # dim=f'Data{self.settings.child("display").value()}', + dim=f'Data0D', + # labels=self.channels_ai[0].name, + # labels='test', + )]) + self.dte_signal.emit(dte) + + def stop(self): + """Stop the current grab hardware wise if necessary""" + self.task.stop() + self.close() + self.emit_status(ThreadCommand('Update_Status', ['Acquisition stopped.'])) + return '' + + def update_tasks(self): + """Set up the counting tasks in the NI card.""" + + + # Create channels + self.channels_ai = [AIThermoChannel(name="cDAQ1Mod1/ai0", + source='Analog_Input', + analog_type='Thermocouple', + value_min=-80.0e-3, + value_max=80.0e-3, + termination='Diff', + thermo_type=DAQ_thermocouples.K), + ] + + self.clock_settings_ai = ClockSettings(frequency=self.settings.child('frequency').value(), + Nsamples=self.settings.child('Nsamples').value(), + repetition=self.live) + + # self.controller.update_task(self.channels_ai, self.clock_settings_ai) + + self.task = nidaqmx.Task() + self.task.ai_channels.add_ai_thrmcpl_chan(physical_channel=self.channels_ai[0].name, + # name_to_assign_to_channel="Channel 01", + min_val=self.channels_ai[0].value_min, + max_val=self.channels_ai[0].value_max, + units=TemperatureUnits.DEG_C, + thermocouple_type=self.channels_ai[0].thermo_type, + # cjc_source=CJCSource.BUILT_IN, + # cjc_val="", + # cjc_channel="", + ) + +if __name__ == '__main__': + # main(__file__) + try: + print("In main") + import nidaqmx + + channels = [AIThermoChannel(name="cDAQ1Mod1/ai0", + source='Analog_Input', analog_type='Thermocouple', + value_min=-80.0e-3, value_max=80.0e-3, + termination='Diff', thermo_type=DAQ_thermocouples.K), + AIThermoChannel(name="cDAQ1Mod1/ai1", + source='Analog_Input', analog_type='Thermocouple', + value_min=-80.0e-3, value_max=80.0e-3, + termination='Diff', thermo_type=DAQ_thermocouples.K), + AIThermoChannel(name="cDAQ1Mod1/ai2", + source='Analog_Input', analog_type='Thermocouple', + value_min=-80.0e-3, value_max=80.0e-3, + termination='Diff', thermo_type=DAQ_thermocouples.K), + AIThermoChannel(name="cDAQ1Mod1/ai3", + source='Analog_Input', analog_type='Thermocouple', + value_min=-80.0e-3, value_max=80.0e-3, + termination='Diff', thermo_type=DAQ_thermocouples.K), + ] + # Create Task + print("DAQ_thermocouples.names ", DAQ_thermocouples.names()) + print("DAQ_thermocouples.K ", DAQ_thermocouples.K) + print("channel.thermo_type ", channels[0].thermo_type) + task = nidaqmx.Task() + for channel in channels: + task.ai_channels.add_ai_thrmcpl_chan(physical_channel=channel.name, + # name_to_assign_to_channel="Channel 01", + min_val=channel.value_min, + max_val=channel.value_max, + units=TemperatureUnits.DEG_C, + thermocouple_type=channel.thermo_type, + # cjc_source=CJCSource.BUILT_IN, + # cjc_val="", + # cjc_channel="", + ) + NIDAQ_Devices = nidaqmx.system.System.local().devices + + print("NIDAQ devices ", NIDAQ_Devices) + print("NIDAQ devices names ", NIDAQ_Devices.device_names) + + Chassis = NIDAQ_Devices[0] + Module01 = NIDAQ_Devices[1] + Module02 = NIDAQ_Devices[2] + print("Chassis={}, Module01={}, Module02={}" .format(Chassis, Module01, Module02)) + + # Test resources + try: + Chassis.self_test_device() + Module01.self_test_device() + Module02.self_test_device() + except Exception as e: + print("Resources test failed: {}" .format(e)) + + print("Chassis: name={}, Num={}".format(Chassis.name, Chassis.product_type)) + print("Module01: name={}, Num={}".format(Module01.name, Module01.product_type)) + print("Module02: name={}, Num={}".format(Module02.name, Module02.product_type)) + + print("channel 01 name : ", channels[0].name) + data = task.read() + print("data = ", data) + print("type(data) = ", type(data)) + print("type(data[0]) = ", type(data[0])) + + task.close() + + except Exception as e: + print("Exception ({}): {}".format(type(e), str(e))) \ No newline at end of file diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py index 84fc60a..3522de3 100644 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py @@ -189,10 +189,15 @@ def __init__(self, termination=DAQ_termination.names()[0], **kwargs): class AIThermoChannel(AIChannel): def __init__(self, thermo_type=DAQ_thermocouples.names()[0], **kwargs): super().__init__(**kwargs) - assert thermo_type in DAQ_thermocouples.names() + # assert thermo_type in DAQ_thermocouples.names() self.thermo_type = thermo_type +class AOChannel(AChannel): + def __init__(self, **kwargs): + super().__init__(**kwargs) + + class Counter(Channel): def __init__(self, edge=Edge.names()[0], **kwargs): assert edge in Edge.names() @@ -208,6 +213,21 @@ def __init__(self, clock_frequency=100, **kwargs): self.counter_type = "Clock Output" +class DigitalChannel(Channel): + def __init__(self, **kwargs): + super().__init__(**kwargs) + + +class DOChannel(DigitalChannel): + def __init__(self, **kwargs): + super().__init__(**kwargs) + + +class DIChannel(DigitalChannel): + def __init__(self, **kwargs): + super().__init__(**kwargs) + + class DAQmx: """Wrapper around the NIDAQmx package giving an easy to use object to instantiate channels and tasks""" def __init__(self): @@ -509,6 +529,18 @@ def register_callback(self, callback, event='done', nsamples=1): elif event == 'Nsamples': self._task.register_every_n_samples_acquired_into_buffer_event(nsamples, callback) + def readAnalog(self, Nchannels, clock_settings): + read = nidaqmx.int32() + N = clock_settings.Nsamples + data = np.zeros(N * Nchannels, dtype=np.float64) + timeout = N * Nchannels * 1 / clock_settings.frequency * 2 # set to twice the time it should take to acquire the data + + self._task.ReadAnalogF64(N, timeout, nidaqmx.DAQmx_Val_GroupByChannel, data, len(data), + nidaqmx.byref(read), None) + if read.value == N: + return data + else: + raise IOError(f'Insufficient number of samples have been read:{read.value}/{N}') def readCounter(self): # return 25 From 2742e2755ab18ae5f8f14cfab780663a8f3da3f7 Mon Sep 17 00:00:00 2001 From: Sebastien Date: Thu, 26 Sep 2024 15:48:54 +0200 Subject: [PATCH 009/140] code adapted to nidaqmx + live visu ok --- .../plugins_0D/daq_0Dviewer_NIDAQmx.py | 299 ++++++++++-------- .../hardware/national_instruments/daqmxni.py | 118 +++++-- 2 files changed, 252 insertions(+), 165 deletions(-) diff --git a/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_NIDAQmx.py b/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_NIDAQmx.py index 2c2fb5b..670bf9c 100644 --- a/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_NIDAQmx.py +++ b/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_NIDAQmx.py @@ -7,11 +7,14 @@ from pymodaq.utils.logger import set_logger, get_module_name logger = set_logger(get_module_name(__file__)) - class DAQ_0DViewer_NIDAQmx(DAQ_Viewer_base): """ Plugin for a 0D data visualization & acquisition with various NI modules plugged in a NI cDAQ. """ + channels_ai: str + clock_settings_ai: str + dict_device_input: dict + live: bool params = comon_parameters+[ {'title': 'Display type:', 'name': 'display', 'type': 'list', 'limits': ['0D', '1D']}, @@ -30,16 +33,18 @@ class DAQ_0DViewer_NIDAQmx(DAQ_Viewer_base): }, {'title': 'Analog Type :', 'name': 'ai_type', 'type': 'list', 'limits': DAQ_analog_types.names(), - 'value': DAQ_analog_types.names()[2] + 'value': DAQ_analog_types.names()[0] }, {'title': 'Min. value:', 'name': 'ai_min', 'type': 'float', 'value': -80e-3, 'min': -1e4}, {'title': 'Max. value:', 'name': 'ai_max', 'type': 'float', 'value': 80e-3, 'max': 1e4}, - {'title': 'Frequency Acq.:', 'name': 'frequency', 'type': 'int', 'value': 4, 'min': 1}, - {'title': 'Nsamples:', 'name': 'Nsamples', 'type': 'int', 'value': 2, 'default': 100, 'min': 1}, - {"title": "Counting channel:", "name": "counter_channel", - "type": "list", "limits": DAQmx.get_NIDAQ_channels(source_type="Counter"), - # 'value': DAQmx.get_NIDAQ_channels(source_type='Counter')[0] - }, + {'title': 'Clock settings:', 'name': 'clock_settings', 'type': 'group', 'children': [ + {'title': 'Frequency Acq.:', 'name': 'frequency', 'type': 'int', 'value': 3, 'min': 1}, + {'title': 'Nsamples:', 'name': 'Nsamples', 'type': 'int', 'value': 10, 'default': 100, 'min': 1}, + {'title': 'Acquisition mode:', 'name': 'acqmode', 'type': 'list', + 'limits': ['Continuous', 'Finite'], 'value': 'Continous'}, + {"title": "Counting channel:", "name": "counter_channel", "type": "list", + "limits": DAQmx.get_NIDAQ_channels(source_type="Counter")}, + ]}, ] def __init__(self, parent=None, params_state=None): @@ -48,10 +53,10 @@ def __init__(self, parent=None, params_state=None): def ini_attributes(self): self.controller: DAQmx = None self.channels_ai = None - self.task = None + self.clock_settings_ai = None + self.dict_device_input = None self.live = False # True during a continuous grab - def commit_settings(self, param: Parameter): """Apply the consequences of a change of value in the detector settings @@ -60,6 +65,7 @@ def commit_settings(self, param: Parameter): param: Parameter A given parameter (within detector_settings) whose value has been changed by the user """ + # if param.name() == "frequency": self.update_tasks() def ini_detector(self, controller=None): @@ -86,24 +92,18 @@ def ini_detector(self, controller=None): info = "DAQ_0D initialized" self.controller.update_NIDAQ_devices() self.controller.update_NIDAQ_channels() - devices = "" - channels = "" - for device in self.controller.devices: - devices += device + " // " - for channel in self.controller.channels: - channels += channel + " // " - self.settings.child('devices').setValue(devices[:-4]) - self.settings.child('channels').setValue(channels[:-4]) - except Exception as e: - print(e) + logger.info("Detected devices: {}".format(self.controller.devices)) + self.settings.child('devices').setValue(str(self.controller.devices)) + logger.info("Detected channels: {}".format(self.controller.channels)) + self.settings.child('channels').setValue(str(self.controller.channels)) + except Exception as err: + logger.error(err) initialized = False info = "Error" - return info, initialized def close(self): """Terminate the communication protocol""" - self.task.close() self.controller.close() pass @@ -118,136 +118,175 @@ def grab_data(self, Naverage=1, **kwargs): others optionals arguments """ self.update_tasks() - self.emit_data(self.read_data()) - self.task.close() - def read_data(self): - - get_data = self.task.read() - - return get_data - - def emit_data(self, data): - print("DATA = ", data) - print("type = ", type(data)) - print("DATA array = ", np.array([data])) + data_from_task = self.controller._task.read() + logger.info("DATA = {}".format(data_from_task)) + + self.emit_data(data_from_task) + + def emit_data(self, data_measurement): + logger.info("OOOOOOOOOOOOO {}".format([chan.name for chan in self.channels_ai])) + # dte = DataToExport(name='NIDAQmx', + # data=[DataFromPlugins(name='NI Analog Input 01', + # data=[np.array([data_measurement[0]])], + # # dim=f'Data{self.settings.child("display").value()}', + # dim=f'Data0D', + # labels=[self.channels_ai[0].name], + # ), + # ]) dte = DataToExport(name='NIDAQmx', - data=[DataFromPlugins(name='NI Analog Input', - data=np.array([data]), + data=[DataFromPlugins(name='NI Analog Input 02', + data=[np.array([data_measurement[2]])], # dim=f'Data{self.settings.child("display").value()}', dim=f'Data0D', - # labels=self.channels_ai[0].name, - # labels='test', - )]) + labels=[self.channels_ai[2].name], + ), + ]) + + # # Dictionary linking devices to channel's physical quantities + # self.dict_device_input = {self.controller.devices[1]: [self.channels_ai[0].name, self.channels_ai[1].name], + # self.controller.devices[3]: self.channels_ai[2].name} + # # self.controller.devices #ci_meas_types + # logger.info("DICT DEVICE LABEL {}".format(self.dict_device_input)) + # logger.info(self.dict_device_input.get(self.controller.devices[1])) + # a = 0 + # b = 0 + # gen = [[ai for ai in self.dict_device_input.get(device)] + # for device in self.dict_device_input if not isinstance(self.dict_device_input.get(device), str)] + \ + # [self.dict_device_input.get(device) + # for device in self.dict_device_input if isinstance(self.dict_device_input.get(device), str)] + # + # logger.info("DATA TO EXPORT = {}".format(gen)) + # + # + # + # dte = DataToExport(name='NIDAQmx', + # data=[DataFromPlugins(name=self.dict_device_input[device], + # data=[np.array([data_measurement[i]]) + # for i in range(len(data_measurement))], + # dim='Data0D', + # labels=[ai for ai in self.dict_device_input.get(device)] + # ) for device in self.dict_device_input]) self.dte_signal.emit(dte) def stop(self): """Stop the current grab hardware wise if necessary""" - self.task.stop() self.close() self.emit_status(ThreadCommand('Update_Status', ['Acquisition stopped.'])) return '' def update_tasks(self): - """Set up the counting tasks in the NI card.""" - - + """Set up the tasks in the NI card.""" # Create channels + ##################### + ''' + Idée: + for Device in Devices: + self.channels_from_device(Device01) = [AIchannel(),AIchannel()......] + self.channels_from_device(Device02) = [AIthermochannel(),AIthermochannel()......] + Emit by mode (as for keithley) === by device + + ''' self.channels_ai = [AIThermoChannel(name="cDAQ1Mod1/ai0", source='Analog_Input', analog_type='Thermocouple', - value_min=-80.0e-3, - value_max=80.0e-3, - termination='Diff', - thermo_type=DAQ_thermocouples.K), + value_min=-20., + value_max=1300., + thermo_type=DAQ_thermocouples.K, + ), + AIThermoChannel(name="cDAQ1Mod1/ai1", + source='Analog_Input', + analog_type='Thermocouple', + value_min=-20., + value_max=1300., + thermo_type=DAQ_thermocouples.K, + ), + AIChannel(name="cDAQ1Mod3/ai0", + source='Analog_Input', + analog_type='Voltage', + value_min=-10., + value_max=10., + termination=DAQ_termination.Diff, + ), ] - - self.clock_settings_ai = ClockSettings(frequency=self.settings.child('frequency').value(), - Nsamples=self.settings.child('Nsamples').value(), + self.clock_settings_ai = ClockSettings(frequency=self.settings.child('clock_settings', 'frequency').value(), + Nsamples=self.settings.child('clock_settings', 'Nsamples').value(), + edge=Edge.Rising, repetition=self.live) - # self.controller.update_task(self.channels_ai, self.clock_settings_ai) + self.controller.update_task(self.channels_ai, self.clock_settings_ai) - self.task = nidaqmx.Task() - self.task.ai_channels.add_ai_thrmcpl_chan(physical_channel=self.channels_ai[0].name, - # name_to_assign_to_channel="Channel 01", - min_val=self.channels_ai[0].value_min, - max_val=self.channels_ai[0].value_max, - units=TemperatureUnits.DEG_C, - thermocouple_type=self.channels_ai[0].thermo_type, - # cjc_source=CJCSource.BUILT_IN, - # cjc_val="", - # cjc_channel="", - ) if __name__ == '__main__': - # main(__file__) - try: - print("In main") - import nidaqmx - - channels = [AIThermoChannel(name="cDAQ1Mod1/ai0", - source='Analog_Input', analog_type='Thermocouple', - value_min=-80.0e-3, value_max=80.0e-3, - termination='Diff', thermo_type=DAQ_thermocouples.K), - AIThermoChannel(name="cDAQ1Mod1/ai1", - source='Analog_Input', analog_type='Thermocouple', - value_min=-80.0e-3, value_max=80.0e-3, - termination='Diff', thermo_type=DAQ_thermocouples.K), - AIThermoChannel(name="cDAQ1Mod1/ai2", - source='Analog_Input', analog_type='Thermocouple', - value_min=-80.0e-3, value_max=80.0e-3, - termination='Diff', thermo_type=DAQ_thermocouples.K), - AIThermoChannel(name="cDAQ1Mod1/ai3", - source='Analog_Input', analog_type='Thermocouple', - value_min=-80.0e-3, value_max=80.0e-3, - termination='Diff', thermo_type=DAQ_thermocouples.K), - ] - # Create Task - print("DAQ_thermocouples.names ", DAQ_thermocouples.names()) - print("DAQ_thermocouples.K ", DAQ_thermocouples.K) - print("channel.thermo_type ", channels[0].thermo_type) - task = nidaqmx.Task() - for channel in channels: - task.ai_channels.add_ai_thrmcpl_chan(physical_channel=channel.name, - # name_to_assign_to_channel="Channel 01", - min_val=channel.value_min, - max_val=channel.value_max, - units=TemperatureUnits.DEG_C, - thermocouple_type=channel.thermo_type, - # cjc_source=CJCSource.BUILT_IN, - # cjc_val="", - # cjc_channel="", - ) - NIDAQ_Devices = nidaqmx.system.System.local().devices - - print("NIDAQ devices ", NIDAQ_Devices) - print("NIDAQ devices names ", NIDAQ_Devices.device_names) - - Chassis = NIDAQ_Devices[0] - Module01 = NIDAQ_Devices[1] - Module02 = NIDAQ_Devices[2] - print("Chassis={}, Module01={}, Module02={}" .format(Chassis, Module01, Module02)) - - # Test resources + """Main section used during development tests""" + main_file = True + if main_file: + main(__file__) + else: try: - Chassis.self_test_device() - Module01.self_test_device() - Module02.self_test_device() - except Exception as e: - print("Resources test failed: {}" .format(e)) + print("In main") + import nidaqmx + + logger.info("DAQ Sources names{}".format(DAQ_NIDAQ_source.names())) + logger.info("DAQ Sources members{}".format(DAQ_NIDAQ_source.members())) + logger.info("DAQ Sources names0{}".format(DAQ_NIDAQ_source.names()[0])) + logger.info("DAQ Sources members0{}".format(DAQ_NIDAQ_source.members()[0])) + channels = [AIThermoChannel(name="cDAQ1Mod1/ai0", + source='Analog_Input', + analog_type='Thermocouple', + value_min=-80.0e-3, + value_max=80.0e-3, + termination='Diff', + thermo_type=DAQ_thermocouples.K), + ] + # Create Task + print("DAQ_thermocouples.names ", DAQ_thermocouples.names()) + print("DAQ_thermocouples.K ", nidaqmx.constants.ThermocoupleType.K) + print("DAQ_thermocouples.K type", type(nidaqmx.constants.ThermocoupleType.K)) + print("DAQ_thermocouples.K ", DAQ_thermocouples.K) + print("DAQ_thermocouples.K type ", type(DAQ_thermocouples.K)) + print("channel.thermo_type ", channels[0].thermo_type) + task = nidaqmx.Task() + for channel in channels: + task.ai_channels.add_ai_thrmcpl_chan(physical_channel=channel.name, + # name_to_assign_to_channel="Channel 01", + min_val=channel.value_min, + max_val=channel.value_max, + units=TemperatureUnits.DEG_C, + thermocouple_type=channel.thermo_type, + # cjc_source=CJCSource.BUILT_IN, + # cjc_val="", + # cjc_channel="", + ) + NIDAQ_Devices = nidaqmx.system.System.local().devices + + print("NIDAQ devices ", NIDAQ_Devices) + print("NIDAQ devices names ", NIDAQ_Devices.device_names) + + Chassis = NIDAQ_Devices[0] + Module01 = NIDAQ_Devices[1] + Module02 = NIDAQ_Devices[2] + print("Chassis={}, Module01={}, Module02={}" .format(Chassis, Module01, Module02)) + + # Test resources + try: + Chassis.self_test_device() + Module01.self_test_device() + Module02.self_test_device() + except Exception as e: + print("Resources test failed: {}" .format(e)) + + print("Chassis: name={}, Num={}".format(Chassis.name, Chassis.product_type)) + print("Module01: name={}, Num={}".format(Module01.name, Module01.product_type)) + print("Module02: name={}, Num={}".format(Module02.name, Module02.product_type)) + + print("channel 01 name : ", channels[0].name) + data = task.read() + print("data = ", data) + print("type(data) = ", type(data)) + print("type(data[0]) = ", type(data[0])) + + task.close() - print("Chassis: name={}, Num={}".format(Chassis.name, Chassis.product_type)) - print("Module01: name={}, Num={}".format(Module01.name, Module01.product_type)) - print("Module02: name={}, Num={}".format(Module02.name, Module02.product_type)) - - print("channel 01 name : ", channels[0].name) - data = task.read() - print("data = ", data) - print("type(data) = ", type(data)) - print("type(data[0]) = ", type(data[0])) - - task.close() - - except Exception as e: - print("Exception ({}): {}".format(type(e), str(e))) \ No newline at end of file + except Exception as e: + print("Exception ({}): {}".format(type(e), str(e))) \ No newline at end of file diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py index 3522de3..186f54a 100644 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py @@ -1,6 +1,5 @@ import time - - +import traceback from enum import IntEnum import numpy as np from pymodaq.utils.logger import set_logger, get_module_name @@ -35,6 +34,10 @@ class DAQ_NIDAQ_source(IntEnum): def names(cls): return [name for name, member in cls.__members__.items()] + @classmethod + def members(cls): + return [member for name, member in cls.__members__.items()] + class DAQ_analog_types(IntEnum): """ @@ -52,6 +55,10 @@ class DAQ_analog_types(IntEnum): def names(cls): return [name for name, member in cls.__members__.items()] + @classmethod + def members(cls): + return [member for name, member in cls.__members__.items()] + @classmethod def values(cls): return [cls[name].value for name, member in cls.__members__.items()] @@ -78,10 +85,14 @@ class DAQ_thermocouples(IntEnum): def names(cls): return [name for name, member in cls.__members__.items()] + @classmethod + def members(cls): + return [member for name, member in cls.__members__.items()] + class DAQ_termination(IntEnum): """ - Enum class of thermocouples type + Enum class of termination type =============== ========== **Attributes** **Type** @@ -97,6 +108,10 @@ class DAQ_termination(IntEnum): def names(cls): return [name for name, member in cls.__members__.items()] + @classmethod + def members(cls): + return [member for name, member in cls.__members__.items()] + class Edge(IntEnum): """ @@ -108,6 +123,10 @@ class Edge(IntEnum): def names(cls): return [name for name, member in cls.__members__.items()] + @classmethod + def members(cls): + return [member for name, member in cls.__members__.items()] + class ClockMode(IntEnum): """ @@ -119,6 +138,10 @@ class ClockMode(IntEnum): def names(cls): return [name for name, member in cls.__members__.items()] + @classmethod + def members(cls): + return [member for name, member in cls.__members__.items()] + class ClockSettingsBase: def __init__(self, Nsamples=1000, repetition=False): @@ -128,10 +151,10 @@ def __init__(self, Nsamples=1000, repetition=False): class ClockSettings(ClockSettingsBase): - def __init__(self, source=None, frequency=1000, Nsamples=1000, edge=Edge.names()[0], repetition=False): + def __init__(self, source=None, frequency=1000, Nsamples=1000, edge=Edge.members()[0], repetition=False): super().__init__(Nsamples, repetition) self.source = source - assert edge in Edge.names() + assert edge in Edge.members() self.frequency = frequency self.edge = edge @@ -145,8 +168,8 @@ def __init__(self, Nsamples=1000, rising_channel='', falling_channel='', class TriggerSettings: - def __init__(self, trig_source='', enable=False, edge=Edge.names()[0], level=0.1): - assert edge in Edge.names() + def __init__(self, trig_source='', enable=False, edge=Edge.members()[0], level=0.1): + assert edge in Edge.members() self.trig_source = trig_source self.enable = enable self.edge = edge @@ -180,16 +203,22 @@ def __init__(self, analog_type=DAQ_analog_types.names()[0], value_min=-10., valu class AIChannel(AChannel): - def __init__(self, termination=DAQ_termination.names()[0], **kwargs): + def __init__(self, termination=DAQ_termination.members(), **kwargs): super().__init__(**kwargs) - assert termination in DAQ_termination.names() + logger.info("AI chan ter{}".format(termination)) + logger.info("AI chan ter{}".format(DAQ_termination.members())) + logger.info("AI chan ter{}".format(DAQ_termination.Diff)) + # assert termination in DAQ_termination.members() self.termination = termination class AIThermoChannel(AIChannel): - def __init__(self, thermo_type=DAQ_thermocouples.names()[0], **kwargs): + def __init__(self, thermo_type=DAQ_thermocouples.members(), **kwargs): super().__init__(**kwargs) - # assert thermo_type in DAQ_thermocouples.names() + logger.info("AIthermo chan th{}".format(thermo_type)) + logger.info("AIthermo th{}".format(DAQ_thermocouples.members())) + logger.info("AIthermo th{}".format(DAQ_thermocouples.K)) + assert thermo_type in DAQ_thermocouples.members() self.thermo_type = thermo_type @@ -199,8 +228,8 @@ def __init__(self, **kwargs): class Counter(Channel): - def __init__(self, edge=Edge.names()[0], **kwargs): - assert edge in Edge.names() + def __init__(self, edge=Edge.members()[0], **kwargs): + assert edge in Edge.members() super().__init__(**kwargs) self.edge = edge self.counter_type = "Edge Counter" @@ -371,17 +400,26 @@ def update_task(self, channels=[], clock_settings=ClockSettings(), trigger_setti self._task = nidaqmx.Task() err_code = None + # logger.info("first {}".format(self._task.timing)) + # logger.info("first stamp {}".format(nidaqmx.task.Timing.first_samp_timestamp_val)) # create all channels one task for one type of channels for channel in channels: + logger.info("channel {}".format(channel)) + logger.info("channel name {}".format(channel.name)) + logger.info("channel source {}".format(channel.source)) + logger.info("channel term{}".format(channel.termination)) if channel.source == 'Analog_Input': # analog input try: + logger.info("DAQ te{}".format(DAQ_termination.Diff)) if channel.analog_type == "Voltage": - self._task.ai_channels.add_ai_voltage_chan(channel.name, "", - DAQ_termination[channel.termination], - channel.value_min, - channel.value_max, - VoltageUnits.VOLTS, None) + self._task.ai_channels.add_ai_voltage_chan(physical_channel=channel.name, + name_to_assign_to_channel="", + terminal_config=channel.termination, + min_val=channel.value_min, + max_val=channel.value_max, + units=VoltageUnits.VOLTS, + custom_scale_name=None) elif channel.analog_type == "Current": self._task.ai_channels.add_ai_current_chan(channel.name, "", @@ -393,12 +431,16 @@ def update_task(self, channels=[], clock_settings=ClockSettings(), trigger_setti 0., None) elif channel.analog_type == "Thermocouple": - self._task.ai_channels.add_ai_thrmcpl_chan(channel.name, "", - channel.value_min, - channel.value_max, - TemperatureUnits.DEG_C, - DAQ_termination[channel.thermo_type], - CJCSource.BUILT_IN, 0., "") + logger.info("DAQ th{}".format(DAQ_thermocouples.K)) + self._task.ai_channels.add_ai_thrmcpl_chan(physical_channel=channel.name, + name_to_assign_to_channel="", + min_val=channel.value_min, + max_val=channel.value_max, + units=TemperatureUnits.DEG_C, + thermocouple_type=channel.thermo_type, + cjc_source=CJCSource.BUILT_IN, + cjc_val=0., + cjc_channel="") except DaqError as e: err_code = e.error_code if not not err_code: @@ -477,16 +519,20 @@ def update_task(self, channels=[], clock_settings=ClockSettings(), trigger_setti mode = AcquisitionType.CONTINUOUS else: mode = AcquisitionType.FINITE - if clock_settings.Nsamples > 1 and err_code == 0: + if clock_settings.Nsamples > 1 and isinstance(err_code, type(None)): try: if isinstance(clock_settings, ClockSettings): - self._task.timing.cfg_samp_clk_timing(clock_settings.source, clock_settings.frequency, - Edge[clock_settings.edge].value, - mode, - clock_settings.Nsamples) - + logger.info('------ mode {}'.format(mode)) + logger.info('------ Nsamples {}'.format(clock_settings.Nsamples)) + self._task.timing.cfg_samp_clk_timing(rate=clock_settings.frequency, + source=clock_settings.source, + active_edge=clock_settings.edge, + sample_mode=mode, + samps_per_chan=clock_settings.Nsamples) elif isinstance(clock_settings, ChangeDetectionSettings): + logger.info("here") self._task.timing.cfg_change_detection_timing(clock_settings.rising_channel, + clock_settings.falling_channel, mode, clock_settings.Nsamples) @@ -501,9 +547,10 @@ def update_task(self, channels=[], clock_settings=ClockSettings(), trigger_setti if channel.source == 'Counter': pass # Maybe here adding the configuration fastCTr0 with Ctr1 etc...? else: - err = self._task.triggers.start_trigger.disable_start_trig() - if err != 0: - raise IOError(self.DAQmxGetErrorString(err)) + pass + # err = self._task.triggers.start_trigger.disable_start_trig() + # if err != 0: + # raise IOError(self.DAQmxGetErrorString(err)) else: if 'PF' in trigger_settings.trig_source: self._task.triggers.start_trigger.disable_start_trig() @@ -516,7 +563,8 @@ def update_task(self, channels=[], clock_settings=ClockSettings(), trigger_setti raise IOError('Unsupported Trigger source') except Exception as e: - print(e) + logger.error("Exception catched: {}".format(e)) + logger.error(traceback.format_exc()) def register_callback(self, callback, event='done', nsamples=1): @@ -614,7 +662,7 @@ def refresh_hardware(self): controller = DAQmx() ch_counter = Counter(name='Dev1/ctr0', source='Counter', - edge=Edge.names()[0]) + edge=Edge.members()[0]) controller.update_task([ch_counter]) controller.start() print(controller.readCounter()) From 2ebd34137006f7df128fd8189ebb9916a450bcf7 Mon Sep 17 00:00:00 2001 From: Sebastien Date: Thu, 5 Dec 2024 16:52:40 +0100 Subject: [PATCH 010/140] Reorder & update nidaqmx constants & variables --- .../hardware/national_instruments/daqmxni.py | 36 ++++++++----------- 1 file changed, 15 insertions(+), 21 deletions(-) diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py index 186f54a..7e2bbd2 100644 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py @@ -24,10 +24,10 @@ class DAQ_NIDAQ_source(IntEnum): =============== ========== """ Analog_Input = 0 - Counter = 1 - Analog_Output = 2 - Digital_Output = 3 - Digital_Input = 4 + Analog_Output = 1 + Counter = 2 + Digital_Input = 3 + Digital_Output = 4 Terminals = 5 @classmethod @@ -151,7 +151,7 @@ def __init__(self, Nsamples=1000, repetition=False): class ClockSettings(ClockSettingsBase): - def __init__(self, source=None, frequency=1000, Nsamples=1000, edge=Edge.members()[0], repetition=False): + def __init__(self, source=None, frequency=1000, Nsamples=1000, edge=Edge.Rising, repetition=False): super().__init__(Nsamples, repetition) self.source = source assert edge in Edge.members() @@ -168,7 +168,7 @@ def __init__(self, Nsamples=1000, rising_channel='', falling_channel='', class TriggerSettings: - def __init__(self, trig_source='', enable=False, edge=Edge.members()[0], level=0.1): + def __init__(self, trig_source='', enable=False, edge=Edge.Rising, level=0.1): assert edge in Edge.members() self.trig_source = trig_source self.enable = enable @@ -177,7 +177,7 @@ def __init__(self, trig_source='', enable=False, edge=Edge.members()[0], level=0 class Channel: - def __init__(self, name='', source=DAQ_NIDAQ_source.names()[0]): + def __init__(self, name='', source=DAQ_NIDAQ_source.Analog_Input): """ Parameters ---------- @@ -189,7 +189,7 @@ def __init__(self, name='', source=DAQ_NIDAQ_source.names()[0]): class AChannel(Channel): - def __init__(self, analog_type=DAQ_analog_types.names()[0], value_min=-10., value_max=+10., **kwargs): + def __init__(self, analog_type=DAQ_analog_types.Voltage, value_min=-10., value_max=+10., **kwargs): """ Parameters ---------- @@ -203,21 +203,15 @@ def __init__(self, analog_type=DAQ_analog_types.names()[0], value_min=-10., valu class AIChannel(AChannel): - def __init__(self, termination=DAQ_termination.members(), **kwargs): + def __init__(self, termination=DAQ_termination.Auto, **kwargs): super().__init__(**kwargs) - logger.info("AI chan ter{}".format(termination)) - logger.info("AI chan ter{}".format(DAQ_termination.members())) - logger.info("AI chan ter{}".format(DAQ_termination.Diff)) - # assert termination in DAQ_termination.members() + assert termination in DAQ_termination.members() self.termination = termination class AIThermoChannel(AIChannel): - def __init__(self, thermo_type=DAQ_thermocouples.members(), **kwargs): + def __init__(self, thermo_type=DAQ_thermocouples.K, **kwargs): super().__init__(**kwargs) - logger.info("AIthermo chan th{}".format(thermo_type)) - logger.info("AIthermo th{}".format(DAQ_thermocouples.members())) - logger.info("AIthermo th{}".format(DAQ_thermocouples.K)) assert thermo_type in DAQ_thermocouples.members() self.thermo_type = thermo_type @@ -228,7 +222,7 @@ def __init__(self, **kwargs): class Counter(Channel): - def __init__(self, edge=Edge.members()[0], **kwargs): + def __init__(self, edge=Edge.Rising, **kwargs): assert edge in Edge.members() super().__init__(**kwargs) self.edge = edge @@ -338,10 +332,10 @@ def get_NIDAQ_channels(cls, devices=None, source_type=None): for source in source_type: if source == DAQ_NIDAQ_source['Analog_Input'].name: # analog input channels = Device(device).ai_physical_chans.channel_names - elif source == DAQ_NIDAQ_source['Counter'].name: # counter - channels = Device(device).ci_physical_chans.channel_names elif source == DAQ_NIDAQ_source['Analog_Output'].name: # analog output channels = Device(device).ao_physical_chans.channel_names + elif source == DAQ_NIDAQ_source['Counter'].name: # counter + channels = Device(device).ci_physical_chans.channel_names elif source == DAQ_NIDAQ_source['Digital_Output'].name: # digital output channels = Device(device).do_lines.channel_names elif source == DAQ_NIDAQ_source['Digital_Input'].name: # digital iutput @@ -450,7 +444,7 @@ def update_task(self, channels=[], clock_settings=ClockSettings(), trigger_setti try: if channel.counter_type == "Edge Counter": self._task.ci_channels.add_ci_count_edges_chan(channel.name, "", - Edge[channel.edge], 0, + channel.edge, 0, CountDirection.COUNT_UP) elif channel.counter_type == "Clock Output": From 9193b7e22948d7145071489132b3fcbcdfbee7b1 Mon Sep 17 00:00:00 2001 From: Sebastien Date: Thu, 5 Dec 2024 16:56:44 +0100 Subject: [PATCH 011/140] Clarify code --- .../hardware/national_instruments/daqmxni.py | 50 +++++++++---------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py index 7e2bbd2..0bc0034 100644 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py @@ -407,34 +407,35 @@ def update_task(self, channels=[], clock_settings=ClockSettings(), trigger_setti try: logger.info("DAQ te{}".format(DAQ_termination.Diff)) if channel.analog_type == "Voltage": - self._task.ai_channels.add_ai_voltage_chan(physical_channel=channel.name, - name_to_assign_to_channel="", - terminal_config=channel.termination, - min_val=channel.value_min, - max_val=channel.value_max, - units=VoltageUnits.VOLTS, - custom_scale_name=None) + self._task.ai_channels.add_ai_voltage_chan(channel.name, + "", + channel.termination, + channel.value_min, + channel.value_max, + VoltageUnits.VOLTS, + "") elif channel.analog_type == "Current": - self._task.ai_channels.add_ai_current_chan(channel.name, "", - DAQ_termination[channel.termination], + self._task.ai_channels.add_ai_current_chan(channel.name, + "", + channel.termination, channel.value_min, channel.value_max, CurrentUnits.AMPS, CurrentShuntResistorLocation.INTERNAL, - 0., None) + 0., + "") elif channel.analog_type == "Thermocouple": - logger.info("DAQ th{}".format(DAQ_thermocouples.K)) - self._task.ai_channels.add_ai_thrmcpl_chan(physical_channel=channel.name, - name_to_assign_to_channel="", - min_val=channel.value_min, - max_val=channel.value_max, - units=TemperatureUnits.DEG_C, - thermocouple_type=channel.thermo_type, - cjc_source=CJCSource.BUILT_IN, - cjc_val=0., - cjc_channel="") + self._task.ai_channels.add_ai_thrmcpl_chan(channel.name, + "", + channel.value_min, + channel.value_max, + TemperatureUnits.DEG_C, + channel.thermo_type, + CJCSource.BUILT_IN, + 0., + "") except DaqError as e: err_code = e.error_code if not not err_code: @@ -563,14 +564,13 @@ def update_task(self, channels=[], clock_settings=ClockSettings(), trigger_setti def register_callback(self, callback, event='done', nsamples=1): if event == 'done': - self._task.register_done_event(Signal.SAMPLE_COMPLETE, callback) + self._task.register_done_event(callback) # NOT SURE HERE elif event == 'sample': - self._task.register_every_n_samples_acquired_into_buffer_event(1, - callback) + self._task.register_every_n_samples_acquired_into_buffer_event(1, callback) elif event == 'Nsamples': - self._task.register_every_n_samples_acquired_into_buffer_event(nsamples, - callback) + self._task.register_every_n_samples_acquired_into_buffer_event(nsamples, callback) + def readAnalog(self, Nchannels, clock_settings): read = nidaqmx.int32() N = clock_settings.Nsamples From 36319084b46c578fb85f3757563a7744b055e46b Mon Sep 17 00:00:00 2001 From: Sebastien Date: Thu, 5 Dec 2024 16:57:48 +0100 Subject: [PATCH 012/140] Remove old PyDAQMX based-on method --- .../hardware/national_instruments/daqmxni.py | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py index 0bc0034..4bbc25c 100644 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py @@ -571,19 +571,6 @@ def register_callback(self, callback, event='done', nsamples=1): elif event == 'Nsamples': self._task.register_every_n_samples_acquired_into_buffer_event(nsamples, callback) - def readAnalog(self, Nchannels, clock_settings): - read = nidaqmx.int32() - N = clock_settings.Nsamples - data = np.zeros(N * Nchannels, dtype=np.float64) - timeout = N * Nchannels * 1 / clock_settings.frequency * 2 # set to twice the time it should take to acquire the data - - self._task.ReadAnalogF64(N, timeout, nidaqmx.DAQmx_Val_GroupByChannel, data, len(data), - nidaqmx.byref(read), None) - if read.value == N: - return data - else: - raise IOError(f'Insufficient number of samples have been read:{read.value}/{N}') - def readCounter(self): # return 25 From a1989ae6ce3a96f2a2d1527bdd17395b3b6f4754 Mon Sep 17 00:00:00 2001 From: Sebastien Date: Thu, 5 Dec 2024 17:02:44 +0100 Subject: [PATCH 013/140] devices attributes not a list of string anymore but a list of nidaqmx.devices objects --- .../plugins_0D/daq_0Dviewer_NIDAQmx.py | 10 ++-------- .../hardware/national_instruments/daqmxni.py | 16 +++++++++------- 2 files changed, 11 insertions(+), 15 deletions(-) diff --git a/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_NIDAQmx.py b/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_NIDAQmx.py index 670bf9c..3d3b1cf 100644 --- a/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_NIDAQmx.py +++ b/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_NIDAQmx.py @@ -18,14 +18,8 @@ class DAQ_0DViewer_NIDAQmx(DAQ_Viewer_base): params = comon_parameters+[ {'title': 'Display type:', 'name': 'display', 'type': 'list', 'limits': ['0D', '1D']}, - {'title': 'Module ref. :', 'name': 'module', 'type': 'list', 'limits': DAQmx.get_NIDAQ_devices(), - 'value': DAQmx.get_NIDAQ_devices()[0] - }, - {'title': 'Devices', 'name': 'devices', 'type': 'text', 'value': ''}, - {'title': 'Channels', 'name': 'channels', 'type': 'text', 'value': ''}, - {'title': 'Analog channel :', 'name': 'ai_channel', 'type': 'list', - 'limits': DAQmx.get_NIDAQ_channels(source_type='Analog_Input'), - 'value': DAQmx.get_NIDAQ_channels(source_type='Analog_Input')[0] + {'title': 'Module ref. :', 'name': 'module', 'type': 'list', 'limits': DAQmx.get_NIDAQ_devices()[1], + 'value': DAQmx.get_NIDAQ_devices()[1][0] }, {'title': 'Analog Source :', 'name': 'ai_srce', 'type': 'list', 'limits': DAQ_NIDAQ_source.names(), diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py index 4bbc25c..e2e8548 100644 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py @@ -255,6 +255,7 @@ class DAQmx: """Wrapper around the NIDAQmx package giving an easy to use object to instantiate channels and tasks""" def __init__(self): self.devices = [] + self.devices_names = [] self.channels = [] self._device = None self._task = None @@ -275,12 +276,13 @@ def device(self): @device.setter def device(self, device): - if device not in self.devices: + if device not in self.devices_names: raise IOError(f'your device: {device} is not known or connected') self._device = device def update_NIDAQ_devices(self): - self.devices = self.get_NIDAQ_devices() + self.devices = self.get_NIDAQ_devices()[0] + self.devices_names = self.get_NIDAQ_devices()[1] @classmethod def get_NIDAQ_devices(cls): @@ -292,15 +294,15 @@ def get_NIDAQ_devices(cls): list of devices as strings to be used in subsequent commands """ try: - devices = nidaqmx.system.System.local().devices.device_names + devices = nidaqmx.system.System.local().devices if devices == ['']: devices = [] - return devices + return devices, devices.device_names except DaqError as e: return e.error_code def update_NIDAQ_channels(self, source_type=None): - self.channels = self.get_NIDAQ_channels(self.devices, source_type=source_type) + self.channels = self.get_NIDAQ_channels(self.devices_names, source_type=source_type) @classmethod def get_NIDAQ_channels(cls, devices=None, source_type=None): @@ -319,7 +321,7 @@ def get_NIDAQ_channels(cls, devices=None, source_type=None): """ if devices is None: - devices = cls.get_NIDAQ_devices() + devices = cls.get_NIDAQ_devices()[1] if source_type is None: source_type = DAQ_NIDAQ_source.names() @@ -368,7 +370,7 @@ def isDigitalTriggeringSupported(cls, device): def getTriggeringSources(cls, devices=None): sources = [] if devices is None: - devices = cls.get_NIDAQ_devices() + devices = cls.get_NIDAQ_devices()[1] for device in devices: if cls.isDigitalTriggeringSupported(device): From 4f7bf5d2b1678894eda2d1c5210ea412af7e4437 Mon Sep 17 00:00:00 2001 From: Sebastien Date: Thu, 5 Dec 2024 17:11:48 +0100 Subject: [PATCH 014/140] Clarify code --- .../hardware/national_instruments/daqmxni.py | 27 +++++++------------ 1 file changed, 9 insertions(+), 18 deletions(-) diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py index e2e8548..806c84a 100644 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py @@ -395,19 +395,13 @@ def update_task(self, channels=[], clock_settings=ClockSettings(), trigger_setti self.c_callback = None self._task = nidaqmx.Task() + logger.info("TASK: {}".format(self._task)) err_code = None - # logger.info("first {}".format(self._task.timing)) - # logger.info("first stamp {}".format(nidaqmx.task.Timing.first_samp_timestamp_val)) # create all channels one task for one type of channels for channel in channels: - logger.info("channel {}".format(channel)) - logger.info("channel name {}".format(channel.name)) - logger.info("channel source {}".format(channel.source)) - logger.info("channel term{}".format(channel.termination)) if channel.source == 'Analog_Input': # analog input try: - logger.info("DAQ te{}".format(DAQ_termination.Diff)) if channel.analog_type == "Voltage": self._task.ai_channels.add_ai_voltage_chan(channel.name, "", @@ -519,17 +513,13 @@ def update_task(self, channels=[], clock_settings=ClockSettings(), trigger_setti if clock_settings.Nsamples > 1 and isinstance(err_code, type(None)): try: if isinstance(clock_settings, ClockSettings): - logger.info('------ mode {}'.format(mode)) - logger.info('------ Nsamples {}'.format(clock_settings.Nsamples)) - self._task.timing.cfg_samp_clk_timing(rate=clock_settings.frequency, - source=clock_settings.source, - active_edge=clock_settings.edge, - sample_mode=mode, - samps_per_chan=clock_settings.Nsamples) + self._task.timing.cfg_samp_clk_timing(clock_settings.frequency, + clock_settings.source, + clock_settings.edge, + mode, + clock_settings.Nsamples) elif isinstance(clock_settings, ChangeDetectionSettings): - logger.info("here") self._task.timing.cfg_change_detection_timing(clock_settings.rising_channel, - clock_settings.falling_channel, mode, clock_settings.Nsamples) @@ -537,6 +527,7 @@ def update_task(self, channels=[], clock_settings=ClockSettings(), trigger_setti err_code = e.error_code if not not err_code: status = self.DAQmxGetErrorString(err_code) + logger.error(traceback.format_exc()) raise IOError(status) for channel in channels: @@ -558,9 +549,9 @@ def update_task(self, channels=[], clock_settings=ClockSettings(), trigger_setti trigger_settings.level) else: raise IOError('Unsupported Trigger source') - + logger.info("Task's channels{}".format(self._task.ai_channels.channel_names)) except Exception as e: - logger.error("Exception catched: {}".format(e)) + logger.error("Exception caught: {}".format(e)) logger.error(traceback.format_exc()) def register_callback(self, callback, event='done', nsamples=1): From daf959c71e547c4a7a014280b9cf7a3ab5253f4b Mon Sep 17 00:00:00 2001 From: Sebastien Date: Thu, 5 Dec 2024 17:20:22 +0100 Subject: [PATCH 015/140] PEP + syntax --- .../national_instruments/daq_NIDAQmx.py | 143 ++++++++++-------- 1 file changed, 76 insertions(+), 67 deletions(-) diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx.py index f0f28b8..5b6e9d9 100644 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx.py +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx.py @@ -74,6 +74,8 @@ def addNew(self, typ): 'removable': True, 'renamable': False} self.addChild(child) + + registerParameterType('groupai', ScalableGroupAI, override=True) @@ -127,6 +129,8 @@ def addNew(self, typ): 'removable': True, 'renamable': False} self.addChild(child) + + registerParameterType('groupao', ScalableGroupAO, override=True) @@ -171,6 +175,8 @@ def addNew(self, typ): 'removable': True, 'renamable': False} self.addChild(child) + + registerParameterType('groupcounter', ScalableGroupCounter, override=True) @@ -204,6 +210,8 @@ def addNew(self, typ): child = {'title': typ, 'name': 'di{:02.0f}'.format(newindex), 'type': 'group', 'children': self.params, 'removable': True, 'renamable': False} self.addChild(child) + + registerParameterType('groupdi', ScalableGroupDI, override=True) @@ -237,54 +245,59 @@ def addNew(self, typ): child = {'title': typ, 'name': 'counter{:02.0f}'.format(newindex), 'type': 'group', 'children': self.params, 'removable': True, 'renamable': False} self.addChild(child) + + registerParameterType('groupdo', ScalableGroupDO, override=True) class DAQ_NIDAQmx_base(DAQmx): data_grabed_signal = Signal(list) - params =[{'title': 'Refresh hardware:', 'name': 'refresh_hardware', 'type': 'bool', 'value': False}, - {'title': 'Signal type:', 'name': 'NIDAQ_type', 'type': 'list', 'limits': DAQ_NIDAQ_source.names()}, - {'title': 'AO Settings:', 'name': 'ao_settings', 'type': 'group', 'children': [ - {'title': 'Waveform:', 'name': 'waveform', 'type': 'list', 'value': 'DC', 'limits': ['DC', 'Sinus', 'Ramp']}, - - {'title': 'Controlled param:', 'name': 'cont_param', 'type': 'list', 'value': 'offset', - 'limits': ['offset', 'amplitude', 'frequency']}, - {'title': 'Waveform Settings:', 'name': 'waveform_settings', 'type': 'group', 'visible': False, 'children': [ - {'title': 'Offset:', 'name': 'offset', 'type': 'float', 'value': 0., }, - {'title': 'Amplitude:', 'name': 'amplitude', 'type': 'float', 'value': 1., }, - {'title': 'Frequency:', 'name': 'frequency', 'type': 'float', 'value': 10., }, - ]}, - ]}, - {'title': 'Clock Settings:', 'name': 'clock_settings', 'type': 'group', 'children': [ - {'title': 'Nsamples:', 'name': 'Nsamples', 'type': 'int', 'value': 1000, 'default': 1000, 'min': 1}, - {'title': 'Frequency:', 'name': 'frequency', 'type': 'float', 'value': 1000., 'default': 1000., - 'min': 0., 'suffix': 'Hz'}, - {'title': 'Repetition?:', 'name': 'repetition', 'type': 'bool', 'value': False, }, - ] - }, - {'title': 'AI Channels:', 'name': 'ai_channels', 'type': 'groupai', - 'limits': DAQmx.get_NIDAQ_channels(source_type='Analog_Input')}, - {'title': 'AO Channels:', 'name': 'ao_channels', 'type': 'groupao', - 'limits': DAQmx.get_NIDAQ_channels(source_type='Analog_Output')}, - {'title': 'DO Channels:', 'name': 'do_channels', 'type': 'groupdo', - 'limits': DAQmx.get_NIDAQ_channels(source_type='Digital_Output')}, - {'title': 'DI Channels:', 'name': 'di_channels', 'type': 'groupdi', - 'limits': DAQmx.get_NIDAQ_channels(source_type='Digital_Input')}, - {'title': 'Counter Settings:', 'name': 'counter_settings', 'type': 'group', 'visible': True, 'children': [ - {'title': 'Counting time (ms):', 'name': 'counting_time', 'type': 'float', 'value': 100., - 'default': 100., 'min': 0.}, - {'title': 'Counting Channels:', 'name': 'counter_channels', 'type': 'groupcounter', - 'limits': DAQmx.get_NIDAQ_channels(source_type='Counter')}, - ]}, - {'title': 'Trigger Settings:', 'name': 'trigger_settings', 'type': 'group', 'visible': True, 'children': [ - {'title': 'Enable?:', 'name': 'enable', 'type': 'bool', 'value': False, }, - {'title': 'Trigger Source:', 'name': 'trigger_channel', 'type': 'list', - 'limits': DAQmx.getTriggeringSources()}, - {'title': 'Edge type:', 'name': 'edge', 'type': 'list', 'limits': Edge.names(), 'visible': False}, - {'title': 'Level:', 'name': 'level', 'type': 'float', 'value': 1., 'visible': False} - ]} - ] + params = [{'title': 'Refresh hardware:', 'name': 'refresh_hardware', 'type': 'bool', 'value': False}, + {'title': 'Signal type:', 'name': 'NIDAQ_type', 'type': 'list', 'limits': DAQ_NIDAQ_source.names()}, + {'title': 'NSamples To Read', 'name': 'nsamplestoread', 'type': 'int', 'value': 1000, 'default': 1000, + 'min': 1}, + {'title': 'AO Settings:', 'name': 'ao_settings', 'type': 'group', 'children': [ + {'title': 'Waveform:', 'name': 'waveform', 'type': 'list', 'value': 'DC', + 'limits': ['DC', 'Sinus', 'Ramp']}, + {'title': 'Controlled param:', 'name': 'cont_param', 'type': 'list', 'value': 'offset', + 'limits': ['offset', 'amplitude', 'frequency']}, + {'title': 'Waveform Settings:', 'name': 'waveform_settings', 'type': 'group', 'visible': False, + 'children': [ + {'title': 'Offset:', 'name': 'offset', 'type': 'float', 'value': 0., }, + {'title': 'Amplitude:', 'name': 'amplitude', 'type': 'float', 'value': 1., }, + {'title': 'Frequency:', 'name': 'frequency', 'type': 'float', 'value': 10., }, + ]}, + ]}, + {'title': 'Clock Settings:', 'name': 'clock_settings', 'type': 'group', 'children': [ + {'title': 'Nsamples:', 'name': 'Nsamples', 'type': 'int', 'value': 1000, 'default': 1000, 'min': 1}, + {'title': 'Frequency:', 'name': 'frequency', 'type': 'float', 'value': 1000., 'default': 1000., + 'min': 0., 'suffix': 'Hz'}, + {'title': 'Repetition?:', 'name': 'repetition', 'type': 'bool', 'value': False, }, + ] + }, + {'title': 'AI Channels:', 'name': 'ai_channels', 'type': 'groupai', + 'limits': DAQmx.get_NIDAQ_channels(source_type='Analog_Input')}, + {'title': 'AO Channels:', 'name': 'ao_channels', 'type': 'groupao', + 'limits': DAQmx.get_NIDAQ_channels(source_type='Analog_Output')}, + {'title': 'DO Channels:', 'name': 'do_channels', 'type': 'groupdo', + 'limits': DAQmx.get_NIDAQ_channels(source_type='Digital_Output')}, + {'title': 'DI Channels:', 'name': 'di_channels', 'type': 'groupdi', + 'limits': DAQmx.get_NIDAQ_channels(source_type='Digital_Input')}, + {'title': 'Counter Settings:', 'name': 'counter_settings', 'type': 'group', 'visible': True, 'children': [ + {'title': 'Counting time (ms):', 'name': 'counting_time', 'type': 'float', 'value': 100., + 'default': 100., 'min': 0.}, + {'title': 'Counting Channels:', 'name': 'counter_channels', 'type': 'groupcounter', + 'limits': DAQmx.get_NIDAQ_channels(source_type='Counter')}, + ]}, + {'title': 'Trigger Settings:', 'name': 'trigger_settings', 'type': 'group', 'visible': True, 'children': [ + {'title': 'Enable?:', 'name': 'enable', 'type': 'bool', 'value': False, }, + {'title': 'Trigger Source:', 'name': 'trigger_channel', 'type': 'list', + 'limits': DAQmx.getTriggeringSources()}, + {'title': 'Edge type:', 'name': 'edge', 'type': 'list', 'limits': Edge.names(), 'visible': False}, + {'title': 'Level:', 'name': 'level', 'type': 'float', 'value': 1., 'visible': False} + ]} + ] def __init__(self): super().__init__() @@ -314,8 +327,8 @@ def commit_settings(self, param: Parameter): # self.update_task() if param.name() == 'NIDAQ_type': - self.update_NIDAQ_channels(param.value()) - if param.value() == DAQ_NIDAQ_source(0).name: #analog input + self.controller.update_NIDAQ_channels(param.value()) + if param.value() == DAQ_NIDAQ_source(0).name: # analog input self.settings.child('clock_settings').show() self.settings.child('ai_channels').show() self.settings.child('ao_channels').hide() @@ -324,7 +337,7 @@ def commit_settings(self, param: Parameter): self.settings.child('do_channels').hide() self.settings.child('di_channels').hide() - elif param.value() == DAQ_NIDAQ_source(1).name: #counter input + elif param.value() == DAQ_NIDAQ_source(1).name: # counter input self.settings.child('clock_settings').hide() self.settings.child('ai_channels').hide() self.settings.child('ao_channels').hide() @@ -333,7 +346,7 @@ def commit_settings(self, param: Parameter): self.settings.child('do_channels').hide() self.settings.child('di_channels').hide() - elif param.value() == DAQ_NIDAQ_source(2).name: #analog output + elif param.value() == DAQ_NIDAQ_source(2).name: # analog output self.settings.child('clock_settings').show() self.settings.child('ai_channels').hide() self.settings.child('ao_channels').show() @@ -342,7 +355,7 @@ def commit_settings(self, param: Parameter): self.settings.child('do_channels').hide() self.settings.child('di_channels').hide() - elif param.value() == DAQ_NIDAQ_source(3).name: # digital output + elif param.value() == DAQ_NIDAQ_source(3).name: # digital output self.settings.child('clock_settings').show() self.settings.child('ai_channels').hide() self.settings.child('ao_channels').hide() @@ -351,7 +364,7 @@ def commit_settings(self, param: Parameter): self.settings.child('do_channels').show() self.settings.child('di_channels').hide() - elif param.value() == DAQ_NIDAQ_source(4).name: # Digital_Input + elif param.value() == DAQ_NIDAQ_source(4).name: # Digital_Input self.settings.child('clock_settings').show() self.settings.child('ai_channels').hide() self.settings.child('ao_channels').show() @@ -634,18 +647,17 @@ class DAQ_NIDAQmx_Actuator(DAQ_Move_base, DAQ_NIDAQmx_base): is_multiaxes = False # set to True if this plugin is controlled for a multiaxis controller (with a unique communication link) stage_names = [] # "list of strings of the multiaxes - params = DAQ_NIDAQmx_base.params +[ - # elements to be added here as dicts in order to control your custom stage - ############ - {'title': 'MultiAxes:', 'name': 'multiaxes', 'type': 'group', 'visible': is_multiaxes, - 'children': [ - {'title': 'is Multiaxes:', 'name': 'ismultiaxes', 'type': 'bool', 'value': is_multiaxes, - 'default': False}, - {'title': 'Status:', 'name': 'multi_status', 'type': 'list', 'value': 'Master', - 'limits': ['Master', 'Slave']}, - {'title': 'Axis:', 'name': 'axis', 'type': 'list', 'limits': stage_names}, - - ]}] + actuator_params + params = DAQ_NIDAQmx_base.params + [ + # elements to be added here as dicts in order to control your custom stage + ############ + {'title': 'MultiAxes:', 'name': 'multiaxes', 'type': 'group', 'visible': is_multiaxes, + 'children': [ + {'title': 'is Multiaxes:', 'name': 'ismultiaxes', 'type': 'bool', 'value': is_multiaxes, + 'default': False}, + {'title': 'Status:', 'name': 'multi_status', 'type': 'list', 'value': 'Master', + 'limits': ['Master', 'Slave']}, + {'title': 'Axis:', 'name': 'axis', 'type': 'list', 'limits': stage_names}, + ]}] + actuator_params def __init__(self, parent=None, params_state=None, control_type="Actuator"): DAQ_Move_base.__init__(self, parent, params_state) # defines settings attribute and various other methods @@ -671,7 +683,6 @@ def get_actuator_value(self) -> DataActuator: self.emit_status(ThreadCommand('check_position', [pos])) return pos - def commit_settings(self, param): """ | Activate any parameter changes on the PI_GCS2 hardware. @@ -721,7 +732,7 @@ def ini_stage(self, controller=None): # check whether this stage is controlled by a multiaxe controller (to be defined for each plugin) # if multiaxes then init the controller here if Master state otherwise use external controller if self.settings['multiaxes', 'ismultiaxes'] and self.settings['multiaxes', - 'multi_status'] == "Slave": + 'multi_status'] == "Slave": if controller is None: raise Exception('no controller has been defined externally while this axe is a slave one') else: @@ -757,13 +768,12 @@ def calulate_waveform(self, value): amp = self.settings['ao_settings', 'waveform_settings', 'amplitude'] offset = self.settings['ao_settings', 'waveform_settings', 'offset'] if waveform == 'Sinus': - values = offset + amp * np.sin(2*np.pi*freq0*time) + values = offset + amp * np.sin(2 * np.pi * freq0 * time) elif waveform == 'Ramp': values = offset + amp * np.linspace(0, 1, Nsamples) return values - def move_Abs(self, position): """ Move the actuator to the absolute target defined by position @@ -776,7 +786,7 @@ def move_Abs(self, position): position = self.set_position_with_scaling(position) # apply scaling if the user specified one if self.settings['NIDAQ_type'] == 'Analog_Output': self.settings.child('ao_settings', 'waveform_settings', - self.settings['ao_settings', 'cont_param']).setValue(position) + self.settings['ao_settings', 'cont_param']).setValue(position) values = self.calulate_waveform(position) self.target_position = position @@ -813,7 +823,7 @@ def move_Rel(self, position): self.target_position = position + self.current_position if self.settings['NIDAQ_type'] == 'Analog_Output': self.settings.child('ao_settings', 'waveform_settings', - self.settings['ao_settings', 'cont_param']).setValue(self.target_position) + self.settings['ao_settings', 'cont_param']).setValue(self.target_position) values = self.calulate_waveform(self.target_position) @@ -860,4 +870,3 @@ def stop_motion(self): self.emit_status(ThreadCommand('Update_Status', ['Some info you want to log'])) self.move_done() # to let the interface know the actuator stopped ############################## - From 7d44cc55902245213c45c12056921f66c6c6f3cd Mon Sep 17 00:00:00 2001 From: Sebastien Date: Thu, 5 Dec 2024 17:22:08 +0100 Subject: [PATCH 016/140] nidaqmx.task parameters --- .../national_instruments/daq_NIDAQmx.py | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx.py index 5b6e9d9..583013d 100644 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx.py +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx.py @@ -401,13 +401,13 @@ def update_task(self): self.channels = self.get_channels_from_settings() self.clock_settings = ClockSettings(frequency=self.settings['clock_settings', 'frequency'], Nsamples=self.settings['clock_settings', 'Nsamples'], - repetition=self.live,) + edge=Edge.Rising, + repetition=self.live, ) self.trigger_settings = \ TriggerSettings(trig_source=self.settings['trigger_settings', 'trigger_channel'], enable=self.settings['trigger_settings', 'enable'], - edge=self.settings['trigger_settings', 'edge'], - level=self.settings['trigger_settings', 'level'],) - + edge=Edge[self.settings['trigger_settings', 'edge']], + level=self.settings['trigger_settings', 'level'], ) if not not self.channels: super().update_task(self.channels, self.clock_settings, trigger_settings=self.trigger_settings) @@ -422,20 +422,21 @@ def get_channels_from_settings(self): source='Analog_Input', analog_type=analog_type, value_min=channel['voltage_settings', 'volt_min'], value_max=channel['voltage_settings', 'volt_max'], - termination=channel['termination'],)) + termination=DAQ_termination[channel['termination']], )) elif analog_type == 'Current': channels.append(AIChannel(name=channel.opts['title'], source='Analog_Input', analog_type=analog_type, value_min=channel['current_settings', 'curr_min'], value_max=channel['current_settings', 'curr_max'], - termination=channel['termination'], )) + termination=DAQ_termination[channel['termination']], )) elif analog_type == 'Thermocouple': channels.append(AIThermoChannel(name=channel.opts['title'], - source='Analog_Input', analog_type=analog_type, - value_min=channel['thermoc_settings', 'T_min'], - value_max=channel['thermoc_settings', 'T_max'], - termination=channel['termination'], - thermo_type=channel['thermoc_settings', 'thermoc_type'],)) + source='Analog_Input', analog_type=analog_type, + value_min=channel['thermoc_settings', 'T_min'], + value_max=channel['thermoc_settings', 'T_max'], + termination=channel['termination'], + thermo_type=DAQ_thermocouples[ + channel['thermoc_settings', 'thermoc_type']], )) elif self.settings['NIDAQ_type'] == DAQ_NIDAQ_source(1).name: # counter input for channel in self.settings.child('counter_settings', 'counter_channels').children(): From 31c5d6a4c0f3ac3146e551f49677a057f394331c Mon Sep 17 00:00:00 2001 From: Sebastien Date: Thu, 5 Dec 2024 17:26:58 +0100 Subject: [PATCH 017/140] PEP + syntax --- .../national_instruments/daq_NIDAQmx.py | 58 ++++++++----------- 1 file changed, 23 insertions(+), 35 deletions(-) diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx.py index 583013d..8641450 100644 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx.py +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx.py @@ -322,9 +322,9 @@ def commit_settings(self, param: Parameter): -------- update_NIDAQ_channels, update_task, DAQ_NIDAQ_source, refresh_hardware """ - # if param.name() == 'NIDAQ_devices': - # self.update_NIDAQ_channels() - # self.update_task() + if param.name() == 'NIDAQ_devices': + self.controller.update_NIDAQ_channels() + self.update_task() if param.name() == 'NIDAQ_type': self.controller.update_NIDAQ_channels(param.value()) @@ -376,7 +376,7 @@ def commit_settings(self, param: Parameter): elif param.name() == 'refresh_hardware': if param.value(): - self.refresh_hardware() + self.controller.refresh_hardware() QtWidgets.QApplication.processEvents() self.settings.child('refresh_hardware').setValue(False) @@ -409,8 +409,8 @@ def update_task(self): edge=Edge[self.settings['trigger_settings', 'edge']], level=self.settings['trigger_settings', 'level'], ) if not not self.channels: - super().update_task(self.channels, self.clock_settings, trigger_settings=self.trigger_settings) - + logger.info("not not self channel") + self.controller.update_task(self.channels, self.clock_settings, trigger_settings=self.trigger_settings) def get_channels_from_settings(self): channels = [] @@ -467,8 +467,7 @@ def stop(self): if not not self.timer: self.timer.stop() QtWidgets.QApplication.processEvents() - DAQmx.stop(self) - + self.controller.stop() class DAQ_NIDAQmx_Viewer(DAQ_Viewer_base, DAQ_NIDAQmx_base): """ @@ -488,13 +487,14 @@ class DAQ_NIDAQmx_Viewer(DAQ_Viewer_base, DAQ_NIDAQmx_base): params = viewer_params + DAQ_NIDAQmx_base.params def __init__(self, parent=None, params_state=None, control_type="0D"): - DAQ_Viewer_base.__init__(self, parent, params_state) #defines settings attribute and various other methods + DAQ_Viewer_base.__init__(self, parent, params_state) # defines settings attribute and various other methods DAQ_NIDAQmx_base.__init__(self) self.live = False self.control_type = control_type # could be "0D", "1D" or "Actuator" if self.control_type == "0D": - self.settings.child('NIDAQ_type').setLimits(['Analog_Input', 'Counter', 'Digital_Input']) # analog input and counter + self.settings.child('NIDAQ_type').setLimits( + ['Analog_Input', 'Counter', 'Digital_Input']) # analog input and counter elif self.control_type == "1D": self.settings.child('NIDAQ_type').setLimits(['Analog_Input']) elif self.control_type == "Actuator": @@ -508,12 +508,10 @@ def __init__(self, parent=None, params_state=None, control_type="0D"): self.timer.timeout.connect(self.counter_done) def stop(self): - try: - self.controller['ai'].task.StopTask() - except: - pass - ############################## - + """Stop the current grab hardware wise if necessary""" + self.controller.stop() + self.live = False + self.emit_status(ThreadCommand('Update_Status', ['Acquisition stopped.'])) return '' def commit_settings(self, param): @@ -535,7 +533,7 @@ def commit_settings(self, param): device = param.opts['title'].split('/')[0] self.settings.child('clock_settings', 'frequency').setOpts(max=self.getAIMaxRate(device)) - ranges = self.getAIVoltageRange(device) + ranges = self.controller.getAIVoltageRange(device) param.child('voltage_settings', 'volt_min').setOpts(limits=[r[0] for r in ranges]) param.child('voltage_settings', 'volt_max').setOpts(limits=[r[1] for r in ranges]) @@ -551,15 +549,10 @@ def ini_detector(self, controller=None): """ self.status.update(edict(initialized=False, info="", x_axis=None, y_axis=None, controller=None)) try: - if self.settings['controller_status'] == "Slave": - if controller is None: - raise Exception('no controller has been defined externally while this detector is a slave one') - else: - self.controller = 'A Nidaqmx task' - else: - self.update_task() + self.controller = self.ini_detector_init(controller, DAQmx()) + self.update_task() - #actions to perform in order to set properly the settings tree options + # actions to perform in order to set properly the settings tree options self.commit_settings(self.settings.child('NIDAQ_type')) self.status.info = "Plugin Initialized" @@ -589,6 +582,7 @@ def grab_data(self, Naverage=1, **kwargs): DAQ_NIDAQ_source """ update = False + if 'live' in kwargs: if kwargs['live'] != self.live: update = True @@ -596,13 +590,8 @@ def grab_data(self, Naverage=1, **kwargs): if update: self.update_task() - if self.settings['NIDAQ_type'] == DAQ_NIDAQ_source(0).name: #analog input - if self.c_callback is None: - self.register_callback(self.emit_data) - elif self.settings['NIDAQ_type'] == DAQ_NIDAQ_source(1).name: #counter input - self.timer.start(self.settings['counter_settings', 'counting_time']) - self.waitTaskDone() - self.start() + if self.controller.task is None: + self.update_task() def emit_data(self, taskhandle, status, callbackdata): channels_name = [ch.name for ch in self.channels] @@ -630,11 +619,10 @@ def counter_done(self): data_counter = self.readCounter(len(self.channels), self.settings['counter_settings', 'counting_time'] * 1e-3) self.data_grabed_signal.emit([DataFromPlugins(name='NI Counter', data=[data_counter / 1e-3], dim='Data0D', - labels=channels_name,)]) - #y_axis=Axis(label='Count Number', units='1/s'))]) + labels=channels_name, )]) + # y_axis=Axis(label='Count Number', units='1/s'))]) self.task.StopTask() - class DAQ_NIDAQmx_Actuator(DAQ_Move_base, DAQ_NIDAQmx_base): """ Wrapper object to access the Mock fonctionnalities, similar wrapper for all controllers. From 612e403179f98b3aef8b7493fda4ccf3d76ea69f Mon Sep 17 00:00:00 2001 From: Sebastien Date: Thu, 5 Dec 2024 17:30:09 +0100 Subject: [PATCH 018/140] Callback method --- .../national_instruments/daq_NIDAQmx.py | 37 ++++++++++--------- 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx.py index 8641450..558b1a9 100644 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx.py +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx.py @@ -593,26 +593,27 @@ def grab_data(self, Naverage=1, **kwargs): if self.controller.task is None: self.update_task() - def emit_data(self, taskhandle, status, callbackdata): - channels_name = [ch.name for ch in self.channels] + self.controller.register_callback(self.emit_data, "Nsamples", self.clock_settings.Nsamples) + self.controller.start() - data_tot = [] - data = self.readAnalog(len(self.channels), self.clock_settings) - N = self.clock_settings.Nsamples + def emit_data(self, task_handle, every_n_samples_event_type, number_of_samples, callback_data): + channels_names = [ch.name for ch in self.channels] + # channels_ai_names = [ch.name for ch in self.channels if ch.source == 'Analog_Input'] + data_from_task = self.controller.task.read(self.settings['nsamplestoread'], timeout=20.0) if self.control_type == "0D": - for ind in range(len(self.channels)): - data_tot.append(np.array([np.mean(data[ind*N:(ind+1)*N])])) - self.data_grabed_signal.emit([DataFromPlugins(name='NI AI', data=data_tot, dim='Data0D', - labels=channels_name)]) - else: - for ind in range(len(self.channels)): - data_tot.append(data[ind*N:(ind+1)*N]) - self.data_grabed_signal.emit([ - DataFromPlugins(name='NI AI', data=data_tot, dim='Data1D', - labels=channels_name, - axes=[Axis(data=np.linspace(0, N / self.clock_settings.frequency, N), - index=0)])]) - return 0 #mandatory for the PyDAQmx callback + if not len(self.controller.task.channels.channel_names) != 1: + data_dfp = [np.array(data_from_task)] + else: + data_dfp = list(map(np.array, data_from_task)) + dte = DataToExport(name='NIDAQmx', + data=[DataFromPlugins(name='NI Analog Input', + data=data_dfp, + dim=f'Data{self.settings.child("display").value()}', + labels=channels_names + ), + ]) + self.dte_signal.emit(dte) + return 0 # mandatory for the NIDAQmx callback def counter_done(self): channels_name = [ch.name for ch in self.channels] From 697d4bed5a24c366a37738c811939ce0ca544c31 Mon Sep 17 00:00:00 2001 From: Sebastien Date: Thu, 5 Dec 2024 17:31:47 +0100 Subject: [PATCH 019/140] PEP + syntax --- .../national_instruments/daq_NIDAQmx.py | 24 +++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx.py index 558b1a9..d5f2e80 100644 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx.py +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx.py @@ -307,7 +307,6 @@ def __init__(self): self.clock_settings = None self.trigger_settings = None self.live = False - self.refresh_hardware() def commit_settings(self, param: Parameter): """ @@ -490,6 +489,7 @@ def __init__(self, parent=None, params_state=None, control_type="0D"): DAQ_Viewer_base.__init__(self, parent, params_state) # defines settings attribute and various other methods DAQ_NIDAQmx_base.__init__(self) + self.Naverage = None self.live = False self.control_type = control_type # could be "0D", "1D" or "Actuator" if self.control_type == "0D": @@ -500,9 +500,10 @@ def __init__(self, parent=None, params_state=None, control_type="0D"): elif self.control_type == "Actuator": self.settings.child('NIDAQ_type').setLimits(['Analog_Output']) + self.settings.child('ao_settings').hide() self.settings.child('ao_channels').hide() - #timer used for the counter + # timer used for the counter self.timer = QtCore.QTimer() self.timer.setSingleShot(True) self.timer.timeout.connect(self.counter_done) @@ -547,7 +548,6 @@ def ini_detector(self, controller=None): -------- daq_utils.ThreadCommand """ - self.status.update(edict(initialized=False, info="", x_axis=None, y_axis=None, controller=None)) try: self.controller = self.ini_detector_init(controller, DAQmx()) self.update_task() @@ -555,16 +555,16 @@ def ini_detector(self, controller=None): # actions to perform in order to set properly the settings tree options self.commit_settings(self.settings.child('NIDAQ_type')) - self.status.info = "Plugin Initialized" - self.status.initialized = True - self.status.controller = controller - return self.status + info = "Plugin Initialized" + initialized = True + return info, initialized except Exception as e: + logger.info(traceback.format_exc()) self.emit_status(ThreadCommand('Update_Status', [str(e), 'log'])) - self.status.info = str(e) - self.status.initialized = False - return self.status + info = str(e) + initialized = False + return info, initialized def grab_data(self, Naverage=1, **kwargs): """ @@ -587,6 +587,10 @@ def grab_data(self, Naverage=1, **kwargs): if kwargs['live'] != self.live: update = True self.live = kwargs['live'] + + if Naverage != self.Naverage: + self.Naverage = Naverage + update = True if update: self.update_task() From 7a317b7eaf8184b7680af20415bf78d9d69e8e92 Mon Sep 17 00:00:00 2001 From: Sebastien Date: Thu, 5 Dec 2024 17:35:13 +0100 Subject: [PATCH 020/140] Optimizing plugin structure by separating Move & Viewer from NIDAQmx base --- .../plugins_0D/daq_0Dviewer_NIDAQmx.py | 15 +- .../national_instruments/daq_NIDAQmx.py | 19 +- .../national_instruments/daq_NIDAQmx_Move.py | 248 ++++++++++++++++++ .../daq_NIDAQmx_Viewer.py | 179 +++++++++++++ 4 files changed, 440 insertions(+), 21 deletions(-) create mode 100644 src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx_Move.py create mode 100644 src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx_Viewer.py diff --git a/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_NIDAQmx.py b/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_NIDAQmx.py index 3d3b1cf..5f3626c 100644 --- a/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_NIDAQmx.py +++ b/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_NIDAQmx.py @@ -1,13 +1,14 @@ -import numpy as np -from pymodaq.utils.daq_utils import ThreadCommand -from pymodaq.utils.data import DataWithAxes, DataToExport, DataSource, DataFromPlugins -from pymodaq.control_modules.viewer_utility_classes import DAQ_Viewer_base, comon_parameters, main -from pymodaq.utils.parameter import Parameter -from pymodaq_plugins_daqmx.hardware.national_instruments.daqmxni import * +from pymodaq.control_modules.viewer_utility_classes import main +from pymodaq.control_modules.viewer_utility_classes import comon_parameters as viewer_params +from pymodaq_plugins_daqmx.hardware.national_instruments.daqmxni import AIChannel, AIThermoChannel, DAQmx, nidaqmx,\ + DAQ_termination, DAQ_thermocouples +from pymodaq_plugins_daqmx.hardware.national_instruments.daq_NIDAQmx import DAQ_NIDAQmx_base +from pymodaq_plugins_daqmx.hardware.national_instruments.daq_NIDAQmx_Viewer import DAQ_NIDAQmx_Viewer from pymodaq.utils.logger import set_logger, get_module_name logger = set_logger(get_module_name(__file__)) -class DAQ_0DViewer_NIDAQmx(DAQ_Viewer_base): + +class DAQ_0DViewer_NIDAQmx(DAQ_NIDAQmx_Viewer): """ Plugin for a 0D data visualization & acquisition with various NI modules plugged in a NI cDAQ. """ diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx.py index d5f2e80..c08509f 100644 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx.py +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx.py @@ -1,20 +1,11 @@ -from qtpy import QtWidgets, QtCore -from qtpy.QtCore import Signal, QThread -from pymodaq.utils.daq_utils import ThreadCommand, getLineInfo -from pymodaq.utils.data import DataFromPlugins, Axis, DataActuator, DataToExport -import numpy as np -from pymodaq.control_modules.viewer_utility_classes import DAQ_Viewer_base -from pymodaq.control_modules.move_utility_classes import DAQ_Move_base -from easydict import EasyDict as edict - -from pymodaq.control_modules.viewer_utility_classes import comon_parameters as viewer_params -from pymodaq.control_modules.move_utility_classes import comon_parameters_fun as actuator_params - +from qtpy import QtWidgets +from qtpy.QtCore import Signal +from pymodaq.utils.logger import set_logger, get_module_name from pymodaq.utils.parameter import Parameter from pymodaq.utils.parameter.pymodaq_ptypes import registerParameterType, GroupParameter - -from .daqmx import DAQmx, DAQ_analog_types, DAQ_thermocouples, DAQ_termination, Edge, DAQ_NIDAQ_source, \ +from .daqmxni import DAQmx, DAQ_analog_types, DAQ_thermocouples, DAQ_termination, Edge, DAQ_NIDAQ_source, \ ClockSettings, AIChannel, Counter, AIThermoChannel, AOChannel, TriggerSettings, DOChannel, DIChannel +logger = set_logger(get_module_name(__file__)) class ScalableGroupAI(GroupParameter): diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx_Move.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx_Move.py new file mode 100644 index 0000000..35d699c --- /dev/null +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx_Move.py @@ -0,0 +1,248 @@ +import numpy as np +from easydict import EasyDict as edict +from qtpy import QtWidgets +from pymodaq.control_modules.move_utility_classes import DAQ_Move_base, comon_parameters as actuator_params +from pymodaq_plugins_daqmx.hardware.national_instruments.daq_NIDAQmx import DAQ_NIDAQmx_base +from pymodaq.utils.daq_utils import ThreadCommand, getLineInfo +from pymodaq.utils.data import DataActuator +from pymodaq.utils.logger import set_logger, get_module_name +logger = set_logger(get_module_name(__file__)) + + +class DAQ_NIDAQmx_Actuator(DAQ_Move_base, DAQ_NIDAQmx_base): + """ + Wrapper object to access the Mock fonctionnalities, similar wrapper for all controllers. + + =============== ============== + **Attributes** **Type** + *params* dictionnary + =============== ============== + """ + _controller_units = 'Volts' + is_multiaxes = False # set to True if this plugin is controlled for a multiaxis controller (with a unique communication link) + stage_names = [] # "list of strings of the multiaxes + + params = DAQ_NIDAQmx_base.params + [ + # elements to be added here as dicts in order to control your custom stage + ############ + {'title': 'MultiAxes:', 'name': 'multiaxes', 'type': 'group', 'visible': is_multiaxes, + 'children': [ + {'title': 'is Multiaxes:', 'name': 'ismultiaxes', 'type': 'bool', 'value': is_multiaxes, + 'default': False}, + {'title': 'Status:', 'name': 'multi_status', 'type': 'list', 'value': 'Master', + 'limits': ['Master', 'Slave']}, + {'title': 'Axis:', 'name': 'axis', 'type': 'list', 'limits': stage_names}, + + ]}] + actuator_params + + def __init__(self, parent=None, params_state=None, control_type="Actuator"): + DAQ_Move_base.__init__(self, parent, params_state) # defines settings attribute and various other methods + DAQ_NIDAQmx_base.__init__(self) + + self.control_type = "Actuator" # could be "0D", "1D" or "Actuator" + self.settings.child('NIDAQ_type').setLimits(['Analog_Output', 'Digital_Output']) + + self.settings.child('clock_settings', 'Nsamples').setValue(1) + + def get_actuator_value(self) -> DataActuator: + """Get the current position from the hardware with scaling conversion. + + Returns + ------- + float: The position obtained after scaling conversion. + """ + + pos = self.target_position + ## + + pos = self.get_position_with_scaling(pos) + self.emit_status(ThreadCommand('check_position', [pos])) + return pos + + def commit_settings(self, param): + """ + | Activate any parameter changes on the PI_GCS2 hardware. + | + | Called after a param_tree_changed signal from DAQ_Move_main. + + """ + + DAQ_NIDAQmx_base.commit_settings(self, param) + if param.name() == 'waveform': + if param.value() == 'DC': + self.settings.child('ao_settings', 'cont_param').setValue('offset') + self.settings.child('ao_settings', 'cont_param').show(not param.value() == 'DC') + self.settings.child('ao_settings', 'waveform_settings').show(not param.value() == 'DC') + + if param.parent() is not None: + if param.parent().name() == 'ao_channels': + device = param.opts['title'].split('/')[0] + self.settings.child('clock_settings', 'frequency').setOpts(max=self.getAOMaxRate(device)) + + ranges = self.getAOVoltageRange(device) + param.child('voltage_settings', 'volt_min').setOpts(limits=[r[0] for r in ranges]) + param.child('voltage_settings', 'volt_max').setOpts(limits=[r[1] for r in ranges]) + + 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 + ------- + self.status (edict): with initialization status: three fields: + * info (str) + * controller (object) initialized controller + *initialized: (bool): False if initialization failed otherwise True + """ + + try: + # initialize the stage and its controller status + # controller is an object that may be passed to other instances of DAQ_Move_Mock in case + # of one controller controlling multiactuators (or detector) + + self.status.update(edict(info="", controller=None, initialized=False)) + + # check whether this stage is controlled by a multiaxe controller (to be defined for each plugin) + # if multiaxes then init the controller here if Master state otherwise use external controller + if self.settings['multiaxes', 'ismultiaxes'] and self.settings['multiaxes', + 'multi_status'] == "Slave": + if controller is None: + raise Exception('no controller has been defined externally while this axe is a slave one') + else: + self.controller = controller + else: + self.controller = 'A Nidaqmx task' + self.update_task() + + # actions to perform in order to set properly the settings tree options + self.commit_settings(self.settings.child('NIDAQ_type')) + + self.status.info = "Plugin Initialized" + self.status.controller = self.controller + self.status.initialized = True + return self.status + + except Exception as e: + self.emit_status(ThreadCommand('Update_Status', [getLineInfo() + str(e), 'log'])) + self.status.info = getLineInfo() + str(e) + self.status.initialized = False + return self.status + + def calulate_waveform(self, value): + waveform = self.settings['ao_settings', 'waveform'] + if waveform == 'DC': + values = np.array([value]) + else: + Nsamples = self.settings['clock_settings', 'Nsamples'] + freq = self.settings['clock_settings', 'frequency'] + time = np.linspace(0, Nsamples / freq, Nsamples, endpoint=False) + + freq0 = self.settings['ao_settings', 'waveform_settings', 'frequency'] + amp = self.settings['ao_settings', 'waveform_settings', 'amplitude'] + offset = self.settings['ao_settings', 'waveform_settings', 'offset'] + if waveform == 'Sinus': + values = offset + amp * np.sin(2 * np.pi * freq0 * time) + elif waveform == 'Ramp': + values = offset + amp * np.linspace(0, 1, Nsamples) + + return values + + def move_Abs(self, position): + """ Move the actuator to the absolute target defined by position + + Parameters + ---------- + position: (flaot) value of the absolute target positioning + """ + + position = self.check_bound(position) # if user checked bounds, the defined bounds are applied here + position = self.set_position_with_scaling(position) # apply scaling if the user specified one + if self.settings['NIDAQ_type'] == 'Analog_Output': + self.settings.child('ao_settings', 'waveform_settings', + self.settings['ao_settings', 'cont_param']).setValue(position) + values = self.calulate_waveform(position) + self.target_position = position + + self.stop() + + if len(values) == 1: + self.writeAnalog(len(values), 1, values, autostart=True) + self.current_position = self.check_position() + self.move_done() + else: + if self.c_callback is None: + self.register_callback(self.move_done_callback) + self.writeAnalog(len(values), 1, values, autostart=False) + self.task.StartTask() + elif self.settings['NIDAQ_type'] == 'Digital_Output': + self.writeDigital(1, np.array([position], dtype=np.uint8), autostart=True) + + def move_done_callback(self, taskhandle, status, callbackdata): + self.current_position = self.check_position() + QtWidgets.QApplication.processEvents() + self.move_done() + self.task.StopTask() + return 0 + + def move_Rel(self, position): + """ Move the actuator to the relative target actuator value defined by position + + Parameters + ---------- + position: (flaot) value of the relative target positioning + """ + + position = self.check_bound(self.current_position + position) - self.current_position + self.target_position = position + self.current_position + if self.settings['NIDAQ_type'] == 'Analog_Output': + self.settings.child('ao_settings', 'waveform_settings', + self.settings['ao_settings', 'cont_param']).setValue(self.target_position) + + values = self.calulate_waveform(self.target_position) + + self.stop() + + if len(values) == 1: + self.writeAnalog(len(values), 1, values, autostart=True) + self.current_position = self.check_position() + self.move_done() + else: + if self.c_callback is None: + self.register_callback(self.move_done_callback) + self.writeAnalog(len(values), 1, values, autostart=False) + self.task.StartTask() + elif self.settings['NIDAQ_type'] == 'Digital_Output': + self.writeDigital(1, np.array([self.target_position], dtype=np.uint8), autostart=True) + + def move_Home(self): + """ + Send the update status thread command. + See Also + -------- + daq_utils.ThreadCommand + """ + + self.stop() + + if self.c_callback is None: + self.register_callback(self.move_done_callback) + self.writeAnalog(1, 1, np.array([0.])) + self.task.StartTask() + + def stop_motion(self): + """ + Call the specific move_done function (depending on the hardware). + + See Also + -------- + move_done + """ + + ## TODO for your custom plugin + self.stop() + self.emit_status(ThreadCommand('Update_Status', ['Some info you want to log'])) + self.move_done() # to let the interface know the actuator stopped + ############################## diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx_Viewer.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx_Viewer.py new file mode 100644 index 0000000..afc3364 --- /dev/null +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx_Viewer.py @@ -0,0 +1,179 @@ +import datetime +import numpy as np +import traceback +from qtpy import QtCore +from .daqmxni import DAQmx +from pymodaq_plugins_daqmx.hardware.national_instruments.daq_NIDAQmx import DAQ_NIDAQmx_base +from pymodaq.control_modules.viewer_utility_classes import DAQ_Viewer_base, comon_parameters as viewer_params +from pymodaq.utils.daq_utils import ThreadCommand +from pymodaq.utils.data import DataFromPlugins, DataToExport +from pymodaq.utils.logger import set_logger, get_module_name +logger = set_logger(get_module_name(__file__)) + + +class DAQ_NIDAQmx_Viewer(DAQ_Viewer_base, DAQ_NIDAQmx_base): + """ + ==================== ======================== + **Attributes** **Type** + *data_grabed_signal* instance of Signal + *params* dictionnary list + *task* + ==================== ======================== + + See Also + -------- + refresh_hardware + """ + + live_mode_available = True + params = viewer_params + DAQ_NIDAQmx_base.params + + def __init__(self, parent=None, params_state=None, control_type="0D"): + DAQ_Viewer_base.__init__(self, parent, params_state) # defines settings attribute and various other methods + DAQ_NIDAQmx_base.__init__(self) + + self.Naverage = None + self.live = False + self.control_type = control_type # could be "0D", "1D" or "Actuator" + if self.control_type == "0D": + self.settings.child('NIDAQ_type').setLimits( + ['Analog_Input', 'Counter', 'Digital_Input']) # analog input and counter + elif self.control_type == "1D": + self.settings.child('NIDAQ_type').setLimits(['Analog_Input']) + elif self.control_type == "Actuator": + self.settings.child('NIDAQ_type').setLimits(['Analog_Output']) + + self.settings.child('ao_settings').hide() + self.settings.child('ao_channels').hide() + + # timer used for the counter + self.timer = QtCore.QTimer() + self.timer.setSingleShot(True) + self.timer.timeout.connect(self.counter_done) + + def stop(self): + """Stop the current grab hardware wise if necessary""" + self.controller.stop() + self.live = False + try: + self.controller.task.StopTask() + logger.info("StopTask() done") + except Exception: + pass + ############################## + + self.emit_status(ThreadCommand('Update_Status', ['Acquisition stopped.'])) + return '' + + def commit_settings(self, param): + """ + Activate the parameters changes in the hardware. + + =============== ================================ =========================== + **Parameters** **Type** **Description** + *param* instance of pyqtgraph.parameter the parameter to activate + =============== ================================ =========================== + + See Also + -------- + update_NIDAQ_channels, update_task, DAQ_NIDAQ_source, refresh_hardware + """ + + if param.parent() is not None: + if param.parent().name() == 'ai_channels': + device = param.opts['title'].split('/')[0] + self.settings.child('clock_settings', 'frequency').setOpts(max=self.getAIMaxRate(device)) + + ranges = self.controller.getAIVoltageRange(device) + param.child('voltage_settings', 'volt_min').setOpts(limits=[r[0] for r in ranges]) + param.child('voltage_settings', 'volt_max').setOpts(limits=[r[1] for r in ranges]) + + DAQ_NIDAQmx_base.commit_settings(self, param) + + def ini_detector(self, controller=None): + """ + Initialisation procedure of the detector. + + See Also + -------- + daq_utils.ThreadCommand + """ + try: + self.controller = self.ini_detector_init(controller, DAQmx()) + self.update_task() + + # actions to perform in order to set properly the settings tree options + self.commit_settings(self.settings.child('NIDAQ_type')) + + info = "Plugin Initialized" + initialized = True + return info, initialized + + except Exception as e: + logger.info(traceback.format_exc()) + self.emit_status(ThreadCommand('Update_Status', [str(e), 'log'])) + info = str(e) + initialized = False + return info, initialized + + def grab_data(self, Naverage=1, **kwargs): + """ + | grab the current values with NIDAQ profile procedure. + | + | Send the data_grabed_signal once done. + + =============== ======== =============================================== + **Parameters** **Type** **Description** + *Naverage* int Number of values to average + =============== ======== =============================================== + + See Also + -------- + DAQ_NIDAQ_source + """ + update = False + + if 'live' in kwargs: + if kwargs['live'] != self.live: + update = True + self.live = kwargs['live'] + + if Naverage != self.Naverage: + self.Naverage = Naverage + update = True + if update: + self.update_task() + + if self.controller.task is None: + self.update_task() + + self.controller.register_callback(self.emit_data, "Nsamples", self.clock_settings.Nsamples) + self.controller.start() + + def emit_data(self, task_handle, every_n_samples_event_type, number_of_samples, callback_data): + channels_names = [ch.name for ch in self.channels] + # channels_ai_names = [ch.name for ch in self.channels if ch.source == 'Analog_Input'] + data_from_task = self.controller.task.read(self.settings['nsamplestoread'], timeout=20.0) + if self.control_type == "0D": + if not len(self.controller.task.channels.channel_names) != 1: + data_dfp = [np.array(data_from_task)] + else: + data_dfp = list(map(np.array, data_from_task)) + dte = DataToExport(name='NIDAQmx', + data=[DataFromPlugins(name='NI Analog Input', + data=data_dfp, + dim=f'Data{self.settings.child("display").value()}', + labels=channels_names + ), + ]) + self.dte_signal.emit(dte) + return 0 # mandatory for the NIDAQmx callback + + def counter_done(self): + channels_name = [ch.name for ch in self.channels] + data_counter = self.readCounter(len(self.channels), + self.settings['counter_settings', 'counting_time'] * 1e-3) + self.data_grabed_signal.emit([DataFromPlugins(name='NI Counter', data=[data_counter / 1e-3], dim='Data0D', + labels=channels_name, )]) + # y_axis=Axis(label='Count Number', units='1/s'))]) + self.task.StopTask() From 8288ab9abec99fb596291f2fb23d87bb6f6d5270 Mon Sep 17 00:00:00 2001 From: Sebastien Date: Thu, 5 Dec 2024 17:36:36 +0100 Subject: [PATCH 021/140] Attributes & methods from inheritance --- .../plugins_0D/daq_0Dviewer_NIDAQmx.py | 187 ++---------------- 1 file changed, 12 insertions(+), 175 deletions(-) diff --git a/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_NIDAQmx.py b/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_NIDAQmx.py index 5f3626c..266bd0a 100644 --- a/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_NIDAQmx.py +++ b/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_NIDAQmx.py @@ -12,205 +12,42 @@ class DAQ_0DViewer_NIDAQmx(DAQ_NIDAQmx_Viewer): """ Plugin for a 0D data visualization & acquisition with various NI modules plugged in a NI cDAQ. """ + channels_ai: str clock_settings_ai: str dict_device_input: dict live: bool + data_tot: list + Naverage: int + ind_average: int - params = comon_parameters+[ + params = viewer_params + [ {'title': 'Display type:', 'name': 'display', 'type': 'list', 'limits': ['0D', '1D']}, {'title': 'Module ref. :', 'name': 'module', 'type': 'list', 'limits': DAQmx.get_NIDAQ_devices()[1], 'value': DAQmx.get_NIDAQ_devices()[1][0] }, - {'title': 'Analog Source :', 'name': 'ai_srce', 'type': 'list', - 'limits': DAQ_NIDAQ_source.names(), - 'value': DAQ_NIDAQ_source.names()[0] - }, - {'title': 'Analog Type :', 'name': 'ai_type', 'type': 'list', - 'limits': DAQ_analog_types.names(), - 'value': DAQ_analog_types.names()[0] - }, - {'title': 'Min. value:', 'name': 'ai_min', 'type': 'float', 'value': -80e-3, 'min': -1e4}, - {'title': 'Max. value:', 'name': 'ai_max', 'type': 'float', 'value': 80e-3, 'max': 1e4}, - {'title': 'Clock settings:', 'name': 'clock_settings', 'type': 'group', 'children': [ - {'title': 'Frequency Acq.:', 'name': 'frequency', 'type': 'int', 'value': 3, 'min': 1}, - {'title': 'Nsamples:', 'name': 'Nsamples', 'type': 'int', 'value': 10, 'default': 100, 'min': 1}, - {'title': 'Acquisition mode:', 'name': 'acqmode', 'type': 'list', - 'limits': ['Continuous', 'Finite'], 'value': 'Continous'}, - {"title": "Counting channel:", "name": "counter_channel", "type": "list", - "limits": DAQmx.get_NIDAQ_channels(source_type="Counter")}, - ]}, - ] + ] + DAQ_NIDAQmx_base.params def __init__(self, parent=None, params_state=None): super().__init__(parent, params_state) def ini_attributes(self): + super().ini_attributes() self.controller: DAQmx = None self.channels_ai = None self.clock_settings_ai = None self.dict_device_input = None self.live = False # True during a continuous grab - - 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() == "frequency": - self.update_tasks() - - def ini_detector(self, controller=None): - """Detector communication initialization - - Parameters - ---------- - controller: (object) - custom object of a PyMoDAQ plugin (Slave case). None if only one actuator/detector by controller - (Master case) - - Returns - ------- - info: str - initialized: bool - False if initialization failed otherwise True - """ - logger.info("Detector 0D initialized") - - try: - self.controller = self.ini_detector_init(controller, DAQmx()) - self.update_tasks() - initialized = True - info = "DAQ_0D initialized" - self.controller.update_NIDAQ_devices() - self.controller.update_NIDAQ_channels() - logger.info("Detected devices: {}".format(self.controller.devices)) - self.settings.child('devices').setValue(str(self.controller.devices)) - logger.info("Detected channels: {}".format(self.controller.channels)) - self.settings.child('channels').setValue(str(self.controller.channels)) - except Exception as err: - logger.error(err) - initialized = False - info = "Error" - return info, initialized + self.data_tot = None + self.Naverage = 1 + self.ind_average = 0 def close(self): - """Terminate the communication protocol""" + self.live = False + self.controller.stop() self.controller.close() pass - def grab_data(self, Naverage=1, **kwargs): - """Start a grab from the detector - - Parameters - ---------- - Naverage: int - Number of hardware averaging not relevant here. - kwargs: dict - others optionals arguments - """ - self.update_tasks() - - data_from_task = self.controller._task.read() - logger.info("DATA = {}".format(data_from_task)) - - self.emit_data(data_from_task) - - def emit_data(self, data_measurement): - logger.info("OOOOOOOOOOOOO {}".format([chan.name for chan in self.channels_ai])) - # dte = DataToExport(name='NIDAQmx', - # data=[DataFromPlugins(name='NI Analog Input 01', - # data=[np.array([data_measurement[0]])], - # # dim=f'Data{self.settings.child("display").value()}', - # dim=f'Data0D', - # labels=[self.channels_ai[0].name], - # ), - # ]) - dte = DataToExport(name='NIDAQmx', - data=[DataFromPlugins(name='NI Analog Input 02', - data=[np.array([data_measurement[2]])], - # dim=f'Data{self.settings.child("display").value()}', - dim=f'Data0D', - labels=[self.channels_ai[2].name], - ), - ]) - - # # Dictionary linking devices to channel's physical quantities - # self.dict_device_input = {self.controller.devices[1]: [self.channels_ai[0].name, self.channels_ai[1].name], - # self.controller.devices[3]: self.channels_ai[2].name} - # # self.controller.devices #ci_meas_types - # logger.info("DICT DEVICE LABEL {}".format(self.dict_device_input)) - # logger.info(self.dict_device_input.get(self.controller.devices[1])) - # a = 0 - # b = 0 - # gen = [[ai for ai in self.dict_device_input.get(device)] - # for device in self.dict_device_input if not isinstance(self.dict_device_input.get(device), str)] + \ - # [self.dict_device_input.get(device) - # for device in self.dict_device_input if isinstance(self.dict_device_input.get(device), str)] - # - # logger.info("DATA TO EXPORT = {}".format(gen)) - # - # - # - # dte = DataToExport(name='NIDAQmx', - # data=[DataFromPlugins(name=self.dict_device_input[device], - # data=[np.array([data_measurement[i]]) - # for i in range(len(data_measurement))], - # dim='Data0D', - # labels=[ai for ai in self.dict_device_input.get(device)] - # ) for device in self.dict_device_input]) - self.dte_signal.emit(dte) - - def stop(self): - """Stop the current grab hardware wise if necessary""" - self.close() - self.emit_status(ThreadCommand('Update_Status', ['Acquisition stopped.'])) - return '' - - def update_tasks(self): - """Set up the tasks in the NI card.""" - # Create channels - ##################### - ''' - Idée: - for Device in Devices: - self.channels_from_device(Device01) = [AIchannel(),AIchannel()......] - self.channels_from_device(Device02) = [AIthermochannel(),AIthermochannel()......] - Emit by mode (as for keithley) === by device - - ''' - self.channels_ai = [AIThermoChannel(name="cDAQ1Mod1/ai0", - source='Analog_Input', - analog_type='Thermocouple', - value_min=-20., - value_max=1300., - thermo_type=DAQ_thermocouples.K, - ), - AIThermoChannel(name="cDAQ1Mod1/ai1", - source='Analog_Input', - analog_type='Thermocouple', - value_min=-20., - value_max=1300., - thermo_type=DAQ_thermocouples.K, - ), - AIChannel(name="cDAQ1Mod3/ai0", - source='Analog_Input', - analog_type='Voltage', - value_min=-10., - value_max=10., - termination=DAQ_termination.Diff, - ), - ] - self.clock_settings_ai = ClockSettings(frequency=self.settings.child('clock_settings', 'frequency').value(), - Nsamples=self.settings.child('clock_settings', 'Nsamples').value(), - edge=Edge.Rising, - repetition=self.live) - - self.controller.update_task(self.channels_ai, self.clock_settings_ai) - if __name__ == '__main__': """Main section used during development tests""" From e0559a36b98d691f0a657cf364e1e5d52d594714 Mon Sep 17 00:00:00 2001 From: Sebastien Date: Thu, 5 Dec 2024 17:39:21 +0100 Subject: [PATCH 022/140] Main file reworked for callback tests --- .../plugins_0D/daq_0Dviewer_NIDAQmx.py | 153 +++++++++++------- 1 file changed, 93 insertions(+), 60 deletions(-) diff --git a/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_NIDAQmx.py b/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_NIDAQmx.py index 266bd0a..6abe3e5 100644 --- a/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_NIDAQmx.py +++ b/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_NIDAQmx.py @@ -51,74 +51,107 @@ def close(self): if __name__ == '__main__': """Main section used during development tests""" - main_file = True + main_file = False if main_file: main(__file__) else: try: print("In main") - import nidaqmx - - logger.info("DAQ Sources names{}".format(DAQ_NIDAQ_source.names())) - logger.info("DAQ Sources members{}".format(DAQ_NIDAQ_source.members())) - logger.info("DAQ Sources names0{}".format(DAQ_NIDAQ_source.names()[0])) - logger.info("DAQ Sources members0{}".format(DAQ_NIDAQ_source.members()[0])) - channels = [AIThermoChannel(name="cDAQ1Mod1/ai0", - source='Analog_Input', - analog_type='Thermocouple', - value_min=-80.0e-3, - value_max=80.0e-3, - termination='Diff', - thermo_type=DAQ_thermocouples.K), - ] - # Create Task - print("DAQ_thermocouples.names ", DAQ_thermocouples.names()) - print("DAQ_thermocouples.K ", nidaqmx.constants.ThermocoupleType.K) - print("DAQ_thermocouples.K type", type(nidaqmx.constants.ThermocoupleType.K)) - print("DAQ_thermocouples.K ", DAQ_thermocouples.K) - print("DAQ_thermocouples.K type ", type(DAQ_thermocouples.K)) - print("channel.thermo_type ", channels[0].thermo_type) - task = nidaqmx.Task() - for channel in channels: - task.ai_channels.add_ai_thrmcpl_chan(physical_channel=channel.name, - # name_to_assign_to_channel="Channel 01", - min_val=channel.value_min, - max_val=channel.value_max, - units=TemperatureUnits.DEG_C, - thermocouple_type=channel.thermo_type, - # cjc_source=CJCSource.BUILT_IN, - # cjc_val="", - # cjc_channel="", - ) - NIDAQ_Devices = nidaqmx.system.System.local().devices - - print("NIDAQ devices ", NIDAQ_Devices) - print("NIDAQ devices names ", NIDAQ_Devices.device_names) - - Chassis = NIDAQ_Devices[0] - Module01 = NIDAQ_Devices[1] - Module02 = NIDAQ_Devices[2] - print("Chassis={}, Module01={}, Module02={}" .format(Chassis, Module01, Module02)) - - # Test resources + import nidaqmx as ni + from pymodaq_plugins_daqmx.hardware.national_instruments.daqmxni import CurrentUnits, TemperatureUnits,\ + VoltageUnits, CJCSource + + # EXPLORE DEVICES + devices = ni.system.System.local().devices + print("devices {}".format(devices)) + print("devices names {}".format(devices.device_names)) + print("devices types {}".format([dev.product_type for dev in devices])) + cdaq = devices[0] + mod1 = cdaq.chassis_module_devices[0] # Equivalent devices[1] + mod2 = devices[2] + mod3 = devices[3] try: - Chassis.self_test_device() - Module01.self_test_device() - Module02.self_test_device() + usb1 = devices[4] except Exception as e: - print("Resources test failed: {}" .format(e)) - - print("Chassis: name={}, Num={}".format(Chassis.name, Chassis.product_type)) - print("Module01: name={}, Num={}".format(Module01.name, Module01.product_type)) - print("Module02: name={}, Num={}".format(Module02.name, Module02.product_type)) + pass + print("cDAQ modules: {}".format(mod.compact_daq_chassis_device.product_type for mod in [mod1, mod2, mod3])) - print("channel 01 name : ", channels[0].name) - data = task.read() - print("data = ", data) - print("type(data) = ", type(data)) - print("type(data[0]) = ", type(data[0])) + # TEST RESOURCES + try: + for device in devices: + device.self_test_device() + except Exception as e: + print("Resources test failed: {}" .format(e)) - task.close() + # CREATE CHANNELS + channels_th = [AIThermoChannel(name="cDAQ1Mod1/ai0", + source='Analog_Input', + analog_type='Thermocouple', + value_min=-100, + value_max=1000, + thermo_type=DAQ_thermocouples.K), + ] + channels_voltage = [AIChannel(name="cDAQ1Mod3/ai0", + source='Analog_Input', + analog_type='voltage', + value_min=-80.0e-3, + value_max=80.0e-3, + termination=DAQ_termination.Auto, + ), + AIChannel(name="cDAQ1Mod3/ai1", + source='Analog_Input', + analog_type='voltage', + value_min=-80.0e-3, + value_max=80.0e-3, + termination=DAQ_termination.Auto, + ), + ] + # CREATE TASK + task_9211 = nidaqmx.Task() + task_9205 = nidaqmx.Task() + + def callback_9211(task_handle, every_n_samples_event_type, number_of_samples, callback_data): + data9211 = task_9211.read(5) + print(data9211) + + def callback_9205(task_handle, every_n_samples_event_type, number_of_samples, callback_data): + data9205 = task_9205.read(5) + print(data9205) + + for channel in channels_th: + task_9211.ai_channels.add_ai_thrmcpl_chan(channel.name, + "", + channel.value_min, + channel.value_max, + TemperatureUnits.DEG_C, + channel.thermo_type, + CJCSource.BUILT_IN, + 0., + "") + for channel in channels_voltage: + task_9205.ai_channels.add_ai_voltage_chan(channel.name, + "", + channel.termination, + channel.value_min, + channel.value_max, + VoltageUnits.VOLTS, + "") + task_9211.timing.cfg_samp_clk_timing(5.0, None, nidaqmx.constants.Edge.RISING, + nidaqmx.constants.AcquisitionType.CONTINUOUS, 5) + task_9211.register_every_n_samples_acquired_into_buffer_event(10, callback_9211) + + task_9205.timing.cfg_samp_clk_timing(10, None, nidaqmx.constants.Edge.RISING, + nidaqmx.constants.AcquisitionType.CONTINUOUS, 10) + task_9205.register_every_n_samples_acquired_into_buffer_event(2, callback_9205) + + task_9211.start() + task_9205.start() + + print("Acquisition in progress... Press enter to stop") + input() + + task_9211.close() + task_9205.close() except Exception as e: - print("Exception ({}): {}".format(type(e), str(e))) \ No newline at end of file + print("Exception ({}): {}".format(type(e), str(e))) From f0e25f3e855e030b2b5d081ead5d3aa5bcbf6035 Mon Sep 17 00:00:00 2001 From: Sebastien Date: Fri, 6 Dec 2024 10:10:09 +0100 Subject: [PATCH 023/140] add config files .toml --- .../resources/config_template.toml | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/pymodaq_plugins_daqmx/resources/config_template.toml b/src/pymodaq_plugins_daqmx/resources/config_template.toml index cf493ee..4a84012 100644 --- a/src/pymodaq_plugins_daqmx/resources/config_template.toml +++ b/src/pymodaq_plugins_daqmx/resources/config_template.toml @@ -1,2 +1,28 @@ title = 'Configuration file of the DAQmx plugin' +[NIDAQ_Devices] + +[NIDAQ_Devices.DEVICE01] +title = "Configuration entry for a NIDAQmx device" +name = "" +product = "" + +[NIDAQ_Devices.DEVICE01.MODULE01] +title = "A module plugged in the NIDAQmx device" +name = "" +product = "" + +[NIDAQ_Devices.DEVICE01.MODULE02] +title = "A module plugged in a NIDAQmx device" +name = "" +product = "" + +[NIDAQ_Devices.DEVICE01.MODULE03] +title = "A module plugged in a NIDAQmx device" +name = "" +product = "" + +[NIDAQ_Devices.DEVICE01.MODULE01.ai.ai0] +current_fields_example = "source, analog_type, value_min, value_max, termination" +voltage_fields_example = "source, analog_type, value_min, value_max, termination" +thrmcpl_fields_example = "source, analog_type, value_min, value_max, thermo_type" \ No newline at end of file From 73717745b1dc756ac8b55fed8e9347c7bbcc97cc Mon Sep 17 00:00:00 2001 From: Sebastien Date: Fri, 6 Dec 2024 11:45:46 +0100 Subject: [PATCH 024/140] Fixed imports --- .../daq_move_plugins/daq_move_DAQmx.py | 2 +- .../daq_move_DAQmx_MultipleScannerControl.py | 1 + .../daq_move_DAQmx_ScannerControl.py | 1 + .../plugins_0D/daq_0Dviewer_DAQmx.py | 2 +- .../plugins_0D/daq_0Dviewer_DAQmx_counter.py | 2 + .../plugins_1D/daq_1Dviewer_DAQmx.py | 2 +- .../national_instruments/daq_NIDAQmx.py | 397 ------------------ 7 files changed, 7 insertions(+), 400 deletions(-) diff --git a/src/pymodaq_plugins_daqmx/daq_move_plugins/daq_move_DAQmx.py b/src/pymodaq_plugins_daqmx/daq_move_plugins/daq_move_DAQmx.py index 0fb267d..f5e26db 100644 --- a/src/pymodaq_plugins_daqmx/daq_move_plugins/daq_move_DAQmx.py +++ b/src/pymodaq_plugins_daqmx/daq_move_plugins/daq_move_DAQmx.py @@ -1,4 +1,4 @@ -from ..hardware.national_instruments.daq_NIDAQmx import DAQ_NIDAQmx_Actuator +from ..hardware.national_instruments.daq_NIDAQmx_Move import DAQ_NIDAQmx_Actuator class DAQ_Move_DAQmx(DAQ_NIDAQmx_Actuator): diff --git a/src/pymodaq_plugins_daqmx/daq_move_plugins/daq_move_DAQmx_MultipleScannerControl.py b/src/pymodaq_plugins_daqmx/daq_move_plugins/daq_move_DAQmx_MultipleScannerControl.py index 3e8bc0c..e95e90e 100644 --- a/src/pymodaq_plugins_daqmx/daq_move_plugins/daq_move_DAQmx_MultipleScannerControl.py +++ b/src/pymodaq_plugins_daqmx/daq_move_plugins/daq_move_DAQmx_MultipleScannerControl.py @@ -11,6 +11,7 @@ import PyDAQmx + class DAQ_Move_DAQmx_MultipleScannerControl(DAQ_Move_base): """Plugin to control a piezo scanners with a NI card. This modules requires a clock channel to handle the timing of the movement and display the position, and this clock channel is shared between the master and diff --git a/src/pymodaq_plugins_daqmx/daq_move_plugins/daq_move_DAQmx_ScannerControl.py b/src/pymodaq_plugins_daqmx/daq_move_plugins/daq_move_DAQmx_ScannerControl.py index 24ca8cf..1a576e6 100644 --- a/src/pymodaq_plugins_daqmx/daq_move_plugins/daq_move_DAQmx_ScannerControl.py +++ b/src/pymodaq_plugins_daqmx/daq_move_plugins/daq_move_DAQmx_ScannerControl.py @@ -11,6 +11,7 @@ import PyDAQmx + class DAQ_Move_DAQmx_ScannerControl(DAQ_Move_base): """Plugin to control a piezo scanner with a NI card. This modules requires a clock channel to handle the timing of the movement and display the position. Avoid using several scanners (ie several analog outputs) diff --git a/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_DAQmx.py b/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_DAQmx.py index 2c8d623..87be685 100644 --- a/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_DAQmx.py +++ b/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_DAQmx.py @@ -1,4 +1,4 @@ -from pymodaq_plugins_daqmx.hardware.national_instruments.daq_NIDAQmx import DAQ_NIDAQmx_Viewer +from pymodaq_plugins_daqmx.hardware.national_instruments.daq_NIDAQmx_Viewer import DAQ_NIDAQmx_Viewer from pymodaq.control_modules.viewer_utility_classes import main diff --git a/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_DAQmx_counter.py b/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_DAQmx_counter.py index b66aa6a..7add9e6 100644 --- a/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_DAQmx_counter.py +++ b/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_DAQmx_counter.py @@ -6,6 +6,8 @@ import nidaqmx from pymodaq_plugins_daqmx.hardware.national_instruments.daqmxni import DAQmx, Edge, Counter + + class DAQ_0DViewer_DAQmx_counter(DAQ_Viewer_base): """ Plugin for a 0D PL counter, based on a NI card. diff --git a/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_1D/daq_1Dviewer_DAQmx.py b/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_1D/daq_1Dviewer_DAQmx.py index fbf9020..6da3bf5 100644 --- a/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_1D/daq_1Dviewer_DAQmx.py +++ b/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_1D/daq_1Dviewer_DAQmx.py @@ -1,4 +1,4 @@ -from pymodaq_plugins_daqmx.hardware.national_instruments.daq_NIDAQmx import DAQ_NIDAQmx_Viewer +from pymodaq_plugins_daqmx.hardware.national_instruments.daq_NIDAQmx_Viewer import DAQ_NIDAQmx_Viewer from pymodaq.control_modules.viewer_utility_classes import main diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx.py index c08509f..4a32af0 100644 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx.py +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx.py @@ -458,400 +458,3 @@ def stop(self): self.timer.stop() QtWidgets.QApplication.processEvents() self.controller.stop() - -class DAQ_NIDAQmx_Viewer(DAQ_Viewer_base, DAQ_NIDAQmx_base): - """ - ==================== ======================== - **Attributes** **Type** - *data_grabed_signal* instance of Signal - *params* dictionnary list - *task* - ==================== ======================== - - See Also - -------- - refresh_hardware - """ - - live_mode_available = True - params = viewer_params + DAQ_NIDAQmx_base.params - - def __init__(self, parent=None, params_state=None, control_type="0D"): - DAQ_Viewer_base.__init__(self, parent, params_state) # defines settings attribute and various other methods - DAQ_NIDAQmx_base.__init__(self) - - self.Naverage = None - self.live = False - self.control_type = control_type # could be "0D", "1D" or "Actuator" - if self.control_type == "0D": - self.settings.child('NIDAQ_type').setLimits( - ['Analog_Input', 'Counter', 'Digital_Input']) # analog input and counter - elif self.control_type == "1D": - self.settings.child('NIDAQ_type').setLimits(['Analog_Input']) - elif self.control_type == "Actuator": - self.settings.child('NIDAQ_type').setLimits(['Analog_Output']) - - self.settings.child('ao_settings').hide() - self.settings.child('ao_channels').hide() - - # timer used for the counter - self.timer = QtCore.QTimer() - self.timer.setSingleShot(True) - self.timer.timeout.connect(self.counter_done) - - def stop(self): - """Stop the current grab hardware wise if necessary""" - self.controller.stop() - self.live = False - self.emit_status(ThreadCommand('Update_Status', ['Acquisition stopped.'])) - return '' - - def commit_settings(self, param): - """ - Activate the parameters changes in the hardware. - - =============== ================================ =========================== - **Parameters** **Type** **Description** - *param* instance of pyqtgraph.parameter the parameter to activate - =============== ================================ =========================== - - See Also - -------- - update_NIDAQ_channels, update_task, DAQ_NIDAQ_source, refresh_hardware - """ - - if param.parent() is not None: - if param.parent().name() == 'ai_channels': - device = param.opts['title'].split('/')[0] - self.settings.child('clock_settings', 'frequency').setOpts(max=self.getAIMaxRate(device)) - - ranges = self.controller.getAIVoltageRange(device) - param.child('voltage_settings', 'volt_min').setOpts(limits=[r[0] for r in ranges]) - param.child('voltage_settings', 'volt_max').setOpts(limits=[r[1] for r in ranges]) - - DAQ_NIDAQmx_base.commit_settings(self, param) - - def ini_detector(self, controller=None): - """ - Initialisation procedure of the detector. - - See Also - -------- - daq_utils.ThreadCommand - """ - try: - self.controller = self.ini_detector_init(controller, DAQmx()) - self.update_task() - - # actions to perform in order to set properly the settings tree options - self.commit_settings(self.settings.child('NIDAQ_type')) - - info = "Plugin Initialized" - initialized = True - return info, initialized - - except Exception as e: - logger.info(traceback.format_exc()) - self.emit_status(ThreadCommand('Update_Status', [str(e), 'log'])) - info = str(e) - initialized = False - return info, initialized - - def grab_data(self, Naverage=1, **kwargs): - """ - | grab the current values with NIDAQ profile procedure. - | - | Send the data_grabed_signal once done. - - =============== ======== =============================================== - **Parameters** **Type** **Description** - *Naverage* int Number of values to average - =============== ======== =============================================== - - See Also - -------- - DAQ_NIDAQ_source - """ - update = False - - if 'live' in kwargs: - if kwargs['live'] != self.live: - update = True - self.live = kwargs['live'] - - if Naverage != self.Naverage: - self.Naverage = Naverage - update = True - if update: - self.update_task() - - if self.controller.task is None: - self.update_task() - - self.controller.register_callback(self.emit_data, "Nsamples", self.clock_settings.Nsamples) - self.controller.start() - - def emit_data(self, task_handle, every_n_samples_event_type, number_of_samples, callback_data): - channels_names = [ch.name for ch in self.channels] - # channels_ai_names = [ch.name for ch in self.channels if ch.source == 'Analog_Input'] - data_from_task = self.controller.task.read(self.settings['nsamplestoread'], timeout=20.0) - if self.control_type == "0D": - if not len(self.controller.task.channels.channel_names) != 1: - data_dfp = [np.array(data_from_task)] - else: - data_dfp = list(map(np.array, data_from_task)) - dte = DataToExport(name='NIDAQmx', - data=[DataFromPlugins(name='NI Analog Input', - data=data_dfp, - dim=f'Data{self.settings.child("display").value()}', - labels=channels_names - ), - ]) - self.dte_signal.emit(dte) - return 0 # mandatory for the NIDAQmx callback - - def counter_done(self): - channels_name = [ch.name for ch in self.channels] - data_counter = self.readCounter(len(self.channels), - self.settings['counter_settings', 'counting_time'] * 1e-3) - self.data_grabed_signal.emit([DataFromPlugins(name='NI Counter', data=[data_counter / 1e-3], dim='Data0D', - labels=channels_name, )]) - # y_axis=Axis(label='Count Number', units='1/s'))]) - self.task.StopTask() - -class DAQ_NIDAQmx_Actuator(DAQ_Move_base, DAQ_NIDAQmx_base): - """ - Wrapper object to access the Mock fonctionnalities, similar wrapper for all controllers. - - =============== ============== - **Attributes** **Type** - *params* dictionnary - =============== ============== - """ - _controller_units = 'Volts' - is_multiaxes = False # set to True if this plugin is controlled for a multiaxis controller (with a unique communication link) - stage_names = [] # "list of strings of the multiaxes - - params = DAQ_NIDAQmx_base.params + [ - # elements to be added here as dicts in order to control your custom stage - ############ - {'title': 'MultiAxes:', 'name': 'multiaxes', 'type': 'group', 'visible': is_multiaxes, - 'children': [ - {'title': 'is Multiaxes:', 'name': 'ismultiaxes', 'type': 'bool', 'value': is_multiaxes, - 'default': False}, - {'title': 'Status:', 'name': 'multi_status', 'type': 'list', 'value': 'Master', - 'limits': ['Master', 'Slave']}, - {'title': 'Axis:', 'name': 'axis', 'type': 'list', 'limits': stage_names}, - ]}] + actuator_params - - def __init__(self, parent=None, params_state=None, control_type="Actuator"): - DAQ_Move_base.__init__(self, parent, params_state) # defines settings attribute and various other methods - DAQ_NIDAQmx_base.__init__(self) - - self.control_type = "Actuator" # could be "0D", "1D" or "Actuator" - self.settings.child('NIDAQ_type').setLimits(['Analog_Output', 'Digital_Output']) - - self.settings.child('clock_settings', 'Nsamples').setValue(1) - - def get_actuator_value(self) -> DataActuator: - """Get the current position from the hardware with scaling conversion. - - Returns - ------- - float: The position obtained after scaling conversion. - """ - - pos = self.target_position - ## - - pos = self.get_position_with_scaling(pos) - self.emit_status(ThreadCommand('check_position', [pos])) - return pos - - def commit_settings(self, param): - """ - | Activate any parameter changes on the PI_GCS2 hardware. - | - | Called after a param_tree_changed signal from DAQ_Move_main. - - """ - - DAQ_NIDAQmx_base.commit_settings(self, param) - if param.name() == 'waveform': - if param.value() == 'DC': - self.settings.child('ao_settings', 'cont_param').setValue('offset') - self.settings.child('ao_settings', 'cont_param').show(not param.value() == 'DC') - self.settings.child('ao_settings', 'waveform_settings').show(not param.value() == 'DC') - - if param.parent() is not None: - if param.parent().name() == 'ao_channels': - device = param.opts['title'].split('/')[0] - self.settings.child('clock_settings', 'frequency').setOpts(max=self.getAOMaxRate(device)) - - ranges = self.getAOVoltageRange(device) - param.child('voltage_settings', 'volt_min').setOpts(limits=[r[0] for r in ranges]) - param.child('voltage_settings', 'volt_max').setOpts(limits=[r[1] for r in ranges]) - - 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 - ------- - self.status (edict): with initialization status: three fields: - * info (str) - * controller (object) initialized controller - *initialized: (bool): False if initialization failed otherwise True - """ - - try: - # initialize the stage and its controller status - # controller is an object that may be passed to other instances of DAQ_Move_Mock in case - # of one controller controlling multiactuators (or detector) - - self.status.update(edict(info="", controller=None, initialized=False)) - - # check whether this stage is controlled by a multiaxe controller (to be defined for each plugin) - # if multiaxes then init the controller here if Master state otherwise use external controller - if self.settings['multiaxes', 'ismultiaxes'] and self.settings['multiaxes', - 'multi_status'] == "Slave": - if controller is None: - raise Exception('no controller has been defined externally while this axe is a slave one') - else: - self.controller = controller - else: - self.controller = 'A Nidaqmx task' - self.update_task() - - # actions to perform in order to set properly the settings tree options - self.commit_settings(self.settings.child('NIDAQ_type')) - - self.status.info = "Plugin Initialized" - self.status.controller = self.controller - self.status.initialized = True - return self.status - - except Exception as e: - self.emit_status(ThreadCommand('Update_Status', [getLineInfo() + str(e), 'log'])) - self.status.info = getLineInfo() + str(e) - self.status.initialized = False - return self.status - - def calulate_waveform(self, value): - waveform = self.settings['ao_settings', 'waveform'] - if waveform == 'DC': - values = np.array([value]) - else: - Nsamples = self.settings['clock_settings', 'Nsamples'] - freq = self.settings['clock_settings', 'frequency'] - time = np.linspace(0, Nsamples / freq, Nsamples, endpoint=False) - - freq0 = self.settings['ao_settings', 'waveform_settings', 'frequency'] - amp = self.settings['ao_settings', 'waveform_settings', 'amplitude'] - offset = self.settings['ao_settings', 'waveform_settings', 'offset'] - if waveform == 'Sinus': - values = offset + amp * np.sin(2 * np.pi * freq0 * time) - elif waveform == 'Ramp': - values = offset + amp * np.linspace(0, 1, Nsamples) - - return values - - def move_Abs(self, position): - """ Move the actuator to the absolute target defined by position - - Parameters - ---------- - position: (flaot) value of the absolute target positioning - """ - - position = self.check_bound(position) # if user checked bounds, the defined bounds are applied here - position = self.set_position_with_scaling(position) # apply scaling if the user specified one - if self.settings['NIDAQ_type'] == 'Analog_Output': - self.settings.child('ao_settings', 'waveform_settings', - self.settings['ao_settings', 'cont_param']).setValue(position) - values = self.calulate_waveform(position) - self.target_position = position - - self.stop() - - if len(values) == 1: - self.writeAnalog(len(values), 1, values, autostart=True) - self.current_position = self.check_position() - self.move_done() - else: - if self.c_callback is None: - self.register_callback(self.move_done_callback) - self.writeAnalog(len(values), 1, values, autostart=False) - self.task.StartTask() - elif self.settings['NIDAQ_type'] == 'Digital_Output': - self.writeDigital(1, np.array([position], dtype=np.uint8), autostart=True) - - def move_done_callback(self, taskhandle, status, callbackdata): - self.current_position = self.check_position() - QtWidgets.QApplication.processEvents() - self.move_done() - self.task.StopTask() - return 0 - - def move_Rel(self, position): - """ Move the actuator to the relative target actuator value defined by position - - Parameters - ---------- - position: (flaot) value of the relative target positioning - """ - - position = self.check_bound(self.current_position + position) - self.current_position - self.target_position = position + self.current_position - if self.settings['NIDAQ_type'] == 'Analog_Output': - self.settings.child('ao_settings', 'waveform_settings', - self.settings['ao_settings', 'cont_param']).setValue(self.target_position) - - values = self.calulate_waveform(self.target_position) - - self.stop() - - if len(values) == 1: - self.writeAnalog(len(values), 1, values, autostart=True) - self.current_position = self.check_position() - self.move_done() - else: - if self.c_callback is None: - self.register_callback(self.move_done_callback) - self.writeAnalog(len(values), 1, values, autostart=False) - self.task.StartTask() - elif self.settings['NIDAQ_type'] == 'Digital_Output': - self.writeDigital(1, np.array([self.target_position], dtype=np.uint8), autostart=True) - - def move_Home(self): - """ - Send the update status thread command. - See Also - -------- - daq_utils.ThreadCommand - """ - - self.stop() - - if self.c_callback is None: - self.register_callback(self.move_done_callback) - self.writeAnalog(1, 1, np.array([0.])) - self.task.StartTask() - - def stop_motion(self): - """ - Call the specific move_done function (depending on the hardware). - - See Also - -------- - move_done - """ - - ## TODO for your custom plugin - self.stop() - self.emit_status(ThreadCommand('Update_Status', ['Some info you want to log'])) - self.move_done() # to let the interface know the actuator stopped - ############################## From 7ba5d556e7dac0aa04877b135721f6f28ce354a9 Mon Sep 17 00:00:00 2001 From: Sebastien Date: Fri, 6 Dec 2024 14:19:08 +0100 Subject: [PATCH 025/140] Import config from .toml files --- src/pymodaq_plugins_daqmx/__init__.py | 3 +++ .../hardware/national_instruments/daqmxni.py | 1 + src/pymodaq_plugins_daqmx/utils.py | 9 +++++++++ 3 files changed, 13 insertions(+) create mode 100644 src/pymodaq_plugins_daqmx/utils.py diff --git a/src/pymodaq_plugins_daqmx/__init__.py b/src/pymodaq_plugins_daqmx/__init__.py index 8a33bda..097668d 100644 --- a/src/pymodaq_plugins_daqmx/__init__.py +++ b/src/pymodaq_plugins_daqmx/__init__.py @@ -1,5 +1,8 @@ from pathlib import Path from pymodaq.utils.logger import set_logger # to be imported by other modules. +from .utils import Config +config = Config() + with open(str(Path(__file__).parent.joinpath('resources/VERSION')), 'r') as fvers: __version__ = fvers.read().strip() \ No newline at end of file diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py index 806c84a..cc0dd18 100644 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py @@ -8,6 +8,7 @@ from nidaqmx.constants import * from nidaqmx.system.device import Device from nidaqmx.errors import DaqError, DAQmxErrors +from pymodaq_plugins_daqmx import config logger = set_logger(get_module_name(__file__)) diff --git a/src/pymodaq_plugins_daqmx/utils.py b/src/pymodaq_plugins_daqmx/utils.py new file mode 100644 index 0000000..fb11e4d --- /dev/null +++ b/src/pymodaq_plugins_daqmx/utils.py @@ -0,0 +1,9 @@ +from pathlib import Path + +from pymodaq.utils.config import BaseConfig, USER + + +class Config(BaseConfig): + """Main class to deal with configuration values for this plugin""" + config_template_path = Path(__file__).parent.joinpath('resources/config_template.toml') + config_name = f"config_{__package__.split('pymodaq_plugins_')[1]}" \ No newline at end of file From 91dbcfe55ccac05651bed046c39cac21e24b053d Mon Sep 17 00:00:00 2001 From: Sebastien Date: Mon, 9 Dec 2024 22:15:29 +0100 Subject: [PATCH 026/140] Add use of toml files: config sequence + generates scalablegroup from config during init --- .../plugins_0D/daq_0Dviewer_NIDAQmx.py | 23 +++-- .../daq_NIDAQmx_Viewer.py | 44 +++++++++- .../hardware/national_instruments/daqmxni.py | 85 +++++++++++++++++++ 3 files changed, 140 insertions(+), 12 deletions(-) diff --git a/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_NIDAQmx.py b/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_NIDAQmx.py index 6abe3e5..fd90a15 100644 --- a/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_NIDAQmx.py +++ b/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_NIDAQmx.py @@ -1,5 +1,6 @@ from pymodaq.control_modules.viewer_utility_classes import main from pymodaq.control_modules.viewer_utility_classes import comon_parameters as viewer_params +from pymodaq_plugins_daqmx import config from pymodaq_plugins_daqmx.hardware.national_instruments.daqmxni import AIChannel, AIThermoChannel, DAQmx, nidaqmx,\ DAQ_termination, DAQ_thermocouples from pymodaq_plugins_daqmx.hardware.national_instruments.daq_NIDAQmx import DAQ_NIDAQmx_base @@ -13,9 +14,13 @@ class DAQ_0DViewer_NIDAQmx(DAQ_NIDAQmx_Viewer): Plugin for a 0D data visualization & acquisition with various NI modules plugged in a NI cDAQ. """ - channels_ai: str - clock_settings_ai: str - dict_device_input: dict + config_channels: list + channels_ai: list + config: config + controller: DAQmx + config_devices: list + config_modules: list + current_device: nidaqmx.system.Device live: bool data_tot: list Naverage: int @@ -33,12 +38,12 @@ def __init__(self, parent=None, params_state=None): def ini_attributes(self): super().ini_attributes() - self.controller: DAQmx = None - self.channels_ai = None - self.clock_settings_ai = None - self.dict_device_input = None - self.live = False # True during a continuous grab - self.data_tot = None + self.channels_ai = [] + self.config = config + self.config_channels = [] + self.config_devices = [] + self.config_modules = [] + self.live = False self.Naverage = 1 self.ind_average = 0 diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx_Viewer.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx_Viewer.py index afc3364..0deb027 100644 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx_Viewer.py +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx_Viewer.py @@ -3,7 +3,7 @@ import traceback from qtpy import QtCore from .daqmxni import DAQmx -from pymodaq_plugins_daqmx.hardware.national_instruments.daq_NIDAQmx import DAQ_NIDAQmx_base +from pymodaq_plugins_daqmx.hardware.national_instruments.daq_NIDAQmx import DAQ_NIDAQmx_base, DAQ_termination from pymodaq.control_modules.viewer_utility_classes import DAQ_Viewer_base, comon_parameters as viewer_params from pymodaq.utils.daq_utils import ThreadCommand from pymodaq.utils.data import DataFromPlugins, DataToExport @@ -32,6 +32,7 @@ def __init__(self, parent=None, params_state=None, control_type="0D"): DAQ_Viewer_base.__init__(self, parent, params_state) # defines settings attribute and various other methods DAQ_NIDAQmx_base.__init__(self) + self.current_device = None self.Naverage = None self.live = False self.control_type = control_type # could be "0D", "1D" or "Actuator" @@ -99,12 +100,49 @@ def ini_detector(self, controller=None): daq_utils.ThreadCommand """ try: + self.current_device = nidaqmx.system.Device(self.settings["devices"]) self.controller = self.ini_detector_init(controller, DAQmx()) - self.update_task() + self.controller.configuration_sequence(self, self.current_device) # actions to perform in order to set properly the settings tree options self.commit_settings(self.settings.child('NIDAQ_type')) - + for ch in self.config_channels: + if self.settings.child("dev_to_use").value() in ch.name: + self.settings.child('ai_channels').addNew(ch.name) + param = [a for a in self.settings.child('ai_channels').childs if a.opts['title'] == ch.name][0] + self.settings.child("ai_channels", param.opts['name'], "ai_type").setValue(ch.analog_type) + param.child("voltage_settings").show(ch.analog_type == "Voltage") + param.child("current_settings").show(ch.analog_type == "Current") + param.child("thermoc_settings").show(ch.analog_type == "Thermocouple") + if ch.analog_type == "Voltage": + self.settings.child("ai_channels", param.opts['name'], "voltage_settings", "volt_min").setValue( + ch.value_min) + self.settings.child("ai_channels", param.opts['name'], "voltage_settings", "volt_max").setValue( + ch.value_max) + self.settings.child("ai_channels", param.opts['name'], "termination").setValue( + ch.termination.name) + elif ch.analog_type == "Current": + self.settings.child("ai_channels", param.opts['name'], "current_settings", "curr_min").setValue( + ch.value_min) + self.settings.child("ai_channels", param.opts['name'], "current_settings", "curr_max").setValue( + ch.value_max) + self.settings.child("ai_channels", param.opts['name'], "termination").setValue( + ch.termination.name) + elif ch.analog_type == "Thermocouple": + self.settings.child("ai_channels", + param.opts['name'], + "thermoc_settings", + "thermoc_type").setValue(ch.thermo_type.name) + self.settings.child("ai_channels", + param.opts['name'], + "thermoc_settings", + "T_min").setValue(ch.value_min) + self.settings.child("ai_channels", + param.opts['name'], + "thermoc_settings", + "T_max").setValue(ch.value_max) + self.settings.child("ai_channels", param.opts['name'], "termination").setValue( + DAQ_termination.Auto) info = "Plugin Initialized" initialized = True return info, initialized diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py index cc0dd18..0093e0a 100644 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py @@ -351,6 +351,91 @@ def get_NIDAQ_channels(cls, devices=None, source_type=None): return channels_tot + def configuration_sequence(self, viewer, current_device): + """Configure each / modules / channels as giver by the user in the configuration file + + Read the .toml file to get the desired hardware configuration, + and send the nidaqmx a sequence which set up each channel. + """ + logger.info("********** CONFIGURATION SEQUENCE INITIALIZED **********") + devices_info = [dev.name + ': ' + dev.product_type for dev in self.devices] + logger.info("Detected devices: {}".format(devices_info)) + try: + viewer.config_devices = [config["NIDAQ_Devices", dev].get('name') for dev in viewer.config["NIDAQ_Devices"] + if "Mod" not in config["NIDAQ_Devices", dev].get('name')] + logger.info(viewer.config_devices) + for dev in config["NIDAQ_Devices"]: + if not isinstance(config["NIDAQ_Devices", dev], dict): + continue + try: + device_name = config["NIDAQ_Devices", dev].get('name') + if not device_name == current_device.name: + continue + device_product = config["NIDAQ_Devices", dev].get('product') + device = nidaqmx.system.device.Device(device_name) + assert device in self.devices and device.product_type == device_product, device.name + except AssertionError as err: + logger.error("Device {} not detected: {}".format(device_name, err)) + continue + for mod in config["NIDAQ_Devices", dev]: + if not isinstance(config["NIDAQ_Devices", dev, mod], dict): + continue + try: + module_name = config["NIDAQ_Devices", dev, mod].get('name') + module_product = config["NIDAQ_Devices", dev, mod].get('product') + module = nidaqmx.system.device.Device(module_name) + assert module in self.devices and module.product_type == module_product, module.name + viewer.config_modules.append(config["NIDAQ_Devices", dev, mod].get('name')) + except AssertionError as err: + logger.error("Module {} not detected: {}".format(module_name, err)) + continue + for source in config["NIDAQ_Devices", dev, mod]: + if not isinstance(config["NIDAQ_Devices", dev, mod, source], dict): + continue + if source == "ai": + ai = config["NIDAQ_Devices", dev, mod, source] + for ch in ai.keys(): + name = module_name + "/" + str(ch) + term = ai[ch].get("termination") + if ai[ch].get("analog_type") == "Voltage": + viewer.config_channels.append(AIChannel + (name=name, + source=ai[ch].get("source"), + analog_type=ai[ch].get("analog_type"), + value_min=float(ai[ch].get("value_min")), + value_max=float(ai[ch].get("value_max")), + termination=DAQ_termination.__getitem__(term), + )) + elif ai[ch].get("analog_type") == "Current": + viewer.config_channels.append(AIChannel + (name=name, + source=ai[ch].get("source"), + analog_type=ai[ch].get("analog_type"), + value_min=float(ai[ch].get("value_min")), + value_max=float(ai[ch].get("value_max")), + termination=DAQ_termination.__getitem__(term), + )) + elif ai[ch].get("analog_type") == "Thermocouple": + th = ai[ch].get("thermo_type") + viewer.config_channels.append(AIThermoChannel + (name=name, + source=ai[ch].get("source"), + analog_type=ai[ch].get("analog_type"), + value_min=float(ai[ch].get("value_min")), + value_max=float(ai[ch].get("value_max")), + thermo_type=DAQ_thermocouples.__getitem__(th), + )) + logger.info("Devices from config: {}".format(viewer.config_devices)) + logger.info("Current device: {}".format(current_device)) + logger.info("Current device modules from config: {}".format(viewer.config_modules)) + logger.info("Current device channels from config: {}".format([ch.name for ch in viewer.config_channels])) + except AssertionError as err: + logger.error("Configuration entries <{}> does not match the hardware ".format(err)) + except Exception as err: + logger.info("Configuration sequence error, verify if your config matches the hardware: {}".format(err)) + pass + logger.info(" ********** CONFIGURATION SEQUENCE SUCCESSFULLY ENDED **********") + @classmethod def getAOMaxRate(cls, device): return Device(device).ao_max_rate From 9d0b6733c8c871fb1601d6edf6bac66cd5c9cffd Mon Sep 17 00:00:00 2001 From: Sebastien Date: Mon, 9 Dec 2024 22:19:14 +0100 Subject: [PATCH 027/140] Minor modifs (PEP, unused imports, info logged) --- .../hardware/national_instruments/daq_NIDAQmx.py | 3 ++- .../hardware/national_instruments/daq_NIDAQmx_Viewer.py | 7 +++---- .../hardware/national_instruments/daqmxni.py | 3 +-- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx.py index 4a32af0..47f6832 100644 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx.py +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx.py @@ -389,6 +389,7 @@ def commit_settings(self, param: Parameter): def update_task(self): self.channels = self.get_channels_from_settings() + logger.info("update_task - channels: {}".format(self.channels)) self.clock_settings = ClockSettings(frequency=self.settings['clock_settings', 'frequency'], Nsamples=self.settings['clock_settings', 'Nsamples'], edge=Edge.Rising, @@ -399,13 +400,13 @@ def update_task(self): edge=Edge[self.settings['trigger_settings', 'edge']], level=self.settings['trigger_settings', 'level'], ) if not not self.channels: - logger.info("not not self channel") self.controller.update_task(self.channels, self.clock_settings, trigger_settings=self.trigger_settings) def get_channels_from_settings(self): channels = [] if self.settings['NIDAQ_type'] == DAQ_NIDAQ_source(0).name: # analog input for channel in self.settings.child('ai_channels').children(): + logger.info("get_channels_from_settings - channel {}".format(channel)) analog_type = channel['ai_type'] if analog_type == 'Voltage': channels.append(AIChannel(name=channel.opts['title'], diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx_Viewer.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx_Viewer.py index 0deb027..147161b 100644 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx_Viewer.py +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx_Viewer.py @@ -1,4 +1,4 @@ -import datetime +import nidaqmx import numpy as np import traceback from qtpy import QtCore @@ -16,7 +16,7 @@ class DAQ_NIDAQmx_Viewer(DAQ_Viewer_base, DAQ_NIDAQmx_base): ==================== ======================== **Attributes** **Type** *data_grabed_signal* instance of Signal - *params* dictionnary list + *params* dictionary list *task* ==================== ======================== @@ -61,8 +61,6 @@ def stop(self): logger.info("StopTask() done") except Exception: pass - ############################## - self.emit_status(ThreadCommand('Update_Status', ['Acquisition stopped.'])) return '' @@ -145,6 +143,7 @@ def ini_detector(self, controller=None): DAQ_termination.Auto) info = "Plugin Initialized" initialized = True + logger.info("Detector 0D initialized") return info, initialized except Exception as e: diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py index 0093e0a..09ab5d8 100644 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py @@ -3,7 +3,6 @@ from enum import IntEnum import numpy as np from pymodaq.utils.logger import set_logger, get_module_name -# from threading import Timer import nidaqmx from nidaqmx.constants import * from nidaqmx.system.device import Device @@ -253,7 +252,7 @@ def __init__(self, **kwargs): class DAQmx: - """Wrapper around the NIDAQmx package giving an easy to use object to instantiate channels and tasks""" + """Wrapper around the NIDAQmx package giving an easy-to-use object to instantiate channels and tasks""" def __init__(self): self.devices = [] self.devices_names = [] From 186b66b4e16846bb0c70a7b75a8216daa2dbbf0d Mon Sep 17 00:00:00 2001 From: Sebastien Date: Mon, 9 Dec 2024 22:22:10 +0100 Subject: [PATCH 028/140] Clarify & optimize code + fix minors problems --- .../plugins_0D/daq_0Dviewer_NIDAQmx.py | 10 +-- .../national_instruments/daq_NIDAQmx.py | 72 ++++++++++--------- .../daq_NIDAQmx_Viewer.py | 9 ++- .../hardware/national_instruments/daqmxni.py | 14 ++-- 4 files changed, 54 insertions(+), 51 deletions(-) diff --git a/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_NIDAQmx.py b/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_NIDAQmx.py index fd90a15..32b2033 100644 --- a/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_NIDAQmx.py +++ b/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_NIDAQmx.py @@ -22,14 +22,15 @@ class DAQ_0DViewer_NIDAQmx(DAQ_NIDAQmx_Viewer): config_modules: list current_device: nidaqmx.system.Device live: bool - data_tot: list Naverage: int - ind_average: int + param_devices = DAQmx.get_NIDAQ_devices().device_names params = viewer_params + [ {'title': 'Display type:', 'name': 'display', 'type': 'list', 'limits': ['0D', '1D']}, - {'title': 'Module ref. :', 'name': 'module', 'type': 'list', 'limits': DAQmx.get_NIDAQ_devices()[1], - 'value': DAQmx.get_NIDAQ_devices()[1][0] + {'title': 'Devices :', 'name': 'devices', 'type': 'list', 'limits': param_devices, + 'value': param_devices[0] + }, + {'title': 'Device To Use:', 'name': 'dev_to_use', 'type': 'list', 'limits': param_devices, }, ] + DAQ_NIDAQmx_base.params @@ -45,7 +46,6 @@ def ini_attributes(self): self.config_modules = [] self.live = False self.Naverage = 1 - self.ind_average = 0 def close(self): self.live = False diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx.py index 47f6832..5d233fa 100644 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx.py +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx.py @@ -24,8 +24,8 @@ class ScalableGroupAI(GroupParameter): params = [{'title': 'AI type:', 'name': 'ai_type', 'type': 'list', 'limits': DAQ_analog_types.names()}, {'title': 'Voltages:', 'name': 'voltage_settings', 'type': 'group', 'children': [ - {'title': 'Voltage Min:', 'name': 'volt_min', 'type': 'list', 'value': -10.}, - {'title': 'Voltage Max:', 'name': 'volt_max', 'type': 'list', 'value': 10.}, + {'title': 'Voltage Min:', 'name': 'volt_min', 'type': 'float', 'value': -10.}, + {'title': 'Voltage Max:', 'name': 'volt_max', 'type': 'float', 'value': 10.}, ]}, {'title': 'Current:', 'name': 'current_settings', 'type': 'group', 'visible': False, 'children': [ {'title': 'Current Min:', 'name': 'curr_min', 'type': 'float', 'value': -1, 'suffix': 'A'}, @@ -46,7 +46,7 @@ def __init__(self, **opts): opts['addList'] = opts['limits'] GroupParameter.__init__(self, **opts) - def addNew(self, typ): + def addNew(self, typ=None): """ Add a child. @@ -101,7 +101,7 @@ def __init__(self, **opts): opts['addList'] = opts['limits'] GroupParameter.__init__(self, **opts) - def addNew(self, typ): + def addNew(self, typ=None): """ Add a child. @@ -147,7 +147,7 @@ def __init__(self, **opts): opts['addList'] = opts['limits'] GroupParameter.__init__(self, **opts) - def addNew(self, typ): + def addNew(self, typ=None): """ Add a child. @@ -183,7 +183,7 @@ def __init__(self, **opts): opts['addList'] = opts['limits'] GroupParameter.__init__(self, **opts) - def addNew(self, typ): + def addNew(self, typ=None): """ Add a child. @@ -218,7 +218,7 @@ def __init__(self, **opts): opts['addList'] = opts['limits'] GroupParameter.__init__(self, **opts) - def addNew(self, typ): + def addNew(self, typ=None): """ Add a child. @@ -241,9 +241,12 @@ def addNew(self, typ): registerParameterType('groupdo', ScalableGroupDO, override=True) -class DAQ_NIDAQmx_base(DAQmx): +class DAQ_NIDAQmx_base: + """ + Base NIDAQmx class for using DAQmx objects from daqmxni.py in the DAQ_NIDAQmx_Move & DAQ_NIDAQmx_Viewer + """ data_grabed_signal = Signal(list) - + param_instance = DAQmx() params = [{'title': 'Refresh hardware:', 'name': 'refresh_hardware', 'type': 'bool', 'value': False}, {'title': 'Signal type:', 'name': 'NIDAQ_type', 'type': 'list', 'limits': DAQ_NIDAQ_source.names()}, {'title': 'NSamples To Read', 'name': 'nsamplestoread', 'type': 'int', 'value': 1000, 'default': 1000, @@ -327,7 +330,16 @@ def commit_settings(self, param: Parameter): self.settings.child('do_channels').hide() self.settings.child('di_channels').hide() - elif param.value() == DAQ_NIDAQ_source(1).name: # counter input + elif param.value() == DAQ_NIDAQ_source(1).name: # analog output + self.settings.child('clock_settings').show() + self.settings.child('ai_channels').hide() + self.settings.child('ao_channels').show() + self.settings.child('ao_settings').show() + self.settings.child('counter_settings').hide() + self.settings.child('do_channels').hide() + self.settings.child('di_channels').hide() + + elif param.value() == DAQ_NIDAQ_source(2).name: # counter input self.settings.child('clock_settings').hide() self.settings.child('ai_channels').hide() self.settings.child('ao_channels').hide() @@ -336,16 +348,16 @@ def commit_settings(self, param: Parameter): self.settings.child('do_channels').hide() self.settings.child('di_channels').hide() - elif param.value() == DAQ_NIDAQ_source(2).name: # analog output + elif param.value() == DAQ_NIDAQ_source(3).name: # Digital_Input self.settings.child('clock_settings').show() self.settings.child('ai_channels').hide() self.settings.child('ao_channels').show() self.settings.child('ao_settings').show() self.settings.child('counter_settings').hide() self.settings.child('do_channels').hide() - self.settings.child('di_channels').hide() + self.settings.child('di_channels').show() - elif param.value() == DAQ_NIDAQ_source(3).name: # digital output + elif param.value() == DAQ_NIDAQ_source(4).name: # digital output self.settings.child('clock_settings').show() self.settings.child('ai_channels').hide() self.settings.child('ao_channels').hide() @@ -353,15 +365,6 @@ def commit_settings(self, param: Parameter): self.settings.child('counter_settings').hide() self.settings.child('do_channels').show() self.settings.child('di_channels').hide() - - elif param.value() == DAQ_NIDAQ_source(4).name: # Digital_Input - self.settings.child('clock_settings').show() - self.settings.child('ai_channels').hide() - self.settings.child('ao_channels').show() - self.settings.child('ao_settings').show() - self.settings.child('counter_settings').hide() - self.settings.child('do_channels').hide() - self.settings.child('di_channels').show() self.update_task() elif param.name() == 'refresh_hardware': @@ -429,12 +432,7 @@ def get_channels_from_settings(self): thermo_type=DAQ_thermocouples[ channel['thermoc_settings', 'thermoc_type']], )) - elif self.settings['NIDAQ_type'] == DAQ_NIDAQ_source(1).name: # counter input - for channel in self.settings.child('counter_settings', 'counter_channels').children(): - channels.append(Counter(name=channel.opts['title'], - source='Counter', edge=channel['edge'])) - - elif self.settings['NIDAQ_type'] == DAQ_NIDAQ_source(2).name: # analog output + elif self.settings['NIDAQ_type'] == DAQ_NIDAQ_source(1).name: # analog output for channel in self.settings.child('ao_channels').children(): analog_type = channel['ao_type'] channels.append(AOChannel(name=channel.opts['title'], @@ -442,14 +440,22 @@ def get_channels_from_settings(self): value_min=channel['voltage_settings', 'volt_min'], value_max=channel['voltage_settings', 'volt_max'], )) - elif self.settings['NIDAQ_type'] == DAQ_NIDAQ_source(3).name: # Digital output - for channel in self.settings.child('do_channels').children(): - channels.append(DOChannel(name=channel.opts['title'], - source='Digital_Output')) - elif self.settings['NIDAQ_type'] == DAQ_NIDAQ_source(4).name: # digital input + + elif self.settings['NIDAQ_type'] == DAQ_NIDAQ_source(2).name: # counter input + for channel in self.settings.child('counter_settings', 'counter_channels').children(): + channels.append(Counter(name=channel.opts['title'], + source='Counter', edge=channel['edge'])) + + elif self.settings['NIDAQ_type'] == DAQ_NIDAQ_source(3).name: # digital input for channel in self.settings.child('di_channels').children(): channels.append(DIChannel(name=channel.opts['title'], source='Digital_Input')) + + elif self.settings['NIDAQ_type'] == DAQ_NIDAQ_source(4).name: # Digital output + for channel in self.settings.child('do_channels').children(): + channels.append(DOChannel(name=channel.opts['title'], + source='Digital_Output')) + channels = [ch for ch in channels if self.settings.child("dev_to_use").value() in ch.name] return channels def stop(self): diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx_Viewer.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx_Viewer.py index 147161b..0069ae2 100644 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx_Viewer.py +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx_Viewer.py @@ -54,11 +54,10 @@ def __init__(self, parent=None, params_state=None, control_type="0D"): def stop(self): """Stop the current grab hardware wise if necessary""" - self.controller.stop() - self.live = False try: - self.controller.task.StopTask() - logger.info("StopTask() done") + self.controller.stop() + self.live = False + logger.info("Acquisition stopped.") except Exception: pass self.emit_status(ThreadCommand('Update_Status', ['Acquisition stopped.'])) @@ -81,7 +80,7 @@ def commit_settings(self, param): if param.parent() is not None: if param.parent().name() == 'ai_channels': device = param.opts['title'].split('/')[0] - self.settings.child('clock_settings', 'frequency').setOpts(max=self.getAIMaxRate(device)) + self.settings.child('clock_settings', 'frequency').setOpts(max=self.controller.getAIMaxRate(device)) ranges = self.controller.getAIVoltageRange(device) param.child('voltage_settings', 'volt_min').setOpts(limits=[r[0] for r in ranges]) diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py index 09ab5d8..c77577c 100644 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py @@ -255,7 +255,6 @@ class DAQmx: """Wrapper around the NIDAQmx package giving an easy-to-use object to instantiate channels and tasks""" def __init__(self): self.devices = [] - self.devices_names = [] self.channels = [] self._device = None self._task = None @@ -276,13 +275,12 @@ def device(self): @device.setter def device(self, device): - if device not in self.devices_names: + if device not in self.devices.device_names: raise IOError(f'your device: {device} is not known or connected') self._device = device def update_NIDAQ_devices(self): - self.devices = self.get_NIDAQ_devices()[0] - self.devices_names = self.get_NIDAQ_devices()[1] + self.devices = self.get_NIDAQ_devices() @classmethod def get_NIDAQ_devices(cls): @@ -297,12 +295,12 @@ def get_NIDAQ_devices(cls): devices = nidaqmx.system.System.local().devices if devices == ['']: devices = [] - return devices, devices.device_names + return devices except DaqError as e: return e.error_code def update_NIDAQ_channels(self, source_type=None): - self.channels = self.get_NIDAQ_channels(self.devices_names, source_type=source_type) + self.channels = self.get_NIDAQ_channels(self.devices.device_names, source_type=source_type) @classmethod def get_NIDAQ_channels(cls, devices=None, source_type=None): @@ -321,7 +319,7 @@ def get_NIDAQ_channels(cls, devices=None, source_type=None): """ if devices is None: - devices = cls.get_NIDAQ_devices()[1] + devices = cls.get_NIDAQ_devices().device_names if source_type is None: source_type = DAQ_NIDAQ_source.names() @@ -455,7 +453,7 @@ def isDigitalTriggeringSupported(cls, device): def getTriggeringSources(cls, devices=None): sources = [] if devices is None: - devices = cls.get_NIDAQ_devices()[1] + devices = cls.get_NIDAQ_devices().device_names for device in devices: if cls.isDigitalTriggeringSupported(device): From 6782d284f46e33b59df5ea884d0a182450c1ede1 Mon Sep 17 00:00:00 2001 From: Sebastien Date: Mon, 9 Dec 2024 22:35:30 +0100 Subject: [PATCH 029/140] Update README --- README.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index d2ae413..6839891 100644 --- a/README.rst +++ b/README.rst @@ -26,6 +26,7 @@ Contributors * Amelie Jarnac * Aurore Finco +* Sébastien Guerrero (sebastien.guerrero@insa-lyon.fr) Instruments =========== @@ -40,6 +41,6 @@ Viewer0D ++++++++ * **DAQmx_PLcounter**: Single photon counting - +* **NIDAQmx: Analog Input (current-voltage-temperature), working with cDAQ & DAQ-USB From 46cf0ca56f0871903b1ae9ac2c91c6ea01bf446c Mon Sep 17 00:00:00 2001 From: Loic Guilmard Date: Wed, 18 Dec 2024 17:09:06 +0100 Subject: [PATCH 030/140] removed CustomEnum to use the ones from nidaq.constants --- .../plugins_0D/daq_0Dviewer_DAQmx.py | 5 +- .../plugins_0D/daq_0Dviewer_NIDAQmx.py | 36 +- .../national_instruments/daq_NIDAQmx.py | 428 +++++++++++++++++- .../hardware/national_instruments/daqmxni.py | 190 +++----- 4 files changed, 497 insertions(+), 162 deletions(-) diff --git a/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_DAQmx.py b/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_DAQmx.py index 87be685..e8dba9f 100644 --- a/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_DAQmx.py +++ b/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_DAQmx.py @@ -1,5 +1,6 @@ -from pymodaq_plugins_daqmx.hardware.national_instruments.daq_NIDAQmx_Viewer import DAQ_NIDAQmx_Viewer -from pymodaq.control_modules.viewer_utility_classes import main +from pymodaq_plugins_daqmx.hardware.national_instruments.daq_NIDAQmx import DAQ_NIDAQmx_Viewer +from pymodaq.control_modules.viewer_utility_classes import main + class DAQ_0DViewer_DAQmx(DAQ_NIDAQmx_Viewer): diff --git a/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_NIDAQmx.py b/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_NIDAQmx.py index 32b2033..10dbd42 100644 --- a/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_NIDAQmx.py +++ b/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_NIDAQmx.py @@ -1,8 +1,10 @@ from pymodaq.control_modules.viewer_utility_classes import main from pymodaq.control_modules.viewer_utility_classes import comon_parameters as viewer_params from pymodaq_plugins_daqmx import config -from pymodaq_plugins_daqmx.hardware.national_instruments.daqmxni import AIChannel, AIThermoChannel, DAQmx, nidaqmx,\ - DAQ_termination, DAQ_thermocouples +from pymodaq_plugins_daqmx.hardware.national_instruments.daqmxni import AIChannel, AIThermoChannel, DAQmx, \ + Task, niconstants +from pymodaq_plugins_daqmx.hardware.national_instruments.daqmxni import UsageTypeAI, ThermocoupleType, \ + TerminalConfiguration from pymodaq_plugins_daqmx.hardware.national_instruments.daq_NIDAQmx import DAQ_NIDAQmx_base from pymodaq_plugins_daqmx.hardware.national_instruments.daq_NIDAQmx_Viewer import DAQ_NIDAQmx_Viewer from pymodaq.utils.logger import set_logger, get_module_name @@ -62,12 +64,10 @@ def close(self): else: try: print("In main") - import nidaqmx as ni - from pymodaq_plugins_daqmx.hardware.national_instruments.daqmxni import CurrentUnits, TemperatureUnits,\ - VoltageUnits, CJCSource + import nidaqmx.system.System # EXPLORE DEVICES - devices = ni.system.System.local().devices + devices = nidaqmx.system.System.local().devices print("devices {}".format(devices)) print("devices names {}".format(devices.device_names)) print("devices types {}".format([dev.product_type for dev in devices])) @@ -94,26 +94,26 @@ def close(self): analog_type='Thermocouple', value_min=-100, value_max=1000, - thermo_type=DAQ_thermocouples.K), + thermo_type=ThermocoupleType.K), ] channels_voltage = [AIChannel(name="cDAQ1Mod3/ai0", source='Analog_Input', analog_type='voltage', value_min=-80.0e-3, value_max=80.0e-3, - termination=DAQ_termination.Auto, + termination=TerminalConfiguration.DEFAULT, ), AIChannel(name="cDAQ1Mod3/ai1", source='Analog_Input', analog_type='voltage', value_min=-80.0e-3, value_max=80.0e-3, - termination=DAQ_termination.Auto, + termination=TerminalConfiguration.DEFAULT, ), ] # CREATE TASK - task_9211 = nidaqmx.Task() - task_9205 = nidaqmx.Task() + task_9211 = Task() + task_9205 = Task() def callback_9211(task_handle, every_n_samples_event_type, number_of_samples, callback_data): data9211 = task_9211.read(5) @@ -128,9 +128,9 @@ def callback_9205(task_handle, every_n_samples_event_type, number_of_samples, ca "", channel.value_min, channel.value_max, - TemperatureUnits.DEG_C, + niconstants.TemperatureUnits.DEG_C, channel.thermo_type, - CJCSource.BUILT_IN, + niconstants.CJCSource.BUILT_IN, 0., "") for channel in channels_voltage: @@ -139,14 +139,14 @@ def callback_9205(task_handle, every_n_samples_event_type, number_of_samples, ca channel.termination, channel.value_min, channel.value_max, - VoltageUnits.VOLTS, + niconstants.VoltageUnits.VOLTS, "") - task_9211.timing.cfg_samp_clk_timing(5.0, None, nidaqmx.constants.Edge.RISING, - nidaqmx.constants.AcquisitionType.CONTINUOUS, 5) + task_9211.timing.cfg_samp_clk_timing(5.0, None, niconstants.Edge.RISING, + niconstants.AcquisitionType.CONTINUOUS, 5) task_9211.register_every_n_samples_acquired_into_buffer_event(10, callback_9211) - task_9205.timing.cfg_samp_clk_timing(10, None, nidaqmx.constants.Edge.RISING, - nidaqmx.constants.AcquisitionType.CONTINUOUS, 10) + task_9205.timing.cfg_samp_clk_timing(10, None, niconstants.Edge.RISING, + niconstants.AcquisitionType.CONTINUOUS, 10) task_9205.register_every_n_samples_acquired_into_buffer_event(2, callback_9205) task_9211.start() diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx.py index 5d233fa..eebfee0 100644 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx.py +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx.py @@ -1,14 +1,23 @@ from qtpy import QtWidgets -from qtpy.QtCore import Signal +from qtpy.QtCore import Signal, QTimer +import numpy as np from pymodaq.utils.logger import set_logger, get_module_name +from pymodaq.control_modules.viewer_utility_classes import DAQ_Viewer_base, comon_parameters as viewer_params +from pymodaq.control_modules.move_utility_classes import DAQ_Move_base, comon_parameters as actuator_params +import traceback from pymodaq.utils.parameter import Parameter from pymodaq.utils.parameter.pymodaq_ptypes import registerParameterType, GroupParameter -from .daqmxni import DAQmx, DAQ_analog_types, DAQ_thermocouples, DAQ_termination, Edge, DAQ_NIDAQ_source, \ +from .daqmxni import DAQmx, Edge, DAQ_NIDAQ_source, \ ClockSettings, AIChannel, Counter, AIThermoChannel, AOChannel, TriggerSettings, DOChannel, DIChannel logger = set_logger(get_module_name(__file__)) +from .daqmxni import UsageTypeAI, ThermocoupleType, TerminalConfiguration +from pymodaq.utils.daq_utils import ThreadCommand, getLineInfo +from pymodaq.utils.data import DataToExport, DataFromPlugins, DataActuator +from easydict import EasyDict as edict class ScalableGroupAI(GroupParameter): + """ | @@ -22,7 +31,7 @@ class ScalableGroupAI(GroupParameter): hardware.DAQ_Move_Stage_type """ - params = [{'title': 'AI type:', 'name': 'ai_type', 'type': 'list', 'limits': DAQ_analog_types.names()}, + params = [{'title': 'AI type:', 'name': 'ai_type', 'type': 'list', 'limits': UsageTypeAI._member_names_}, {'title': 'Voltages:', 'name': 'voltage_settings', 'type': 'group', 'children': [ {'title': 'Voltage Min:', 'name': 'volt_min', 'type': 'float', 'value': -10.}, {'title': 'Voltage Max:', 'name': 'volt_max', 'type': 'float', 'value': 10.}, @@ -32,12 +41,12 @@ class ScalableGroupAI(GroupParameter): {'title': 'Current Max:', 'name': 'curr_max', 'type': 'float', 'value': 1, 'suffix': 'A'}, ]}, {'title': 'Thermocouple:', 'name': 'thermoc_settings', 'type': 'group', 'visible': False, 'children': [ - {'title': 'Thc. type:', 'name': 'thermoc_type', 'type': 'list', 'limits': DAQ_thermocouples.names(), + {'title': 'Thc. type:', 'name': 'thermoc_type', 'type': 'list', 'limits': ThermocoupleType._member_names_, 'value': 'K'}, {'title': 'Temp. Min (°C):', 'name': 'T_min', 'type': 'float', 'value': 0, 'suffix': '°C'}, {'title': 'Temp. Max (°C):', 'name': 'T_max', 'type': 'float', 'value': 50, 'suffix': '°C'}, ]}, - {'title': 'Termination:', 'name': 'termination', 'type': 'list', 'limits': DAQ_termination.names()}, + {'title': 'Termination:', 'name': 'termination', 'type': 'list', 'limits': TerminalConfiguration._member_names_}, ] def __init__(self, **opts): @@ -84,7 +93,7 @@ class ScalableGroupAO(GroupParameter): hardware.DAQ_Move_Stage_type """ - params = [{'title': 'AO type:', 'name': 'ao_type', 'type': 'list', 'limits': DAQ_analog_types.names()[0:2]}, + params = [{'title': 'AO type:', 'name': 'ao_type', 'type': 'list', 'limits': UsageTypeAI._member_names_[0:2]}, {'title': 'Voltages:', 'name': 'voltage_settings', 'type': 'group', 'children': [ {'title': 'Voltage Min:', 'name': 'volt_min', 'type': 'list', 'value': -10., }, {'title': 'Voltage Max:', 'name': 'volt_max', 'type': 'list', 'value': 10., }, @@ -395,7 +404,7 @@ def update_task(self): logger.info("update_task - channels: {}".format(self.channels)) self.clock_settings = ClockSettings(frequency=self.settings['clock_settings', 'frequency'], Nsamples=self.settings['clock_settings', 'Nsamples'], - edge=Edge.Rising, + edge=Edge.RISING, repetition=self.live, ) self.trigger_settings = \ TriggerSettings(trig_source=self.settings['trigger_settings', 'trigger_channel'], @@ -416,20 +425,20 @@ def get_channels_from_settings(self): source='Analog_Input', analog_type=analog_type, value_min=channel['voltage_settings', 'volt_min'], value_max=channel['voltage_settings', 'volt_max'], - termination=DAQ_termination[channel['termination']], )) + termination=TerminalConfiguration[channel['termination']], )) elif analog_type == 'Current': channels.append(AIChannel(name=channel.opts['title'], source='Analog_Input', analog_type=analog_type, value_min=channel['current_settings', 'curr_min'], value_max=channel['current_settings', 'curr_max'], - termination=DAQ_termination[channel['termination']], )) + termination=TerminalConfiguration[channel['termination']], )) elif analog_type == 'Thermocouple': channels.append(AIThermoChannel(name=channel.opts['title'], source='Analog_Input', analog_type=analog_type, value_min=channel['thermoc_settings', 'T_min'], value_max=channel['thermoc_settings', 'T_max'], termination=channel['termination'], - thermo_type=DAQ_thermocouples[ + thermo_type=ThermocoupleType[ channel['thermoc_settings', 'thermoc_type']], )) elif self.settings['NIDAQ_type'] == DAQ_NIDAQ_source(1).name: # analog output @@ -465,3 +474,402 @@ def stop(self): self.timer.stop() QtWidgets.QApplication.processEvents() self.controller.stop() + + +class DAQ_NIDAQmx_Viewer(DAQ_Viewer_base, DAQ_NIDAQmx_base): + """ + ==================== ======================== + **Attributes** **Type** + *data_grabed_signal* instance of Signal + *params* dictionnary list + *task* + ==================== ======================== + + See Also + -------- + refresh_hardware + """ + + live_mode_available = True + params = viewer_params + DAQ_NIDAQmx_base.params + + def __init__(self, parent=None, params_state=None, control_type="0D"): + DAQ_Viewer_base.__init__(self, parent, params_state) # defines settings attribute and various other methods + DAQ_NIDAQmx_base.__init__(self) + + self.Naverage = None + self.live = False + self.control_type = control_type # could be "0D", "1D" or "Actuator" + if self.control_type == "0D": + self.settings.child('NIDAQ_type').setLimits( + ['Analog_Input', 'Counter', 'Digital_Input']) # analog input and counter + elif self.control_type == "1D": + self.settings.child('NIDAQ_type').setLimits(['Analog_Input']) + elif self.control_type == "Actuator": + self.settings.child('NIDAQ_type').setLimits(['Analog_Output']) + + self.settings.child('ao_settings').hide() + self.settings.child('ao_channels').hide() + + # timer used for the counter + self.timer = QTimer() + self.timer.setSingleShot(True) + self.timer.timeout.connect(self.counter_done) + + def stop(self): + """Stop the current grab hardware wise if necessary""" + self.controller.stop() + self.live = False + self.emit_status(ThreadCommand('Update_Status', ['Acquisition stopped.'])) + return '' + + def commit_settings(self, param): + """ + Activate the parameters changes in the hardware. + + =============== ================================ =========================== + **Parameters** **Type** **Description** + *param* instance of pyqtgraph.parameter the parameter to activate + =============== ================================ =========================== + + See Also + -------- + update_NIDAQ_channels, update_task, DAQ_NIDAQ_source, refresh_hardware + """ + + if param.parent() is not None: + if param.parent().name() == 'ai_channels': + device = param.opts['title'].split('/')[0] + self.settings.child('clock_settings', 'frequency').setOpts(max=self.getAIMaxRate(device)) + + ranges = self.controller.getAIVoltageRange(device) + param.child('voltage_settings', 'volt_min').setOpts(limits=[r[0] for r in ranges]) + param.child('voltage_settings', 'volt_max').setOpts(limits=[r[1] for r in ranges]) + + DAQ_NIDAQmx_base.commit_settings(self, param) + + def ini_detector(self, controller=None): + """ + Initialisation procedure of the detector. + + See Also + -------- + daq_utils.ThreadCommand + """ + try: + self.controller = self.ini_detector_init(controller, DAQmx()) + self.update_task() + + # actions to perform in order to set properly the settings tree options + self.commit_settings(self.settings.child('NIDAQ_type')) + + info = "Plugin Initialized" + initialized = True + return info, initialized + + except Exception as e: + logger.info(traceback.format_exc()) + self.emit_status(ThreadCommand('Update_Status', [str(e), 'log'])) + info = str(e) + initialized = False + return info, initialized + + def grab_data(self, Naverage=1, **kwargs): + """ + | grab the current values with NIDAQ profile procedure. + | + | Send the data_grabed_signal once done. + + =============== ======== =============================================== + **Parameters** **Type** **Description** + *Naverage* int Number of values to average + =============== ======== =============================================== + + See Also + -------- + DAQ_NIDAQ_source + """ + update = False + + if 'live' in kwargs: + if kwargs['live'] != self.live: + update = True + self.live = kwargs['live'] + + if Naverage != self.Naverage: + self.Naverage = Naverage + update = True + if update: + self.update_task() + + if self.controller.task is None: + self.update_task() + + self.controller.register_callback(self.emit_data, "Nsamples", self.clock_settings.Nsamples) + self.controller.start() + + def emit_data(self, task_handle, every_n_samples_event_type, number_of_samples, callback_data): + channels_names = [ch.name for ch in self.channels] + # channels_ai_names = [ch.name for ch in self.channels if ch.source == 'Analog_Input'] + data_from_task = self.controller.task.read(self.settings['nsamplestoread'], timeout=20.0) + if self.control_type == "0D": + if not len(self.controller.task.channels.channel_names) != 1: + data_dfp = [np.array(data_from_task)] + else: + data_dfp = list(map(np.array, data_from_task)) + dte = DataToExport(name='NIDAQmx', + data=[DataFromPlugins(name='NI Analog Input', + data=data_dfp, + dim=f'Data{self.settings.child("display").value()}', + labels=channels_names + ), + ]) + self.dte_signal.emit(dte) + return 0 # mandatory for the NIDAQmx callback + + def counter_done(self): + channels_name = [ch.name for ch in self.channels] + data_counter = self.readCounter(len(self.channels), + self.settings['counter_settings', 'counting_time'] * 1e-3) + self.data_grabed_signal.emit([DataFromPlugins(name='NI Counter', data=[data_counter / 1e-3], dim='Data0D', + labels=channels_name, )]) + # y_axis=Axis(label='Count Number', units='1/s'))]) + self.task.StopTask() + +class DAQ_NIDAQmx_Actuator(DAQ_Move_base, DAQ_NIDAQmx_base): + """ + Wrapper object to access the Mock fonctionnalities, similar wrapper for all controllers. + + =============== ============== + **Attributes** **Type** + *params* dictionnary + =============== ============== + """ + _controller_units = 'Volts' + is_multiaxes = False # set to True if this plugin is controlled for a multiaxis controller (with a unique communication link) + stage_names = [] # "list of strings of the multiaxes + + params = DAQ_NIDAQmx_base.params + [ + # elements to be added here as dicts in order to control your custom stage + ############ + {'title': 'MultiAxes:', 'name': 'multiaxes', 'type': 'group', 'visible': is_multiaxes, + 'children': [ + {'title': 'is Multiaxes:', 'name': 'ismultiaxes', 'type': 'bool', 'value': is_multiaxes, + 'default': False}, + {'title': 'Status:', 'name': 'multi_status', 'type': 'list', 'value': 'Master', + 'limits': ['Master', 'Slave']}, + {'title': 'Axis:', 'name': 'axis', 'type': 'list', 'limits': stage_names}, + ]}] + actuator_params + + def __init__(self, parent=None, params_state=None, control_type="Actuator"): + DAQ_Move_base.__init__(self, parent, params_state) # defines settings attribute and various other methods + DAQ_NIDAQmx_base.__init__(self) + + self.control_type = "Actuator" # could be "0D", "1D" or "Actuator" + self.settings.child('NIDAQ_type').setLimits(['Analog_Output', 'Digital_Output']) + + self.settings.child('clock_settings', 'Nsamples').setValue(1) + + def get_actuator_value(self) -> DataActuator: + """Get the current position from the hardware with scaling conversion. + + Returns + ------- + float: The position obtained after scaling conversion. + """ + + pos = self.target_position + ## + + pos = self.get_position_with_scaling(pos) + self.emit_status(ThreadCommand('check_position', [pos])) + return pos + + def commit_settings(self, param): + """ + | Activate any parameter changes on the PI_GCS2 hardware. + | + | Called after a param_tree_changed signal from DAQ_Move_main. + + """ + + DAQ_NIDAQmx_base.commit_settings(self, param) + if param.name() == 'waveform': + if param.value() == 'DC': + self.settings.child('ao_settings', 'cont_param').setValue('offset') + self.settings.child('ao_settings', 'cont_param').show(not param.value() == 'DC') + self.settings.child('ao_settings', 'waveform_settings').show(not param.value() == 'DC') + + if param.parent() is not None: + if param.parent().name() == 'ao_channels': + device = param.opts['title'].split('/')[0] + self.settings.child('clock_settings', 'frequency').setOpts(max=self.getAOMaxRate(device)) + + ranges = self.getAOVoltageRange(device) + param.child('voltage_settings', 'volt_min').setOpts(limits=[r[0] for r in ranges]) + param.child('voltage_settings', 'volt_max').setOpts(limits=[r[1] for r in ranges]) + + 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 + ------- + self.status (edict): with initialization status: three fields: + * info (str) + * controller (object) initialized controller + *initialized: (bool): False if initialization failed otherwise True + """ + + try: + # initialize the stage and its controller status + # controller is an object that may be passed to other instances of DAQ_Move_Mock in case + # of one controller controlling multiactuators (or detector) + + self.status.update(edict(info="", controller=None, initialized=False)) + + # check whether this stage is controlled by a multiaxe controller (to be defined for each plugin) + # if multiaxes then init the controller here if Master state otherwise use external controller + if self.settings['multiaxes', 'ismultiaxes'] and self.settings['multiaxes', + 'multi_status'] == "Slave": + if controller is None: + raise Exception('no controller has been defined externally while this axe is a slave one') + else: + self.controller = controller + else: + self.controller = 'A Nidaqmx task' + self.update_task() + + # actions to perform in order to set properly the settings tree options + self.commit_settings(self.settings.child('NIDAQ_type')) + + self.status.info = "Plugin Initialized" + self.status.controller = self.controller + self.status.initialized = True + return self.status + + except Exception as e: + self.emit_status(ThreadCommand('Update_Status', [getLineInfo() + str(e), 'log'])) + self.status.info = getLineInfo() + str(e) + self.status.initialized = False + return self.status + + def calulate_waveform(self, value): + waveform = self.settings['ao_settings', 'waveform'] + if waveform == 'DC': + values = np.array([value]) + else: + Nsamples = self.settings['clock_settings', 'Nsamples'] + freq = self.settings['clock_settings', 'frequency'] + time = np.linspace(0, Nsamples / freq, Nsamples, endpoint=False) + + freq0 = self.settings['ao_settings', 'waveform_settings', 'frequency'] + amp = self.settings['ao_settings', 'waveform_settings', 'amplitude'] + offset = self.settings['ao_settings', 'waveform_settings', 'offset'] + if waveform == 'Sinus': + values = offset + amp * np.sin(2 * np.pi * freq0 * time) + elif waveform == 'Ramp': + values = offset + amp * np.linspace(0, 1, Nsamples) + + return values + + def move_Abs(self, position): + """ Move the actuator to the absolute target defined by position + + Parameters + ---------- + position: (flaot) value of the absolute target positioning + """ + + position = self.check_bound(position) # if user checked bounds, the defined bounds are applied here + position = self.set_position_with_scaling(position) # apply scaling if the user specified one + if self.settings['NIDAQ_type'] == 'Analog_Output': + self.settings.child('ao_settings', 'waveform_settings', + self.settings['ao_settings', 'cont_param']).setValue(position) + values = self.calulate_waveform(position) + self.target_position = position + + self.stop() + + if len(values) == 1: + self.writeAnalog(len(values), 1, values, autostart=True) + self.current_position = self.check_position() + self.move_done() + else: + if self.c_callback is None: + self.register_callback(self.move_done_callback) + self.writeAnalog(len(values), 1, values, autostart=False) + self.task.StartTask() + elif self.settings['NIDAQ_type'] == 'Digital_Output': + self.writeDigital(1, np.array([position], dtype=np.uint8), autostart=True) + + def move_done_callback(self, taskhandle, status, callbackdata): + self.current_position = self.check_position() + QtWidgets.QApplication.processEvents() + self.move_done() + self.task.StopTask() + return 0 + + def move_Rel(self, position): + """ Move the actuator to the relative target actuator value defined by position + + Parameters + ---------- + position: (flaot) value of the relative target positioning + """ + + position = self.check_bound(self.current_position + position) - self.current_position + self.target_position = position + self.current_position + if self.settings['NIDAQ_type'] == 'Analog_Output': + self.settings.child('ao_settings', 'waveform_settings', + self.settings['ao_settings', 'cont_param']).setValue(self.target_position) + + values = self.calulate_waveform(self.target_position) + + self.stop() + + if len(values) == 1: + self.writeAnalog(len(values), 1, values, autostart=True) + self.current_position = self.check_position() + self.move_done() + else: + if self.c_callback is None: + self.register_callback(self.move_done_callback) + self.writeAnalog(len(values), 1, values, autostart=False) + self.task.StartTask() + elif self.settings['NIDAQ_type'] == 'Digital_Output': + self.writeDigital(1, np.array([self.target_position], dtype=np.uint8), autostart=True) + + def move_Home(self): + """ + Send the update status thread command. + See Also + -------- + daq_utils.ThreadCommand + """ + + self.stop() + + if self.c_callback is None: + self.register_callback(self.move_done_callback) + self.writeAnalog(1, 1, np.array([0.])) + self.task.StartTask() + + def stop_motion(self): + """ + Call the specific move_done function (depending on the hardware). + + See Also + -------- + move_done + """ + + ## TODO for your custom plugin + self.stop() + self.emit_status(ThreadCommand('Update_Status', ['Some info you want to log'])) + self.move_done() # to let the interface know the actuator stopped + ############################## +>>>>>>> eda1396 (removed CustomEnum to use the ones from nidaq.constants) diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py index c77577c..19aac63 100644 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py @@ -3,9 +3,14 @@ from enum import IntEnum import numpy as np from pymodaq.utils.logger import set_logger, get_module_name -import nidaqmx -from nidaqmx.constants import * + +from nidaqmx.constants import UsageTypeAI, ThermocoupleType, TerminalConfiguration, Edge, AcquisitionType, \ + VoltageUnits, CurrentUnits, CurrentShuntResistorLocation, TemperatureUnits, \ + CJCSource, CountDirection, Level, FrequencyUnits, TimeUnits, LineGrouping +from nidaqmx.system import System + from nidaqmx.system.device import Device +from nidaqmx import Task from nidaqmx.errors import DaqError, DAQmxErrors from pymodaq_plugins_daqmx import config @@ -13,134 +18,55 @@ logger = set_logger(get_module_name(__file__)) -class DAQ_NIDAQ_source(IntEnum): - """ - Enum class of NIDAQ_source - - =============== ========== - **Attributes** **Type** - *Analog_Input* int - *Counter* int - =============== ========== - """ - Analog_Input = 0 - Analog_Output = 1 - Counter = 2 - Digital_Input = 3 - Digital_Output = 4 - Terminals = 5 - - @classmethod - def names(cls): - return [name for name, member in cls.__members__.items()] - - @classmethod - def members(cls): - return [member for name, member in cls.__members__.items()] - - -class DAQ_analog_types(IntEnum): +class IntEnumExtend(IntEnum): """ - Enum class of Ai types - - =============== ========== - **Attributes** **Type** - =============== ========== + This class expose 3 privates and undocumented methods of Enum """ - Voltage = UsageTypeAI.VOLTAGE.value - Current = UsageTypeAI.CURRENT.value - Thermocouple = UsageTypeAI.TEMPERATURE_THERMOCOUPLE.value - @classmethod def names(cls): - return [name for name, member in cls.__members__.items()] + return cls._member_names_ + # [item.name for item in cls.__members__.items()] + # [name for name, member in cls.__members__.items()] @classmethod def members(cls): - return [member for name, member in cls.__members__.items()] + return list(cls._member_map_.values()) + # [member for name, member in cls.__members__.items()] + # [item.member for item in cls.__members__.items()] @classmethod def values(cls): - return [cls[name].value for name, member in cls.__members__.items()] - - -class DAQ_thermocouples(IntEnum): - """ - Enum class of thermocouples type - - =============== ========== - **Attributes** **Type** - =============== ========== - """ - J = ThermocoupleType.J.value - K = ThermocoupleType.K.value - N = ThermocoupleType.N.value - R = ThermocoupleType.R.value - S = ThermocoupleType.S.value - T = ThermocoupleType.T.value - B = ThermocoupleType.B.value - E = ThermocoupleType.E.value - - @classmethod - def names(cls): - return [name for name, member in cls.__members__.items()] + return [cls[name].value for name in cls._member_names_] + # [cls[name].value for name, member in cls.__members__.items()] - @classmethod - def members(cls): - return [member for name, member in cls.__members__.items()] - -class DAQ_termination(IntEnum): +class DAQ_NIDAQ_source(IntEnumExtend): """ - Enum class of termination type + Enum class of NIDAQ_source =============== ========== **Attributes** **Type** + *Analog_Input* int + *Counter* int =============== ========== """ - Auto = TerminalConfiguration.DEFAULT.value - RSE = TerminalConfiguration.RSE.value - NRSE = TerminalConfiguration.NRSE.value - Diff = TerminalConfiguration.DIFF.value - Pseudodiff = TerminalConfiguration.PSEUDO_DIFF.value - - @classmethod - def names(cls): - return [name for name, member in cls.__members__.items()] - - @classmethod - def members(cls): - return [member for name, member in cls.__members__.items()] - - -class Edge(IntEnum): - """ - """ - Rising = Edge.RISING.value - Falling = Edge.FALLING.value - - @classmethod - def names(cls): - return [name for name, member in cls.__members__.items()] - - @classmethod - def members(cls): - return [member for name, member in cls.__members__.items()] - - -class ClockMode(IntEnum): - """ - """ - Finite = AcquisitionType.FINITE.value - Continuous = AcquisitionType.CONTINUOUS.value + Analog_Input = 0 + Analog_Output = 1 + Counter = 2 + Digital_Input = 3 + Digital_Output = 4 + Terminals = 5 - @classmethod - def names(cls): - return [name for name, member in cls.__members__.items()] - @classmethod - def members(cls): - return [member for name, member in cls.__members__.items()] +# class DAQ_analog_types(UsageTypeAI): +# """ +# Enum class of Ai types +# +# =============== ========== +# **Attributes** **Type** +# =============== ========== +# """ +# Thermocouple = UsageTypeAI.TEMPERATURE_THERMOCOUPLE.value class ClockSettingsBase: @@ -151,7 +77,7 @@ def __init__(self, Nsamples=1000, repetition=False): class ClockSettings(ClockSettingsBase): - def __init__(self, source=None, frequency=1000, Nsamples=1000, edge=Edge.Rising, repetition=False): + def __init__(self, source=None, frequency=1000, Nsamples=1000, edge=Edge.RISING, repetition=False): super().__init__(Nsamples, repetition) self.source = source assert edge in Edge.members() @@ -168,7 +94,7 @@ def __init__(self, Nsamples=1000, rising_channel='', falling_channel='', class TriggerSettings: - def __init__(self, trig_source='', enable=False, edge=Edge.Rising, level=0.1): + def __init__(self, trig_source='', enable=False, edge=Edge.RISING, level=0.1): assert edge in Edge.members() self.trig_source = trig_source self.enable = enable @@ -189,7 +115,7 @@ def __init__(self, name='', source=DAQ_NIDAQ_source.Analog_Input): class AChannel(Channel): - def __init__(self, analog_type=DAQ_analog_types.Voltage, value_min=-10., value_max=+10., **kwargs): + def __init__(self, analog_type=UsageTypeAI.VOLTAGE, value_min=-10., value_max=+10., **kwargs): """ Parameters ---------- @@ -203,16 +129,16 @@ def __init__(self, analog_type=DAQ_analog_types.Voltage, value_min=-10., value_m class AIChannel(AChannel): - def __init__(self, termination=DAQ_termination.Auto, **kwargs): + def __init__(self, termination=TerminalConfiguration.DEFAULT, **kwargs): super().__init__(**kwargs) - assert termination in DAQ_termination.members() + assert termination in TerminalConfiguration._member_map_.values() self.termination = termination class AIThermoChannel(AIChannel): - def __init__(self, thermo_type=DAQ_thermocouples.K, **kwargs): + def __init__(self, thermo_type=ThermocoupleType.K, **kwargs): super().__init__(**kwargs) - assert thermo_type in DAQ_thermocouples.members() + assert thermo_type in ThermocoupleType._member_map_.values() self.thermo_type = thermo_type @@ -222,8 +148,8 @@ def __init__(self, **kwargs): class Counter(Channel): - def __init__(self, edge=Edge.Rising, **kwargs): - assert edge in Edge.members() + def __init__(self, edge=Edge.RISING, **kwargs): + assert edge in Edge._member_map_.values() super().__init__(**kwargs) self.edge = edge self.counter_type = "Edge Counter" @@ -292,7 +218,7 @@ def get_NIDAQ_devices(cls): list of devices as strings to be used in subsequent commands """ try: - devices = nidaqmx.system.System.local().devices + devices = System.local().devices if devices == ['']: devices = [] return devices @@ -330,17 +256,17 @@ def get_NIDAQ_channels(cls, devices=None, source_type=None): if not not devices: for device in devices: for source in source_type: - if source == DAQ_NIDAQ_source['Analog_Input'].name: # analog input + if source == DAQ_NIDAQ_source.Analog_Input.name: # analog input channels = Device(device).ai_physical_chans.channel_names - elif source == DAQ_NIDAQ_source['Analog_Output'].name: # analog output + elif source == DAQ_NIDAQ_source.Analog_Output.name: # analog output channels = Device(device).ao_physical_chans.channel_names - elif source == DAQ_NIDAQ_source['Counter'].name: # counter + elif source == DAQ_NIDAQ_source.Counter.name: # counter channels = Device(device).ci_physical_chans.channel_names - elif source == DAQ_NIDAQ_source['Digital_Output'].name: # digital output + elif source == DAQ_NIDAQ_source.Digital_Output.name: # digital output channels = Device(device).do_lines.channel_names - elif source == DAQ_NIDAQ_source['Digital_Input'].name: # digital iutput + elif source == DAQ_NIDAQ_source.Digital_Input.name: # digital iutput channels = Device(device).di_lines.channel_names - elif source == DAQ_NIDAQ_source['Terminals'].name: # terminals + elif source == DAQ_NIDAQ_source.Terminals.name: # terminals channels = Device(device).terminals if channels != ['']: @@ -471,13 +397,13 @@ def update_task(self, channels=[], clock_settings=ClockSettings(), trigger_setti try: if self._task is not None: - if isinstance(self._task, nidaqmx.Task): + if isinstance(self._task, Task): self._task.close() self._task = None self.c_callback = None - self._task = nidaqmx.Task() + self._task = Task() logger.info("TASK: {}".format(self._task)) err_code = None @@ -662,12 +588,12 @@ def readCounter(self): @classmethod def getAIVoltageRange(cls, device='Dev1'): - ret = nidaqmx.system.System.local().devices[device].ai_voltage_rngs + ret = System.local().devices[device].ai_voltage_rngs # todo self.devices[device].ai_voltage_rngs return [tuple(ret[6:8])] @classmethod def getAOVoltageRange(cls, device='Dev1'): - ret = nidaqmx.system.System.local().devices[device].ao_voltage_rngs + ret = System.local().devices[device].ao_voltage_rngs # todo self.devices[device].ao_voltage_rngs return [tuple(ret)] # [(-10., 10.)] Why this format is needed?? def stop(self): @@ -710,8 +636,8 @@ def refresh_hardware(self): -------- update_NIDAQ_devices, update_NIDAQ_channels """ - devices = self.update_NIDAQ_devices() - self.update_NIDAQ_channels(devices) + self.update_NIDAQ_devices() + self.update_NIDAQ_channels() if __name__ == '__main__': From 17d444df6b75f9c0ecb6dc570a940abeab22ba3b Mon Sep 17 00:00:00 2001 From: Loic Guilmard Date: Wed, 18 Dec 2024 17:09:41 +0100 Subject: [PATCH 031/140] migrated to pyproject.toml --- hatch_build.py | 10 ++++++++ plugin_info.toml | 23 ----------------- pyproject.toml | 64 ++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 74 insertions(+), 23 deletions(-) create mode 100644 hatch_build.py delete mode 100644 plugin_info.toml create mode 100644 pyproject.toml diff --git a/hatch_build.py b/hatch_build.py new file mode 100644 index 0000000..a0208ee --- /dev/null +++ b/hatch_build.py @@ -0,0 +1,10 @@ +from pathlib import Path +from hatchling.metadata.plugin.interface import MetadataHookInterface +from pymodaq_utils.resources.hatch_build_plugins import update_metadata_from_toml + +here = Path(__file__).absolute().parent + + +class PluginInfoTomlHook(MetadataHookInterface): + def update(self, metadata: dict) -> None: + update_metadata_from_toml(metadata, here) diff --git a/plugin_info.toml b/plugin_info.toml deleted file mode 100644 index 946fe9e..0000000 --- a/plugin_info.toml +++ /dev/null @@ -1,23 +0,0 @@ -## To modify by developper(s) of the plugin - -[plugin-info] -SHORT_PLUGIN_NAME = 'daqmx' #for instance daqmx -package-url = 'https://github.com/PyMoDAQ/pymodaq_plugins_daqmx' #to modify -description = 'Hardware plugins for PyMoDAQ using the NiDAQmx framework (pydaqmx wrapper)' - -author = 'Sébastien Weber' -author-email = 'sebastien.weber@cemes.fr' -license = 'MIT' - -[plugin-install] -#packages required for your plugin: -packages-required = ['pydaqmx','nidaqmx','pymodaq>4.0.1'] -## - -[features] # defines the plugin features contained into this plugin -instruments = true # true if plugin contains instrument classes (else false, notice the lowercase for toml files) -extensions = false # true if plugins contains dashboard extensions -pid_models = false # true if plugins contains pid models -h5exporters = false # true if plugin contains custom h5 file exporters -scanners = false # true if plugin contains custom scan layout (daq_scan extensions) - diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..329457c --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,64 @@ +[features] # defines the plugin features contained into this plugin +instruments = true # true if plugin contains instrument classes (else false, notice the lowercase for toml files) +extensions = false # true if plugins contains dashboard extensions +models = false # true if plugins contains pid models +h5exporters = false # true if plugin contains custom h5 file exporters +scanners = false # true if plugin contains custom scan layout (daq_scan extensions) + +[urls] +package-url = 'https://github.com/PyMoDAQ/pymodaq_plugins_daqmx' + +[project] +name = "pymodaq_plugins_daqmx" +description = 'Hardware plugins for PyMoDAQ using the NiDAQmx framework (nidaq wrapper)' +dependencies = [ + "pymodaq>=4.4", + "pydaqmx", + "nidaqmx" +] + +authors = [ + {name = 'Sébastien Weber', email = 'sebastien.weber@cemes.fr'}, + {name = 'Sébastien Guerrero', email = 'sebastien.guerrero@insa-lyon.fr'}, +] +maintainers = [ + {name = 'Sébastien Weber', email = 'sebastien.weber@cemes.fr'}, + {name = 'Sébastien Guerrero', email = 'sebastien.guerrero@insa-lyon.fr'}, +] + +# nottodo: leave everything below as is! + +dynamic = ["version", "urls", "entry-points"] +readme = "README.rst" +license = { file="LICENSE" } +requires-python = ">=3.8" + +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Science/Research", + "License :: OSI Approved :: MIT License", + "Natural Language :: English", + "Operating System :: OS Independent", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Topic :: Scientific/Engineering :: Human Machine Interfaces", + "Topic :: Scientific/Engineering :: Visualization", + "Topic :: Software Development :: Libraries :: Python Modules", + "Topic :: Software Development :: User Interfaces", +] + +[build-system] +requires = [ + "hatchling>=1.9.0", + "hatch-vcs", "toml", + "pymodaq_utils>=0.0.6", +] +build-backend = "hatchling.build" + +[tool.hatch.metadata.hooks.custom] + +[tool.hatch.version] +source = "vcs" + From ba2ec80cf5b2697f9ff28d7356f173ff0ba48ead Mon Sep 17 00:00:00 2001 From: Loic Guilmard Date: Thu, 19 Dec 2024 10:44:06 +0100 Subject: [PATCH 032/140] extended nidaqmx.constants : UsageTypeAI, Edge, TerminalConfiguration, ThermocoupleType --- .../hardware/national_instruments/__init__.py | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/__init__.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/__init__.py index e69de29..987567a 100644 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/__init__.py +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/__init__.py @@ -0,0 +1,47 @@ +from aenum import extend_enum +from types import MethodType +from nidaqmx.constants import UsageTypeAI, Edge, TerminalConfiguration, ThermocoupleType + + +def members(classref): + """ + Return the list of the members of cls.__members__.items() + """ + return list(classref._member_map_.values()) + + +def names(classref): + """ + Return the list of the names of the enum members + """ + return classref._member_names_ + + +extend_enum(UsageTypeAI, "Thermocouple", UsageTypeAI.TEMPERATURE_THERMOCOUPLE.value) + + +extend_enum(TerminalConfiguration, "Auto", TerminalConfiguration.DEFAULT.value) + + +UsageTypeAI.members = MethodType(members, UsageTypeAI) + + +UsageTypeAI.names = MethodType(names, UsageTypeAI) + + +Edge.members = MethodType(members, Edge) + + +Edge.names = MethodType(names, Edge) + + +TerminalConfiguration.members = MethodType(members, TerminalConfiguration) + + +TerminalConfiguration.names = MethodType(names, TerminalConfiguration) + + +ThermocoupleType.members = MethodType(members, ThermocoupleType) + + +ThermocoupleType.names = MethodType(names, ThermocoupleType) From d91685624ac9407b77e06c0a466b4ae4bc5ff9db Mon Sep 17 00:00:00 2001 From: Loic Guilmard Date: Thu, 19 Dec 2024 10:47:46 +0100 Subject: [PATCH 033/140] Using the extended nidaqmx.constants --- .../national_instruments/daq_NIDAQmx.py | 15 ++++++----- .../hardware/national_instruments/daqmxni.py | 25 ++++++------------- 2 files changed, 15 insertions(+), 25 deletions(-) diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx.py index eebfee0..b9caa0b 100644 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx.py +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx.py @@ -7,10 +7,9 @@ import traceback from pymodaq.utils.parameter import Parameter from pymodaq.utils.parameter.pymodaq_ptypes import registerParameterType, GroupParameter -from .daqmxni import DAQmx, Edge, DAQ_NIDAQ_source, \ - ClockSettings, AIChannel, Counter, AIThermoChannel, AOChannel, TriggerSettings, DOChannel, DIChannel -logger = set_logger(get_module_name(__file__)) -from .daqmxni import UsageTypeAI, ThermocoupleType, TerminalConfiguration +from .daqmxni import DAQmx, Edge, DAQ_NIDAQ_source, ClockSettings, AIChannel, Counter, \ + AIThermoChannel, AOChannel, TriggerSettings, DOChannel, DIChannel +from . import UsageTypeAI, ThermocoupleType, TerminalConfiguration from pymodaq.utils.daq_utils import ThreadCommand, getLineInfo from pymodaq.utils.data import DataToExport, DataFromPlugins, DataActuator from easydict import EasyDict as edict @@ -31,7 +30,7 @@ class ScalableGroupAI(GroupParameter): hardware.DAQ_Move_Stage_type """ - params = [{'title': 'AI type:', 'name': 'ai_type', 'type': 'list', 'limits': UsageTypeAI._member_names_}, + params = [{'title': 'AI type:', 'name': 'ai_type', 'type': 'list', 'limits': UsageTypeAI.names()}, {'title': 'Voltages:', 'name': 'voltage_settings', 'type': 'group', 'children': [ {'title': 'Voltage Min:', 'name': 'volt_min', 'type': 'float', 'value': -10.}, {'title': 'Voltage Max:', 'name': 'volt_max', 'type': 'float', 'value': 10.}, @@ -41,12 +40,12 @@ class ScalableGroupAI(GroupParameter): {'title': 'Current Max:', 'name': 'curr_max', 'type': 'float', 'value': 1, 'suffix': 'A'}, ]}, {'title': 'Thermocouple:', 'name': 'thermoc_settings', 'type': 'group', 'visible': False, 'children': [ - {'title': 'Thc. type:', 'name': 'thermoc_type', 'type': 'list', 'limits': ThermocoupleType._member_names_, + {'title': 'Thc. type:', 'name': 'thermoc_type', 'type': 'list', 'limits': ThermocoupleType.names(), 'value': 'K'}, {'title': 'Temp. Min (°C):', 'name': 'T_min', 'type': 'float', 'value': 0, 'suffix': '°C'}, {'title': 'Temp. Max (°C):', 'name': 'T_max', 'type': 'float', 'value': 50, 'suffix': '°C'}, ]}, - {'title': 'Termination:', 'name': 'termination', 'type': 'list', 'limits': TerminalConfiguration._member_names_}, + {'title': 'Termination:', 'name': 'termination', 'type': 'list', 'limits': TerminalConfiguration.names()}, ] def __init__(self, **opts): @@ -93,7 +92,7 @@ class ScalableGroupAO(GroupParameter): hardware.DAQ_Move_Stage_type """ - params = [{'title': 'AO type:', 'name': 'ao_type', 'type': 'list', 'limits': UsageTypeAI._member_names_[0:2]}, + params = [{'title': 'AO type:', 'name': 'ao_type', 'type': 'list', 'limits': UsageTypeAI.names()[0:2]}, {'title': 'Voltages:', 'name': 'voltage_settings', 'type': 'group', 'children': [ {'title': 'Voltage Min:', 'name': 'volt_min', 'type': 'list', 'value': -10., }, {'title': 'Voltage Max:', 'name': 'volt_max', 'type': 'list', 'value': 10., }, diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py index 19aac63..5a4e2f3 100644 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py @@ -4,11 +4,13 @@ import numpy as np from pymodaq.utils.logger import set_logger, get_module_name -from nidaqmx.constants import UsageTypeAI, ThermocoupleType, TerminalConfiguration, Edge, AcquisitionType, \ - VoltageUnits, CurrentUnits, CurrentShuntResistorLocation, TemperatureUnits, \ - CJCSource, CountDirection, Level, FrequencyUnits, TimeUnits, LineGrouping from nidaqmx.system import System +from nidaqmx.constants import AcquisitionType, VoltageUnits, CurrentUnits, CurrentShuntResistorLocation, \ + TemperatureUnits, CJCSource, CountDirection, Level, FrequencyUnits, TimeUnits, \ + LineGrouping +from . import UsageTypeAI, Edge, TerminalConfiguration, ThermocoupleType + from nidaqmx.system.device import Device from nidaqmx import Task from nidaqmx.errors import DaqError, DAQmxErrors @@ -58,17 +60,6 @@ class DAQ_NIDAQ_source(IntEnumExtend): Terminals = 5 -# class DAQ_analog_types(UsageTypeAI): -# """ -# Enum class of Ai types -# -# =============== ========== -# **Attributes** **Type** -# =============== ========== -# """ -# Thermocouple = UsageTypeAI.TEMPERATURE_THERMOCOUPLE.value - - class ClockSettingsBase: def __init__(self, Nsamples=1000, repetition=False): @@ -131,14 +122,14 @@ def __init__(self, analog_type=UsageTypeAI.VOLTAGE, value_min=-10., value_max=+1 class AIChannel(AChannel): def __init__(self, termination=TerminalConfiguration.DEFAULT, **kwargs): super().__init__(**kwargs) - assert termination in TerminalConfiguration._member_map_.values() + assert termination in TerminalConfiguration.members() self.termination = termination class AIThermoChannel(AIChannel): def __init__(self, thermo_type=ThermocoupleType.K, **kwargs): super().__init__(**kwargs) - assert thermo_type in ThermocoupleType._member_map_.values() + assert thermo_type in ThermocoupleType.members() self.thermo_type = thermo_type @@ -149,7 +140,7 @@ def __init__(self, **kwargs): class Counter(Channel): def __init__(self, edge=Edge.RISING, **kwargs): - assert edge in Edge._member_map_.values() + assert edge in Edge.members() super().__init__(**kwargs) self.edge = edge self.counter_type = "Edge Counter" From 0e4be2eca11dc5bb386f06132227ec962662bd5a Mon Sep 17 00:00:00 2001 From: Loic Guilmard Date: Thu, 19 Dec 2024 10:51:20 +0100 Subject: [PATCH 034/140] [Cleaning] aliasing task and System, using explicit enum.Input and PEPing --- .../plugins_0D/daq_0Dviewer_NIDAQmx.py | 10 +++--- .../national_instruments/daq_NIDAQmx.py | 34 ++++++++++++------- .../hardware/national_instruments/daqmxni.py | 23 ++++++------- 3 files changed, 37 insertions(+), 30 deletions(-) diff --git a/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_NIDAQmx.py b/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_NIDAQmx.py index 10dbd42..b5cdb49 100644 --- a/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_NIDAQmx.py +++ b/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_NIDAQmx.py @@ -2,7 +2,7 @@ from pymodaq.control_modules.viewer_utility_classes import comon_parameters as viewer_params from pymodaq_plugins_daqmx import config from pymodaq_plugins_daqmx.hardware.national_instruments.daqmxni import AIChannel, AIThermoChannel, DAQmx, \ - Task, niconstants + niTask, niconstants from pymodaq_plugins_daqmx.hardware.national_instruments.daqmxni import UsageTypeAI, ThermocoupleType, \ TerminalConfiguration from pymodaq_plugins_daqmx.hardware.national_instruments.daq_NIDAQmx import DAQ_NIDAQmx_base @@ -64,10 +64,10 @@ def close(self): else: try: print("In main") - import nidaqmx.system.System + from pymodaq_plugins_daqmx.hardware.national_instruments.daqmxni import niSystem # EXPLORE DEVICES - devices = nidaqmx.system.System.local().devices + devices = niSystem.local().devices print("devices {}".format(devices)) print("devices names {}".format(devices.device_names)) print("devices types {}".format([dev.product_type for dev in devices])) @@ -112,8 +112,8 @@ def close(self): ), ] # CREATE TASK - task_9211 = Task() - task_9205 = Task() + task_9211 = niTask() + task_9205 = niTask() def callback_9211(task_handle, every_n_samples_event_type, number_of_samples, callback_data): data9211 = task_9211.read(5) diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx.py index b9caa0b..095ef82 100644 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx.py +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx.py @@ -12,7 +12,10 @@ from . import UsageTypeAI, ThermocoupleType, TerminalConfiguration from pymodaq.utils.daq_utils import ThreadCommand, getLineInfo from pymodaq.utils.data import DataToExport, DataFromPlugins, DataActuator -from easydict import EasyDict as edict +from easydict import EasyDict as eDict + + +logger = set_logger(get_module_name(__file__)) class ScalableGroupAI(GroupParameter): @@ -329,7 +332,7 @@ def commit_settings(self, param: Parameter): if param.name() == 'NIDAQ_type': self.controller.update_NIDAQ_channels(param.value()) - if param.value() == DAQ_NIDAQ_source(0).name: # analog input + if param.value() == DAQ_NIDAQ_source.Analog_Input.name: # analog input self.settings.child('clock_settings').show() self.settings.child('ai_channels').show() self.settings.child('ao_channels').hide() @@ -338,7 +341,7 @@ def commit_settings(self, param: Parameter): self.settings.child('do_channels').hide() self.settings.child('di_channels').hide() - elif param.value() == DAQ_NIDAQ_source(1).name: # analog output + elif param.value() == DAQ_NIDAQ_source.Analog_Output.name: # analog output self.settings.child('clock_settings').show() self.settings.child('ai_channels').hide() self.settings.child('ao_channels').show() @@ -347,7 +350,7 @@ def commit_settings(self, param: Parameter): self.settings.child('do_channels').hide() self.settings.child('di_channels').hide() - elif param.value() == DAQ_NIDAQ_source(2).name: # counter input + elif param.value() == DAQ_NIDAQ_source.Counter.name: # counter input self.settings.child('clock_settings').hide() self.settings.child('ai_channels').hide() self.settings.child('ao_channels').hide() @@ -356,7 +359,7 @@ def commit_settings(self, param: Parameter): self.settings.child('do_channels').hide() self.settings.child('di_channels').hide() - elif param.value() == DAQ_NIDAQ_source(3).name: # Digital_Input + elif param.value() == DAQ_NIDAQ_source.Digital_Input.name: # Digital_Input self.settings.child('clock_settings').show() self.settings.child('ai_channels').hide() self.settings.child('ao_channels').show() @@ -365,7 +368,8 @@ def commit_settings(self, param: Parameter): self.settings.child('do_channels').hide() self.settings.child('di_channels').show() - elif param.value() == DAQ_NIDAQ_source(4).name: # digital output + + elif param.value() == DAQ_NIDAQ_source.Digital_Output.name: # digital output self.settings.child('clock_settings').show() self.settings.child('ai_channels').hide() self.settings.child('ao_channels').hide() @@ -373,6 +377,7 @@ def commit_settings(self, param: Parameter): self.settings.child('counter_settings').hide() self.settings.child('do_channels').show() self.settings.child('di_channels').hide() + self.update_task() elif param.name() == 'refresh_hardware': @@ -415,7 +420,7 @@ def update_task(self): def get_channels_from_settings(self): channels = [] - if self.settings['NIDAQ_type'] == DAQ_NIDAQ_source(0).name: # analog input + if self.settings['NIDAQ_type'] == DAQ_NIDAQ_source.Analog_Input.name: # analog input for channel in self.settings.child('ai_channels').children(): logger.info("get_channels_from_settings - channel {}".format(channel)) analog_type = channel['ai_type'] @@ -440,7 +445,7 @@ def get_channels_from_settings(self): thermo_type=ThermocoupleType[ channel['thermoc_settings', 'thermoc_type']], )) - elif self.settings['NIDAQ_type'] == DAQ_NIDAQ_source(1).name: # analog output + elif self.settings['NIDAQ_type'] == DAQ_NIDAQ_source.Analog_Output.name: # analog output for channel in self.settings.child('ao_channels').children(): analog_type = channel['ao_type'] channels.append(AOChannel(name=channel.opts['title'], @@ -449,20 +454,22 @@ def get_channels_from_settings(self): value_max=channel['voltage_settings', 'volt_max'], )) - elif self.settings['NIDAQ_type'] == DAQ_NIDAQ_source(2).name: # counter input + elif self.settings['NIDAQ_type'] == DAQ_NIDAQ_source.Counter.name: # counter input for channel in self.settings.child('counter_settings', 'counter_channels').children(): channels.append(Counter(name=channel.opts['title'], source='Counter', edge=channel['edge'])) - elif self.settings['NIDAQ_type'] == DAQ_NIDAQ_source(3).name: # digital input + + elif self.settings['NIDAQ_type'] == DAQ_NIDAQ_source.Digital_Input.name: # digital input for channel in self.settings.child('di_channels').children(): channels.append(DIChannel(name=channel.opts['title'], source='Digital_Input')) - elif self.settings['NIDAQ_type'] == DAQ_NIDAQ_source(4).name: # Digital output + elif self.settings['NIDAQ_type'] == DAQ_NIDAQ_source.Digital_Output.name: # Digital output for channel in self.settings.child('do_channels').children(): channels.append(DOChannel(name=channel.opts['title'], source='Digital_Output')) + channels = [ch for ch in channels if self.settings.child("dev_to_use").value() in ch.name] return channels @@ -635,6 +642,7 @@ def counter_done(self): # y_axis=Axis(label='Count Number', units='1/s'))]) self.task.StopTask() + class DAQ_NIDAQmx_Actuator(DAQ_Move_base, DAQ_NIDAQmx_base): """ Wrapper object to access the Mock fonctionnalities, similar wrapper for all controllers. @@ -717,7 +725,7 @@ def ini_stage(self, controller=None): Returns ------- - self.status (edict): with initialization status: three fields: + self.status (easydict): with initialization status: three fields: * info (str) * controller (object) initialized controller *initialized: (bool): False if initialization failed otherwise True @@ -728,7 +736,7 @@ def ini_stage(self, controller=None): # controller is an object that may be passed to other instances of DAQ_Move_Mock in case # of one controller controlling multiactuators (or detector) - self.status.update(edict(info="", controller=None, initialized=False)) + self.status.update(eDict(info="", controller=None, initialized=False)) # check whether this stage is controlled by a multiaxe controller (to be defined for each plugin) # if multiaxes then init the controller here if Master state otherwise use external controller diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py index 5a4e2f3..606bdea 100644 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py @@ -4,15 +4,14 @@ import numpy as np from pymodaq.utils.logger import set_logger, get_module_name -from nidaqmx.system import System - from nidaqmx.constants import AcquisitionType, VoltageUnits, CurrentUnits, CurrentShuntResistorLocation, \ TemperatureUnits, CJCSource, CountDirection, Level, FrequencyUnits, TimeUnits, \ LineGrouping from . import UsageTypeAI, Edge, TerminalConfiguration, ThermocoupleType +from nidaqmx.system import System as niSystem from nidaqmx.system.device import Device -from nidaqmx import Task +from nidaqmx import Task as niTask from nidaqmx.errors import DaqError, DAQmxErrors from pymodaq_plugins_daqmx import config @@ -209,7 +208,7 @@ def get_NIDAQ_devices(cls): list of devices as strings to be used in subsequent commands """ try: - devices = System.local().devices + devices = niSystem.local().devices if devices == ['']: devices = [] return devices @@ -286,7 +285,7 @@ def configuration_sequence(self, viewer, current_device): if not device_name == current_device.name: continue device_product = config["NIDAQ_Devices", dev].get('product') - device = nidaqmx.system.device.Device(device_name) + device = Device(device_name) assert device in self.devices and device.product_type == device_product, device.name except AssertionError as err: logger.error("Device {} not detected: {}".format(device_name, err)) @@ -297,7 +296,7 @@ def configuration_sequence(self, viewer, current_device): try: module_name = config["NIDAQ_Devices", dev, mod].get('name') module_product = config["NIDAQ_Devices", dev, mod].get('product') - module = nidaqmx.system.device.Device(module_name) + module = Device(module_name) assert module in self.devices and module.product_type == module_product, module.name viewer.config_modules.append(config["NIDAQ_Devices", dev, mod].get('name')) except AssertionError as err: @@ -318,7 +317,7 @@ def configuration_sequence(self, viewer, current_device): analog_type=ai[ch].get("analog_type"), value_min=float(ai[ch].get("value_min")), value_max=float(ai[ch].get("value_max")), - termination=DAQ_termination.__getitem__(term), + termination=TerminalConfiguration.__getitem__(term), )) elif ai[ch].get("analog_type") == "Current": viewer.config_channels.append(AIChannel @@ -327,7 +326,7 @@ def configuration_sequence(self, viewer, current_device): analog_type=ai[ch].get("analog_type"), value_min=float(ai[ch].get("value_min")), value_max=float(ai[ch].get("value_max")), - termination=DAQ_termination.__getitem__(term), + termination=TerminalConfiguration.__getitem__(term), )) elif ai[ch].get("analog_type") == "Thermocouple": th = ai[ch].get("thermo_type") @@ -388,13 +387,13 @@ def update_task(self, channels=[], clock_settings=ClockSettings(), trigger_setti try: if self._task is not None: - if isinstance(self._task, Task): + if isinstance(self._task, niTask): self._task.close() self._task = None self.c_callback = None - self._task = Task() + self._task = niTask() logger.info("TASK: {}".format(self._task)) err_code = None @@ -579,12 +578,12 @@ def readCounter(self): @classmethod def getAIVoltageRange(cls, device='Dev1'): - ret = System.local().devices[device].ai_voltage_rngs # todo self.devices[device].ai_voltage_rngs + ret = niSystem.local().devices[device].ai_voltage_rngs # todo self.devices[device].ai_voltage_rngs return [tuple(ret[6:8])] @classmethod def getAOVoltageRange(cls, device='Dev1'): - ret = System.local().devices[device].ao_voltage_rngs # todo self.devices[device].ao_voltage_rngs + ret = niSystem.local().devices[device].ao_voltage_rngs # todo self.devices[device].ao_voltage_rngs return [tuple(ret)] # [(-10., 10.)] Why this format is needed?? def stop(self): From 974fbd17a57dfdca2149fe9096a6caada9a501bc Mon Sep 17 00:00:00 2001 From: Loic Guilmard Date: Thu, 19 Dec 2024 12:38:41 +0100 Subject: [PATCH 035/140] [Cleaning] aliasing Device and typos --- .../national_instruments/daq_NIDAQmx.py | 4 +-- .../hardware/national_instruments/daqmxni.py | 32 +++++++++---------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx.py index 095ef82..83eb936 100644 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx.py +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx.py @@ -878,5 +878,5 @@ def stop_motion(self): self.stop() self.emit_status(ThreadCommand('Update_Status', ['Some info you want to log'])) self.move_done() # to let the interface know the actuator stopped - ############################## ->>>>>>> eda1396 (removed CustomEnum to use the ones from nidaq.constants) + + diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py index 606bdea..5d4de57 100644 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py @@ -10,7 +10,7 @@ from . import UsageTypeAI, Edge, TerminalConfiguration, ThermocoupleType from nidaqmx.system import System as niSystem -from nidaqmx.system.device import Device +from nidaqmx.system.device import Device as niDevice from nidaqmx import Task as niTask from nidaqmx.errors import DaqError, DAQmxErrors from pymodaq_plugins_daqmx import config @@ -247,17 +247,17 @@ def get_NIDAQ_channels(cls, devices=None, source_type=None): for device in devices: for source in source_type: if source == DAQ_NIDAQ_source.Analog_Input.name: # analog input - channels = Device(device).ai_physical_chans.channel_names + channels = niDevice(device).ai_physical_chans.channel_names elif source == DAQ_NIDAQ_source.Analog_Output.name: # analog output - channels = Device(device).ao_physical_chans.channel_names + channels = niDevice(device).ao_physical_chans.channel_names elif source == DAQ_NIDAQ_source.Counter.name: # counter - channels = Device(device).ci_physical_chans.channel_names + channels = niDevice(device).ci_physical_chans.channel_names elif source == DAQ_NIDAQ_source.Digital_Output.name: # digital output - channels = Device(device).do_lines.channel_names + channels = niDevice(device).do_lines.channel_names elif source == DAQ_NIDAQ_source.Digital_Input.name: # digital iutput - channels = Device(device).di_lines.channel_names + channels = niDevice(device).di_lines.channel_names elif source == DAQ_NIDAQ_source.Terminals.name: # terminals - channels = Device(device).terminals + channels = niDevice(device).terminals if channels != ['']: channels_tot.extend(channels) @@ -285,7 +285,7 @@ def configuration_sequence(self, viewer, current_device): if not device_name == current_device.name: continue device_product = config["NIDAQ_Devices", dev].get('product') - device = Device(device_name) + device = niDevice(device_name) assert device in self.devices and device.product_type == device_product, device.name except AssertionError as err: logger.error("Device {} not detected: {}".format(device_name, err)) @@ -296,7 +296,7 @@ def configuration_sequence(self, viewer, current_device): try: module_name = config["NIDAQ_Devices", dev, mod].get('name') module_product = config["NIDAQ_Devices", dev, mod].get('product') - module = Device(module_name) + module = niDevice(module_name) assert module in self.devices and module.product_type == module_product, module.name viewer.config_modules.append(config["NIDAQ_Devices", dev, mod].get('name')) except AssertionError as err: @@ -336,7 +336,7 @@ def configuration_sequence(self, viewer, current_device): analog_type=ai[ch].get("analog_type"), value_min=float(ai[ch].get("value_min")), value_max=float(ai[ch].get("value_max")), - thermo_type=DAQ_thermocouples.__getitem__(th), + thermo_type=ThermocoupleType.__getitem__(th), )) logger.info("Devices from config: {}".format(viewer.config_devices)) logger.info("Current device: {}".format(current_device)) @@ -351,19 +351,19 @@ def configuration_sequence(self, viewer, current_device): @classmethod def getAOMaxRate(cls, device): - return Device(device).ao_max_rate + return niDevice(device).ao_max_rate @classmethod def getAIMaxRate(cls, device): - return Device(device).ai_max_single_chan_rate + return niDevice(device).ai_max_single_chan_rate @classmethod def isAnalogTriggeringSupported(cls, device): - return Device(device).anlg_trig_supported + return niDevice(device).anlg_trig_supported @classmethod def isDigitalTriggeringSupported(cls, device): - return Device(device).dig_trig_supported + return niDevice(device).dig_trig_supported @classmethod def getTriggeringSources(cls, devices=None): @@ -373,12 +373,12 @@ def getTriggeringSources(cls, devices=None): for device in devices: if cls.isDigitalTriggeringSupported(device): - string = Device(device).terminals + string = niDevice(device).terminals channels = [chan for chan in string if 'PFI' in chan] if channels != ['']: sources.extend(channels) if cls.isAnalogTriggeringSupported(device): - channels = Device(device).ai_physical_chans.channel_names + channels = niDevice(device).ai_physical_chans.channel_names if channels != ['']: sources.extend(channels) return sources From a964c7da279e4ee1a9ee8333e94fe8a7ad0def58 Mon Sep 17 00:00:00 2001 From: Loic Guilmard Date: Thu, 19 Dec 2024 14:26:18 +0100 Subject: [PATCH 036/140] Fixes --- .../plugins_0D/daq_0Dviewer_NIDAQmx.py | 24 +++++++++---------- .../national_instruments/daq_NIDAQmx.py | 23 +++++++++--------- .../daq_NIDAQmx_Viewer.py | 4 ++-- 3 files changed, 25 insertions(+), 26 deletions(-) diff --git a/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_NIDAQmx.py b/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_NIDAQmx.py index b5cdb49..94fd711 100644 --- a/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_NIDAQmx.py +++ b/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_NIDAQmx.py @@ -1,10 +1,10 @@ from pymodaq.control_modules.viewer_utility_classes import main from pymodaq.control_modules.viewer_utility_classes import comon_parameters as viewer_params from pymodaq_plugins_daqmx import config -from pymodaq_plugins_daqmx.hardware.national_instruments.daqmxni import AIChannel, AIThermoChannel, DAQmx, \ - niTask, niconstants +from pymodaq_plugins_daqmx.hardware.national_instruments.daqmxni import AIChannel, AIThermoChannel, DAQmx, niTask, \ + niDevice, TemperatureUnits, CJCSource, VoltageUnits, AcquisitionType from pymodaq_plugins_daqmx.hardware.national_instruments.daqmxni import UsageTypeAI, ThermocoupleType, \ - TerminalConfiguration + TerminalConfiguration, Edge from pymodaq_plugins_daqmx.hardware.national_instruments.daq_NIDAQmx import DAQ_NIDAQmx_base from pymodaq_plugins_daqmx.hardware.national_instruments.daq_NIDAQmx_Viewer import DAQ_NIDAQmx_Viewer from pymodaq.utils.logger import set_logger, get_module_name @@ -18,11 +18,11 @@ class DAQ_0DViewer_NIDAQmx(DAQ_NIDAQmx_Viewer): config_channels: list channels_ai: list - config: config + config: config # todo review Useful/Unused controller: DAQmx config_devices: list config_modules: list - current_device: nidaqmx.system.Device + current_device: niDevice live: bool Naverage: int @@ -128,9 +128,9 @@ def callback_9205(task_handle, every_n_samples_event_type, number_of_samples, ca "", channel.value_min, channel.value_max, - niconstants.TemperatureUnits.DEG_C, + TemperatureUnits.DEG_C, channel.thermo_type, - niconstants.CJCSource.BUILT_IN, + CJCSource.BUILT_IN, 0., "") for channel in channels_voltage: @@ -139,14 +139,14 @@ def callback_9205(task_handle, every_n_samples_event_type, number_of_samples, ca channel.termination, channel.value_min, channel.value_max, - niconstants.VoltageUnits.VOLTS, + VoltageUnits.VOLTS, "") - task_9211.timing.cfg_samp_clk_timing(5.0, None, niconstants.Edge.RISING, - niconstants.AcquisitionType.CONTINUOUS, 5) + task_9211.timing.cfg_samp_clk_timing(5.0, None, Edge.RISING, + AcquisitionType.CONTINUOUS, 5) task_9211.register_every_n_samples_acquired_into_buffer_event(10, callback_9211) - task_9205.timing.cfg_samp_clk_timing(10, None, niconstants.Edge.RISING, - niconstants.AcquisitionType.CONTINUOUS, 10) + task_9205.timing.cfg_samp_clk_timing(10, None, Edge.RISING, + AcquisitionType.CONTINUOUS, 10) task_9205.register_every_n_samples_acquired_into_buffer_event(2, callback_9205) task_9211.start() diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx.py index 83eb936..073f2f9 100644 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx.py +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx.py @@ -7,9 +7,9 @@ import traceback from pymodaq.utils.parameter import Parameter from pymodaq.utils.parameter.pymodaq_ptypes import registerParameterType, GroupParameter -from .daqmxni import DAQmx, Edge, DAQ_NIDAQ_source, ClockSettings, AIChannel, Counter, \ - AIThermoChannel, AOChannel, TriggerSettings, DOChannel, DIChannel -from . import UsageTypeAI, ThermocoupleType, TerminalConfiguration +from pymodaq_plugins_daqmx.hardware.national_instruments.daqmxni import DAQmx, Edge, DAQ_NIDAQ_source, ClockSettings, \ + AIChannel, Counter, AIThermoChannel, AOChannel, TriggerSettings, DOChannel, DIChannel, UsageTypeAI, \ + ThermocoupleType, TerminalConfiguration from pymodaq.utils.daq_utils import ThreadCommand, getLineInfo from pymodaq.utils.data import DataToExport, DataFromPlugins, DataActuator from easydict import EasyDict as eDict @@ -413,7 +413,7 @@ def update_task(self): self.trigger_settings = \ TriggerSettings(trig_source=self.settings['trigger_settings', 'trigger_channel'], enable=self.settings['trigger_settings', 'enable'], - edge=Edge[self.settings['trigger_settings', 'edge']], + edge=Edge[self.settings['trigger_settings', 'edge'].upper()], level=self.settings['trigger_settings', 'level'], ) if not not self.channels: self.controller.update_task(self.channels, self.clock_settings, trigger_settings=self.trigger_settings) @@ -429,19 +429,19 @@ def get_channels_from_settings(self): source='Analog_Input', analog_type=analog_type, value_min=channel['voltage_settings', 'volt_min'], value_max=channel['voltage_settings', 'volt_max'], - termination=TerminalConfiguration[channel['termination']], )) + termination=TerminalConfiguration[channel['termination'].upper()], )) elif analog_type == 'Current': channels.append(AIChannel(name=channel.opts['title'], source='Analog_Input', analog_type=analog_type, value_min=channel['current_settings', 'curr_min'], value_max=channel['current_settings', 'curr_max'], - termination=TerminalConfiguration[channel['termination']], )) + termination=TerminalConfiguration[channel['termination'].upper()], )) elif analog_type == 'Thermocouple': channels.append(AIThermoChannel(name=channel.opts['title'], source='Analog_Input', analog_type=analog_type, value_min=channel['thermoc_settings', 'T_min'], value_max=channel['thermoc_settings', 'T_max'], - termination=channel['termination'], + termination=TerminalConfiguration[channel['termination'].upper()], thermo_type=ThermocoupleType[ channel['thermoc_settings', 'thermoc_type']], )) @@ -459,7 +459,6 @@ def get_channels_from_settings(self): channels.append(Counter(name=channel.opts['title'], source='Counter', edge=channel['edge'])) - elif self.settings['NIDAQ_type'] == DAQ_NIDAQ_source.Digital_Input.name: # digital input for channel in self.settings.child('di_channels').children(): channels.append(DIChannel(name=channel.opts['title'], @@ -666,7 +665,7 @@ class DAQ_NIDAQmx_Actuator(DAQ_Move_base, DAQ_NIDAQmx_base): {'title': 'Status:', 'name': 'multi_status', 'type': 'list', 'value': 'Master', 'limits': ['Master', 'Slave']}, {'title': 'Axis:', 'name': 'axis', 'type': 'list', 'limits': stage_names}, - ]}] + actuator_params + ]}] + actuator_params() def __init__(self, parent=None, params_state=None, control_type="Actuator"): DAQ_Move_base.__init__(self, parent, params_state) # defines settings attribute and various other methods @@ -783,7 +782,7 @@ def calulate_waveform(self, value): return values - def move_Abs(self, position): + def move_abs(self, position): """ Move the actuator to the absolute target defined by position Parameters @@ -820,7 +819,7 @@ def move_done_callback(self, taskhandle, status, callbackdata): self.task.StopTask() return 0 - def move_Rel(self, position): + def move_rel(self, position): """ Move the actuator to the relative target actuator value defined by position Parameters @@ -850,7 +849,7 @@ def move_Rel(self, position): elif self.settings['NIDAQ_type'] == 'Digital_Output': self.writeDigital(1, np.array([self.target_position], dtype=np.uint8), autostart=True) - def move_Home(self): + def move_home(self): """ Send the update status thread command. See Also diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx_Viewer.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx_Viewer.py index 0069ae2..248da5c 100644 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx_Viewer.py +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx_Viewer.py @@ -3,7 +3,7 @@ import traceback from qtpy import QtCore from .daqmxni import DAQmx -from pymodaq_plugins_daqmx.hardware.national_instruments.daq_NIDAQmx import DAQ_NIDAQmx_base, DAQ_termination +from pymodaq_plugins_daqmx.hardware.national_instruments.daq_NIDAQmx import DAQ_NIDAQmx_base, TerminalConfiguration from pymodaq.control_modules.viewer_utility_classes import DAQ_Viewer_base, comon_parameters as viewer_params from pymodaq.utils.daq_utils import ThreadCommand from pymodaq.utils.data import DataFromPlugins, DataToExport @@ -139,7 +139,7 @@ def ini_detector(self, controller=None): "thermoc_settings", "T_max").setValue(ch.value_max) self.settings.child("ai_channels", param.opts['name'], "termination").setValue( - DAQ_termination.Auto) + TerminalConfiguration.DEFAULT) info = "Plugin Initialized" initialized = True logger.info("Detector 0D initialized") From d4731a9cb44e5dc10b424394325809f520aeb145 Mon Sep 17 00:00:00 2001 From: Loic Guilmard Date: Thu, 19 Dec 2024 15:58:33 +0100 Subject: [PATCH 037/140] using enums instead of hardcoded strings. --- .../national_instruments/daq_NIDAQmx.py | 9 ++--- .../daq_NIDAQmx_Viewer.py | 15 ++++---- .../hardware/national_instruments/daqmxni.py | 38 +++++++++---------- 3 files changed, 31 insertions(+), 31 deletions(-) diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx.py index 073f2f9..a772ede 100644 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx.py +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx.py @@ -34,7 +34,7 @@ class ScalableGroupAI(GroupParameter): """ params = [{'title': 'AI type:', 'name': 'ai_type', 'type': 'list', 'limits': UsageTypeAI.names()}, - {'title': 'Voltages:', 'name': 'voltage_settings', 'type': 'group', 'children': [ + {'title': 'Voltage:', 'name': 'voltage_settings', 'type': 'group', 'children': [ {'title': 'Voltage Min:', 'name': 'volt_min', 'type': 'float', 'value': -10.}, {'title': 'Voltage Max:', 'name': 'volt_max', 'type': 'float', 'value': 10.}, ]}, @@ -424,24 +424,23 @@ def get_channels_from_settings(self): for channel in self.settings.child('ai_channels').children(): logger.info("get_channels_from_settings - channel {}".format(channel)) analog_type = channel['ai_type'] - if analog_type == 'Voltage': + if analog_type == UsageTypeAI.VOLTAGE.name: channels.append(AIChannel(name=channel.opts['title'], source='Analog_Input', analog_type=analog_type, value_min=channel['voltage_settings', 'volt_min'], value_max=channel['voltage_settings', 'volt_max'], termination=TerminalConfiguration[channel['termination'].upper()], )) - elif analog_type == 'Current': + elif analog_type == UsageTypeAI.CURRENT.name: channels.append(AIChannel(name=channel.opts['title'], source='Analog_Input', analog_type=analog_type, value_min=channel['current_settings', 'curr_min'], value_max=channel['current_settings', 'curr_max'], termination=TerminalConfiguration[channel['termination'].upper()], )) - elif analog_type == 'Thermocouple': + elif analog_type == UsageTypeAI.TEMPERATURE_THERMOCOUPLE.name: channels.append(AIThermoChannel(name=channel.opts['title'], source='Analog_Input', analog_type=analog_type, value_min=channel['thermoc_settings', 'T_min'], value_max=channel['thermoc_settings', 'T_max'], - termination=TerminalConfiguration[channel['termination'].upper()], thermo_type=ThermocoupleType[ channel['thermoc_settings', 'thermoc_type']], )) diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx_Viewer.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx_Viewer.py index 248da5c..03f3efa 100644 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx_Viewer.py +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx_Viewer.py @@ -3,7 +3,8 @@ import traceback from qtpy import QtCore from .daqmxni import DAQmx -from pymodaq_plugins_daqmx.hardware.national_instruments.daq_NIDAQmx import DAQ_NIDAQmx_base, TerminalConfiguration +from pymodaq_plugins_daqmx.hardware.national_instruments.daq_NIDAQmx import DAQ_NIDAQmx_base, TerminalConfiguration, \ + UsageTypeAI from pymodaq.control_modules.viewer_utility_classes import DAQ_Viewer_base, comon_parameters as viewer_params from pymodaq.utils.daq_utils import ThreadCommand from pymodaq.utils.data import DataFromPlugins, DataToExport @@ -108,24 +109,24 @@ def ini_detector(self, controller=None): self.settings.child('ai_channels').addNew(ch.name) param = [a for a in self.settings.child('ai_channels').childs if a.opts['title'] == ch.name][0] self.settings.child("ai_channels", param.opts['name'], "ai_type").setValue(ch.analog_type) - param.child("voltage_settings").show(ch.analog_type == "Voltage") - param.child("current_settings").show(ch.analog_type == "Current") - param.child("thermoc_settings").show(ch.analog_type == "Thermocouple") - if ch.analog_type == "Voltage": + param.child("voltage_settings").show(ch.analog_type == UsageTypeAI.VOLTAGE.name) + param.child("current_settings").show(ch.analog_type == UsageTypeAI.CURRENT.name) + param.child("thermoc_settings").show(ch.analog_type == UsageTypeAI.TEMPERATURE_THERMOCOUPLE.name) + if ch.analog_type == UsageTypeAI.VOLTAGE.name: self.settings.child("ai_channels", param.opts['name'], "voltage_settings", "volt_min").setValue( ch.value_min) self.settings.child("ai_channels", param.opts['name'], "voltage_settings", "volt_max").setValue( ch.value_max) self.settings.child("ai_channels", param.opts['name'], "termination").setValue( ch.termination.name) - elif ch.analog_type == "Current": + elif ch.analog_type == UsageTypeAI.CURRENT.name: self.settings.child("ai_channels", param.opts['name'], "current_settings", "curr_min").setValue( ch.value_min) self.settings.child("ai_channels", param.opts['name'], "current_settings", "curr_max").setValue( ch.value_max) self.settings.child("ai_channels", param.opts['name'], "termination").setValue( ch.termination.name) - elif ch.analog_type == "Thermocouple": + elif ch.analog_type == UsageTypeAI.TEMPERATURE_THERMOCOUPLE.name: self.settings.child("ai_channels", param.opts['name'], "thermoc_settings", diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py index 5d4de57..3e2efac 100644 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py @@ -310,7 +310,7 @@ def configuration_sequence(self, viewer, current_device): for ch in ai.keys(): name = module_name + "/" + str(ch) term = ai[ch].get("termination") - if ai[ch].get("analog_type") == "Voltage": + if ai[ch].get("analog_type") == UsageTypeAI.VOLTAGE.name: viewer.config_channels.append(AIChannel (name=name, source=ai[ch].get("source"), @@ -319,7 +319,7 @@ def configuration_sequence(self, viewer, current_device): value_max=float(ai[ch].get("value_max")), termination=TerminalConfiguration.__getitem__(term), )) - elif ai[ch].get("analog_type") == "Current": + elif ai[ch].get("analog_type") == UsageTypeAI.CURRENT.name: viewer.config_channels.append(AIChannel (name=name, source=ai[ch].get("source"), @@ -328,16 +328,16 @@ def configuration_sequence(self, viewer, current_device): value_max=float(ai[ch].get("value_max")), termination=TerminalConfiguration.__getitem__(term), )) - elif ai[ch].get("analog_type") == "Thermocouple": + elif ai[ch].get("analog_type") == UsageTypeAI.TEMPERATURE_THERMOCOUPLE.name: th = ai[ch].get("thermo_type") viewer.config_channels.append(AIThermoChannel - (name=name, - source=ai[ch].get("source"), - analog_type=ai[ch].get("analog_type"), - value_min=float(ai[ch].get("value_min")), - value_max=float(ai[ch].get("value_max")), - thermo_type=ThermocoupleType.__getitem__(th), - )) + (name=name, + source=ai[ch].get("source"), + analog_type=ai[ch].get("analog_type"), + value_min=float(ai[ch].get("value_min")), + value_max=float(ai[ch].get("value_max")), + thermo_type=ThermocoupleType.__getitem__(th), + )) logger.info("Devices from config: {}".format(viewer.config_devices)) logger.info("Current device: {}".format(current_device)) logger.info("Current device modules from config: {}".format(viewer.config_modules)) @@ -399,9 +399,9 @@ def update_task(self, channels=[], clock_settings=ClockSettings(), trigger_setti # create all channels one task for one type of channels for channel in channels: - if channel.source == 'Analog_Input': # analog input + if channel.source == DAQ_NIDAQ_source.Digital_Input.name: # analog input try: - if channel.analog_type == "Voltage": + if channel.analog_type == UsageTypeAI.VOLTAGE.name: self._task.ai_channels.add_ai_voltage_chan(channel.name, "", channel.termination, @@ -410,7 +410,7 @@ def update_task(self, channels=[], clock_settings=ClockSettings(), trigger_setti VoltageUnits.VOLTS, "") - elif channel.analog_type == "Current": + elif channel.analog_type == UsageTypeAI.CURRENT.name: self._task.ai_channels.add_ai_current_chan(channel.name, "", channel.termination, @@ -421,7 +421,7 @@ def update_task(self, channels=[], clock_settings=ClockSettings(), trigger_setti 0., "") - elif channel.analog_type == "Thermocouple": + elif channel.analog_type == UsageTypeAI.CURRENT.name: self._task.ai_channels.add_ai_thrmcpl_chan(channel.name, "", channel.value_min, @@ -467,15 +467,15 @@ def update_task(self, channels=[], clock_settings=ClockSettings(), trigger_setti if not not err_code: status = self.DAQmxGetErrorString(err_code) raise IOError(status) - elif channel.source == 'Analog_Output': # Analog_Output + elif channel.source == DAQ_NIDAQ_source.Analog_Output.name: # Analog_Output try: - if channel.analog_type == "Voltage": + if channel.analog_type == UsageTypeAI.VOLTAGE.name: self._task.ao_channels.add_ao_voltage_chan(channel.name, "", channel.value_min, channel.value_max, VoltageUnits.VOLTS, None) - elif channel.analog_type == "Current": + elif channel.analog_type == UsageTypeAI.CURRENT.name: self._task.ao_channels.add_ao_current_chan(channel.name, "", channel.value_min, channel.value_max, @@ -485,7 +485,7 @@ def update_task(self, channels=[], clock_settings=ClockSettings(), trigger_setti if not not err_code: status = self.DAQmxGetErrorString(err_code) raise IOError(status) - elif channel.source == 'Digital_Output': + elif channel.source == DAQ_NIDAQ_source.Digital_Output.name: try: self._task.do_channels.add_do_chan(channel.name, "", LineGrouping.CHAN_PER_LINE) @@ -494,7 +494,7 @@ def update_task(self, channels=[], clock_settings=ClockSettings(), trigger_setti if not not err_code: status = self.DAQmxGetErrorString(err_code) raise IOError(status) - elif channel.source == 'Digital_Input': # Digital_Input + elif channel.source == DAQ_NIDAQ_source.Digital_Input.name: # Digital_Input try: self._task.di_channels.add_di_chan(channel.name, "", LineGrouping.CHAN_PER_LINE) From 9797e14b50a7eee13d92d4cf7b7d2629f29b31a2 Mon Sep 17 00:00:00 2001 From: Loic Guilmard Date: Thu, 19 Dec 2024 18:19:21 +0100 Subject: [PATCH 038/140] Added DAQ_NIDAQ_source OD, 1D, Actuator --- .../national_instruments/daq_NIDAQmx.py | 42 +++++++++---------- .../daq_NIDAQmx_Viewer.py | 16 ++++--- .../hardware/national_instruments/daqmxni.py | 20 ++++++++- 3 files changed, 48 insertions(+), 30 deletions(-) diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx.py index a772ede..926f713 100644 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx.py +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx.py @@ -282,18 +282,18 @@ class DAQ_NIDAQmx_base: ] }, {'title': 'AI Channels:', 'name': 'ai_channels', 'type': 'groupai', - 'limits': DAQmx.get_NIDAQ_channels(source_type='Analog_Input')}, + 'limits': DAQmx.get_NIDAQ_channels(source_type=DAQ_NIDAQ_source.Analog_Input.name)}, {'title': 'AO Channels:', 'name': 'ao_channels', 'type': 'groupao', - 'limits': DAQmx.get_NIDAQ_channels(source_type='Analog_Output')}, + 'limits': DAQmx.get_NIDAQ_channels(source_type=DAQ_NIDAQ_source.Analog_Output.name)}, {'title': 'DO Channels:', 'name': 'do_channels', 'type': 'groupdo', - 'limits': DAQmx.get_NIDAQ_channels(source_type='Digital_Output')}, + 'limits': DAQmx.get_NIDAQ_channels(source_type=DAQ_NIDAQ_source.Digital_Output.name)}, {'title': 'DI Channels:', 'name': 'di_channels', 'type': 'groupdi', - 'limits': DAQmx.get_NIDAQ_channels(source_type='Digital_Input')}, + 'limits': DAQmx.get_NIDAQ_channels(source_type=DAQ_NIDAQ_source.Digital_Input.name)}, {'title': 'Counter Settings:', 'name': 'counter_settings', 'type': 'group', 'visible': True, 'children': [ {'title': 'Counting time (ms):', 'name': 'counting_time', 'type': 'float', 'value': 100., 'default': 100., 'min': 0.}, {'title': 'Counting Channels:', 'name': 'counter_channels', 'type': 'groupcounter', - 'limits': DAQmx.get_NIDAQ_channels(source_type='Counter')}, + 'limits': DAQmx.get_NIDAQ_channels(source_type=DAQ_NIDAQ_source.Counter.name)}, ]}, {'title': 'Trigger Settings:', 'name': 'trigger_settings', 'type': 'group', 'visible': True, 'children': [ {'title': 'Enable?:', 'name': 'enable', 'type': 'bool', 'value': False, }, @@ -368,7 +368,6 @@ def commit_settings(self, param: Parameter): self.settings.child('do_channels').hide() self.settings.child('di_channels').show() - elif param.value() == DAQ_NIDAQ_source.Digital_Output.name: # digital output self.settings.child('clock_settings').show() self.settings.child('ai_channels').hide() @@ -387,14 +386,14 @@ def commit_settings(self, param: Parameter): self.settings.child('refresh_hardware').setValue(False) elif param.name() == 'ai_type': - param.parent().child('voltage_settings').show(param.value() == 'Voltage') - param.parent().child('current_settings').show(param.value() == 'Current') - param.parent().child('thermoc_settings').show(param.value() == 'Thermocouple') + param.parent().child('voltage_settings').show(param.value() == UsageTypeAI.Voltage.name) + param.parent().child('current_settings').show(param.value() == UsageTypeAI.Current.name) + param.parent().child('thermoc_settings').show(param.value() == UsageTypeAI.TEMPERATURE_THERMOCOUPLE.name) self.update_task() elif param.name() == 'ao_type': - param.parent().child('voltage_settings').show(param.value() == 'Voltage') - param.parent().child('current_settings').show(param.value() == 'Current') + param.parent().child('voltage_settings').show(param.value() == UsageTypeAI.Voltage.name) + param.parent().child('current_settings').show(param.value() == UsageTypeAI.Current.name) self.update_task() elif param.name() == 'trigger_channel': @@ -423,22 +422,22 @@ def get_channels_from_settings(self): if self.settings['NIDAQ_type'] == DAQ_NIDAQ_source.Analog_Input.name: # analog input for channel in self.settings.child('ai_channels').children(): logger.info("get_channels_from_settings - channel {}".format(channel)) - analog_type = channel['ai_type'] + analog_type = channel['ai_type'] # todo UsageTypeAI[channel['ai_type'].upper()].name if analog_type == UsageTypeAI.VOLTAGE.name: channels.append(AIChannel(name=channel.opts['title'], - source='Analog_Input', analog_type=analog_type, + source=DAQ_NIDAQ_source.Analog_Input.name, analog_type=analog_type, value_min=channel['voltage_settings', 'volt_min'], value_max=channel['voltage_settings', 'volt_max'], termination=TerminalConfiguration[channel['termination'].upper()], )) elif analog_type == UsageTypeAI.CURRENT.name: channels.append(AIChannel(name=channel.opts['title'], - source='Analog_Input', analog_type=analog_type, + source=DAQ_NIDAQ_source.Analog_Input.name, analog_type=analog_type, value_min=channel['current_settings', 'curr_min'], value_max=channel['current_settings', 'curr_max'], termination=TerminalConfiguration[channel['termination'].upper()], )) elif analog_type == UsageTypeAI.TEMPERATURE_THERMOCOUPLE.name: channels.append(AIThermoChannel(name=channel.opts['title'], - source='Analog_Input', analog_type=analog_type, + source=DAQ_NIDAQ_source.Analog_Input.name, analog_type=analog_type, value_min=channel['thermoc_settings', 'T_min'], value_max=channel['thermoc_settings', 'T_max'], thermo_type=ThermocoupleType[ @@ -448,7 +447,7 @@ def get_channels_from_settings(self): for channel in self.settings.child('ao_channels').children(): analog_type = channel['ao_type'] channels.append(AOChannel(name=channel.opts['title'], - source='Analog_Output', analog_type=analog_type, + source=DAQ_NIDAQ_source.Analog_Output.name, analog_type=analog_type, value_min=channel['voltage_settings', 'volt_min'], value_max=channel['voltage_settings', 'volt_max'], )) @@ -461,12 +460,12 @@ def get_channels_from_settings(self): elif self.settings['NIDAQ_type'] == DAQ_NIDAQ_source.Digital_Input.name: # digital input for channel in self.settings.child('di_channels').children(): channels.append(DIChannel(name=channel.opts['title'], - source='Digital_Input')) + source=DAQ_NIDAQ_source.Digital_Input.name)) elif self.settings['NIDAQ_type'] == DAQ_NIDAQ_source.Digital_Output.name: # Digital output for channel in self.settings.child('do_channels').children(): channels.append(DOChannel(name=channel.opts['title'], - source='Digital_Output')) + source=DAQ_NIDAQ_source.Digital_Output.name)) channels = [ch for ch in channels if self.settings.child("dev_to_use").value() in ch.name] return channels @@ -505,12 +504,11 @@ def __init__(self, parent=None, params_state=None, control_type="0D"): self.live = False self.control_type = control_type # could be "0D", "1D" or "Actuator" if self.control_type == "0D": - self.settings.child('NIDAQ_type').setLimits( - ['Analog_Input', 'Counter', 'Digital_Input']) # analog input and counter + self.settings.child('NIDAQ_type').setLimits(DAQ_NIDAQ_source.Source0D()) # analog input and counter elif self.control_type == "1D": - self.settings.child('NIDAQ_type').setLimits(['Analog_Input']) + self.settings.child('NIDAQ_type').setLimits([DAQ_NIDAQ_source.sources1D()]) elif self.control_type == "Actuator": - self.settings.child('NIDAQ_type').setLimits(['Analog_Output']) + self.settings.child('NIDAQ_type').setLimits([DAQ_NIDAQ_source.Actuator()]) self.settings.child('ao_settings').hide() self.settings.child('ao_channels').hide() diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx_Viewer.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx_Viewer.py index 03f3efa..f660ac7 100644 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx_Viewer.py +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx_Viewer.py @@ -4,7 +4,7 @@ from qtpy import QtCore from .daqmxni import DAQmx from pymodaq_plugins_daqmx.hardware.national_instruments.daq_NIDAQmx import DAQ_NIDAQmx_base, TerminalConfiguration, \ - UsageTypeAI + UsageTypeAI, DAQ_NIDAQ_source from pymodaq.control_modules.viewer_utility_classes import DAQ_Viewer_base, comon_parameters as viewer_params from pymodaq.utils.daq_utils import ThreadCommand from pymodaq.utils.data import DataFromPlugins, DataToExport @@ -38,12 +38,11 @@ def __init__(self, parent=None, params_state=None, control_type="0D"): self.live = False self.control_type = control_type # could be "0D", "1D" or "Actuator" if self.control_type == "0D": - self.settings.child('NIDAQ_type').setLimits( - ['Analog_Input', 'Counter', 'Digital_Input']) # analog input and counter + self.settings.child('NIDAQ_type').setLimits(DAQ_NIDAQ_source.sources0D()) # analog input and counter elif self.control_type == "1D": - self.settings.child('NIDAQ_type').setLimits(['Analog_Input']) + self.settings.child('NIDAQ_type').setLimits(DAQ_NIDAQ_source.sources1D()) elif self.control_type == "Actuator": - self.settings.child('NIDAQ_type').setLimits(['Analog_Output']) + self.settings.child('NIDAQ_type').setLimits(DAQ_NIDAQ_source.Actuator()) self.settings.child('ao_settings').hide() self.settings.child('ao_channels').hide() @@ -105,10 +104,15 @@ def ini_detector(self, controller=None): # actions to perform in order to set properly the settings tree options self.commit_settings(self.settings.child('NIDAQ_type')) for ch in self.config_channels: + try: + ch.analog_type = ch.analog_type.upper() + ch.termination = ch.termination.upper() + except: + pass if self.settings.child("dev_to_use").value() in ch.name: self.settings.child('ai_channels').addNew(ch.name) param = [a for a in self.settings.child('ai_channels').childs if a.opts['title'] == ch.name][0] - self.settings.child("ai_channels", param.opts['name'], "ai_type").setValue(ch.analog_type) + self.settings.child("ai_channels", param.opts['name'], "ai_type").setValue(ch.analog_type.upper()) param.child("voltage_settings").show(ch.analog_type == UsageTypeAI.VOLTAGE.name) param.child("current_settings").show(ch.analog_type == UsageTypeAI.CURRENT.name) param.child("thermoc_settings").show(ch.analog_type == UsageTypeAI.TEMPERATURE_THERMOCOUPLE.name) diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py index 3e2efac..9326413 100644 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py @@ -7,7 +7,7 @@ from nidaqmx.constants import AcquisitionType, VoltageUnits, CurrentUnits, CurrentShuntResistorLocation, \ TemperatureUnits, CJCSource, CountDirection, Level, FrequencyUnits, TimeUnits, \ LineGrouping -from . import UsageTypeAI, Edge, TerminalConfiguration, ThermocoupleType +from pymodaq_plugins_daqmx.hardware.national_instruments import UsageTypeAI, Edge, TerminalConfiguration, ThermocoupleType from nidaqmx.system import System as niSystem from nidaqmx.system.device import Device as niDevice @@ -58,6 +58,15 @@ class DAQ_NIDAQ_source(IntEnumExtend): Digital_Output = 4 Terminals = 5 + def sources0D(self): + return [self.Analog_Input.name, self.Counter.name, self.Digital_Input.name] + + def sources1D(self): + return [self.Analog_Input.name] + + def Actuator(self): + return [self.Analog_Output.name] + class ClockSettingsBase: def __init__(self, Nsamples=1000, repetition=False): @@ -275,7 +284,7 @@ def configuration_sequence(self, viewer, current_device): logger.info("Detected devices: {}".format(devices_info)) try: viewer.config_devices = [config["NIDAQ_Devices", dev].get('name') for dev in viewer.config["NIDAQ_Devices"] - if "Mod" not in config["NIDAQ_Devices", dev].get('name')] + if "Mod" not in config["NIDAQ_Devices", dev].get('name')] logger.info(viewer.config_devices) for dev in config["NIDAQ_Devices"]: if not isinstance(config["NIDAQ_Devices", dev], dict): @@ -319,6 +328,13 @@ def configuration_sequence(self, viewer, current_device): value_max=float(ai[ch].get("value_max")), termination=TerminalConfiguration.__getitem__(term), )) + (name=name, + source=ai[ch].get("source"), + analog_type=ai[ch].get("analog_type"), + value_min=float(ai[ch].get("value_min")), + value_max=float(ai[ch].get("value_max")), + termination=TerminalConfiguration.__getitem__(term), + )) elif ai[ch].get("analog_type") == UsageTypeAI.CURRENT.name: viewer.config_channels.append(AIChannel (name=name, From bf1df4be7ed016179fa555271aa4a26747022fe1 Mon Sep 17 00:00:00 2001 From: Loic GUILMARD Date: Fri, 20 Dec 2024 11:57:56 +0100 Subject: [PATCH 039/140] enums typos and mix-ups --- .../national_instruments/daq_NIDAQmx.py | 2 +- .../hardware/national_instruments/daqmxni.py | 21 ++++++++----------- 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx.py index 926f713..43c0d97 100644 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx.py +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx.py @@ -414,7 +414,7 @@ def update_task(self): enable=self.settings['trigger_settings', 'enable'], edge=Edge[self.settings['trigger_settings', 'edge'].upper()], level=self.settings['trigger_settings', 'level'], ) - if not not self.channels: + if self.channels: self.controller.update_task(self.channels, self.clock_settings, trigger_settings=self.trigger_settings) def get_channels_from_settings(self): diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py index 9326413..aedea58 100644 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py @@ -58,12 +58,15 @@ class DAQ_NIDAQ_source(IntEnumExtend): Digital_Output = 4 Terminals = 5 + @classmethod def sources0D(self): - return [self.Analog_Input.name, self.Counter.name, self.Digital_Input.name] + return [self.Analog_Input.name, DAQ_NIDAQ_source.Counter.name, DAQ_NIDAQ_source.Digital_Input.name] + @classmethod def sources1D(self): return [self.Analog_Input.name] + @classmethod def Actuator(self): return [self.Analog_Output.name] @@ -321,13 +324,6 @@ def configuration_sequence(self, viewer, current_device): term = ai[ch].get("termination") if ai[ch].get("analog_type") == UsageTypeAI.VOLTAGE.name: viewer.config_channels.append(AIChannel - (name=name, - source=ai[ch].get("source"), - analog_type=ai[ch].get("analog_type"), - value_min=float(ai[ch].get("value_min")), - value_max=float(ai[ch].get("value_max")), - termination=TerminalConfiguration.__getitem__(term), - )) (name=name, source=ai[ch].get("source"), analog_type=ai[ch].get("analog_type"), @@ -344,7 +340,8 @@ def configuration_sequence(self, viewer, current_device): value_max=float(ai[ch].get("value_max")), termination=TerminalConfiguration.__getitem__(term), )) - elif ai[ch].get("analog_type") == UsageTypeAI.TEMPERATURE_THERMOCOUPLE.name: + elif ai[ch].get("analog_type") == UsageTypeAI.TEMPERATURE_THERMOCOUPLE.name or \ + ai[ch].get("analog_type") == "Thermocouple": th = ai[ch].get("thermo_type") viewer.config_channels.append(AIThermoChannel (name=name, @@ -415,7 +412,7 @@ def update_task(self, channels=[], clock_settings=ClockSettings(), trigger_setti # create all channels one task for one type of channels for channel in channels: - if channel.source == DAQ_NIDAQ_source.Digital_Input.name: # analog input + if channel.source == DAQ_NIDAQ_source.Analog_Input.name: # analog input try: if channel.analog_type == UsageTypeAI.VOLTAGE.name: self._task.ai_channels.add_ai_voltage_chan(channel.name, @@ -437,7 +434,7 @@ def update_task(self, channels=[], clock_settings=ClockSettings(), trigger_setti 0., "") - elif channel.analog_type == UsageTypeAI.CURRENT.name: + elif channel.analog_type == UsageTypeAI.TEMPERATURE_THERMOCOUPLE.name: self._task.ai_channels.add_ai_thrmcpl_chan(channel.name, "", channel.value_min, @@ -449,7 +446,7 @@ def update_task(self, channels=[], clock_settings=ClockSettings(), trigger_setti "") except DaqError as e: err_code = e.error_code - if not not err_code: + if err_code: status = self.DAQmxGetErrorString(err_code) raise IOError(status) elif channel.source == 'Counter': # counter From 7db15965ea0bbf8d906392a43daf9dd31cee63c7 Mon Sep 17 00:00:00 2001 From: Loic GUILMARD Date: Fri, 20 Dec 2024 11:58:10 +0100 Subject: [PATCH 040/140] Added aenum to dependencies --- pyproject.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 329457c..3726a60 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,7 +14,8 @@ description = 'Hardware plugins for PyMoDAQ using the NiDAQmx framework (nidaq w dependencies = [ "pymodaq>=4.4", "pydaqmx", - "nidaqmx" + "nidaqmx", + "aenum" ] authors = [ From 004e910e3455b4adc7433a8cec09c9047fd8dfa3 Mon Sep 17 00:00:00 2001 From: Sebastien Date: Tue, 7 Jan 2025 10:51:11 +0100 Subject: [PATCH 041/140] Fix viewer & move double implementation --- .../national_instruments/daq_NIDAQmx.py | 405 +----------------- .../national_instruments/daq_NIDAQmx_Move.py | 12 +- 2 files changed, 6 insertions(+), 411 deletions(-) diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx.py index 43c0d97..61b6443 100644 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx.py +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx.py @@ -1,18 +1,11 @@ from qtpy import QtWidgets -from qtpy.QtCore import Signal, QTimer -import numpy as np +from qtpy.QtCore import Signal from pymodaq.utils.logger import set_logger, get_module_name -from pymodaq.control_modules.viewer_utility_classes import DAQ_Viewer_base, comon_parameters as viewer_params -from pymodaq.control_modules.move_utility_classes import DAQ_Move_base, comon_parameters as actuator_params -import traceback from pymodaq.utils.parameter import Parameter from pymodaq.utils.parameter.pymodaq_ptypes import registerParameterType, GroupParameter from pymodaq_plugins_daqmx.hardware.national_instruments.daqmxni import DAQmx, Edge, DAQ_NIDAQ_source, ClockSettings, \ AIChannel, Counter, AIThermoChannel, AOChannel, TriggerSettings, DOChannel, DIChannel, UsageTypeAI, \ ThermocoupleType, TerminalConfiguration -from pymodaq.utils.daq_utils import ThreadCommand, getLineInfo -from pymodaq.utils.data import DataToExport, DataFromPlugins, DataActuator -from easydict import EasyDict as eDict logger = set_logger(get_module_name(__file__)) @@ -479,400 +472,4 @@ def stop(self): self.controller.stop() -class DAQ_NIDAQmx_Viewer(DAQ_Viewer_base, DAQ_NIDAQmx_base): - """ - ==================== ======================== - **Attributes** **Type** - *data_grabed_signal* instance of Signal - *params* dictionnary list - *task* - ==================== ======================== - - See Also - -------- - refresh_hardware - """ - - live_mode_available = True - params = viewer_params + DAQ_NIDAQmx_base.params - - def __init__(self, parent=None, params_state=None, control_type="0D"): - DAQ_Viewer_base.__init__(self, parent, params_state) # defines settings attribute and various other methods - DAQ_NIDAQmx_base.__init__(self) - - self.Naverage = None - self.live = False - self.control_type = control_type # could be "0D", "1D" or "Actuator" - if self.control_type == "0D": - self.settings.child('NIDAQ_type').setLimits(DAQ_NIDAQ_source.Source0D()) # analog input and counter - elif self.control_type == "1D": - self.settings.child('NIDAQ_type').setLimits([DAQ_NIDAQ_source.sources1D()]) - elif self.control_type == "Actuator": - self.settings.child('NIDAQ_type').setLimits([DAQ_NIDAQ_source.Actuator()]) - - self.settings.child('ao_settings').hide() - self.settings.child('ao_channels').hide() - - # timer used for the counter - self.timer = QTimer() - self.timer.setSingleShot(True) - self.timer.timeout.connect(self.counter_done) - - def stop(self): - """Stop the current grab hardware wise if necessary""" - self.controller.stop() - self.live = False - self.emit_status(ThreadCommand('Update_Status', ['Acquisition stopped.'])) - return '' - - def commit_settings(self, param): - """ - Activate the parameters changes in the hardware. - - =============== ================================ =========================== - **Parameters** **Type** **Description** - *param* instance of pyqtgraph.parameter the parameter to activate - =============== ================================ =========================== - - See Also - -------- - update_NIDAQ_channels, update_task, DAQ_NIDAQ_source, refresh_hardware - """ - - if param.parent() is not None: - if param.parent().name() == 'ai_channels': - device = param.opts['title'].split('/')[0] - self.settings.child('clock_settings', 'frequency').setOpts(max=self.getAIMaxRate(device)) - - ranges = self.controller.getAIVoltageRange(device) - param.child('voltage_settings', 'volt_min').setOpts(limits=[r[0] for r in ranges]) - param.child('voltage_settings', 'volt_max').setOpts(limits=[r[1] for r in ranges]) - - DAQ_NIDAQmx_base.commit_settings(self, param) - - def ini_detector(self, controller=None): - """ - Initialisation procedure of the detector. - - See Also - -------- - daq_utils.ThreadCommand - """ - try: - self.controller = self.ini_detector_init(controller, DAQmx()) - self.update_task() - - # actions to perform in order to set properly the settings tree options - self.commit_settings(self.settings.child('NIDAQ_type')) - - info = "Plugin Initialized" - initialized = True - return info, initialized - - except Exception as e: - logger.info(traceback.format_exc()) - self.emit_status(ThreadCommand('Update_Status', [str(e), 'log'])) - info = str(e) - initialized = False - return info, initialized - - def grab_data(self, Naverage=1, **kwargs): - """ - | grab the current values with NIDAQ profile procedure. - | - | Send the data_grabed_signal once done. - - =============== ======== =============================================== - **Parameters** **Type** **Description** - *Naverage* int Number of values to average - =============== ======== =============================================== - - See Also - -------- - DAQ_NIDAQ_source - """ - update = False - - if 'live' in kwargs: - if kwargs['live'] != self.live: - update = True - self.live = kwargs['live'] - - if Naverage != self.Naverage: - self.Naverage = Naverage - update = True - if update: - self.update_task() - - if self.controller.task is None: - self.update_task() - - self.controller.register_callback(self.emit_data, "Nsamples", self.clock_settings.Nsamples) - self.controller.start() - - def emit_data(self, task_handle, every_n_samples_event_type, number_of_samples, callback_data): - channels_names = [ch.name for ch in self.channels] - # channels_ai_names = [ch.name for ch in self.channels if ch.source == 'Analog_Input'] - data_from_task = self.controller.task.read(self.settings['nsamplestoread'], timeout=20.0) - if self.control_type == "0D": - if not len(self.controller.task.channels.channel_names) != 1: - data_dfp = [np.array(data_from_task)] - else: - data_dfp = list(map(np.array, data_from_task)) - dte = DataToExport(name='NIDAQmx', - data=[DataFromPlugins(name='NI Analog Input', - data=data_dfp, - dim=f'Data{self.settings.child("display").value()}', - labels=channels_names - ), - ]) - self.dte_signal.emit(dte) - return 0 # mandatory for the NIDAQmx callback - - def counter_done(self): - channels_name = [ch.name for ch in self.channels] - data_counter = self.readCounter(len(self.channels), - self.settings['counter_settings', 'counting_time'] * 1e-3) - self.data_grabed_signal.emit([DataFromPlugins(name='NI Counter', data=[data_counter / 1e-3], dim='Data0D', - labels=channels_name, )]) - # y_axis=Axis(label='Count Number', units='1/s'))]) - self.task.StopTask() - - -class DAQ_NIDAQmx_Actuator(DAQ_Move_base, DAQ_NIDAQmx_base): - """ - Wrapper object to access the Mock fonctionnalities, similar wrapper for all controllers. - - =============== ============== - **Attributes** **Type** - *params* dictionnary - =============== ============== - """ - _controller_units = 'Volts' - is_multiaxes = False # set to True if this plugin is controlled for a multiaxis controller (with a unique communication link) - stage_names = [] # "list of strings of the multiaxes - - params = DAQ_NIDAQmx_base.params + [ - # elements to be added here as dicts in order to control your custom stage - ############ - {'title': 'MultiAxes:', 'name': 'multiaxes', 'type': 'group', 'visible': is_multiaxes, - 'children': [ - {'title': 'is Multiaxes:', 'name': 'ismultiaxes', 'type': 'bool', 'value': is_multiaxes, - 'default': False}, - {'title': 'Status:', 'name': 'multi_status', 'type': 'list', 'value': 'Master', - 'limits': ['Master', 'Slave']}, - {'title': 'Axis:', 'name': 'axis', 'type': 'list', 'limits': stage_names}, - ]}] + actuator_params() - - def __init__(self, parent=None, params_state=None, control_type="Actuator"): - DAQ_Move_base.__init__(self, parent, params_state) # defines settings attribute and various other methods - DAQ_NIDAQmx_base.__init__(self) - - self.control_type = "Actuator" # could be "0D", "1D" or "Actuator" - self.settings.child('NIDAQ_type').setLimits(['Analog_Output', 'Digital_Output']) - - self.settings.child('clock_settings', 'Nsamples').setValue(1) - - def get_actuator_value(self) -> DataActuator: - """Get the current position from the hardware with scaling conversion. - - Returns - ------- - float: The position obtained after scaling conversion. - """ - - pos = self.target_position - ## - - pos = self.get_position_with_scaling(pos) - self.emit_status(ThreadCommand('check_position', [pos])) - return pos - - def commit_settings(self, param): - """ - | Activate any parameter changes on the PI_GCS2 hardware. - | - | Called after a param_tree_changed signal from DAQ_Move_main. - - """ - - DAQ_NIDAQmx_base.commit_settings(self, param) - if param.name() == 'waveform': - if param.value() == 'DC': - self.settings.child('ao_settings', 'cont_param').setValue('offset') - self.settings.child('ao_settings', 'cont_param').show(not param.value() == 'DC') - self.settings.child('ao_settings', 'waveform_settings').show(not param.value() == 'DC') - - if param.parent() is not None: - if param.parent().name() == 'ao_channels': - device = param.opts['title'].split('/')[0] - self.settings.child('clock_settings', 'frequency').setOpts(max=self.getAOMaxRate(device)) - - ranges = self.getAOVoltageRange(device) - param.child('voltage_settings', 'volt_min').setOpts(limits=[r[0] for r in ranges]) - param.child('voltage_settings', 'volt_max').setOpts(limits=[r[1] for r in ranges]) - - 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 - ------- - self.status (easydict): with initialization status: three fields: - * info (str) - * controller (object) initialized controller - *initialized: (bool): False if initialization failed otherwise True - """ - - try: - # initialize the stage and its controller status - # controller is an object that may be passed to other instances of DAQ_Move_Mock in case - # of one controller controlling multiactuators (or detector) - - self.status.update(eDict(info="", controller=None, initialized=False)) - - # check whether this stage is controlled by a multiaxe controller (to be defined for each plugin) - # if multiaxes then init the controller here if Master state otherwise use external controller - if self.settings['multiaxes', 'ismultiaxes'] and self.settings['multiaxes', - 'multi_status'] == "Slave": - if controller is None: - raise Exception('no controller has been defined externally while this axe is a slave one') - else: - self.controller = controller - else: - self.controller = 'A Nidaqmx task' - self.update_task() - - # actions to perform in order to set properly the settings tree options - self.commit_settings(self.settings.child('NIDAQ_type')) - - self.status.info = "Plugin Initialized" - self.status.controller = self.controller - self.status.initialized = True - return self.status - - except Exception as e: - self.emit_status(ThreadCommand('Update_Status', [getLineInfo() + str(e), 'log'])) - self.status.info = getLineInfo() + str(e) - self.status.initialized = False - return self.status - - def calulate_waveform(self, value): - waveform = self.settings['ao_settings', 'waveform'] - if waveform == 'DC': - values = np.array([value]) - else: - Nsamples = self.settings['clock_settings', 'Nsamples'] - freq = self.settings['clock_settings', 'frequency'] - time = np.linspace(0, Nsamples / freq, Nsamples, endpoint=False) - - freq0 = self.settings['ao_settings', 'waveform_settings', 'frequency'] - amp = self.settings['ao_settings', 'waveform_settings', 'amplitude'] - offset = self.settings['ao_settings', 'waveform_settings', 'offset'] - if waveform == 'Sinus': - values = offset + amp * np.sin(2 * np.pi * freq0 * time) - elif waveform == 'Ramp': - values = offset + amp * np.linspace(0, 1, Nsamples) - - return values - - def move_abs(self, position): - """ Move the actuator to the absolute target defined by position - - Parameters - ---------- - position: (flaot) value of the absolute target positioning - """ - - position = self.check_bound(position) # if user checked bounds, the defined bounds are applied here - position = self.set_position_with_scaling(position) # apply scaling if the user specified one - if self.settings['NIDAQ_type'] == 'Analog_Output': - self.settings.child('ao_settings', 'waveform_settings', - self.settings['ao_settings', 'cont_param']).setValue(position) - values = self.calulate_waveform(position) - self.target_position = position - - self.stop() - - if len(values) == 1: - self.writeAnalog(len(values), 1, values, autostart=True) - self.current_position = self.check_position() - self.move_done() - else: - if self.c_callback is None: - self.register_callback(self.move_done_callback) - self.writeAnalog(len(values), 1, values, autostart=False) - self.task.StartTask() - elif self.settings['NIDAQ_type'] == 'Digital_Output': - self.writeDigital(1, np.array([position], dtype=np.uint8), autostart=True) - - def move_done_callback(self, taskhandle, status, callbackdata): - self.current_position = self.check_position() - QtWidgets.QApplication.processEvents() - self.move_done() - self.task.StopTask() - return 0 - - def move_rel(self, position): - """ Move the actuator to the relative target actuator value defined by position - - Parameters - ---------- - position: (flaot) value of the relative target positioning - """ - - position = self.check_bound(self.current_position + position) - self.current_position - self.target_position = position + self.current_position - if self.settings['NIDAQ_type'] == 'Analog_Output': - self.settings.child('ao_settings', 'waveform_settings', - self.settings['ao_settings', 'cont_param']).setValue(self.target_position) - - values = self.calulate_waveform(self.target_position) - - self.stop() - - if len(values) == 1: - self.writeAnalog(len(values), 1, values, autostart=True) - self.current_position = self.check_position() - self.move_done() - else: - if self.c_callback is None: - self.register_callback(self.move_done_callback) - self.writeAnalog(len(values), 1, values, autostart=False) - self.task.StartTask() - elif self.settings['NIDAQ_type'] == 'Digital_Output': - self.writeDigital(1, np.array([self.target_position], dtype=np.uint8), autostart=True) - - def move_home(self): - """ - Send the update status thread command. - See Also - -------- - daq_utils.ThreadCommand - """ - - self.stop() - - if self.c_callback is None: - self.register_callback(self.move_done_callback) - self.writeAnalog(1, 1, np.array([0.])) - self.task.StartTask() - - def stop_motion(self): - """ - Call the specific move_done function (depending on the hardware). - - See Also - -------- - move_done - """ - - ## TODO for your custom plugin - self.stop() - self.emit_status(ThreadCommand('Update_Status', ['Some info you want to log'])) - self.move_done() # to let the interface know the actuator stopped - diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx_Move.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx_Move.py index 35d699c..d38ca0b 100644 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx_Move.py +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx_Move.py @@ -32,8 +32,7 @@ class DAQ_NIDAQmx_Actuator(DAQ_Move_base, DAQ_NIDAQmx_base): {'title': 'Status:', 'name': 'multi_status', 'type': 'list', 'value': 'Master', 'limits': ['Master', 'Slave']}, {'title': 'Axis:', 'name': 'axis', 'type': 'list', 'limits': stage_names}, - - ]}] + actuator_params + ]}] + actuator_params() def __init__(self, parent=None, params_state=None, control_type="Actuator"): DAQ_Move_base.__init__(self, parent, params_state) # defines settings attribute and various other methods @@ -92,7 +91,7 @@ def ini_stage(self, controller=None): Returns ------- - self.status (edict): with initialization status: three fields: + self.status (easydict): with initialization status: three fields: * info (str) * controller (object) initialized controller *initialized: (bool): False if initialization failed otherwise True @@ -150,7 +149,7 @@ def calulate_waveform(self, value): return values - def move_Abs(self, position): + def move_abs(self, position): """ Move the actuator to the absolute target defined by position Parameters @@ -187,7 +186,7 @@ def move_done_callback(self, taskhandle, status, callbackdata): self.task.StopTask() return 0 - def move_Rel(self, position): + def move_rel(self, position): """ Move the actuator to the relative target actuator value defined by position Parameters @@ -217,7 +216,7 @@ def move_Rel(self, position): elif self.settings['NIDAQ_type'] == 'Digital_Output': self.writeDigital(1, np.array([self.target_position], dtype=np.uint8), autostart=True) - def move_Home(self): + def move_home(self): """ Send the update status thread command. See Also @@ -245,4 +244,3 @@ def stop_motion(self): self.stop() self.emit_status(ThreadCommand('Update_Status', ['Some info you want to log'])) self.move_done() # to let the interface know the actuator stopped - ############################## From 90c72b52302ea6d5435dbf6ce343fb80fd255f0e Mon Sep 17 00:00:00 2001 From: Sebastien Date: Thu, 9 Jan 2025 11:20:20 +0100 Subject: [PATCH 042/140] Remove use of deprecated .members() method --- .../hardware/national_instruments/daqmxni.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py index aedea58..0b12562 100644 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py @@ -82,7 +82,7 @@ class ClockSettings(ClockSettingsBase): def __init__(self, source=None, frequency=1000, Nsamples=1000, edge=Edge.RISING, repetition=False): super().__init__(Nsamples, repetition) self.source = source - assert edge in Edge.members() + assert edge in Edge self.frequency = frequency self.edge = edge @@ -97,7 +97,7 @@ def __init__(self, Nsamples=1000, rising_channel='', falling_channel='', class TriggerSettings: def __init__(self, trig_source='', enable=False, edge=Edge.RISING, level=0.1): - assert edge in Edge.members() + assert edge in Edge self.trig_source = trig_source self.enable = enable self.edge = edge @@ -133,14 +133,14 @@ def __init__(self, analog_type=UsageTypeAI.VOLTAGE, value_min=-10., value_max=+1 class AIChannel(AChannel): def __init__(self, termination=TerminalConfiguration.DEFAULT, **kwargs): super().__init__(**kwargs) - assert termination in TerminalConfiguration.members() + assert termination in TerminalConfiguration self.termination = termination class AIThermoChannel(AIChannel): def __init__(self, thermo_type=ThermocoupleType.K, **kwargs): super().__init__(**kwargs) - assert thermo_type in ThermocoupleType.members() + assert thermo_type in ThermocoupleType self.thermo_type = thermo_type @@ -151,7 +151,7 @@ def __init__(self, **kwargs): class Counter(Channel): def __init__(self, edge=Edge.RISING, **kwargs): - assert edge in Edge.members() + assert edge in Edge super().__init__(**kwargs) self.edge = edge self.counter_type = "Edge Counter" From 8ec8fe2e46dc3b89c7ec9c01032940720da351e7 Mon Sep 17 00:00:00 2001 From: Sebastien Date: Fri, 10 Jan 2025 15:57:15 +0100 Subject: [PATCH 043/140] [Cleaning] Useless infos removed, spaces & main --- .../plugins_0D/daq_0Dviewer_NIDAQmx.py | 2 -- .../national_instruments/daq_NIDAQmx.py | 2 +- .../hardware/national_instruments/daqmxni.py | 17 ++--------------- 3 files changed, 3 insertions(+), 18 deletions(-) diff --git a/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_NIDAQmx.py b/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_NIDAQmx.py index 94fd711..575c76b 100644 --- a/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_NIDAQmx.py +++ b/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_NIDAQmx.py @@ -32,8 +32,6 @@ class DAQ_0DViewer_NIDAQmx(DAQ_NIDAQmx_Viewer): {'title': 'Devices :', 'name': 'devices', 'type': 'list', 'limits': param_devices, 'value': param_devices[0] }, - {'title': 'Device To Use:', 'name': 'dev_to_use', 'type': 'list', 'limits': param_devices, - }, ] + DAQ_NIDAQmx_base.params def __init__(self, parent=None, params_state=None): diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx.py index 61b6443..290c45d 100644 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx.py +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx.py @@ -460,7 +460,7 @@ def get_channels_from_settings(self): channels.append(DOChannel(name=channel.opts['title'], source=DAQ_NIDAQ_source.Digital_Output.name)) - channels = [ch for ch in channels if self.settings.child("dev_to_use").value() in ch.name] + channels = [ch for ch in channels if self.settings.child("devices").value() in ch.name] return channels def stop(self): diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py index 0b12562..c64c8b6 100644 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py @@ -288,7 +288,6 @@ def configuration_sequence(self, viewer, current_device): try: viewer.config_devices = [config["NIDAQ_Devices", dev].get('name') for dev in viewer.config["NIDAQ_Devices"] if "Mod" not in config["NIDAQ_Devices", dev].get('name')] - logger.info(viewer.config_devices) for dev in config["NIDAQ_Devices"]: if not isinstance(config["NIDAQ_Devices", dev], dict): continue @@ -352,9 +351,8 @@ def configuration_sequence(self, viewer, current_device): thermo_type=ThermocoupleType.__getitem__(th), )) logger.info("Devices from config: {}".format(viewer.config_devices)) - logger.info("Current device: {}".format(current_device)) - logger.info("Current device modules from config: {}".format(viewer.config_modules)) - logger.info("Current device channels from config: {}".format([ch.name for ch in viewer.config_channels])) + logger.info("Modules from config: {}".format(viewer.config_modules)) + logger.info("Channels from config: {}".format([ch.name for ch in viewer.config_channels])) except AssertionError as err: logger.error("Configuration entries <{}> does not match the hardware ".format(err)) except Exception as err: @@ -644,15 +642,4 @@ def refresh_hardware(self): if __name__ == '__main__': - print(DAQmx.get_NIDAQ_channels()) - controller = DAQmx() - ch_counter = Counter(name='Dev1/ctr0', - source='Counter', - edge=Edge.members()[0]) - controller.update_task([ch_counter]) - controller.start() - print(controller.readCounter()) - controller.stop() - controller.close() - pass From 750db1bffe2894cf4aff5b4c85a79bf865afafd6 Mon Sep 17 00:00:00 2001 From: Sebastien Date: Fri, 17 Jan 2025 14:29:50 +0100 Subject: [PATCH 044/140] [Optimizing] nidaqmx enum constants in their simplest use + channels added more efficiently + small cleaning of useless info or methods --- .../hardware/national_instruments/__init__.py | 47 -------- .../national_instruments/daq_NIDAQmx.py | 79 ++++++------- .../daq_NIDAQmx_Viewer.py | 22 ++-- .../hardware/national_instruments/daqmxni.py | 106 +++++++----------- 4 files changed, 90 insertions(+), 164 deletions(-) diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/__init__.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/__init__.py index 987567a..e69de29 100644 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/__init__.py +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/__init__.py @@ -1,47 +0,0 @@ -from aenum import extend_enum -from types import MethodType -from nidaqmx.constants import UsageTypeAI, Edge, TerminalConfiguration, ThermocoupleType - - -def members(classref): - """ - Return the list of the members of cls.__members__.items() - """ - return list(classref._member_map_.values()) - - -def names(classref): - """ - Return the list of the names of the enum members - """ - return classref._member_names_ - - -extend_enum(UsageTypeAI, "Thermocouple", UsageTypeAI.TEMPERATURE_THERMOCOUPLE.value) - - -extend_enum(TerminalConfiguration, "Auto", TerminalConfiguration.DEFAULT.value) - - -UsageTypeAI.members = MethodType(members, UsageTypeAI) - - -UsageTypeAI.names = MethodType(names, UsageTypeAI) - - -Edge.members = MethodType(members, Edge) - - -Edge.names = MethodType(names, Edge) - - -TerminalConfiguration.members = MethodType(members, TerminalConfiguration) - - -TerminalConfiguration.names = MethodType(names, TerminalConfiguration) - - -ThermocoupleType.members = MethodType(members, ThermocoupleType) - - -ThermocoupleType.names = MethodType(names, ThermocoupleType) diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx.py index 290c45d..d49d032 100644 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx.py +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx.py @@ -4,7 +4,7 @@ from pymodaq.utils.parameter import Parameter from pymodaq.utils.parameter.pymodaq_ptypes import registerParameterType, GroupParameter from pymodaq_plugins_daqmx.hardware.national_instruments.daqmxni import DAQmx, Edge, DAQ_NIDAQ_source, ClockSettings, \ - AIChannel, Counter, AIThermoChannel, AOChannel, TriggerSettings, DOChannel, DIChannel, UsageTypeAI, \ + AIChannel, Counter, AIThermoChannel, AOChannel, TriggerSettings, DOChannel, DIChannel, UsageTypeAI, UsageTypeAO, \ ThermocoupleType, TerminalConfiguration @@ -26,7 +26,7 @@ class ScalableGroupAI(GroupParameter): hardware.DAQ_Move_Stage_type """ - params = [{'title': 'AI type:', 'name': 'ai_type', 'type': 'list', 'limits': UsageTypeAI.names()}, + params = [{'title': 'AI type:', 'name': 'ai_type', 'type': 'list', 'limits': [Uai.name for Uai in UsageTypeAI]}, {'title': 'Voltage:', 'name': 'voltage_settings', 'type': 'group', 'children': [ {'title': 'Voltage Min:', 'name': 'volt_min', 'type': 'float', 'value': -10.}, {'title': 'Voltage Max:', 'name': 'volt_max', 'type': 'float', 'value': 10.}, @@ -36,12 +36,13 @@ class ScalableGroupAI(GroupParameter): {'title': 'Current Max:', 'name': 'curr_max', 'type': 'float', 'value': 1, 'suffix': 'A'}, ]}, {'title': 'Thermocouple:', 'name': 'thermoc_settings', 'type': 'group', 'visible': False, 'children': [ - {'title': 'Thc. type:', 'name': 'thermoc_type', 'type': 'list', 'limits': ThermocoupleType.names(), - 'value': 'K'}, + {'title': 'Thc. type:', 'name': 'thermoc_type', 'type': 'list', + 'limits': [Th.name for Th in ThermocoupleType], 'value': 'K'}, {'title': 'Temp. Min (°C):', 'name': 'T_min', 'type': 'float', 'value': 0, 'suffix': '°C'}, {'title': 'Temp. Max (°C):', 'name': 'T_max', 'type': 'float', 'value': 50, 'suffix': '°C'}, ]}, - {'title': 'Termination:', 'name': 'termination', 'type': 'list', 'limits': TerminalConfiguration.names()}, + {'title': 'Termination:', 'name': 'termination', 'type': 'list', + 'limits': [Te.name for Te in TerminalConfiguration]}, ] def __init__(self, **opts): @@ -88,7 +89,7 @@ class ScalableGroupAO(GroupParameter): hardware.DAQ_Move_Stage_type """ - params = [{'title': 'AO type:', 'name': 'ao_type', 'type': 'list', 'limits': UsageTypeAI.names()[0:2]}, + params = [{'title': 'AO type:', 'name': 'ao_type', 'type': 'list', 'limits': [Uao.name for Uao in UsageTypeAO]}, {'title': 'Voltages:', 'name': 'voltage_settings', 'type': 'group', 'children': [ {'title': 'Voltage Min:', 'name': 'volt_min', 'type': 'list', 'value': -10., }, {'title': 'Voltage Max:', 'name': 'volt_max', 'type': 'list', 'value': 10., }, @@ -143,7 +144,7 @@ class ScalableGroupCounter(GroupParameter): hardware.DAQ_Move_Stage_type """ - params = [{'title': 'Edge type:', 'name': 'edge', 'type': 'list', 'limits': Edge.names()}, ] + params = [{'title': 'Edge type:', 'name': 'edge', 'type': 'list', 'limits': [e.name for e in Edge]}, ] def __init__(self, **opts): opts['type'] = 'groupcounter' @@ -252,7 +253,8 @@ class DAQ_NIDAQmx_base: data_grabed_signal = Signal(list) param_instance = DAQmx() params = [{'title': 'Refresh hardware:', 'name': 'refresh_hardware', 'type': 'bool', 'value': False}, - {'title': 'Signal type:', 'name': 'NIDAQ_type', 'type': 'list', 'limits': DAQ_NIDAQ_source.names()}, + {'title': 'Signal type:', 'name': 'NIDAQ_type', 'type': 'list', + 'limits': [Ds.name for Ds in DAQ_NIDAQ_source]}, {'title': 'NSamples To Read', 'name': 'nsamplestoread', 'type': 'int', 'value': 1000, 'default': 1000, 'min': 1}, {'title': 'AO Settings:', 'name': 'ao_settings', 'type': 'group', 'children': [ @@ -275,24 +277,25 @@ class DAQ_NIDAQmx_base: ] }, {'title': 'AI Channels:', 'name': 'ai_channels', 'type': 'groupai', - 'limits': DAQmx.get_NIDAQ_channels(source_type=DAQ_NIDAQ_source.Analog_Input.name)}, + 'limits': DAQmx.get_NIDAQ_channels(source_type=DAQ_NIDAQ_source.Analog_Input)}, {'title': 'AO Channels:', 'name': 'ao_channels', 'type': 'groupao', - 'limits': DAQmx.get_NIDAQ_channels(source_type=DAQ_NIDAQ_source.Analog_Output.name)}, + 'limits': DAQmx.get_NIDAQ_channels(source_type=DAQ_NIDAQ_source.Analog_Output)}, {'title': 'DO Channels:', 'name': 'do_channels', 'type': 'groupdo', - 'limits': DAQmx.get_NIDAQ_channels(source_type=DAQ_NIDAQ_source.Digital_Output.name)}, + 'limits': DAQmx.get_NIDAQ_channels(source_type=DAQ_NIDAQ_source.Digital_Output)}, {'title': 'DI Channels:', 'name': 'di_channels', 'type': 'groupdi', - 'limits': DAQmx.get_NIDAQ_channels(source_type=DAQ_NIDAQ_source.Digital_Input.name)}, + 'limits': DAQmx.get_NIDAQ_channels(source_type=DAQ_NIDAQ_source.Digital_Input)}, {'title': 'Counter Settings:', 'name': 'counter_settings', 'type': 'group', 'visible': True, 'children': [ {'title': 'Counting time (ms):', 'name': 'counting_time', 'type': 'float', 'value': 100., 'default': 100., 'min': 0.}, {'title': 'Counting Channels:', 'name': 'counter_channels', 'type': 'groupcounter', - 'limits': DAQmx.get_NIDAQ_channels(source_type=DAQ_NIDAQ_source.Counter.name)}, + 'limits': DAQmx.get_NIDAQ_channels(source_type=DAQ_NIDAQ_source.Counter)}, ]}, {'title': 'Trigger Settings:', 'name': 'trigger_settings', 'type': 'group', 'visible': True, 'children': [ {'title': 'Enable?:', 'name': 'enable', 'type': 'bool', 'value': False, }, {'title': 'Trigger Source:', 'name': 'trigger_channel', 'type': 'list', 'limits': DAQmx.getTriggeringSources()}, - {'title': 'Edge type:', 'name': 'edge', 'type': 'list', 'limits': Edge.names(), 'visible': False}, + {'title': 'Edge type:', 'name': 'edge', 'type': 'list', 'limits': [e.name for e in Edge], + 'visible': False}, {'title': 'Level:', 'name': 'level', 'type': 'float', 'value': 1., 'visible': False} ]} ] @@ -321,7 +324,6 @@ def commit_settings(self, param: Parameter): """ if param.name() == 'NIDAQ_devices': self.controller.update_NIDAQ_channels() - self.update_task() if param.name() == 'NIDAQ_type': self.controller.update_NIDAQ_channels(param.value()) @@ -370,8 +372,6 @@ def commit_settings(self, param: Parameter): self.settings.child('do_channels').show() self.settings.child('di_channels').hide() - self.update_task() - elif param.name() == 'refresh_hardware': if param.value(): self.controller.refresh_hardware() @@ -379,25 +379,19 @@ def commit_settings(self, param: Parameter): self.settings.child('refresh_hardware').setValue(False) elif param.name() == 'ai_type': - param.parent().child('voltage_settings').show(param.value() == UsageTypeAI.Voltage.name) - param.parent().child('current_settings').show(param.value() == UsageTypeAI.Current.name) + param.parent().child('voltage_settings').show(param.value() == UsageTypeAI.VOLTAGE.name) + param.parent().child('current_settings').show(param.value() == UsageTypeAI.CURRENT.name) param.parent().child('thermoc_settings').show(param.value() == UsageTypeAI.TEMPERATURE_THERMOCOUPLE.name) - self.update_task() elif param.name() == 'ao_type': - param.parent().child('voltage_settings').show(param.value() == UsageTypeAI.Voltage.name) - param.parent().child('current_settings').show(param.value() == UsageTypeAI.Current.name) - self.update_task() + param.parent().child('voltage_settings').show(param.value() == UsageTypeAI.VOLTAGE.name) + param.parent().child('current_settings').show(param.value() == UsageTypeAI.CURRENT.name) elif param.name() == 'trigger_channel': param.parent().child('level').show('PF' not in param.opts['title']) - else: - self.update_task() - def update_task(self): self.channels = self.get_channels_from_settings() - logger.info("update_task - channels: {}".format(self.channels)) self.clock_settings = ClockSettings(frequency=self.settings['clock_settings', 'frequency'], Nsamples=self.settings['clock_settings', 'Nsamples'], edge=Edge.RISING, @@ -405,7 +399,7 @@ def update_task(self): self.trigger_settings = \ TriggerSettings(trig_source=self.settings['trigger_settings', 'trigger_channel'], enable=self.settings['trigger_settings', 'enable'], - edge=Edge[self.settings['trigger_settings', 'edge'].upper()], + edge=Edge[self.settings['trigger_settings', 'edge']], level=self.settings['trigger_settings', 'level'], ) if self.channels: self.controller.update_task(self.channels, self.clock_settings, trigger_settings=self.trigger_settings) @@ -414,23 +408,22 @@ def get_channels_from_settings(self): channels = [] if self.settings['NIDAQ_type'] == DAQ_NIDAQ_source.Analog_Input.name: # analog input for channel in self.settings.child('ai_channels').children(): - logger.info("get_channels_from_settings - channel {}".format(channel)) - analog_type = channel['ai_type'] # todo UsageTypeAI[channel['ai_type'].upper()].name - if analog_type == UsageTypeAI.VOLTAGE.name: + analog_type = UsageTypeAI[channel['ai_type']] + if analog_type == UsageTypeAI.VOLTAGE: channels.append(AIChannel(name=channel.opts['title'], - source=DAQ_NIDAQ_source.Analog_Input.name, analog_type=analog_type, + source=DAQ_NIDAQ_source.Analog_Input, analog_type=analog_type, value_min=channel['voltage_settings', 'volt_min'], value_max=channel['voltage_settings', 'volt_max'], - termination=TerminalConfiguration[channel['termination'].upper()], )) - elif analog_type == UsageTypeAI.CURRENT.name: + termination=TerminalConfiguration[channel['termination']], )) + elif analog_type == UsageTypeAI.CURRENT: channels.append(AIChannel(name=channel.opts['title'], - source=DAQ_NIDAQ_source.Analog_Input.name, analog_type=analog_type, + source=DAQ_NIDAQ_source.Analog_Input, analog_type=analog_type, value_min=channel['current_settings', 'curr_min'], value_max=channel['current_settings', 'curr_max'], - termination=TerminalConfiguration[channel['termination'].upper()], )) - elif analog_type == UsageTypeAI.TEMPERATURE_THERMOCOUPLE.name: + termination=TerminalConfiguration[channel['termination']], )) + elif analog_type == UsageTypeAI.TEMPERATURE_THERMOCOUPLE: channels.append(AIThermoChannel(name=channel.opts['title'], - source=DAQ_NIDAQ_source.Analog_Input.name, analog_type=analog_type, + source=DAQ_NIDAQ_source.Analog_Input, analog_type=analog_type, value_min=channel['thermoc_settings', 'T_min'], value_max=channel['thermoc_settings', 'T_max'], thermo_type=ThermocoupleType[ @@ -438,9 +431,9 @@ def get_channels_from_settings(self): elif self.settings['NIDAQ_type'] == DAQ_NIDAQ_source.Analog_Output.name: # analog output for channel in self.settings.child('ao_channels').children(): - analog_type = channel['ao_type'] + analog_type = UsageTypeAO[channel['ao_type']] channels.append(AOChannel(name=channel.opts['title'], - source=DAQ_NIDAQ_source.Analog_Output.name, analog_type=analog_type, + source=DAQ_NIDAQ_source.Analog_Output, analog_type=analog_type, value_min=channel['voltage_settings', 'volt_min'], value_max=channel['voltage_settings', 'volt_max'], )) @@ -448,17 +441,17 @@ def get_channels_from_settings(self): elif self.settings['NIDAQ_type'] == DAQ_NIDAQ_source.Counter.name: # counter input for channel in self.settings.child('counter_settings', 'counter_channels').children(): channels.append(Counter(name=channel.opts['title'], - source='Counter', edge=channel['edge'])) + source=DAQ_NIDAQ_source.Counter, edge=Edge[channel['edge']])) elif self.settings['NIDAQ_type'] == DAQ_NIDAQ_source.Digital_Input.name: # digital input for channel in self.settings.child('di_channels').children(): channels.append(DIChannel(name=channel.opts['title'], - source=DAQ_NIDAQ_source.Digital_Input.name)) + source=DAQ_NIDAQ_source.Digital_Input)) elif self.settings['NIDAQ_type'] == DAQ_NIDAQ_source.Digital_Output.name: # Digital output for channel in self.settings.child('do_channels').children(): channels.append(DOChannel(name=channel.opts['title'], - source=DAQ_NIDAQ_source.Digital_Output.name)) + source=DAQ_NIDAQ_source.Digital_Output)) channels = [ch for ch in channels if self.settings.child("devices").value() in ch.name] return channels diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx_Viewer.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx_Viewer.py index f660ac7..1cebbdf 100644 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx_Viewer.py +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx_Viewer.py @@ -105,32 +105,32 @@ def ini_detector(self, controller=None): self.commit_settings(self.settings.child('NIDAQ_type')) for ch in self.config_channels: try: - ch.analog_type = ch.analog_type.upper() - ch.termination = ch.termination.upper() + ch.analog_type = ch.analog_type + ch.termination = ch.termination except: pass - if self.settings.child("dev_to_use").value() in ch.name: + if self.settings.child("devices").value() in ch.name: self.settings.child('ai_channels').addNew(ch.name) param = [a for a in self.settings.child('ai_channels').childs if a.opts['title'] == ch.name][0] - self.settings.child("ai_channels", param.opts['name'], "ai_type").setValue(ch.analog_type.upper()) - param.child("voltage_settings").show(ch.analog_type == UsageTypeAI.VOLTAGE.name) - param.child("current_settings").show(ch.analog_type == UsageTypeAI.CURRENT.name) - param.child("thermoc_settings").show(ch.analog_type == UsageTypeAI.TEMPERATURE_THERMOCOUPLE.name) - if ch.analog_type == UsageTypeAI.VOLTAGE.name: + self.settings.child("ai_channels", param.opts['name'], "ai_type").setValue(ch.analog_type.name) + param.child("voltage_settings").show(ch.analog_type == UsageTypeAI.VOLTAGE) + param.child("current_settings").show(ch.analog_type == UsageTypeAI.CURRENT) + param.child("thermoc_settings").show(ch.analog_type == UsageTypeAI.TEMPERATURE_THERMOCOUPLE) + if ch.analog_type == UsageTypeAI.VOLTAGE: self.settings.child("ai_channels", param.opts['name'], "voltage_settings", "volt_min").setValue( ch.value_min) self.settings.child("ai_channels", param.opts['name'], "voltage_settings", "volt_max").setValue( ch.value_max) self.settings.child("ai_channels", param.opts['name'], "termination").setValue( ch.termination.name) - elif ch.analog_type == UsageTypeAI.CURRENT.name: + elif ch.analog_type == UsageTypeAI.CURRENT: self.settings.child("ai_channels", param.opts['name'], "current_settings", "curr_min").setValue( ch.value_min) self.settings.child("ai_channels", param.opts['name'], "current_settings", "curr_max").setValue( ch.value_max) self.settings.child("ai_channels", param.opts['name'], "termination").setValue( ch.termination.name) - elif ch.analog_type == UsageTypeAI.TEMPERATURE_THERMOCOUPLE.name: + elif ch.analog_type == UsageTypeAI.TEMPERATURE_THERMOCOUPLE: self.settings.child("ai_channels", param.opts['name'], "thermoc_settings", @@ -144,7 +144,7 @@ def ini_detector(self, controller=None): "thermoc_settings", "T_max").setValue(ch.value_max) self.settings.child("ai_channels", param.opts['name'], "termination").setValue( - TerminalConfiguration.DEFAULT) + TerminalConfiguration.DEFAULT.name) info = "Plugin Initialized" initialized = True logger.info("Detector 0D initialized") diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py index c64c8b6..59a15f8 100644 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py @@ -1,13 +1,12 @@ import time import traceback -from enum import IntEnum +from enum import Enum import numpy as np from pymodaq.utils.logger import set_logger, get_module_name from nidaqmx.constants import AcquisitionType, VoltageUnits, CurrentUnits, CurrentShuntResistorLocation, \ TemperatureUnits, CJCSource, CountDirection, Level, FrequencyUnits, TimeUnits, \ - LineGrouping -from pymodaq_plugins_daqmx.hardware.national_instruments import UsageTypeAI, Edge, TerminalConfiguration, ThermocoupleType + LineGrouping, UsageTypeAI, UsageTypeAO, Edge, TerminalConfiguration, ThermocoupleType from nidaqmx.system import System as niSystem from nidaqmx.system.device import Device as niDevice @@ -19,29 +18,7 @@ logger = set_logger(get_module_name(__file__)) -class IntEnumExtend(IntEnum): - """ - This class expose 3 privates and undocumented methods of Enum - """ - @classmethod - def names(cls): - return cls._member_names_ - # [item.name for item in cls.__members__.items()] - # [name for name, member in cls.__members__.items()] - - @classmethod - def members(cls): - return list(cls._member_map_.values()) - # [member for name, member in cls.__members__.items()] - # [item.member for item in cls.__members__.items()] - - @classmethod - def values(cls): - return [cls[name].value for name in cls._member_names_] - # [cls[name].value for name, member in cls.__members__.items()] - - -class DAQ_NIDAQ_source(IntEnumExtend): +class DAQ_NIDAQ_source(Enum): """ Enum class of NIDAQ_source @@ -112,7 +89,7 @@ def __init__(self, name='', source=DAQ_NIDAQ_source.Analog_Input): """ self.name = name - assert source in DAQ_NIDAQ_source.names() + assert source in DAQ_NIDAQ_source self.source = source @@ -127,6 +104,7 @@ def __init__(self, analog_type=UsageTypeAI.VOLTAGE, value_min=-10., value_max=+1 super().__init__(**kwargs) self.value_min = value_min self.value_max = value_max + assert analog_type in UsageTypeAI self.analog_type = analog_type @@ -151,8 +129,8 @@ def __init__(self, **kwargs): class Counter(Channel): def __init__(self, edge=Edge.RISING, **kwargs): - assert edge in Edge super().__init__(**kwargs) + assert edge in Edge self.edge = edge self.counter_type = "Edge Counter" @@ -250,7 +228,7 @@ def get_NIDAQ_channels(cls, devices=None, source_type=None): devices = cls.get_NIDAQ_devices().device_names if source_type is None: - source_type = DAQ_NIDAQ_source.names() + source_type = DAQ_NIDAQ_source if not isinstance(source_type, list): source_type = [source_type] channels_tot = [] @@ -258,17 +236,17 @@ def get_NIDAQ_channels(cls, devices=None, source_type=None): if not not devices: for device in devices: for source in source_type: - if source == DAQ_NIDAQ_source.Analog_Input.name: # analog input + if source == DAQ_NIDAQ_source.Analog_Input: # analog input channels = niDevice(device).ai_physical_chans.channel_names - elif source == DAQ_NIDAQ_source.Analog_Output.name: # analog output + elif source == DAQ_NIDAQ_source.Analog_Output: # analog output channels = niDevice(device).ao_physical_chans.channel_names - elif source == DAQ_NIDAQ_source.Counter.name: # counter + elif source == DAQ_NIDAQ_source.Counter: # counter channels = niDevice(device).ci_physical_chans.channel_names - elif source == DAQ_NIDAQ_source.Digital_Output.name: # digital output + elif source == DAQ_NIDAQ_source.Digital_Output: # digital output channels = niDevice(device).do_lines.channel_names - elif source == DAQ_NIDAQ_source.Digital_Input.name: # digital iutput + elif source == DAQ_NIDAQ_source.Digital_Input: # digital iutput channels = niDevice(device).di_lines.channel_names - elif source == DAQ_NIDAQ_source.Terminals.name: # terminals + elif source == DAQ_NIDAQ_source.Terminals: # terminals channels = niDevice(device).terminals if channels != ['']: @@ -313,42 +291,44 @@ def configuration_sequence(self, viewer, current_device): except AssertionError as err: logger.error("Module {} not detected: {}".format(module_name, err)) continue - for source in config["NIDAQ_Devices", dev, mod]: - if not isinstance(config["NIDAQ_Devices", dev, mod, source], dict): + for src in config["NIDAQ_Devices", dev, mod]: + if not isinstance(config["NIDAQ_Devices", dev, mod, src], dict): continue - if source == "ai": - ai = config["NIDAQ_Devices", dev, mod, source] + if src == "ai": + ai = config["NIDAQ_Devices", dev, mod, src] for ch in ai.keys(): name = module_name + "/" + str(ch) - term = ai[ch].get("termination") - if ai[ch].get("analog_type") == UsageTypeAI.VOLTAGE.name: + source = DAQ_NIDAQ_source[ai[ch].get("source")] + analog_type = UsageTypeAI[ai[ch].get("analog_type")] + if analog_type == UsageTypeAI.VOLTAGE: + term = TerminalConfiguration[ai[ch].get("termination")] viewer.config_channels.append(AIChannel (name=name, - source=ai[ch].get("source"), - analog_type=ai[ch].get("analog_type"), + source=source, + analog_type=analog_type, value_min=float(ai[ch].get("value_min")), value_max=float(ai[ch].get("value_max")), - termination=TerminalConfiguration.__getitem__(term), + termination=term, )) - elif ai[ch].get("analog_type") == UsageTypeAI.CURRENT.name: + elif analog_type == UsageTypeAI.CURRENT: + term = TerminalConfiguration[ai[ch].get("termination")] viewer.config_channels.append(AIChannel (name=name, - source=ai[ch].get("source"), - analog_type=ai[ch].get("analog_type"), + source=source, + analog_type=analog_type, value_min=float(ai[ch].get("value_min")), value_max=float(ai[ch].get("value_max")), - termination=TerminalConfiguration.__getitem__(term), + termination=term, )) - elif ai[ch].get("analog_type") == UsageTypeAI.TEMPERATURE_THERMOCOUPLE.name or \ - ai[ch].get("analog_type") == "Thermocouple": - th = ai[ch].get("thermo_type") + elif analog_type == UsageTypeAI.TEMPERATURE_THERMOCOUPLE: + th = ThermocoupleType[ai[ch].get("thermo_type")] viewer.config_channels.append(AIThermoChannel (name=name, - source=ai[ch].get("source"), - analog_type=ai[ch].get("analog_type"), + source=source, + analog_type=analog_type, value_min=float(ai[ch].get("value_min")), value_max=float(ai[ch].get("value_max")), - thermo_type=ThermocoupleType.__getitem__(th), + thermo_type=th, )) logger.info("Devices from config: {}".format(viewer.config_devices)) logger.info("Modules from config: {}".format(viewer.config_modules)) @@ -410,9 +390,9 @@ def update_task(self, channels=[], clock_settings=ClockSettings(), trigger_setti # create all channels one task for one type of channels for channel in channels: - if channel.source == DAQ_NIDAQ_source.Analog_Input.name: # analog input + if channel.source == DAQ_NIDAQ_source.Analog_Input: # analog input try: - if channel.analog_type == UsageTypeAI.VOLTAGE.name: + if channel.analog_type == UsageTypeAI.VOLTAGE: self._task.ai_channels.add_ai_voltage_chan(channel.name, "", channel.termination, @@ -421,7 +401,7 @@ def update_task(self, channels=[], clock_settings=ClockSettings(), trigger_setti VoltageUnits.VOLTS, "") - elif channel.analog_type == UsageTypeAI.CURRENT.name: + elif channel.analog_type == UsageTypeAI.CURRENT: self._task.ai_channels.add_ai_current_chan(channel.name, "", channel.termination, @@ -432,7 +412,7 @@ def update_task(self, channels=[], clock_settings=ClockSettings(), trigger_setti 0., "") - elif channel.analog_type == UsageTypeAI.TEMPERATURE_THERMOCOUPLE.name: + elif channel.analog_type == UsageTypeAI.TEMPERATURE_THERMOCOUPLE: self._task.ai_channels.add_ai_thrmcpl_chan(channel.name, "", channel.value_min, @@ -478,15 +458,15 @@ def update_task(self, channels=[], clock_settings=ClockSettings(), trigger_setti if not not err_code: status = self.DAQmxGetErrorString(err_code) raise IOError(status) - elif channel.source == DAQ_NIDAQ_source.Analog_Output.name: # Analog_Output + elif channel.source == DAQ_NIDAQ_source.Analog_Output: # Analog_Output try: - if channel.analog_type == UsageTypeAI.VOLTAGE.name: + if channel.analog_type == UsageTypeAI.VOLTAGE: self._task.ao_channels.add_ao_voltage_chan(channel.name, "", channel.value_min, channel.value_max, VoltageUnits.VOLTS, None) - elif channel.analog_type == UsageTypeAI.CURRENT.name: + elif channel.analog_type == UsageTypeAI.CURRENT: self._task.ao_channels.add_ao_current_chan(channel.name, "", channel.value_min, channel.value_max, @@ -496,7 +476,7 @@ def update_task(self, channels=[], clock_settings=ClockSettings(), trigger_setti if not not err_code: status = self.DAQmxGetErrorString(err_code) raise IOError(status) - elif channel.source == DAQ_NIDAQ_source.Digital_Output.name: + elif channel.source == DAQ_NIDAQ_source.Digital_Output: try: self._task.do_channels.add_do_chan(channel.name, "", LineGrouping.CHAN_PER_LINE) @@ -505,7 +485,7 @@ def update_task(self, channels=[], clock_settings=ClockSettings(), trigger_setti if not not err_code: status = self.DAQmxGetErrorString(err_code) raise IOError(status) - elif channel.source == DAQ_NIDAQ_source.Digital_Input.name: # Digital_Input + elif channel.source == DAQ_NIDAQ_source.Digital_Input: # Digital_Input try: self._task.di_channels.add_di_chan(channel.name, "", LineGrouping.CHAN_PER_LINE) From 67ef2f827ec9abc27ad4d9639713bc7f20c9c5f4 Mon Sep 17 00:00:00 2001 From: Sebastien Date: Tue, 21 Jan 2025 16:13:13 +0100 Subject: [PATCH 045/140] unused or extra codes removed --- .../daq_move_plugins/daq_move_NIDAQmx.py | 0 .../plugins_0D/daq_0Dviewer_DAQmx_counter.py | 138 ------------------ .../counter_clock_nidaqmx.py | 42 ------ .../counter_simple_nidaqmx.py | 41 ------ 4 files changed, 221 deletions(-) delete mode 100644 src/pymodaq_plugins_daqmx/daq_move_plugins/daq_move_NIDAQmx.py delete mode 100644 src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_DAQmx_counter.py delete mode 100644 src/pymodaq_plugins_daqmx/hardware/national_instruments/counter_clock_nidaqmx.py delete mode 100644 src/pymodaq_plugins_daqmx/hardware/national_instruments/counter_simple_nidaqmx.py diff --git a/src/pymodaq_plugins_daqmx/daq_move_plugins/daq_move_NIDAQmx.py b/src/pymodaq_plugins_daqmx/daq_move_plugins/daq_move_NIDAQmx.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_DAQmx_counter.py b/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_DAQmx_counter.py deleted file mode 100644 index 7add9e6..0000000 --- a/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_DAQmx_counter.py +++ /dev/null @@ -1,138 +0,0 @@ -import numpy as np -from pymodaq.utils.daq_utils import ThreadCommand -from pymodaq.utils.data import DataWithAxes, DataToExport, DataSource -from pymodaq.control_modules.viewer_utility_classes import DAQ_Viewer_base, comon_parameters, main -from pymodaq.utils.parameter import Parameter -import nidaqmx - -from pymodaq_plugins_daqmx.hardware.national_instruments.daqmxni import DAQmx, Edge, Counter - - -class DAQ_0DViewer_DAQmx_counter(DAQ_Viewer_base): - """ - Plugin for a 0D PL counter, based on a NI card. - """ - params = comon_parameters+[ - {"title": "Counting channel:", "name": "counter_channel", - "type": "list", "limits": DAQmx.get_NIDAQ_channels(source_type="Counter")}, - {"title": "Acq. Time (s):", "name": "acq_time", - "type": "float", "value": 1., "default": 1.}, - ] - - def ini_attributes(self): - self.controller = None - self.clock_channel = None - self.counter_channel = None - self.live = False # True during a continuous grab - self.counting_time = 0.1 - - 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() == "acq_time": - self.counting_time = param.value() - self.update_tasks() - - def ini_detector(self, controller=None): - """Detector communication initialization - - Parameters - ---------- - controller: (object) - custom object of a PyMoDAQ plugin (Slave case). None if only one actuator/detector by controller - (Master case) - - Returns - ------- - info: str - initialized: bool - False if initialization failed otherwise True - """ - self.controller = DAQmx() #{"clock": DAQmx(), "counter": DAQmx()} - try: - self.update_tasks() - initialized = True - info = "NI card based counter" - self.counting_time = self.settings.child("acq_time").value() - except Exception as e: - print(e) - initialized = False - info = "Error" - - return info, initialized - - def close(self): - """Terminate the communication protocol""" - pass - #self.controller["clock"].close() - #self.controller["counter"].close() - - def grab_data(self, Naverage=1, **kwargs): - """Start a grab from the detector - - Parameters - ---------- - Naverage: int - Number of hardware averaging not relevant here. - kwargs: dict - others optionals arguments - """ - #update = False # to decide if we do the initial set up or not - - - #if 'live' in kwargs: - # if kwargs['live'] != self.live: - # update = True - # self.live = kwargs['live'] - # """ - #if 'live' in kwargs: - # if kwargs['live'] == self.live: - # update = False # we are already live - # self.live = kwargs['live'] - # """ - - #if update: - # self.update_tasks() - # self.controller["clock"].start() - - read_data = 32#self.controller.readCounter()#, counting_time=self.counting_time) - data = read_data #/self.counting_time # convert to cts/s - self.dte_signal.emit(DataToExport(name='Counts', - data=[DataWithAxes(name='Counts', data=[np.array([data])], - source=DataSource['raw'], - dim='Data0D', labels=['Counts (Hz)'])])) - - def stop(self): - """Stop the current grab hardware wise if necessary""" - self.task.stop() - self.close() - self.emit_status(ThreadCommand('Update_Status', ['Acquisition stopped.'])) - return '' - - def update_tasks(self): - """Set up the counting tasks in the NI card.""" - # Create channels - - self.counter_channel = Counter(name=self.settings.child("counter_channel").value(), - source="Counter", edge=Edge.names()[0]) - # - #self.controller["clock"].update_task(channels=[self.clock_channel], - # clock_settings=ClockSettings(), - # trigger_settings=TriggerSettings()) - #self.controller["clock"].task.CfgImplicitTiming(DAQmx_Val_ContSamps, 1) - # - #self.controller["counter"].update_task(channels=[self.counter_channel], - # clock_settings=ClockSettings(), - # trigger_settings=TriggerSettings()) - - # connect the clock to the counter - #self.controller["counter"].task.SetSampClkSrc("/" + self.clock_channel.name + "InternalOutput") - - -if __name__ == '__main__': - main(__file__) diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/counter_clock_nidaqmx.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/counter_clock_nidaqmx.py deleted file mode 100644 index 2f5577f..0000000 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/counter_clock_nidaqmx.py +++ /dev/null @@ -1,42 +0,0 @@ -import nidaqmx -import numpy as np -from nidaqmx.constants import Edge, AcquisitionType, FillMode, LineGrouping, CountDirection -from nidaqmx.stream_readers import CounterReader -from ctypes import c_uint32 - -def main(): - error = 0 - task_handle = nidaqmx.Task() - data = np.zeros(0, dtype=np.uint32) - read = np.uint32() - - try: - # DAQmx Configure Code - task_handle.ci_channels.add_ci_count_edges_chan("Dev1/ctr0", edge=Edge.RISING, initial_count=0, count_direction=CountDirection.COUNT_UP) - task_handle.timing.cfg_samp_clk_timing(source="/Dev1/Ctr1InternalOutput", rate=1000.0, active_edge=Edge.RISING, sample_mode=AcquisitionType.CONTINUOUS, samps_per_chan=1000) - - counter_reader=CounterReader(task_handle.in_stream) - # DAQmx Start Code - task_handle.start() - - print("En cours de lecture en continu. Appuyez sur Ctrl+C pour interrompre\n") - while True: - # DAQmx Read Code - #data=task_handle.read(number_of_samples_per_channel=1000, timeout=10.0) - #task_handle.in_stream.read_all_avail_samp - read=counter_reader.read_many_sample_uint32(data) - print("\rAcquis {} échantillons".format(read), end='', flush=True) - - except Exception as e: - print("\n") - if task_handle.is_task_done() == 0: - task_handle.stop() - task_handle.close() - print("Erreur DAQmx : {}".format(str(e))) - - finally: - print("\nFin du programme. Appuyez sur la touche Entrée pour quitter\n") - input() - -if __name__ == "__main__": - main() diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/counter_simple_nidaqmx.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/counter_simple_nidaqmx.py deleted file mode 100644 index d6ceedc..0000000 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/counter_simple_nidaqmx.py +++ /dev/null @@ -1,41 +0,0 @@ -import nidaqmx -import numpy as np -import time - -# Constantes équivalentes aux macros en C -DAQmx_Val_Rising = nidaqmx.constants.Edge.RISING -DAQmx_Val_CountUp = nidaqmx.constants.CountDirection.COUNT_UP - -def main(): - error = 0 - task_handle = nidaqmx.Task() - data = np.zeros(1, dtype=np.uint32) - - try: - # DAQmx Configure Code - task_handle.ci_channels.add_ci_count_edges_chan("Dev1/ctr0", edge=DAQmx_Val_Rising, initial_count=0, count_direction=DAQmx_Val_CountUp) - - # DAQmx Start Code - task_handle.start() - - print("En cours de lecture en continu. Appuyez sur Ctrl+C pour interrompre\n") - while True: - # DAQmx Read Code - data=task_handle.read(number_of_samples_per_channel=1) - - print("\rCompteur : {}".format(data[0]), end='', flush=True) - time.sleep(0.1) # Intervalle pour éviter une utilisation excessive du processeur - - except Exception as e: - print("\n") - if task_handle.is_task_done() == 0: - task_handle.stop() - task_handle.close() - print("Erreur DAQmx : {}".format(str(e))) - - finally: - print("\nFin du programme. Appuyez sur la touche Entrée pour quitter\n") - input() - -if __name__ == "__main__": - main() From 6a03efc638fe4b5f9af9e43b303b7ca20e681950 Mon Sep 17 00:00:00 2001 From: Sebastien Date: Tue, 21 Jan 2025 16:16:22 +0100 Subject: [PATCH 046/140] PyDAQmx based-on Analog Input codes deprecated (use daq_0Dviewer_NIDAQmx.py instead) --- .../plugins_0D/daq_0Dviewer_DAQmxAI.py | 168 --------------- .../plugins_0D/daq_0Dviewer_DAQmxDualAI.py | 201 ------------------ 2 files changed, 369 deletions(-) delete mode 100644 src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_DAQmxAI.py delete mode 100644 src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_DAQmxDualAI.py diff --git a/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_DAQmxAI.py b/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_DAQmxAI.py deleted file mode 100644 index 35383e2..0000000 --- a/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_DAQmxAI.py +++ /dev/null @@ -1,168 +0,0 @@ -import numpy as np -from easydict import EasyDict as edict -from pymodaq.utils.daq_utils import ThreadCommand, getLineInfo -from pymodaq.utils.data import DataFromPlugins, Axis -from pymodaq.utils.logger import set_logger, get_module_name -from pymodaq.control_modules.viewer_utility_classes import DAQ_Viewer_base, comon_parameters, main - -from pymodaq_plugins_daqmx.hardware.national_instruments.daqmx import DAQmx, DAQ_analog_types, DAQ_thermocouples,\ - DAQ_termination, Edge, DAQ_NIDAQ_source, \ - ClockSettings, AIChannel, Counter, AIThermoChannel, AOChannel, TriggerSettings, DOChannel, DIChannel - -logger = set_logger(get_module_name(__file__)) - - -class DAQ_0DViewer_DAQmxAI(DAQ_Viewer_base): - """Specific DAQmx plugin for getting analog input data as 0D or 1D data - - """ - params = comon_parameters+[ - {'title': 'Display type:', 'name': 'display', 'type': 'list', 'limits': ['0D', '1D']}, - {'title': 'Frequency Acq.:', 'name': 'frequency', 'type': 'int', 'value': 1000, 'min': 1}, - {'title': 'Nsamples:', 'name': 'Nsamples', 'type': 'int', 'value': 100, 'default': 100, 'min': 1}, - {'title': 'AI:', 'name': 'ai_channel', 'type': 'list', - 'limits': DAQmx.get_NIDAQ_channels(source_type='Analog_Input'), - 'value': DAQmx.get_NIDAQ_channels(source_type='Analog_Input')[0]}, - ] - hardware_averaging = True - live_mode_available = True - - def __init__(self, parent=None, params_state=None): - super().__init__(parent, params_state) - - self.channels_ai = None - self.clock_settings = None - self.data_tot = None - self.live = False - self.Naverage = 1 - self.ind_average = 0 - - def commit_settings(self, param): - """ - """ - - self.update_tasks() - - def ini_detector(self, controller=None): - """Detector communication initialization - - Parameters - ---------- - controller: (object) custom object of a PyMoDAQ plugin (Slave case). None if only one detector by controller (Master case) - - Returns - ------- - self.status (edict): with initialization status: three fields: - * info (str) - * controller (object) initialized controller - *initialized: (bool): False if initialization failed otherwise True - """ - self.controller = self.ini_detector_init(controller, dict(ai=DAQmx())) - - self.update_tasks() - - info = "Current measurement ready" - initialized = True - - return info, initialized - - def update_tasks(self): - - self.channels_ai = [AIChannel(name=self.settings.child('ai_channel').value(), - source='Analog_Input', analog_type='Voltage', - value_min=-10., value_max=10., termination='Diff', ), - ] - - self.clock_settings_ai = ClockSettings(frequency=self.settings.child('frequency').value(), - Nsamples=self.settings.child('Nsamples').value(), repetition=self.live) - - self.controller['ai'].update_task(self.channels_ai, self.clock_settings_ai) - - def close(self): - """ - Terminate the communication protocol - """ - pass - ## - - def grab_data(self, Naverage=1, **kwargs): - """ - - Parameters - ---------- - Naverage: (int) Number of hardware averaging - kwargs: (dict) of others optionals arguments - """ - - - update = False - - if 'live' in kwargs: - if kwargs['live'] != self.live: - update = True - self.live = kwargs['live'] - - if Naverage != self.Naverage: - self.Naverage = Naverage - update = True - - if update: - self.update_tasks() - - self.ind_average = 0 - self.data_tot = np.zeros((len(self.channels_ai), self.clock_settings_ai.Nsamples)) - - while not self.controller['ai'].isTaskDone(): - self.stop() - if self.controller['ai'].c_callback is None: - self.controller['ai'].register_callback(self.read_data, 'Nsamples', self.clock_settings_ai.Nsamples) - self.controller['ai'].task.StartTask() - #self.read_data(None, None, None, None) - - def read_data(self, taskhandle, status, samples=0, callbackdata=None): - #print(f'going to read {self.clock_settings_ai.Nsamples} samples, callbakc {samples}') - data = self.controller['ai'].readAnalog(len(self.channels_ai), self.clock_settings_ai) - if not self.live: - self.stop() - self.ind_average += 1 - self.data_tot += 1 / self.Naverage * data - - if self.ind_average == self.Naverage: - self.emit_data(self.data_tot) - self.ind_average = 0 - self.data_tot = np.zeros((len(self.channels_ai), self.clock_settings_ai.Nsamples)) - - return 0 #mandatory for the PyDAQmx callback - - def emit_data(self, data): - channels_name = [ch.name for ch in self.channels_ai] - - if self.settings.child('display').value() == '0D': - data = np.mean(data, 1) - - #data = np.squeeze(data) - # print(f'shape is {data.shape}') - # print(data) - if len(self.channels_ai) == 1 and data.size == 1: - data_export = [np.array([data[0]])] - else: - data_export = [np.squeeze(data[ind, :]) for ind in range(len(self.channels_ai))] - - # print(f'data len is {len(data_export)} and shape is {data_export[0].shape}') - self.data_grabed_signal.emit([DataFromPlugins( - name='NI AI', - data=data_export, - dim=f'Data{self.settings.child("display").value()}', labels=channels_name)]) - - def stop(self): - try: - self.controller['ai'].task.StopTask() - except: - pass - ############################## - - return '' - - -if __name__ == '__main__': - main(__file__) diff --git a/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_DAQmxDualAI.py b/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_DAQmxDualAI.py deleted file mode 100644 index d66792c..0000000 --- a/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_DAQmxDualAI.py +++ /dev/null @@ -1,201 +0,0 @@ -import numpy as np -from qtpy.QtCore import QThread -from easydict import EasyDict as edict -from pymodaq.utils.daq_utils import ThreadCommand, getLineInfo -from pymodaq.utils.data import DataFromPlugins, Axis -from pymodaq.utils.logger import set_logger, get_module_name -from pymodaq.control_modules.viewer_utility_classes import DAQ_Viewer_base, comon_parameters, main - -from pymodaq_plugins_daqmx.hardware.national_instruments.daqmx import DAQmx, DAQ_analog_types, DAQ_thermocouples,\ - DAQ_termination, Edge, DAQ_NIDAQ_source, \ - ClockSettings, AIChannel, Counter, AIThermoChannel, AOChannel, TriggerSettings, DOChannel, DIChannel - -logger = set_logger(get_module_name(__file__)) - - -class DAQ_0DViewer_DAQmxDualAI(DAQ_Viewer_base): - """ - """ - params = comon_parameters+[ - {'title': 'Display type:', 'name': 'display', 'type': 'list', 'limits': ['0D', '1D']}, - {'title': 'Frequency Acq.:', 'name': 'frequency', 'type': 'int', 'value': 1000, 'min': 1}, - {'title': 'Nsamples:', 'name': 'Nsamples', 'type': 'int', 'value': 100, 'default': 100, 'min': 1}, - {'title': 'AI0:', 'name': 'ai_channel0', 'type': 'list', - 'limits': DAQmx.get_NIDAQ_channels(source_type='Analog_Input'), - 'value': DAQmx.get_NIDAQ_channels(source_type='Analog_Input')[2]}, - {'title': 'AI1:', 'name': 'ai_channel1', 'type': 'list', - 'limits': DAQmx.get_NIDAQ_channels(source_type='Analog_Input'), - 'value': DAQmx.get_NIDAQ_channels(source_type='Analog_Input')[3]}, - ] - hardware_averaging = True - live_mode_available = True - - def __init__(self, parent=None, params_state=None): - super().__init__(parent, params_state) - - self.channels_ai = None - self.clock_settings = None - self.data_tot = None - self.live = False - self.Naverage = 1 - self.ind_average = 0 - - def commit_settings(self, param): - """ - """ - - self.update_tasks() - - def ini_detector(self, controller=None): - """Detector communication initialization - - Parameters - ---------- - controller: (object) custom object of a PyMoDAQ plugin (Slave case). None if only one detector by controller (Master case) - - Returns - ------- - self.status (edict): with initialization status: three fields: - * info (str) - * controller (object) initialized controller - *initialized: (bool): False if initialization failed otherwise True - """ - - try: - self.status.update(edict(initialized=False,info="", x_axis=None,y_axis=None,controller=None)) - if self.settings.child(('controller_status')).value() == "Slave": - if controller is None: - raise Exception('no controller has been defined externally while this detector is a slave one') - else: - self.controller = controller - else: - - self.controller = dict(ai=DAQmx()) - ##################################### - - self.update_tasks() - - - self.status.info = "Current measurement ready" - self.status.initialized = True - self.status.controller = self.controller - return self.status - - except Exception as e: - self.emit_status(ThreadCommand('Update_Status', [getLineInfo() + str(e), 'log'])) - self.status.info = getLineInfo() + str(e) - self.status.initialized = False - return self.status - - def update_tasks(self): - - self.channels_ai = [AIChannel(name=self.settings.child('ai_channel0').value(), - source='Analog_Input', analog_type='Voltage', - value_min=-10., value_max=10., termination='Diff', ), - AIChannel(name=self.settings.child('ai_channel1').value(), - source='Analog_Input', analog_type='Voltage', - value_min=-10., value_max=10., termination='Diff', ), - ] - - self.clock_settings_ai = ClockSettings(frequency=self.settings.child('frequency').value(), - Nsamples=self.settings.child('Nsamples').value(), repetition=self.live) - - self.controller['ai'].update_task(self.channels_ai, self.clock_settings_ai) - - - - def close(self): - """ - Terminate the communication protocol - """ - pass - ## - - def grab_data(self, Naverage=1, **kwargs): - """ - - Parameters - ---------- - Naverage: (int) Number of hardware averaging - kwargs: (dict) of others optionals arguments - """ - - - update = False - - if 'live' in kwargs: - if kwargs['live'] != self.live: - update = True - self.live = kwargs['live'] - - if Naverage != self.Naverage: - self.Naverage = Naverage - update = True - - if update: - self.update_tasks() - - self.ind_average = 0 - self.data_tot = np.zeros((len(self.channels_ai), self.clock_settings_ai.Nsamples)) - - while not self.controller['ai'].isTaskDone(): - self.stop() - if self.controller['ai'].c_callback is None: - self.controller['ai'].register_callback(self.read_data, 'Nsamples', self.clock_settings_ai.Nsamples) - self.controller['ai'].task.StartTask() - #QThread.msleep(500) - #self.read_data(None, 0) - - def read_data(self, taskhandle, status, samples=0, callbackdata=None): - #print(f'going to read {self.clock_settings_ai.Nsamples} samples, callbakc {samples}') - data = self.controller['ai'].readAnalog(len(self.channels_ai), self.clock_settings_ai) - if not self.live: - self.stop() - self.ind_average += 1 - - self.data_tot += 1 / self.Naverage * data.reshape(len(self.channels_ai), self.clock_settings_ai.Nsamples) - - if self.ind_average == self.Naverage: - self.emit_data(self.data_tot) - self.ind_average = 0 - self.data_tot = np.zeros((len(self.channels_ai), self.clock_settings_ai.Nsamples)) - - return 0 #mandatory for the PyDAQmx callback - - def emit_data(self, data): - channels_name = [ch.name for ch in self.channels_ai] - - if self.settings.child('display').value() == '0D': - data = np.mean(data, 1) - - #data = np.squeeze(data) - # print(f'shape is {data.shape}') - # print(data) - if len(self.channels_ai) == 1 and data.size == 1: - data_export = [np.array([data[0]])] - else: - data_export = [np.squeeze(data[ind]) for ind in range(len(self.channels_ai))] - - if self.settings.child('display').value() == '0D': - datatosend = [np.array([data_export[0]]), np.array([data_export[1]])] - else: - datatosend = [d for d in data_export] - - # print(f'data len is {len(data_export)} and shape is {data_export[0].shape}') - self.data_grabed_signal.emit([DataFromPlugins( - name='NI AI', - data=datatosend, - dim=f'Data{self.settings.child("display").value()}', labels=channels_name)]) - - def stop(self): - try: - self.controller['ai'].task.StopTask() - except: - pass - ############################## - - return '' - - -if __name__ == '__main__': - main(__file__) From df14254fdf389fc298a8df2b95f272373bf40333 Mon Sep 17 00:00:00 2001 From: Sebastien Date: Tue, 21 Jan 2025 16:16:22 +0100 Subject: [PATCH 047/140] PyDAQmx based-on Analog Input codes deprecated (use daq_0Dviewer_NIDAQmx.py instead) --- .../plugins_0D/daq_0Dviewer_DAQmx.py | 26 --- .../plugins_0D/daq_0Dviewer_DAQmxAI.py | 168 --------------- .../plugins_0D/daq_0Dviewer_DAQmxDualAI.py | 201 ------------------ 3 files changed, 395 deletions(-) delete mode 100644 src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_DAQmx.py delete mode 100644 src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_DAQmxAI.py delete mode 100644 src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_DAQmxDualAI.py diff --git a/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_DAQmx.py b/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_DAQmx.py deleted file mode 100644 index e8dba9f..0000000 --- a/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_DAQmx.py +++ /dev/null @@ -1,26 +0,0 @@ -from pymodaq_plugins_daqmx.hardware.national_instruments.daq_NIDAQmx import DAQ_NIDAQmx_Viewer -from pymodaq.control_modules.viewer_utility_classes import main - - - -class DAQ_0DViewer_DAQmx(DAQ_NIDAQmx_Viewer): - """ - ==================== ======================== - **Attributes** **Type** - *data_grabed_signal* instance of Signal - *params* dictionnary list - *task* - ==================== ======================== - - See Also - -------- - refresh_hardware - """ - control_type = "0D" # could be "0D", "1D" - - def __init__(self, *args, **kwargs): - super().__init__(*args, control_type=self.control_type, **kwargs) - - -if __name__ == '__main__': - main(__file__, False) diff --git a/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_DAQmxAI.py b/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_DAQmxAI.py deleted file mode 100644 index 35383e2..0000000 --- a/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_DAQmxAI.py +++ /dev/null @@ -1,168 +0,0 @@ -import numpy as np -from easydict import EasyDict as edict -from pymodaq.utils.daq_utils import ThreadCommand, getLineInfo -from pymodaq.utils.data import DataFromPlugins, Axis -from pymodaq.utils.logger import set_logger, get_module_name -from pymodaq.control_modules.viewer_utility_classes import DAQ_Viewer_base, comon_parameters, main - -from pymodaq_plugins_daqmx.hardware.national_instruments.daqmx import DAQmx, DAQ_analog_types, DAQ_thermocouples,\ - DAQ_termination, Edge, DAQ_NIDAQ_source, \ - ClockSettings, AIChannel, Counter, AIThermoChannel, AOChannel, TriggerSettings, DOChannel, DIChannel - -logger = set_logger(get_module_name(__file__)) - - -class DAQ_0DViewer_DAQmxAI(DAQ_Viewer_base): - """Specific DAQmx plugin for getting analog input data as 0D or 1D data - - """ - params = comon_parameters+[ - {'title': 'Display type:', 'name': 'display', 'type': 'list', 'limits': ['0D', '1D']}, - {'title': 'Frequency Acq.:', 'name': 'frequency', 'type': 'int', 'value': 1000, 'min': 1}, - {'title': 'Nsamples:', 'name': 'Nsamples', 'type': 'int', 'value': 100, 'default': 100, 'min': 1}, - {'title': 'AI:', 'name': 'ai_channel', 'type': 'list', - 'limits': DAQmx.get_NIDAQ_channels(source_type='Analog_Input'), - 'value': DAQmx.get_NIDAQ_channels(source_type='Analog_Input')[0]}, - ] - hardware_averaging = True - live_mode_available = True - - def __init__(self, parent=None, params_state=None): - super().__init__(parent, params_state) - - self.channels_ai = None - self.clock_settings = None - self.data_tot = None - self.live = False - self.Naverage = 1 - self.ind_average = 0 - - def commit_settings(self, param): - """ - """ - - self.update_tasks() - - def ini_detector(self, controller=None): - """Detector communication initialization - - Parameters - ---------- - controller: (object) custom object of a PyMoDAQ plugin (Slave case). None if only one detector by controller (Master case) - - Returns - ------- - self.status (edict): with initialization status: three fields: - * info (str) - * controller (object) initialized controller - *initialized: (bool): False if initialization failed otherwise True - """ - self.controller = self.ini_detector_init(controller, dict(ai=DAQmx())) - - self.update_tasks() - - info = "Current measurement ready" - initialized = True - - return info, initialized - - def update_tasks(self): - - self.channels_ai = [AIChannel(name=self.settings.child('ai_channel').value(), - source='Analog_Input', analog_type='Voltage', - value_min=-10., value_max=10., termination='Diff', ), - ] - - self.clock_settings_ai = ClockSettings(frequency=self.settings.child('frequency').value(), - Nsamples=self.settings.child('Nsamples').value(), repetition=self.live) - - self.controller['ai'].update_task(self.channels_ai, self.clock_settings_ai) - - def close(self): - """ - Terminate the communication protocol - """ - pass - ## - - def grab_data(self, Naverage=1, **kwargs): - """ - - Parameters - ---------- - Naverage: (int) Number of hardware averaging - kwargs: (dict) of others optionals arguments - """ - - - update = False - - if 'live' in kwargs: - if kwargs['live'] != self.live: - update = True - self.live = kwargs['live'] - - if Naverage != self.Naverage: - self.Naverage = Naverage - update = True - - if update: - self.update_tasks() - - self.ind_average = 0 - self.data_tot = np.zeros((len(self.channels_ai), self.clock_settings_ai.Nsamples)) - - while not self.controller['ai'].isTaskDone(): - self.stop() - if self.controller['ai'].c_callback is None: - self.controller['ai'].register_callback(self.read_data, 'Nsamples', self.clock_settings_ai.Nsamples) - self.controller['ai'].task.StartTask() - #self.read_data(None, None, None, None) - - def read_data(self, taskhandle, status, samples=0, callbackdata=None): - #print(f'going to read {self.clock_settings_ai.Nsamples} samples, callbakc {samples}') - data = self.controller['ai'].readAnalog(len(self.channels_ai), self.clock_settings_ai) - if not self.live: - self.stop() - self.ind_average += 1 - self.data_tot += 1 / self.Naverage * data - - if self.ind_average == self.Naverage: - self.emit_data(self.data_tot) - self.ind_average = 0 - self.data_tot = np.zeros((len(self.channels_ai), self.clock_settings_ai.Nsamples)) - - return 0 #mandatory for the PyDAQmx callback - - def emit_data(self, data): - channels_name = [ch.name for ch in self.channels_ai] - - if self.settings.child('display').value() == '0D': - data = np.mean(data, 1) - - #data = np.squeeze(data) - # print(f'shape is {data.shape}') - # print(data) - if len(self.channels_ai) == 1 and data.size == 1: - data_export = [np.array([data[0]])] - else: - data_export = [np.squeeze(data[ind, :]) for ind in range(len(self.channels_ai))] - - # print(f'data len is {len(data_export)} and shape is {data_export[0].shape}') - self.data_grabed_signal.emit([DataFromPlugins( - name='NI AI', - data=data_export, - dim=f'Data{self.settings.child("display").value()}', labels=channels_name)]) - - def stop(self): - try: - self.controller['ai'].task.StopTask() - except: - pass - ############################## - - return '' - - -if __name__ == '__main__': - main(__file__) diff --git a/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_DAQmxDualAI.py b/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_DAQmxDualAI.py deleted file mode 100644 index d66792c..0000000 --- a/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_DAQmxDualAI.py +++ /dev/null @@ -1,201 +0,0 @@ -import numpy as np -from qtpy.QtCore import QThread -from easydict import EasyDict as edict -from pymodaq.utils.daq_utils import ThreadCommand, getLineInfo -from pymodaq.utils.data import DataFromPlugins, Axis -from pymodaq.utils.logger import set_logger, get_module_name -from pymodaq.control_modules.viewer_utility_classes import DAQ_Viewer_base, comon_parameters, main - -from pymodaq_plugins_daqmx.hardware.national_instruments.daqmx import DAQmx, DAQ_analog_types, DAQ_thermocouples,\ - DAQ_termination, Edge, DAQ_NIDAQ_source, \ - ClockSettings, AIChannel, Counter, AIThermoChannel, AOChannel, TriggerSettings, DOChannel, DIChannel - -logger = set_logger(get_module_name(__file__)) - - -class DAQ_0DViewer_DAQmxDualAI(DAQ_Viewer_base): - """ - """ - params = comon_parameters+[ - {'title': 'Display type:', 'name': 'display', 'type': 'list', 'limits': ['0D', '1D']}, - {'title': 'Frequency Acq.:', 'name': 'frequency', 'type': 'int', 'value': 1000, 'min': 1}, - {'title': 'Nsamples:', 'name': 'Nsamples', 'type': 'int', 'value': 100, 'default': 100, 'min': 1}, - {'title': 'AI0:', 'name': 'ai_channel0', 'type': 'list', - 'limits': DAQmx.get_NIDAQ_channels(source_type='Analog_Input'), - 'value': DAQmx.get_NIDAQ_channels(source_type='Analog_Input')[2]}, - {'title': 'AI1:', 'name': 'ai_channel1', 'type': 'list', - 'limits': DAQmx.get_NIDAQ_channels(source_type='Analog_Input'), - 'value': DAQmx.get_NIDAQ_channels(source_type='Analog_Input')[3]}, - ] - hardware_averaging = True - live_mode_available = True - - def __init__(self, parent=None, params_state=None): - super().__init__(parent, params_state) - - self.channels_ai = None - self.clock_settings = None - self.data_tot = None - self.live = False - self.Naverage = 1 - self.ind_average = 0 - - def commit_settings(self, param): - """ - """ - - self.update_tasks() - - def ini_detector(self, controller=None): - """Detector communication initialization - - Parameters - ---------- - controller: (object) custom object of a PyMoDAQ plugin (Slave case). None if only one detector by controller (Master case) - - Returns - ------- - self.status (edict): with initialization status: three fields: - * info (str) - * controller (object) initialized controller - *initialized: (bool): False if initialization failed otherwise True - """ - - try: - self.status.update(edict(initialized=False,info="", x_axis=None,y_axis=None,controller=None)) - if self.settings.child(('controller_status')).value() == "Slave": - if controller is None: - raise Exception('no controller has been defined externally while this detector is a slave one') - else: - self.controller = controller - else: - - self.controller = dict(ai=DAQmx()) - ##################################### - - self.update_tasks() - - - self.status.info = "Current measurement ready" - self.status.initialized = True - self.status.controller = self.controller - return self.status - - except Exception as e: - self.emit_status(ThreadCommand('Update_Status', [getLineInfo() + str(e), 'log'])) - self.status.info = getLineInfo() + str(e) - self.status.initialized = False - return self.status - - def update_tasks(self): - - self.channels_ai = [AIChannel(name=self.settings.child('ai_channel0').value(), - source='Analog_Input', analog_type='Voltage', - value_min=-10., value_max=10., termination='Diff', ), - AIChannel(name=self.settings.child('ai_channel1').value(), - source='Analog_Input', analog_type='Voltage', - value_min=-10., value_max=10., termination='Diff', ), - ] - - self.clock_settings_ai = ClockSettings(frequency=self.settings.child('frequency').value(), - Nsamples=self.settings.child('Nsamples').value(), repetition=self.live) - - self.controller['ai'].update_task(self.channels_ai, self.clock_settings_ai) - - - - def close(self): - """ - Terminate the communication protocol - """ - pass - ## - - def grab_data(self, Naverage=1, **kwargs): - """ - - Parameters - ---------- - Naverage: (int) Number of hardware averaging - kwargs: (dict) of others optionals arguments - """ - - - update = False - - if 'live' in kwargs: - if kwargs['live'] != self.live: - update = True - self.live = kwargs['live'] - - if Naverage != self.Naverage: - self.Naverage = Naverage - update = True - - if update: - self.update_tasks() - - self.ind_average = 0 - self.data_tot = np.zeros((len(self.channels_ai), self.clock_settings_ai.Nsamples)) - - while not self.controller['ai'].isTaskDone(): - self.stop() - if self.controller['ai'].c_callback is None: - self.controller['ai'].register_callback(self.read_data, 'Nsamples', self.clock_settings_ai.Nsamples) - self.controller['ai'].task.StartTask() - #QThread.msleep(500) - #self.read_data(None, 0) - - def read_data(self, taskhandle, status, samples=0, callbackdata=None): - #print(f'going to read {self.clock_settings_ai.Nsamples} samples, callbakc {samples}') - data = self.controller['ai'].readAnalog(len(self.channels_ai), self.clock_settings_ai) - if not self.live: - self.stop() - self.ind_average += 1 - - self.data_tot += 1 / self.Naverage * data.reshape(len(self.channels_ai), self.clock_settings_ai.Nsamples) - - if self.ind_average == self.Naverage: - self.emit_data(self.data_tot) - self.ind_average = 0 - self.data_tot = np.zeros((len(self.channels_ai), self.clock_settings_ai.Nsamples)) - - return 0 #mandatory for the PyDAQmx callback - - def emit_data(self, data): - channels_name = [ch.name for ch in self.channels_ai] - - if self.settings.child('display').value() == '0D': - data = np.mean(data, 1) - - #data = np.squeeze(data) - # print(f'shape is {data.shape}') - # print(data) - if len(self.channels_ai) == 1 and data.size == 1: - data_export = [np.array([data[0]])] - else: - data_export = [np.squeeze(data[ind]) for ind in range(len(self.channels_ai))] - - if self.settings.child('display').value() == '0D': - datatosend = [np.array([data_export[0]]), np.array([data_export[1]])] - else: - datatosend = [d for d in data_export] - - # print(f'data len is {len(data_export)} and shape is {data_export[0].shape}') - self.data_grabed_signal.emit([DataFromPlugins( - name='NI AI', - data=datatosend, - dim=f'Data{self.settings.child("display").value()}', labels=channels_name)]) - - def stop(self): - try: - self.controller['ai'].task.StopTask() - except: - pass - ############################## - - return '' - - -if __name__ == '__main__': - main(__file__) From 90941284350db028a3498db63d96af83635a2d92 Mon Sep 17 00:00:00 2001 From: Aurore Finco Date: Tue, 21 Jan 2025 18:01:30 +0100 Subject: [PATCH 048/140] Typos in daq_NIDAQmx_Move.py --- .../hardware/national_instruments/daq_NIDAQmx_Move.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx_Move.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx_Move.py index d38ca0b..396e910 100644 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx_Move.py +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx_Move.py @@ -154,7 +154,7 @@ def move_abs(self, position): Parameters ---------- - position: (flaot) value of the absolute target positioning + position: (float) value of the absolute target positioning """ position = self.check_bound(position) # if user checked bounds, the defined bounds are applied here @@ -191,7 +191,7 @@ def move_rel(self, position): Parameters ---------- - position: (flaot) value of the relative target positioning + position: (float) value of the relative target positioning """ position = self.check_bound(self.current_position + position) - self.current_position From f721cfd53ce989bd502d3fbad58ee23a95670574 Mon Sep 17 00:00:00 2001 From: Sebastien Date: Fri, 24 Jan 2025 15:44:29 +0100 Subject: [PATCH 049/140] DAQ_NIDAQ_source enum replaced by nidaqmx ChannelType enum --- .../national_instruments/daq_NIDAQmx.py | 82 +++++++++++------ .../daq_NIDAQmx_Viewer.py | 16 ++-- .../hardware/national_instruments/daqmxni.py | 91 ++++++++----------- 3 files changed, 95 insertions(+), 94 deletions(-) diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx.py index d49d032..0b8876a 100644 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx.py +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx.py @@ -3,9 +3,9 @@ from pymodaq.utils.logger import set_logger, get_module_name from pymodaq.utils.parameter import Parameter from pymodaq.utils.parameter.pymodaq_ptypes import registerParameterType, GroupParameter -from pymodaq_plugins_daqmx.hardware.national_instruments.daqmxni import DAQmx, Edge, DAQ_NIDAQ_source, ClockSettings, \ - AIChannel, Counter, AIThermoChannel, AOChannel, TriggerSettings, DOChannel, DIChannel, UsageTypeAI, UsageTypeAO, \ - ThermocoupleType, TerminalConfiguration +from pymodaq_plugins_daqmx.hardware.national_instruments.daqmxni import DAQmx, Edge, ChannelType, ClockSettings, \ + AIChannel, AIThermoChannel, AOChannel, CIChannel, COChannel, DOChannel, DIChannel, UsageTypeAI, UsageTypeAO, \ + ThermocoupleType, TerminalConfiguration, TriggerSettings logger = set_logger(get_module_name(__file__)) @@ -254,7 +254,7 @@ class DAQ_NIDAQmx_base: param_instance = DAQmx() params = [{'title': 'Refresh hardware:', 'name': 'refresh_hardware', 'type': 'bool', 'value': False}, {'title': 'Signal type:', 'name': 'NIDAQ_type', 'type': 'list', - 'limits': [Ds.name for Ds in DAQ_NIDAQ_source]}, + 'limits': [Ds.name for Ds in ChannelType]}, {'title': 'NSamples To Read', 'name': 'nsamplestoread', 'type': 'int', 'value': 1000, 'default': 1000, 'min': 1}, {'title': 'AO Settings:', 'name': 'ao_settings', 'type': 'group', 'children': [ @@ -277,18 +277,20 @@ class DAQ_NIDAQmx_base: ] }, {'title': 'AI Channels:', 'name': 'ai_channels', 'type': 'groupai', - 'limits': DAQmx.get_NIDAQ_channels(source_type=DAQ_NIDAQ_source.Analog_Input)}, + 'limits': DAQmx.get_NIDAQ_channels(source_type=ChannelType.ANALOG_INPUT)}, {'title': 'AO Channels:', 'name': 'ao_channels', 'type': 'groupao', - 'limits': DAQmx.get_NIDAQ_channels(source_type=DAQ_NIDAQ_source.Analog_Output)}, + 'limits': DAQmx.get_NIDAQ_channels(source_type=ChannelType.ANALOG_OUTPUT)}, {'title': 'DO Channels:', 'name': 'do_channels', 'type': 'groupdo', - 'limits': DAQmx.get_NIDAQ_channels(source_type=DAQ_NIDAQ_source.Digital_Output)}, + 'limits': DAQmx.get_NIDAQ_channels(source_type=ChannelType.DIGITAL_OUTPUT)}, {'title': 'DI Channels:', 'name': 'di_channels', 'type': 'groupdi', - 'limits': DAQmx.get_NIDAQ_channels(source_type=DAQ_NIDAQ_source.Digital_Input)}, + 'limits': DAQmx.get_NIDAQ_channels(source_type=ChannelType.DIGITAL_INPUT)}, {'title': 'Counter Settings:', 'name': 'counter_settings', 'type': 'group', 'visible': True, 'children': [ {'title': 'Counting time (ms):', 'name': 'counting_time', 'type': 'float', 'value': 100., 'default': 100., 'min': 0.}, - {'title': 'Counting Channels:', 'name': 'counter_channels', 'type': 'groupcounter', - 'limits': DAQmx.get_NIDAQ_channels(source_type=DAQ_NIDAQ_source.Counter)}, + {'title': 'CI Channels:', 'name': 'ci_channels', 'type': 'groupcounter', + 'limits': DAQmx.get_NIDAQ_channels(source_type=ChannelType.COUNTER_INPUT)}, + {'title': 'CO Channels:', 'name': 'co_channels', 'type': 'groupcounter', + 'limits': DAQmx.get_NIDAQ_channels(source_type=ChannelType.COUNTER_OUTPUT)}, ]}, {'title': 'Trigger Settings:', 'name': 'trigger_settings', 'type': 'group', 'visible': True, 'children': [ {'title': 'Enable?:', 'name': 'enable', 'type': 'bool', 'value': False, }, @@ -327,7 +329,7 @@ def commit_settings(self, param: Parameter): if param.name() == 'NIDAQ_type': self.controller.update_NIDAQ_channels(param.value()) - if param.value() == DAQ_NIDAQ_source.Analog_Input.name: # analog input + if param.value() == ChannelType.ANALOG_INPUT.name: # analog input self.settings.child('clock_settings').show() self.settings.child('ai_channels').show() self.settings.child('ao_channels').hide() @@ -336,7 +338,7 @@ def commit_settings(self, param: Parameter): self.settings.child('do_channels').hide() self.settings.child('di_channels').hide() - elif param.value() == DAQ_NIDAQ_source.Analog_Output.name: # analog output + elif param.value() == ChannelType.ANALOG_OUTPUT.name: # analog output self.settings.child('clock_settings').show() self.settings.child('ai_channels').hide() self.settings.child('ao_channels').show() @@ -345,16 +347,25 @@ def commit_settings(self, param: Parameter): self.settings.child('do_channels').hide() self.settings.child('di_channels').hide() - elif param.value() == DAQ_NIDAQ_source.Counter.name: # counter input + elif param.value() == ChannelType.COUNTER_INPUT.name: # counter input self.settings.child('clock_settings').hide() self.settings.child('ai_channels').hide() self.settings.child('ao_channels').hide() self.settings.child('ao_settings').hide() - self.settings.child('counter_settings').show() + self.settings.child('counter_settings', 'ci_channels').show() self.settings.child('do_channels').hide() self.settings.child('di_channels').hide() - elif param.value() == DAQ_NIDAQ_source.Digital_Input.name: # Digital_Input + elif param.value() == ChannelType.COUNTER_OUTPUT.name: # counter output + self.settings.child('clock_settings').hide() + self.settings.child('ai_channels').hide() + self.settings.child('ao_channels').hide() + self.settings.child('ao_settings').hide() + self.settings.child('counter_settings', 'co_channels').show() + self.settings.child('do_channels').hide() + self.settings.child('di_channels').hide() + + elif param.value() == ChannelType.DIGITAL_INPUT.name: # Digital_Input self.settings.child('clock_settings').show() self.settings.child('ai_channels').hide() self.settings.child('ao_channels').show() @@ -363,7 +374,7 @@ def commit_settings(self, param: Parameter): self.settings.child('do_channels').hide() self.settings.child('di_channels').show() - elif param.value() == DAQ_NIDAQ_source.Digital_Output.name: # digital output + elif param.value() == ChannelType.DIGITAL_OUTPUT.name: # digital output self.settings.child('clock_settings').show() self.settings.child('ai_channels').hide() self.settings.child('ao_channels').hide() @@ -406,52 +417,63 @@ def update_task(self): def get_channels_from_settings(self): channels = [] - if self.settings['NIDAQ_type'] == DAQ_NIDAQ_source.Analog_Input.name: # analog input + if self.settings['NIDAQ_type'] == ChannelType.ANALOG_INPUT.name: # analog input + source = ChannelType.ANALOG_INPUT for channel in self.settings.child('ai_channels').children(): analog_type = UsageTypeAI[channel['ai_type']] if analog_type == UsageTypeAI.VOLTAGE: channels.append(AIChannel(name=channel.opts['title'], - source=DAQ_NIDAQ_source.Analog_Input, analog_type=analog_type, + source=source, analog_type=analog_type, value_min=channel['voltage_settings', 'volt_min'], value_max=channel['voltage_settings', 'volt_max'], termination=TerminalConfiguration[channel['termination']], )) elif analog_type == UsageTypeAI.CURRENT: channels.append(AIChannel(name=channel.opts['title'], - source=DAQ_NIDAQ_source.Analog_Input, analog_type=analog_type, + source=source, analog_type=analog_type, value_min=channel['current_settings', 'curr_min'], value_max=channel['current_settings', 'curr_max'], termination=TerminalConfiguration[channel['termination']], )) elif analog_type == UsageTypeAI.TEMPERATURE_THERMOCOUPLE: channels.append(AIThermoChannel(name=channel.opts['title'], - source=DAQ_NIDAQ_source.Analog_Input, analog_type=analog_type, + source=source, analog_type=analog_type, value_min=channel['thermoc_settings', 'T_min'], value_max=channel['thermoc_settings', 'T_max'], thermo_type=ThermocoupleType[ channel['thermoc_settings', 'thermoc_type']], )) - elif self.settings['NIDAQ_type'] == DAQ_NIDAQ_source.Analog_Output.name: # analog output + elif self.settings['NIDAQ_type'] == ChannelType.ANALOG_OUTPUT.name: # analog output + source = ChannelType.ANALOG_OUTPUT for channel in self.settings.child('ao_channels').children(): analog_type = UsageTypeAO[channel['ao_type']] channels.append(AOChannel(name=channel.opts['title'], - source=DAQ_NIDAQ_source.Analog_Output, analog_type=analog_type, + source=source, analog_type=analog_type, value_min=channel['voltage_settings', 'volt_min'], value_max=channel['voltage_settings', 'volt_max'], )) - elif self.settings['NIDAQ_type'] == DAQ_NIDAQ_source.Counter.name: # counter input - for channel in self.settings.child('counter_settings', 'counter_channels').children(): - channels.append(Counter(name=channel.opts['title'], - source=DAQ_NIDAQ_source.Counter, edge=Edge[channel['edge']])) + elif self.settings['NIDAQ_type'] == ChannelType.COUNTER_INPUT.name: # counter input + source = ChannelType.COUNTER_INPUT + for channel in self.settings.child('counter_settings', 'ci_channels').children(): + channels.append(CIChannel(name=channel.opts['title'], + source=source, edge=Edge[channel['edge']])) + + elif self.settings['NIDAQ_type'] == ChannelType.COUNTER_OUTPUT.name: # counter output + source = ChannelType.COUNTER_OUTPUT + for channel in self.settings.child('counter_settings', 'co_channels').children(): + channels.append(COChannel(name=channel.opts['title'], + source=source, edge=Edge[channel['edge']])) - elif self.settings['NIDAQ_type'] == DAQ_NIDAQ_source.Digital_Input.name: # digital input + elif self.settings['NIDAQ_type'] == ChannelType.DIGITAL_INPUT.name: # digital input + source = ChannelType.DIGITAL_INPUT for channel in self.settings.child('di_channels').children(): channels.append(DIChannel(name=channel.opts['title'], - source=DAQ_NIDAQ_source.Digital_Input)) + source=source)) - elif self.settings['NIDAQ_type'] == DAQ_NIDAQ_source.Digital_Output.name: # Digital output + elif self.settings['NIDAQ_type'] == ChannelType.DIGITAL_OUTPUT.name: # Digital output + source = ChannelType.DIGITAL_OUTPUT for channel in self.settings.child('do_channels').children(): channels.append(DOChannel(name=channel.opts['title'], - source=DAQ_NIDAQ_source.Digital_Output)) + source=source)) channels = [ch for ch in channels if self.settings.child("devices").value() in ch.name] return channels diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx_Viewer.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx_Viewer.py index 1cebbdf..238051d 100644 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx_Viewer.py +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx_Viewer.py @@ -4,7 +4,7 @@ from qtpy import QtCore from .daqmxni import DAQmx from pymodaq_plugins_daqmx.hardware.national_instruments.daq_NIDAQmx import DAQ_NIDAQmx_base, TerminalConfiguration, \ - UsageTypeAI, DAQ_NIDAQ_source + UsageTypeAI, ChannelType from pymodaq.control_modules.viewer_utility_classes import DAQ_Viewer_base, comon_parameters as viewer_params from pymodaq.utils.daq_utils import ThreadCommand from pymodaq.utils.data import DataFromPlugins, DataToExport @@ -38,11 +38,13 @@ def __init__(self, parent=None, params_state=None, control_type="0D"): self.live = False self.control_type = control_type # could be "0D", "1D" or "Actuator" if self.control_type == "0D": - self.settings.child('NIDAQ_type').setLimits(DAQ_NIDAQ_source.sources0D()) # analog input and counter + self.settings.child('NIDAQ_type').setLimits([ChannelType.ANALOG_INPUT.name, + ChannelType.COUNTER_INPUT.name, + ChannelType.DIGITAL_INPUT.name]) # analog & digital input + counter elif self.control_type == "1D": - self.settings.child('NIDAQ_type').setLimits(DAQ_NIDAQ_source.sources1D()) + self.settings.child('NIDAQ_type').setLimits(ChannelType.ANALOG_INPUT.name) elif self.control_type == "Actuator": - self.settings.child('NIDAQ_type').setLimits(DAQ_NIDAQ_source.Actuator()) + self.settings.child('NIDAQ_type').setLimits(ChannelType.ANALOG_OUTPUT.name, ChannelType.COUNTER_OUTPUT.name) self.settings.child('ao_settings').hide() self.settings.child('ao_channels').hide() @@ -74,7 +76,7 @@ def commit_settings(self, param): See Also -------- - update_NIDAQ_channels, update_task, DAQ_NIDAQ_source, refresh_hardware + update_NIDAQ_channels, update_task, refresh_hardware """ if param.parent() is not None: @@ -167,10 +169,6 @@ def grab_data(self, Naverage=1, **kwargs): **Parameters** **Type** **Description** *Naverage* int Number of values to average =============== ======== =============================================== - - See Also - -------- - DAQ_NIDAQ_source """ update = False diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py index 59a15f8..53ca05a 100644 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py @@ -6,7 +6,8 @@ from nidaqmx.constants import AcquisitionType, VoltageUnits, CurrentUnits, CurrentShuntResistorLocation, \ TemperatureUnits, CJCSource, CountDirection, Level, FrequencyUnits, TimeUnits, \ - LineGrouping, UsageTypeAI, UsageTypeAO, Edge, TerminalConfiguration, ThermocoupleType + LineGrouping, UsageTypeAI, UsageTypeAO, UsageTypeCI, UsageTypeCO, Edge, \ + TerminalConfiguration, ThermocoupleType, ChannelType from nidaqmx.system import System as niSystem from nidaqmx.system.device import Device as niDevice @@ -18,36 +19,6 @@ logger = set_logger(get_module_name(__file__)) -class DAQ_NIDAQ_source(Enum): - """ - Enum class of NIDAQ_source - - =============== ========== - **Attributes** **Type** - *Analog_Input* int - *Counter* int - =============== ========== - """ - Analog_Input = 0 - Analog_Output = 1 - Counter = 2 - Digital_Input = 3 - Digital_Output = 4 - Terminals = 5 - - @classmethod - def sources0D(self): - return [self.Analog_Input.name, DAQ_NIDAQ_source.Counter.name, DAQ_NIDAQ_source.Digital_Input.name] - - @classmethod - def sources1D(self): - return [self.Analog_Input.name] - - @classmethod - def Actuator(self): - return [self.Analog_Output.name] - - class ClockSettingsBase: def __init__(self, Nsamples=1000, repetition=False): @@ -82,14 +53,14 @@ def __init__(self, trig_source='', enable=False, edge=Edge.RISING, level=0.1): class Channel: - def __init__(self, name='', source=DAQ_NIDAQ_source.Analog_Input): + def __init__(self, name='', source=ChannelType.ANALOG_INPUT): """ Parameters ---------- """ self.name = name - assert source in DAQ_NIDAQ_source + assert source in ChannelType self.source = source @@ -127,19 +98,30 @@ def __init__(self, **kwargs): super().__init__(**kwargs) -class Counter(Channel): - def __init__(self, edge=Edge.RISING, **kwargs): +class CIChannel(Channel): + def __init__(self, edge=Edge.RISING, counter_type=UsageTypeCI.COUNT_EDGES, **kwargs): super().__init__(**kwargs) assert edge in Edge + assert counter_type in UsageTypeCI self.edge = edge - self.counter_type = "Edge Counter" + self.counter_type = counter_type -class ClockCounter(Counter): - def __init__(self, clock_frequency=100, **kwargs): +class COChannel(Channel): + def __init__(self, edge=Edge.RISING, counter_type=UsageTypeCO.PULSE_FREQUENCY, **kwargs): + super().__init__(**kwargs) + assert edge in Edge + assert counter_type in UsageTypeCO + self.edge = edge + self.counter_type = counter_type + + +class ClockCounter(COChannel): + def __init__(self, clock_frequency=100, counter_type=UsageTypeCO.PULSE_FREQUENCY, **kwargs): + super().counter_type = counter_type super().__init__(**kwargs) self.clock_frequency = clock_frequency - self.counter_type = "Clock Output" + self.counter_type = counter_type class DigitalChannel(Channel): @@ -217,7 +199,7 @@ def get_NIDAQ_channels(cls, devices=None, source_type=None): devices: list list of strings, each one being a connected device source_type: str - One of the entries of DAQ_NIDAQ_source enum + Channels possible types Returns ------- @@ -228,7 +210,7 @@ def get_NIDAQ_channels(cls, devices=None, source_type=None): devices = cls.get_NIDAQ_devices().device_names if source_type is None: - source_type = DAQ_NIDAQ_source + source_type = ChannelType if not isinstance(source_type, list): source_type = [source_type] channels_tot = [] @@ -236,19 +218,18 @@ def get_NIDAQ_channels(cls, devices=None, source_type=None): if not not devices: for device in devices: for source in source_type: - if source == DAQ_NIDAQ_source.Analog_Input: # analog input + if source == ChannelType.ANALOG_INPUT: # analog input channels = niDevice(device).ai_physical_chans.channel_names - elif source == DAQ_NIDAQ_source.Analog_Output: # analog output + elif source == ChannelType.ANALOG_OUTPUT: # analog output channels = niDevice(device).ao_physical_chans.channel_names - elif source == DAQ_NIDAQ_source.Counter: # counter + elif source == ChannelType.COUNTER_INPUT: # counter input channels = niDevice(device).ci_physical_chans.channel_names - elif source == DAQ_NIDAQ_source.Digital_Output: # digital output - channels = niDevice(device).do_lines.channel_names - elif source == DAQ_NIDAQ_source.Digital_Input: # digital iutput + elif source == ChannelType.COUNTER_OUTPUT: # counter output + channels = niDevice(device).co_physical_chans.channel_names + elif source == ChannelType.DIGITAL_INPUT: # digital input channels = niDevice(device).di_lines.channel_names - elif source == DAQ_NIDAQ_source.Terminals: # terminals - channels = niDevice(device).terminals - + elif source == ChannelType.DIGITAL_OUTPUT: # digital output + channels = niDevice(device).do_lines.channel_names if channels != ['']: channels_tot.extend(channels) @@ -298,7 +279,7 @@ def configuration_sequence(self, viewer, current_device): ai = config["NIDAQ_Devices", dev, mod, src] for ch in ai.keys(): name = module_name + "/" + str(ch) - source = DAQ_NIDAQ_source[ai[ch].get("source")] + source = ChannelType[ai[ch].get("source")] analog_type = UsageTypeAI[ai[ch].get("analog_type")] if analog_type == UsageTypeAI.VOLTAGE: term = TerminalConfiguration[ai[ch].get("termination")] @@ -390,7 +371,7 @@ def update_task(self, channels=[], clock_settings=ClockSettings(), trigger_setti # create all channels one task for one type of channels for channel in channels: - if channel.source == DAQ_NIDAQ_source.Analog_Input: # analog input + if channel.source == ChannelType.ANALOG_INPUT: # analog input try: if channel.analog_type == UsageTypeAI.VOLTAGE: self._task.ai_channels.add_ai_voltage_chan(channel.name, @@ -458,7 +439,7 @@ def update_task(self, channels=[], clock_settings=ClockSettings(), trigger_setti if not not err_code: status = self.DAQmxGetErrorString(err_code) raise IOError(status) - elif channel.source == DAQ_NIDAQ_source.Analog_Output: # Analog_Output + elif channel.source == ChannelType.ANALOG_OUTPUT: # Analog_Output try: if channel.analog_type == UsageTypeAI.VOLTAGE: self._task.ao_channels.add_ao_voltage_chan(channel.name, "", @@ -476,7 +457,7 @@ def update_task(self, channels=[], clock_settings=ClockSettings(), trigger_setti if not not err_code: status = self.DAQmxGetErrorString(err_code) raise IOError(status) - elif channel.source == DAQ_NIDAQ_source.Digital_Output: + elif channel.source == ChannelType.DIGITAL_OUTPUT: try: self._task.do_channels.add_do_chan(channel.name, "", LineGrouping.CHAN_PER_LINE) @@ -485,7 +466,7 @@ def update_task(self, channels=[], clock_settings=ClockSettings(), trigger_setti if not not err_code: status = self.DAQmxGetErrorString(err_code) raise IOError(status) - elif channel.source == DAQ_NIDAQ_source.Digital_Input: # Digital_Input + elif channel.source == ChannelType.DIGITAL_INPUT: # Digital_Input try: self._task.di_channels.add_di_chan(channel.name, "", LineGrouping.CHAN_PER_LINE) From a2d9312ee6cde23192002b3b76c816bf4227672f Mon Sep 17 00:00:00 2001 From: Sebastien Date: Fri, 24 Jan 2025 15:45:37 +0100 Subject: [PATCH 050/140] README update --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 6839891..3187a00 100644 --- a/README.rst +++ b/README.rst @@ -41,6 +41,6 @@ Viewer0D ++++++++ * **DAQmx_PLcounter**: Single photon counting -* **NIDAQmx: Analog Input (current-voltage-temperature), working with cDAQ & DAQ-USB +* **NIDAQmx: For now (01/2025) Only Analog Input measurements (current-voltage-temperature), working with cDAQ & DAQ-USB From 92e97449bc365b6af2a4ecac598b53357979a2bd Mon Sep 17 00:00:00 2001 From: Sebastien Date: Mon, 27 Jan 2025 10:19:06 +0100 Subject: [PATCH 051/140] Renaming DAQmx object as NIDAQmx one --- .../plugins_0D/daq_0Dviewer_NIDAQmx.py | 6 +++--- .../national_instruments/daq_NIDAQmx.py | 18 +++++++++--------- .../national_instruments/daq_NIDAQmx_Viewer.py | 4 ++-- .../hardware/national_instruments/daqmxni.py | 2 +- 4 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_NIDAQmx.py b/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_NIDAQmx.py index 575c76b..df9ae89 100644 --- a/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_NIDAQmx.py +++ b/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_NIDAQmx.py @@ -1,7 +1,7 @@ from pymodaq.control_modules.viewer_utility_classes import main from pymodaq.control_modules.viewer_utility_classes import comon_parameters as viewer_params from pymodaq_plugins_daqmx import config -from pymodaq_plugins_daqmx.hardware.national_instruments.daqmxni import AIChannel, AIThermoChannel, DAQmx, niTask, \ +from pymodaq_plugins_daqmx.hardware.national_instruments.daqmxni import AIChannel, AIThermoChannel, NIDAQmx, niTask, \ niDevice, TemperatureUnits, CJCSource, VoltageUnits, AcquisitionType from pymodaq_plugins_daqmx.hardware.national_instruments.daqmxni import UsageTypeAI, ThermocoupleType, \ TerminalConfiguration, Edge @@ -19,14 +19,14 @@ class DAQ_0DViewer_NIDAQmx(DAQ_NIDAQmx_Viewer): config_channels: list channels_ai: list config: config # todo review Useful/Unused - controller: DAQmx + controller: NIDAQmx config_devices: list config_modules: list current_device: niDevice live: bool Naverage: int - param_devices = DAQmx.get_NIDAQ_devices().device_names + param_devices = NIDAQmx.get_NIDAQ_devices().device_names params = viewer_params + [ {'title': 'Display type:', 'name': 'display', 'type': 'list', 'limits': ['0D', '1D']}, {'title': 'Devices :', 'name': 'devices', 'type': 'list', 'limits': param_devices, diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx.py index 0b8876a..7218073 100644 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx.py +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx.py @@ -3,7 +3,7 @@ from pymodaq.utils.logger import set_logger, get_module_name from pymodaq.utils.parameter import Parameter from pymodaq.utils.parameter.pymodaq_ptypes import registerParameterType, GroupParameter -from pymodaq_plugins_daqmx.hardware.national_instruments.daqmxni import DAQmx, Edge, ChannelType, ClockSettings, \ +from pymodaq_plugins_daqmx.hardware.national_instruments.daqmxni import NIDAQmx, Edge, ChannelType, ClockSettings, \ AIChannel, AIThermoChannel, AOChannel, CIChannel, COChannel, DOChannel, DIChannel, UsageTypeAI, UsageTypeAO, \ ThermocoupleType, TerminalConfiguration, TriggerSettings @@ -251,7 +251,7 @@ class DAQ_NIDAQmx_base: Base NIDAQmx class for using DAQmx objects from daqmxni.py in the DAQ_NIDAQmx_Move & DAQ_NIDAQmx_Viewer """ data_grabed_signal = Signal(list) - param_instance = DAQmx() + param_instance = NIDAQmx() params = [{'title': 'Refresh hardware:', 'name': 'refresh_hardware', 'type': 'bool', 'value': False}, {'title': 'Signal type:', 'name': 'NIDAQ_type', 'type': 'list', 'limits': [Ds.name for Ds in ChannelType]}, @@ -277,25 +277,25 @@ class DAQ_NIDAQmx_base: ] }, {'title': 'AI Channels:', 'name': 'ai_channels', 'type': 'groupai', - 'limits': DAQmx.get_NIDAQ_channels(source_type=ChannelType.ANALOG_INPUT)}, + 'limits': NIDAQmx.get_NIDAQ_channels(source_type=ChannelType.ANALOG_INPUT)}, {'title': 'AO Channels:', 'name': 'ao_channels', 'type': 'groupao', - 'limits': DAQmx.get_NIDAQ_channels(source_type=ChannelType.ANALOG_OUTPUT)}, + 'limits': NIDAQmx.get_NIDAQ_channels(source_type=ChannelType.ANALOG_OUTPUT)}, {'title': 'DO Channels:', 'name': 'do_channels', 'type': 'groupdo', - 'limits': DAQmx.get_NIDAQ_channels(source_type=ChannelType.DIGITAL_OUTPUT)}, + 'limits': NIDAQmx.get_NIDAQ_channels(source_type=ChannelType.DIGITAL_OUTPUT)}, {'title': 'DI Channels:', 'name': 'di_channels', 'type': 'groupdi', - 'limits': DAQmx.get_NIDAQ_channels(source_type=ChannelType.DIGITAL_INPUT)}, + 'limits': NIDAQmx.get_NIDAQ_channels(source_type=ChannelType.DIGITAL_INPUT)}, {'title': 'Counter Settings:', 'name': 'counter_settings', 'type': 'group', 'visible': True, 'children': [ {'title': 'Counting time (ms):', 'name': 'counting_time', 'type': 'float', 'value': 100., 'default': 100., 'min': 0.}, {'title': 'CI Channels:', 'name': 'ci_channels', 'type': 'groupcounter', - 'limits': DAQmx.get_NIDAQ_channels(source_type=ChannelType.COUNTER_INPUT)}, + 'limits': NIDAQmx.get_NIDAQ_channels(source_type=ChannelType.COUNTER_INPUT)}, {'title': 'CO Channels:', 'name': 'co_channels', 'type': 'groupcounter', - 'limits': DAQmx.get_NIDAQ_channels(source_type=ChannelType.COUNTER_OUTPUT)}, + 'limits': NIDAQmx.get_NIDAQ_channels(source_type=ChannelType.COUNTER_OUTPUT)}, ]}, {'title': 'Trigger Settings:', 'name': 'trigger_settings', 'type': 'group', 'visible': True, 'children': [ {'title': 'Enable?:', 'name': 'enable', 'type': 'bool', 'value': False, }, {'title': 'Trigger Source:', 'name': 'trigger_channel', 'type': 'list', - 'limits': DAQmx.getTriggeringSources()}, + 'limits': NIDAQmx.getTriggeringSources()}, {'title': 'Edge type:', 'name': 'edge', 'type': 'list', 'limits': [e.name for e in Edge], 'visible': False}, {'title': 'Level:', 'name': 'level', 'type': 'float', 'value': 1., 'visible': False} diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx_Viewer.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx_Viewer.py index 238051d..7bd33b2 100644 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx_Viewer.py +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx_Viewer.py @@ -2,7 +2,7 @@ import numpy as np import traceback from qtpy import QtCore -from .daqmxni import DAQmx +from .daqmxni import NIDAQmx from pymodaq_plugins_daqmx.hardware.national_instruments.daq_NIDAQmx import DAQ_NIDAQmx_base, TerminalConfiguration, \ UsageTypeAI, ChannelType from pymodaq.control_modules.viewer_utility_classes import DAQ_Viewer_base, comon_parameters as viewer_params @@ -100,7 +100,7 @@ def ini_detector(self, controller=None): """ try: self.current_device = nidaqmx.system.Device(self.settings["devices"]) - self.controller = self.ini_detector_init(controller, DAQmx()) + self.controller = self.ini_detector_init(controller, NIDAQmx()) self.controller.configuration_sequence(self, self.current_device) # actions to perform in order to set properly the settings tree options diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py index 53ca05a..039fb6e 100644 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py @@ -139,7 +139,7 @@ def __init__(self, **kwargs): super().__init__(**kwargs) -class DAQmx: +class NIDAQmx: """Wrapper around the NIDAQmx package giving an easy-to-use object to instantiate channels and tasks""" def __init__(self): self.devices = [] From a17455cb8d2246618abb6de65c99994492a49667 Mon Sep 17 00:00:00 2001 From: Sebastien Date: Mon, 27 Jan 2025 10:28:29 +0100 Subject: [PATCH 052/140] README update --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 3187a00..b4f3222 100644 --- a/README.rst +++ b/README.rst @@ -41,6 +41,6 @@ Viewer0D ++++++++ * **DAQmx_PLcounter**: Single photon counting -* **NIDAQmx: For now (01/2025) Only Analog Input measurements (current-voltage-temperature), working with cDAQ & DAQ-USB +* **NIDAQmx**: For now (01/2025) Only Analog Input tested and working. (current-voltage-temperature measurements on cDAQ & DAQ-USB) From 34983c6edfdc0144a5c7fb12f72ea40fd17dd473 Mon Sep 17 00:00:00 2001 From: Sebastien Date: Mon, 27 Jan 2025 10:44:56 +0100 Subject: [PATCH 053/140] Renaming new nidaqmx based-on codes (NIDAQmx_base, NIDAQmx_Viewer, NIDAQmx_Move) to let previous PYDAQmx based-on daq_NIDAQmx.py reachable --- .../daq_move_plugins/daq_move_DAQmx.py | 2 +- .../plugins_0D/daq_0Dviewer_NIDAQmx.py | 4 +- .../plugins_1D/daq_1Dviewer_DAQmx.py | 2 +- .../{daq_NIDAQmx_Move.py => NIDAQmx_Move.py} | 2 +- ...aq_NIDAQmx_Viewer.py => NIDAQmx_Viewer.py} | 2 +- .../national_instruments/NIDAQmx_base.py | 490 ++++++++++++ .../national_instruments/daq_NIDAQmx.py | 716 +++++++++++++----- 7 files changed, 1040 insertions(+), 178 deletions(-) rename src/pymodaq_plugins_daqmx/hardware/national_instruments/{daq_NIDAQmx_Move.py => NIDAQmx_Move.py} (99%) rename src/pymodaq_plugins_daqmx/hardware/national_instruments/{daq_NIDAQmx_Viewer.py => NIDAQmx_Viewer.py} (98%) create mode 100644 src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_base.py diff --git a/src/pymodaq_plugins_daqmx/daq_move_plugins/daq_move_DAQmx.py b/src/pymodaq_plugins_daqmx/daq_move_plugins/daq_move_DAQmx.py index f5e26db..f627b51 100644 --- a/src/pymodaq_plugins_daqmx/daq_move_plugins/daq_move_DAQmx.py +++ b/src/pymodaq_plugins_daqmx/daq_move_plugins/daq_move_DAQmx.py @@ -1,4 +1,4 @@ -from ..hardware.national_instruments.daq_NIDAQmx_Move import DAQ_NIDAQmx_Actuator +from ..hardware.national_instruments.NIDAQmx_Move import DAQ_NIDAQmx_Actuator class DAQ_Move_DAQmx(DAQ_NIDAQmx_Actuator): diff --git a/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_NIDAQmx.py b/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_NIDAQmx.py index df9ae89..0c3895b 100644 --- a/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_NIDAQmx.py +++ b/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_NIDAQmx.py @@ -5,8 +5,8 @@ niDevice, TemperatureUnits, CJCSource, VoltageUnits, AcquisitionType from pymodaq_plugins_daqmx.hardware.national_instruments.daqmxni import UsageTypeAI, ThermocoupleType, \ TerminalConfiguration, Edge -from pymodaq_plugins_daqmx.hardware.national_instruments.daq_NIDAQmx import DAQ_NIDAQmx_base -from pymodaq_plugins_daqmx.hardware.national_instruments.daq_NIDAQmx_Viewer import DAQ_NIDAQmx_Viewer +from pymodaq_plugins_daqmx.hardware.national_instruments.NIDAQmx_base import DAQ_NIDAQmx_base +from pymodaq_plugins_daqmx.hardware.national_instruments.NIDAQmx_Viewer import DAQ_NIDAQmx_Viewer from pymodaq.utils.logger import set_logger, get_module_name logger = set_logger(get_module_name(__file__)) diff --git a/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_1D/daq_1Dviewer_DAQmx.py b/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_1D/daq_1Dviewer_DAQmx.py index 6da3bf5..ebe032f 100644 --- a/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_1D/daq_1Dviewer_DAQmx.py +++ b/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_1D/daq_1Dviewer_DAQmx.py @@ -1,4 +1,4 @@ -from pymodaq_plugins_daqmx.hardware.national_instruments.daq_NIDAQmx_Viewer import DAQ_NIDAQmx_Viewer +from pymodaq_plugins_daqmx.hardware.national_instruments.NIDAQmx_Viewer import DAQ_NIDAQmx_Viewer from pymodaq.control_modules.viewer_utility_classes import main diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx_Move.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_Move.py similarity index 99% rename from src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx_Move.py rename to src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_Move.py index d38ca0b..3ad598d 100644 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx_Move.py +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_Move.py @@ -2,7 +2,7 @@ from easydict import EasyDict as edict from qtpy import QtWidgets from pymodaq.control_modules.move_utility_classes import DAQ_Move_base, comon_parameters as actuator_params -from pymodaq_plugins_daqmx.hardware.national_instruments.daq_NIDAQmx import DAQ_NIDAQmx_base +from pymodaq_plugins_daqmx.hardware.national_instruments.NIDAQmx_base import DAQ_NIDAQmx_base from pymodaq.utils.daq_utils import ThreadCommand, getLineInfo from pymodaq.utils.data import DataActuator from pymodaq.utils.logger import set_logger, get_module_name diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx_Viewer.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_Viewer.py similarity index 98% rename from src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx_Viewer.py rename to src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_Viewer.py index 7bd33b2..ba15f27 100644 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx_Viewer.py +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_Viewer.py @@ -3,7 +3,7 @@ import traceback from qtpy import QtCore from .daqmxni import NIDAQmx -from pymodaq_plugins_daqmx.hardware.national_instruments.daq_NIDAQmx import DAQ_NIDAQmx_base, TerminalConfiguration, \ +from pymodaq_plugins_daqmx.hardware.national_instruments.NIDAQmx_base import DAQ_NIDAQmx_base, TerminalConfiguration, \ UsageTypeAI, ChannelType from pymodaq.control_modules.viewer_utility_classes import DAQ_Viewer_base, comon_parameters as viewer_params from pymodaq.utils.daq_utils import ThreadCommand diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_base.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_base.py new file mode 100644 index 0000000..7218073 --- /dev/null +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_base.py @@ -0,0 +1,490 @@ +from qtpy import QtWidgets +from qtpy.QtCore import Signal +from pymodaq.utils.logger import set_logger, get_module_name +from pymodaq.utils.parameter import Parameter +from pymodaq.utils.parameter.pymodaq_ptypes import registerParameterType, GroupParameter +from pymodaq_plugins_daqmx.hardware.national_instruments.daqmxni import NIDAQmx, Edge, ChannelType, ClockSettings, \ + AIChannel, AIThermoChannel, AOChannel, CIChannel, COChannel, DOChannel, DIChannel, UsageTypeAI, UsageTypeAO, \ + ThermocoupleType, TerminalConfiguration, TriggerSettings + + +logger = set_logger(get_module_name(__file__)) + + +class ScalableGroupAI(GroupParameter): + + """ + | + + ================ ============= + **Attributes** **Type** + *opts* dictionnary + ================ ============= + + See Also + -------- + hardware.DAQ_Move_Stage_type + """ + + params = [{'title': 'AI type:', 'name': 'ai_type', 'type': 'list', 'limits': [Uai.name for Uai in UsageTypeAI]}, + {'title': 'Voltage:', 'name': 'voltage_settings', 'type': 'group', 'children': [ + {'title': 'Voltage Min:', 'name': 'volt_min', 'type': 'float', 'value': -10.}, + {'title': 'Voltage Max:', 'name': 'volt_max', 'type': 'float', 'value': 10.}, + ]}, + {'title': 'Current:', 'name': 'current_settings', 'type': 'group', 'visible': False, 'children': [ + {'title': 'Current Min:', 'name': 'curr_min', 'type': 'float', 'value': -1, 'suffix': 'A'}, + {'title': 'Current Max:', 'name': 'curr_max', 'type': 'float', 'value': 1, 'suffix': 'A'}, + ]}, + {'title': 'Thermocouple:', 'name': 'thermoc_settings', 'type': 'group', 'visible': False, 'children': [ + {'title': 'Thc. type:', 'name': 'thermoc_type', 'type': 'list', + 'limits': [Th.name for Th in ThermocoupleType], 'value': 'K'}, + {'title': 'Temp. Min (°C):', 'name': 'T_min', 'type': 'float', 'value': 0, 'suffix': '°C'}, + {'title': 'Temp. Max (°C):', 'name': 'T_max', 'type': 'float', 'value': 50, 'suffix': '°C'}, + ]}, + {'title': 'Termination:', 'name': 'termination', 'type': 'list', + 'limits': [Te.name for Te in TerminalConfiguration]}, + ] + + def __init__(self, **opts): + opts['type'] = 'groupai' + opts['addText'] = "Add" + opts['addList'] = opts['limits'] + GroupParameter.__init__(self, **opts) + + def addNew(self, typ=None): + """ + Add a child. + + =============== =========== + **Parameters** **Type** + *typ* string + =============== =========== + """ + childnames = [par.name() for par in self.children()] + if childnames == []: + newindex = 0 + else: + newindex = len(childnames) + + child = {'title': typ, 'name': 'ai{:02.0f}'.format(newindex), 'type': 'group', 'children': self.params, + 'removable': True, 'renamable': False} + + self.addChild(child) + + +registerParameterType('groupai', ScalableGroupAI, override=True) + + +class ScalableGroupAO(GroupParameter): + """ + | + + ================ ============= + **Attributes** **Type** + *opts* dictionnary + ================ ============= + + See Also + -------- + hardware.DAQ_Move_Stage_type + """ + + params = [{'title': 'AO type:', 'name': 'ao_type', 'type': 'list', 'limits': [Uao.name for Uao in UsageTypeAO]}, + {'title': 'Voltages:', 'name': 'voltage_settings', 'type': 'group', 'children': [ + {'title': 'Voltage Min:', 'name': 'volt_min', 'type': 'list', 'value': -10., }, + {'title': 'Voltage Max:', 'name': 'volt_max', 'type': 'list', 'value': 10., }, + ]}, + {'title': 'Current:', 'name': 'current_settings', 'type': 'group', 'visible': False, 'children': [ + {'title': 'Current Min:', 'name': 'curr_min', 'type': 'float', 'value': -1, 'suffix': 'A'}, + {'title': 'Current Max:', 'name': 'curr_max', 'type': 'float', 'value': 1, 'suffix': 'A'}, + ]}, + ] + + def __init__(self, **opts): + opts['type'] = 'groupao' + opts['addText'] = "Add" + opts['addList'] = opts['limits'] + GroupParameter.__init__(self, **opts) + + def addNew(self, typ=None): + """ + Add a child. + + =============== =========== + **Parameters** **Type** + *typ* string + =============== =========== + """ + childnames = [par.name() for par in self.children()] + if childnames == []: + newindex = 0 + else: + newindex = len(childnames) + + child = {'title': typ, 'name': 'ao{:02.0f}'.format(newindex), 'type': 'group', 'children': self.params, + 'removable': True, 'renamable': False} + + self.addChild(child) + + +registerParameterType('groupao', ScalableGroupAO, override=True) + + +class ScalableGroupCounter(GroupParameter): + """ + | + + ================ ============= + **Attributes** **Type** + *opts* dictionnary + ================ ============= + + See Also + -------- + hardware.DAQ_Move_Stage_type + """ + + params = [{'title': 'Edge type:', 'name': 'edge', 'type': 'list', 'limits': [e.name for e in Edge]}, ] + + def __init__(self, **opts): + opts['type'] = 'groupcounter' + opts['addText'] = "Add" + opts['addList'] = opts['limits'] + GroupParameter.__init__(self, **opts) + + def addNew(self, typ=None): + """ + Add a child. + + =============== =========== + **Parameters** **Type** + *typ* string + =============== =========== + """ + childnames = [par.name() for par in self.children()] + if childnames == []: + newindex = 0 + else: + newindex = len(childnames) + + child = {'title': typ, 'name': 'counter{:02.0f}'.format(newindex), 'type': 'group', 'children': self.params, + 'removable': True, 'renamable': False} + + self.addChild(child) + + +registerParameterType('groupcounter', ScalableGroupCounter, override=True) + + +class ScalableGroupDI(GroupParameter): + """ + """ + + params = [] + + def __init__(self, **opts): + opts['type'] = 'groupdi' + opts['addText'] = "Add" + opts['addList'] = opts['limits'] + GroupParameter.__init__(self, **opts) + + def addNew(self, typ=None): + """ + Add a child. + + =============== =========== + **Parameters** **Type** + *typ* string + =============== =========== + """ + childnames = [par.name() for par in self.children()] + if childnames == []: + newindex = 0 + else: + newindex = len(childnames) + + child = {'title': typ, 'name': 'di{:02.0f}'.format(newindex), 'type': 'group', 'children': self.params, + 'removable': True, 'renamable': False} + self.addChild(child) + + +registerParameterType('groupdi', ScalableGroupDI, override=True) + + +class ScalableGroupDO(GroupParameter): + """ + """ + + params = [] + + def __init__(self, **opts): + opts['type'] = 'groupdo' + opts['addText'] = "Add" + opts['addList'] = opts['limits'] + GroupParameter.__init__(self, **opts) + + def addNew(self, typ=None): + """ + Add a child. + + =============== =========== + **Parameters** **Type** + *typ* string + =============== =========== + """ + childnames = [par.name() for par in self.children()] + if childnames == []: + newindex = 0 + else: + newindex = len(childnames) + + child = {'title': typ, 'name': 'counter{:02.0f}'.format(newindex), 'type': 'group', 'children': self.params, + 'removable': True, 'renamable': False} + self.addChild(child) + + +registerParameterType('groupdo', ScalableGroupDO, override=True) + + +class DAQ_NIDAQmx_base: + """ + Base NIDAQmx class for using DAQmx objects from daqmxni.py in the DAQ_NIDAQmx_Move & DAQ_NIDAQmx_Viewer + """ + data_grabed_signal = Signal(list) + param_instance = NIDAQmx() + params = [{'title': 'Refresh hardware:', 'name': 'refresh_hardware', 'type': 'bool', 'value': False}, + {'title': 'Signal type:', 'name': 'NIDAQ_type', 'type': 'list', + 'limits': [Ds.name for Ds in ChannelType]}, + {'title': 'NSamples To Read', 'name': 'nsamplestoread', 'type': 'int', 'value': 1000, 'default': 1000, + 'min': 1}, + {'title': 'AO Settings:', 'name': 'ao_settings', 'type': 'group', 'children': [ + {'title': 'Waveform:', 'name': 'waveform', 'type': 'list', 'value': 'DC', + 'limits': ['DC', 'Sinus', 'Ramp']}, + {'title': 'Controlled param:', 'name': 'cont_param', 'type': 'list', 'value': 'offset', + 'limits': ['offset', 'amplitude', 'frequency']}, + {'title': 'Waveform Settings:', 'name': 'waveform_settings', 'type': 'group', 'visible': False, + 'children': [ + {'title': 'Offset:', 'name': 'offset', 'type': 'float', 'value': 0., }, + {'title': 'Amplitude:', 'name': 'amplitude', 'type': 'float', 'value': 1., }, + {'title': 'Frequency:', 'name': 'frequency', 'type': 'float', 'value': 10., }, + ]}, + ]}, + {'title': 'Clock Settings:', 'name': 'clock_settings', 'type': 'group', 'children': [ + {'title': 'Nsamples:', 'name': 'Nsamples', 'type': 'int', 'value': 1000, 'default': 1000, 'min': 1}, + {'title': 'Frequency:', 'name': 'frequency', 'type': 'float', 'value': 1000., 'default': 1000., + 'min': 0., 'suffix': 'Hz'}, + {'title': 'Repetition?:', 'name': 'repetition', 'type': 'bool', 'value': False, }, + ] + }, + {'title': 'AI Channels:', 'name': 'ai_channels', 'type': 'groupai', + 'limits': NIDAQmx.get_NIDAQ_channels(source_type=ChannelType.ANALOG_INPUT)}, + {'title': 'AO Channels:', 'name': 'ao_channels', 'type': 'groupao', + 'limits': NIDAQmx.get_NIDAQ_channels(source_type=ChannelType.ANALOG_OUTPUT)}, + {'title': 'DO Channels:', 'name': 'do_channels', 'type': 'groupdo', + 'limits': NIDAQmx.get_NIDAQ_channels(source_type=ChannelType.DIGITAL_OUTPUT)}, + {'title': 'DI Channels:', 'name': 'di_channels', 'type': 'groupdi', + 'limits': NIDAQmx.get_NIDAQ_channels(source_type=ChannelType.DIGITAL_INPUT)}, + {'title': 'Counter Settings:', 'name': 'counter_settings', 'type': 'group', 'visible': True, 'children': [ + {'title': 'Counting time (ms):', 'name': 'counting_time', 'type': 'float', 'value': 100., + 'default': 100., 'min': 0.}, + {'title': 'CI Channels:', 'name': 'ci_channels', 'type': 'groupcounter', + 'limits': NIDAQmx.get_NIDAQ_channels(source_type=ChannelType.COUNTER_INPUT)}, + {'title': 'CO Channels:', 'name': 'co_channels', 'type': 'groupcounter', + 'limits': NIDAQmx.get_NIDAQ_channels(source_type=ChannelType.COUNTER_OUTPUT)}, + ]}, + {'title': 'Trigger Settings:', 'name': 'trigger_settings', 'type': 'group', 'visible': True, 'children': [ + {'title': 'Enable?:', 'name': 'enable', 'type': 'bool', 'value': False, }, + {'title': 'Trigger Source:', 'name': 'trigger_channel', 'type': 'list', + 'limits': NIDAQmx.getTriggeringSources()}, + {'title': 'Edge type:', 'name': 'edge', 'type': 'list', 'limits': [e.name for e in Edge], + 'visible': False}, + {'title': 'Level:', 'name': 'level', 'type': 'float', 'value': 1., 'visible': False} + ]} + ] + + def __init__(self): + super().__init__() + + self.timer = None + self.channels = None + self.clock_settings = None + self.trigger_settings = None + self.live = False + + def commit_settings(self, param: Parameter): + """ + Activate the parameters changes in the hardware. + + =============== ================================ =========================== + **Parameters** **Type** **Description** + *param* instance of pyqtgraph.parameter the parameter to activate + =============== ================================ =========================== + + See Also + -------- + update_NIDAQ_channels, update_task, DAQ_NIDAQ_source, refresh_hardware + """ + if param.name() == 'NIDAQ_devices': + self.controller.update_NIDAQ_channels() + + if param.name() == 'NIDAQ_type': + self.controller.update_NIDAQ_channels(param.value()) + if param.value() == ChannelType.ANALOG_INPUT.name: # analog input + self.settings.child('clock_settings').show() + self.settings.child('ai_channels').show() + self.settings.child('ao_channels').hide() + self.settings.child('ao_settings').hide() + self.settings.child('counter_settings').hide() + self.settings.child('do_channels').hide() + self.settings.child('di_channels').hide() + + elif param.value() == ChannelType.ANALOG_OUTPUT.name: # analog output + self.settings.child('clock_settings').show() + self.settings.child('ai_channels').hide() + self.settings.child('ao_channels').show() + self.settings.child('ao_settings').show() + self.settings.child('counter_settings').hide() + self.settings.child('do_channels').hide() + self.settings.child('di_channels').hide() + + elif param.value() == ChannelType.COUNTER_INPUT.name: # counter input + self.settings.child('clock_settings').hide() + self.settings.child('ai_channels').hide() + self.settings.child('ao_channels').hide() + self.settings.child('ao_settings').hide() + self.settings.child('counter_settings', 'ci_channels').show() + self.settings.child('do_channels').hide() + self.settings.child('di_channels').hide() + + elif param.value() == ChannelType.COUNTER_OUTPUT.name: # counter output + self.settings.child('clock_settings').hide() + self.settings.child('ai_channels').hide() + self.settings.child('ao_channels').hide() + self.settings.child('ao_settings').hide() + self.settings.child('counter_settings', 'co_channels').show() + self.settings.child('do_channels').hide() + self.settings.child('di_channels').hide() + + elif param.value() == ChannelType.DIGITAL_INPUT.name: # Digital_Input + self.settings.child('clock_settings').show() + self.settings.child('ai_channels').hide() + self.settings.child('ao_channels').show() + self.settings.child('ao_settings').show() + self.settings.child('counter_settings').hide() + self.settings.child('do_channels').hide() + self.settings.child('di_channels').show() + + elif param.value() == ChannelType.DIGITAL_OUTPUT.name: # digital output + self.settings.child('clock_settings').show() + self.settings.child('ai_channels').hide() + self.settings.child('ao_channels').hide() + self.settings.child('ao_settings').hide() + self.settings.child('counter_settings').hide() + self.settings.child('do_channels').show() + self.settings.child('di_channels').hide() + + elif param.name() == 'refresh_hardware': + if param.value(): + self.controller.refresh_hardware() + QtWidgets.QApplication.processEvents() + self.settings.child('refresh_hardware').setValue(False) + + elif param.name() == 'ai_type': + param.parent().child('voltage_settings').show(param.value() == UsageTypeAI.VOLTAGE.name) + param.parent().child('current_settings').show(param.value() == UsageTypeAI.CURRENT.name) + param.parent().child('thermoc_settings').show(param.value() == UsageTypeAI.TEMPERATURE_THERMOCOUPLE.name) + + elif param.name() == 'ao_type': + param.parent().child('voltage_settings').show(param.value() == UsageTypeAI.VOLTAGE.name) + param.parent().child('current_settings').show(param.value() == UsageTypeAI.CURRENT.name) + + elif param.name() == 'trigger_channel': + param.parent().child('level').show('PF' not in param.opts['title']) + + def update_task(self): + self.channels = self.get_channels_from_settings() + self.clock_settings = ClockSettings(frequency=self.settings['clock_settings', 'frequency'], + Nsamples=self.settings['clock_settings', 'Nsamples'], + edge=Edge.RISING, + repetition=self.live, ) + self.trigger_settings = \ + TriggerSettings(trig_source=self.settings['trigger_settings', 'trigger_channel'], + enable=self.settings['trigger_settings', 'enable'], + edge=Edge[self.settings['trigger_settings', 'edge']], + level=self.settings['trigger_settings', 'level'], ) + if self.channels: + self.controller.update_task(self.channels, self.clock_settings, trigger_settings=self.trigger_settings) + + def get_channels_from_settings(self): + channels = [] + if self.settings['NIDAQ_type'] == ChannelType.ANALOG_INPUT.name: # analog input + source = ChannelType.ANALOG_INPUT + for channel in self.settings.child('ai_channels').children(): + analog_type = UsageTypeAI[channel['ai_type']] + if analog_type == UsageTypeAI.VOLTAGE: + channels.append(AIChannel(name=channel.opts['title'], + source=source, analog_type=analog_type, + value_min=channel['voltage_settings', 'volt_min'], + value_max=channel['voltage_settings', 'volt_max'], + termination=TerminalConfiguration[channel['termination']], )) + elif analog_type == UsageTypeAI.CURRENT: + channels.append(AIChannel(name=channel.opts['title'], + source=source, analog_type=analog_type, + value_min=channel['current_settings', 'curr_min'], + value_max=channel['current_settings', 'curr_max'], + termination=TerminalConfiguration[channel['termination']], )) + elif analog_type == UsageTypeAI.TEMPERATURE_THERMOCOUPLE: + channels.append(AIThermoChannel(name=channel.opts['title'], + source=source, analog_type=analog_type, + value_min=channel['thermoc_settings', 'T_min'], + value_max=channel['thermoc_settings', 'T_max'], + thermo_type=ThermocoupleType[ + channel['thermoc_settings', 'thermoc_type']], )) + + elif self.settings['NIDAQ_type'] == ChannelType.ANALOG_OUTPUT.name: # analog output + source = ChannelType.ANALOG_OUTPUT + for channel in self.settings.child('ao_channels').children(): + analog_type = UsageTypeAO[channel['ao_type']] + channels.append(AOChannel(name=channel.opts['title'], + source=source, analog_type=analog_type, + value_min=channel['voltage_settings', 'volt_min'], + value_max=channel['voltage_settings', 'volt_max'], + )) + + elif self.settings['NIDAQ_type'] == ChannelType.COUNTER_INPUT.name: # counter input + source = ChannelType.COUNTER_INPUT + for channel in self.settings.child('counter_settings', 'ci_channels').children(): + channels.append(CIChannel(name=channel.opts['title'], + source=source, edge=Edge[channel['edge']])) + + elif self.settings['NIDAQ_type'] == ChannelType.COUNTER_OUTPUT.name: # counter output + source = ChannelType.COUNTER_OUTPUT + for channel in self.settings.child('counter_settings', 'co_channels').children(): + channels.append(COChannel(name=channel.opts['title'], + source=source, edge=Edge[channel['edge']])) + + elif self.settings['NIDAQ_type'] == ChannelType.DIGITAL_INPUT.name: # digital input + source = ChannelType.DIGITAL_INPUT + for channel in self.settings.child('di_channels').children(): + channels.append(DIChannel(name=channel.opts['title'], + source=source)) + + elif self.settings['NIDAQ_type'] == ChannelType.DIGITAL_OUTPUT.name: # Digital output + source = ChannelType.DIGITAL_OUTPUT + for channel in self.settings.child('do_channels').children(): + channels.append(DOChannel(name=channel.opts['title'], + source=source)) + + channels = [ch for ch in channels if self.settings.child("devices").value() in ch.name] + return channels + + def stop(self): + """ + """ + if not not self.timer: + self.timer.stop() + QtWidgets.QApplication.processEvents() + self.controller.stop() + + + diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx.py index 7218073..003479e 100644 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx.py +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx.py @@ -1,18 +1,23 @@ -from qtpy import QtWidgets -from qtpy.QtCore import Signal -from pymodaq.utils.logger import set_logger, get_module_name +from qtpy import QtWidgets, QtCore +from qtpy.QtCore import Signal, QThread +from pymodaq.utils.daq_utils import ThreadCommand, getLineInfo +from pymodaq.utils.data import DataFromPlugins, Axis, DataActuator, DataToExport +import numpy as np +from pymodaq.control_modules.viewer_utility_classes import DAQ_Viewer_base +from pymodaq.control_modules.move_utility_classes import DAQ_Move_base +from easydict import EasyDict as edict + +from pymodaq.control_modules.viewer_utility_classes import comon_parameters as viewer_params +from pymodaq.control_modules.move_utility_classes import comon_parameters_fun as actuator_params + from pymodaq.utils.parameter import Parameter from pymodaq.utils.parameter.pymodaq_ptypes import registerParameterType, GroupParameter -from pymodaq_plugins_daqmx.hardware.national_instruments.daqmxni import NIDAQmx, Edge, ChannelType, ClockSettings, \ - AIChannel, AIThermoChannel, AOChannel, CIChannel, COChannel, DOChannel, DIChannel, UsageTypeAI, UsageTypeAO, \ - ThermocoupleType, TerminalConfiguration, TriggerSettings - -logger = set_logger(get_module_name(__file__)) +from .daqmx import DAQmx, DAQ_analog_types, DAQ_thermocouples, DAQ_termination, Edge, DAQ_NIDAQ_source, \ + ClockSettings, AIChannel, Counter, AIThermoChannel, AOChannel, TriggerSettings, DOChannel, DIChannel class ScalableGroupAI(GroupParameter): - """ | @@ -26,23 +31,22 @@ class ScalableGroupAI(GroupParameter): hardware.DAQ_Move_Stage_type """ - params = [{'title': 'AI type:', 'name': 'ai_type', 'type': 'list', 'limits': [Uai.name for Uai in UsageTypeAI]}, - {'title': 'Voltage:', 'name': 'voltage_settings', 'type': 'group', 'children': [ - {'title': 'Voltage Min:', 'name': 'volt_min', 'type': 'float', 'value': -10.}, - {'title': 'Voltage Max:', 'name': 'volt_max', 'type': 'float', 'value': 10.}, + params = [{'title': 'AI type:', 'name': 'ai_type', 'type': 'list', 'limits': DAQ_analog_types.names()}, + {'title': 'Voltages:', 'name': 'voltage_settings', 'type': 'group', 'children': [ + {'title': 'Voltage Min:', 'name': 'volt_min', 'type': 'list', 'value': -10.}, + {'title': 'Voltage Max:', 'name': 'volt_max', 'type': 'list', 'value': 10.}, ]}, {'title': 'Current:', 'name': 'current_settings', 'type': 'group', 'visible': False, 'children': [ {'title': 'Current Min:', 'name': 'curr_min', 'type': 'float', 'value': -1, 'suffix': 'A'}, {'title': 'Current Max:', 'name': 'curr_max', 'type': 'float', 'value': 1, 'suffix': 'A'}, ]}, {'title': 'Thermocouple:', 'name': 'thermoc_settings', 'type': 'group', 'visible': False, 'children': [ - {'title': 'Thc. type:', 'name': 'thermoc_type', 'type': 'list', - 'limits': [Th.name for Th in ThermocoupleType], 'value': 'K'}, + {'title': 'Thc. type:', 'name': 'thermoc_type', 'type': 'list', 'limits': DAQ_thermocouples.names(), + 'value': 'K'}, {'title': 'Temp. Min (°C):', 'name': 'T_min', 'type': 'float', 'value': 0, 'suffix': '°C'}, {'title': 'Temp. Max (°C):', 'name': 'T_max', 'type': 'float', 'value': 50, 'suffix': '°C'}, ]}, - {'title': 'Termination:', 'name': 'termination', 'type': 'list', - 'limits': [Te.name for Te in TerminalConfiguration]}, + {'title': 'Termination:', 'name': 'termination', 'type': 'list', 'limits': DAQ_termination.names()}, ] def __init__(self, **opts): @@ -51,7 +55,7 @@ def __init__(self, **opts): opts['addList'] = opts['limits'] GroupParameter.__init__(self, **opts) - def addNew(self, typ=None): + def addNew(self, typ): """ Add a child. @@ -70,8 +74,6 @@ def addNew(self, typ=None): 'removable': True, 'renamable': False} self.addChild(child) - - registerParameterType('groupai', ScalableGroupAI, override=True) @@ -89,7 +91,7 @@ class ScalableGroupAO(GroupParameter): hardware.DAQ_Move_Stage_type """ - params = [{'title': 'AO type:', 'name': 'ao_type', 'type': 'list', 'limits': [Uao.name for Uao in UsageTypeAO]}, + params = [{'title': 'AO type:', 'name': 'ao_type', 'type': 'list', 'limits': DAQ_analog_types.names()[0:2]}, {'title': 'Voltages:', 'name': 'voltage_settings', 'type': 'group', 'children': [ {'title': 'Voltage Min:', 'name': 'volt_min', 'type': 'list', 'value': -10., }, {'title': 'Voltage Max:', 'name': 'volt_max', 'type': 'list', 'value': 10., }, @@ -106,7 +108,7 @@ def __init__(self, **opts): opts['addList'] = opts['limits'] GroupParameter.__init__(self, **opts) - def addNew(self, typ=None): + def addNew(self, typ): """ Add a child. @@ -125,8 +127,6 @@ def addNew(self, typ=None): 'removable': True, 'renamable': False} self.addChild(child) - - registerParameterType('groupao', ScalableGroupAO, override=True) @@ -144,7 +144,7 @@ class ScalableGroupCounter(GroupParameter): hardware.DAQ_Move_Stage_type """ - params = [{'title': 'Edge type:', 'name': 'edge', 'type': 'list', 'limits': [e.name for e in Edge]}, ] + params = [{'title': 'Edge type:', 'name': 'edge', 'type': 'list', 'limits': Edge.names()}, ] def __init__(self, **opts): opts['type'] = 'groupcounter' @@ -152,7 +152,7 @@ def __init__(self, **opts): opts['addList'] = opts['limits'] GroupParameter.__init__(self, **opts) - def addNew(self, typ=None): + def addNew(self, typ): """ Add a child. @@ -171,8 +171,6 @@ def addNew(self, typ=None): 'removable': True, 'renamable': False} self.addChild(child) - - registerParameterType('groupcounter', ScalableGroupCounter, override=True) @@ -188,7 +186,7 @@ def __init__(self, **opts): opts['addList'] = opts['limits'] GroupParameter.__init__(self, **opts) - def addNew(self, typ=None): + def addNew(self, typ): """ Add a child. @@ -206,8 +204,6 @@ def addNew(self, typ=None): child = {'title': typ, 'name': 'di{:02.0f}'.format(newindex), 'type': 'group', 'children': self.params, 'removable': True, 'renamable': False} self.addChild(child) - - registerParameterType('groupdi', ScalableGroupDI, override=True) @@ -223,7 +219,7 @@ def __init__(self, **opts): opts['addList'] = opts['limits'] GroupParameter.__init__(self, **opts) - def addNew(self, typ=None): + def addNew(self, typ): """ Add a child. @@ -241,66 +237,54 @@ def addNew(self, typ=None): child = {'title': typ, 'name': 'counter{:02.0f}'.format(newindex), 'type': 'group', 'children': self.params, 'removable': True, 'renamable': False} self.addChild(child) - - registerParameterType('groupdo', ScalableGroupDO, override=True) -class DAQ_NIDAQmx_base: - """ - Base NIDAQmx class for using DAQmx objects from daqmxni.py in the DAQ_NIDAQmx_Move & DAQ_NIDAQmx_Viewer - """ +class DAQ_NIDAQmx_base(DAQmx): data_grabed_signal = Signal(list) - param_instance = NIDAQmx() - params = [{'title': 'Refresh hardware:', 'name': 'refresh_hardware', 'type': 'bool', 'value': False}, - {'title': 'Signal type:', 'name': 'NIDAQ_type', 'type': 'list', - 'limits': [Ds.name for Ds in ChannelType]}, - {'title': 'NSamples To Read', 'name': 'nsamplestoread', 'type': 'int', 'value': 1000, 'default': 1000, - 'min': 1}, - {'title': 'AO Settings:', 'name': 'ao_settings', 'type': 'group', 'children': [ - {'title': 'Waveform:', 'name': 'waveform', 'type': 'list', 'value': 'DC', - 'limits': ['DC', 'Sinus', 'Ramp']}, - {'title': 'Controlled param:', 'name': 'cont_param', 'type': 'list', 'value': 'offset', - 'limits': ['offset', 'amplitude', 'frequency']}, - {'title': 'Waveform Settings:', 'name': 'waveform_settings', 'type': 'group', 'visible': False, - 'children': [ - {'title': 'Offset:', 'name': 'offset', 'type': 'float', 'value': 0., }, - {'title': 'Amplitude:', 'name': 'amplitude', 'type': 'float', 'value': 1., }, - {'title': 'Frequency:', 'name': 'frequency', 'type': 'float', 'value': 10., }, - ]}, - ]}, - {'title': 'Clock Settings:', 'name': 'clock_settings', 'type': 'group', 'children': [ - {'title': 'Nsamples:', 'name': 'Nsamples', 'type': 'int', 'value': 1000, 'default': 1000, 'min': 1}, - {'title': 'Frequency:', 'name': 'frequency', 'type': 'float', 'value': 1000., 'default': 1000., - 'min': 0., 'suffix': 'Hz'}, - {'title': 'Repetition?:', 'name': 'repetition', 'type': 'bool', 'value': False, }, - ] - }, - {'title': 'AI Channels:', 'name': 'ai_channels', 'type': 'groupai', - 'limits': NIDAQmx.get_NIDAQ_channels(source_type=ChannelType.ANALOG_INPUT)}, - {'title': 'AO Channels:', 'name': 'ao_channels', 'type': 'groupao', - 'limits': NIDAQmx.get_NIDAQ_channels(source_type=ChannelType.ANALOG_OUTPUT)}, - {'title': 'DO Channels:', 'name': 'do_channels', 'type': 'groupdo', - 'limits': NIDAQmx.get_NIDAQ_channels(source_type=ChannelType.DIGITAL_OUTPUT)}, - {'title': 'DI Channels:', 'name': 'di_channels', 'type': 'groupdi', - 'limits': NIDAQmx.get_NIDAQ_channels(source_type=ChannelType.DIGITAL_INPUT)}, - {'title': 'Counter Settings:', 'name': 'counter_settings', 'type': 'group', 'visible': True, 'children': [ - {'title': 'Counting time (ms):', 'name': 'counting_time', 'type': 'float', 'value': 100., - 'default': 100., 'min': 0.}, - {'title': 'CI Channels:', 'name': 'ci_channels', 'type': 'groupcounter', - 'limits': NIDAQmx.get_NIDAQ_channels(source_type=ChannelType.COUNTER_INPUT)}, - {'title': 'CO Channels:', 'name': 'co_channels', 'type': 'groupcounter', - 'limits': NIDAQmx.get_NIDAQ_channels(source_type=ChannelType.COUNTER_OUTPUT)}, - ]}, - {'title': 'Trigger Settings:', 'name': 'trigger_settings', 'type': 'group', 'visible': True, 'children': [ - {'title': 'Enable?:', 'name': 'enable', 'type': 'bool', 'value': False, }, - {'title': 'Trigger Source:', 'name': 'trigger_channel', 'type': 'list', - 'limits': NIDAQmx.getTriggeringSources()}, - {'title': 'Edge type:', 'name': 'edge', 'type': 'list', 'limits': [e.name for e in Edge], - 'visible': False}, - {'title': 'Level:', 'name': 'level', 'type': 'float', 'value': 1., 'visible': False} - ]} - ] + + params =[{'title': 'Refresh hardware:', 'name': 'refresh_hardware', 'type': 'bool', 'value': False}, + {'title': 'Signal type:', 'name': 'NIDAQ_type', 'type': 'list', 'limits': DAQ_NIDAQ_source.names()}, + {'title': 'AO Settings:', 'name': 'ao_settings', 'type': 'group', 'children': [ + {'title': 'Waveform:', 'name': 'waveform', 'type': 'list', 'value': 'DC', 'limits': ['DC', 'Sinus', 'Ramp']}, + + {'title': 'Controlled param:', 'name': 'cont_param', 'type': 'list', 'value': 'offset', + 'limits': ['offset', 'amplitude', 'frequency']}, + {'title': 'Waveform Settings:', 'name': 'waveform_settings', 'type': 'group', 'visible': False, 'children': [ + {'title': 'Offset:', 'name': 'offset', 'type': 'float', 'value': 0., }, + {'title': 'Amplitude:', 'name': 'amplitude', 'type': 'float', 'value': 1., }, + {'title': 'Frequency:', 'name': 'frequency', 'type': 'float', 'value': 10., }, + ]}, + ]}, + {'title': 'Clock Settings:', 'name': 'clock_settings', 'type': 'group', 'children': [ + {'title': 'Nsamples:', 'name': 'Nsamples', 'type': 'int', 'value': 1000, 'default': 1000, 'min': 1}, + {'title': 'Frequency:', 'name': 'frequency', 'type': 'float', 'value': 1000., 'default': 1000., + 'min': 0., 'suffix': 'Hz'}, + {'title': 'Repetition?:', 'name': 'repetition', 'type': 'bool', 'value': False, }, + ] + }, + {'title': 'AI Channels:', 'name': 'ai_channels', 'type': 'groupai', + 'limits': DAQmx.get_NIDAQ_channels(source_type='Analog_Input')}, + {'title': 'AO Channels:', 'name': 'ao_channels', 'type': 'groupao', + 'limits': DAQmx.get_NIDAQ_channels(source_type='Analog_Output')}, + {'title': 'DO Channels:', 'name': 'do_channels', 'type': 'groupdo', + 'limits': DAQmx.get_NIDAQ_channels(source_type='Digital_Output')}, + {'title': 'DI Channels:', 'name': 'di_channels', 'type': 'groupdi', + 'limits': DAQmx.get_NIDAQ_channels(source_type='Digital_Input')}, + {'title': 'Counter Settings:', 'name': 'counter_settings', 'type': 'group', 'visible': True, 'children': [ + {'title': 'Counting time (ms):', 'name': 'counting_time', 'type': 'float', 'value': 100., + 'default': 100., 'min': 0.}, + {'title': 'Counting Channels:', 'name': 'counter_channels', 'type': 'groupcounter', + 'limits': DAQmx.get_NIDAQ_channels(source_type='Counter')}, + ]}, + {'title': 'Trigger Settings:', 'name': 'trigger_settings', 'type': 'group', 'visible': True, 'children': [ + {'title': 'Enable?:', 'name': 'enable', 'type': 'bool', 'value': False, }, + {'title': 'Trigger Source:', 'name': 'trigger_channel', 'type': 'list', + 'limits': DAQmx.getTriggeringSources()}, + {'title': 'Edge type:', 'name': 'edge', 'type': 'list', 'limits': Edge.names(), 'visible': False}, + {'title': 'Level:', 'name': 'level', 'type': 'float', 'value': 1., 'visible': False} + ]} + ] def __init__(self): super().__init__() @@ -310,6 +294,7 @@ def __init__(self): self.clock_settings = None self.trigger_settings = None self.live = False + self.refresh_hardware() def commit_settings(self, param: Parameter): """ @@ -324,12 +309,13 @@ def commit_settings(self, param: Parameter): -------- update_NIDAQ_channels, update_task, DAQ_NIDAQ_source, refresh_hardware """ - if param.name() == 'NIDAQ_devices': - self.controller.update_NIDAQ_channels() + # if param.name() == 'NIDAQ_devices': + # self.update_NIDAQ_channels() + # self.update_task() if param.name() == 'NIDAQ_type': - self.controller.update_NIDAQ_channels(param.value()) - if param.value() == ChannelType.ANALOG_INPUT.name: # analog input + self.update_NIDAQ_channels(param.value()) + if param.value() == DAQ_NIDAQ_source(0).name: #analog input self.settings.child('clock_settings').show() self.settings.child('ai_channels').show() self.settings.child('ao_channels').hide() @@ -338,43 +324,25 @@ def commit_settings(self, param: Parameter): self.settings.child('do_channels').hide() self.settings.child('di_channels').hide() - elif param.value() == ChannelType.ANALOG_OUTPUT.name: # analog output - self.settings.child('clock_settings').show() - self.settings.child('ai_channels').hide() - self.settings.child('ao_channels').show() - self.settings.child('ao_settings').show() - self.settings.child('counter_settings').hide() - self.settings.child('do_channels').hide() - self.settings.child('di_channels').hide() - - elif param.value() == ChannelType.COUNTER_INPUT.name: # counter input - self.settings.child('clock_settings').hide() - self.settings.child('ai_channels').hide() - self.settings.child('ao_channels').hide() - self.settings.child('ao_settings').hide() - self.settings.child('counter_settings', 'ci_channels').show() - self.settings.child('do_channels').hide() - self.settings.child('di_channels').hide() - - elif param.value() == ChannelType.COUNTER_OUTPUT.name: # counter output + elif param.value() == DAQ_NIDAQ_source(1).name: #counter input self.settings.child('clock_settings').hide() self.settings.child('ai_channels').hide() self.settings.child('ao_channels').hide() self.settings.child('ao_settings').hide() - self.settings.child('counter_settings', 'co_channels').show() + self.settings.child('counter_settings').show() self.settings.child('do_channels').hide() self.settings.child('di_channels').hide() - elif param.value() == ChannelType.DIGITAL_INPUT.name: # Digital_Input + elif param.value() == DAQ_NIDAQ_source(2).name: #analog output self.settings.child('clock_settings').show() self.settings.child('ai_channels').hide() self.settings.child('ao_channels').show() self.settings.child('ao_settings').show() self.settings.child('counter_settings').hide() self.settings.child('do_channels').hide() - self.settings.child('di_channels').show() + self.settings.child('di_channels').hide() - elif param.value() == ChannelType.DIGITAL_OUTPUT.name: # digital output + elif param.value() == DAQ_NIDAQ_source(3).name: # digital output self.settings.child('clock_settings').show() self.settings.child('ai_channels').hide() self.settings.child('ao_channels').hide() @@ -383,99 +351,100 @@ def commit_settings(self, param: Parameter): self.settings.child('do_channels').show() self.settings.child('di_channels').hide() + elif param.value() == DAQ_NIDAQ_source(4).name: # Digital_Input + self.settings.child('clock_settings').show() + self.settings.child('ai_channels').hide() + self.settings.child('ao_channels').show() + self.settings.child('ao_settings').show() + self.settings.child('counter_settings').hide() + self.settings.child('do_channels').hide() + self.settings.child('di_channels').show() + self.update_task() + elif param.name() == 'refresh_hardware': if param.value(): - self.controller.refresh_hardware() + self.refresh_hardware() QtWidgets.QApplication.processEvents() self.settings.child('refresh_hardware').setValue(False) elif param.name() == 'ai_type': - param.parent().child('voltage_settings').show(param.value() == UsageTypeAI.VOLTAGE.name) - param.parent().child('current_settings').show(param.value() == UsageTypeAI.CURRENT.name) - param.parent().child('thermoc_settings').show(param.value() == UsageTypeAI.TEMPERATURE_THERMOCOUPLE.name) + param.parent().child('voltage_settings').show(param.value() == 'Voltage') + param.parent().child('current_settings').show(param.value() == 'Current') + param.parent().child('thermoc_settings').show(param.value() == 'Thermocouple') + self.update_task() elif param.name() == 'ao_type': - param.parent().child('voltage_settings').show(param.value() == UsageTypeAI.VOLTAGE.name) - param.parent().child('current_settings').show(param.value() == UsageTypeAI.CURRENT.name) + param.parent().child('voltage_settings').show(param.value() == 'Voltage') + param.parent().child('current_settings').show(param.value() == 'Current') + self.update_task() elif param.name() == 'trigger_channel': param.parent().child('level').show('PF' not in param.opts['title']) + else: + self.update_task() + def update_task(self): self.channels = self.get_channels_from_settings() self.clock_settings = ClockSettings(frequency=self.settings['clock_settings', 'frequency'], Nsamples=self.settings['clock_settings', 'Nsamples'], - edge=Edge.RISING, - repetition=self.live, ) + repetition=self.live,) self.trigger_settings = \ TriggerSettings(trig_source=self.settings['trigger_settings', 'trigger_channel'], enable=self.settings['trigger_settings', 'enable'], - edge=Edge[self.settings['trigger_settings', 'edge']], - level=self.settings['trigger_settings', 'level'], ) - if self.channels: - self.controller.update_task(self.channels, self.clock_settings, trigger_settings=self.trigger_settings) + edge=self.settings['trigger_settings', 'edge'], + level=self.settings['trigger_settings', 'level'],) + + if not not self.channels: + super().update_task(self.channels, self.clock_settings, trigger_settings=self.trigger_settings) + def get_channels_from_settings(self): channels = [] - if self.settings['NIDAQ_type'] == ChannelType.ANALOG_INPUT.name: # analog input - source = ChannelType.ANALOG_INPUT + if self.settings['NIDAQ_type'] == DAQ_NIDAQ_source(0).name: # analog input for channel in self.settings.child('ai_channels').children(): - analog_type = UsageTypeAI[channel['ai_type']] - if analog_type == UsageTypeAI.VOLTAGE: + analog_type = channel['ai_type'] + if analog_type == 'Voltage': channels.append(AIChannel(name=channel.opts['title'], - source=source, analog_type=analog_type, + source='Analog_Input', analog_type=analog_type, value_min=channel['voltage_settings', 'volt_min'], value_max=channel['voltage_settings', 'volt_max'], - termination=TerminalConfiguration[channel['termination']], )) - elif analog_type == UsageTypeAI.CURRENT: + termination=channel['termination'],)) + elif analog_type == 'Current': channels.append(AIChannel(name=channel.opts['title'], - source=source, analog_type=analog_type, + source='Analog_Input', analog_type=analog_type, value_min=channel['current_settings', 'curr_min'], value_max=channel['current_settings', 'curr_max'], - termination=TerminalConfiguration[channel['termination']], )) - elif analog_type == UsageTypeAI.TEMPERATURE_THERMOCOUPLE: + termination=channel['termination'], )) + elif analog_type == 'Thermocouple': channels.append(AIThermoChannel(name=channel.opts['title'], - source=source, analog_type=analog_type, - value_min=channel['thermoc_settings', 'T_min'], - value_max=channel['thermoc_settings', 'T_max'], - thermo_type=ThermocoupleType[ - channel['thermoc_settings', 'thermoc_type']], )) - - elif self.settings['NIDAQ_type'] == ChannelType.ANALOG_OUTPUT.name: # analog output - source = ChannelType.ANALOG_OUTPUT + source='Analog_Input', analog_type=analog_type, + value_min=channel['thermoc_settings', 'T_min'], + value_max=channel['thermoc_settings', 'T_max'], + termination=channel['termination'], + thermo_type=channel['thermoc_settings', 'thermoc_type'],)) + + elif self.settings['NIDAQ_type'] == DAQ_NIDAQ_source(1).name: # counter input + for channel in self.settings.child('counter_settings', 'counter_channels').children(): + channels.append(Counter(name=channel.opts['title'], + source='Counter', edge=channel['edge'])) + + elif self.settings['NIDAQ_type'] == DAQ_NIDAQ_source(2).name: # analog output for channel in self.settings.child('ao_channels').children(): - analog_type = UsageTypeAO[channel['ao_type']] + analog_type = channel['ao_type'] channels.append(AOChannel(name=channel.opts['title'], - source=source, analog_type=analog_type, + source='Analog_Output', analog_type=analog_type, value_min=channel['voltage_settings', 'volt_min'], value_max=channel['voltage_settings', 'volt_max'], )) - - elif self.settings['NIDAQ_type'] == ChannelType.COUNTER_INPUT.name: # counter input - source = ChannelType.COUNTER_INPUT - for channel in self.settings.child('counter_settings', 'ci_channels').children(): - channels.append(CIChannel(name=channel.opts['title'], - source=source, edge=Edge[channel['edge']])) - - elif self.settings['NIDAQ_type'] == ChannelType.COUNTER_OUTPUT.name: # counter output - source = ChannelType.COUNTER_OUTPUT - for channel in self.settings.child('counter_settings', 'co_channels').children(): - channels.append(COChannel(name=channel.opts['title'], - source=source, edge=Edge[channel['edge']])) - - elif self.settings['NIDAQ_type'] == ChannelType.DIGITAL_INPUT.name: # digital input - source = ChannelType.DIGITAL_INPUT - for channel in self.settings.child('di_channels').children(): - channels.append(DIChannel(name=channel.opts['title'], - source=source)) - - elif self.settings['NIDAQ_type'] == ChannelType.DIGITAL_OUTPUT.name: # Digital output - source = ChannelType.DIGITAL_OUTPUT + elif self.settings['NIDAQ_type'] == DAQ_NIDAQ_source(3).name: # Digital output for channel in self.settings.child('do_channels').children(): channels.append(DOChannel(name=channel.opts['title'], - source=source)) - - channels = [ch for ch in channels if self.settings.child("devices").value() in ch.name] + source='Digital_Output')) + elif self.settings['NIDAQ_type'] == DAQ_NIDAQ_source(4).name: # digital input + for channel in self.settings.child('di_channels').children(): + channels.append(DIChannel(name=channel.opts['title'], + source='Digital_Input')) return channels def stop(self): @@ -484,7 +453,410 @@ def stop(self): if not not self.timer: self.timer.stop() QtWidgets.QApplication.processEvents() - self.controller.stop() + DAQmx.stop(self) + + +class DAQ_NIDAQmx_Viewer(DAQ_Viewer_base, DAQ_NIDAQmx_base): + """ + ==================== ======================== + **Attributes** **Type** + *data_grabed_signal* instance of Signal + *params* dictionnary list + *task* + ==================== ======================== + + See Also + -------- + refresh_hardware + """ + + live_mode_available = True + params = viewer_params + DAQ_NIDAQmx_base.params + + def __init__(self, parent=None, params_state=None, control_type="0D"): + DAQ_Viewer_base.__init__(self, parent, params_state) #defines settings attribute and various other methods + DAQ_NIDAQmx_base.__init__(self) + + self.live = False + self.control_type = control_type # could be "0D", "1D" or "Actuator" + if self.control_type == "0D": + self.settings.child('NIDAQ_type').setLimits(['Analog_Input', 'Counter', 'Digital_Input']) # analog input and counter + elif self.control_type == "1D": + self.settings.child('NIDAQ_type').setLimits(['Analog_Input']) + elif self.control_type == "Actuator": + self.settings.child('NIDAQ_type').setLimits(['Analog_Output']) + + self.settings.child('ao_channels').hide() + + #timer used for the counter + self.timer = QtCore.QTimer() + self.timer.setSingleShot(True) + self.timer.timeout.connect(self.counter_done) + + def stop(self): + try: + self.controller['ai'].task.StopTask() + except: + pass + ############################## + + return '' + + def commit_settings(self, param): + """ + Activate the parameters changes in the hardware. + + =============== ================================ =========================== + **Parameters** **Type** **Description** + *param* instance of pyqtgraph.parameter the parameter to activate + =============== ================================ =========================== + + See Also + -------- + update_NIDAQ_channels, update_task, DAQ_NIDAQ_source, refresh_hardware + """ + + if param.parent() is not None: + if param.parent().name() == 'ai_channels': + device = param.opts['title'].split('/')[0] + self.settings.child('clock_settings', 'frequency').setOpts(max=self.getAIMaxRate(device)) + + ranges = self.getAIVoltageRange(device) + param.child('voltage_settings', 'volt_min').setOpts(limits=[r[0] for r in ranges]) + param.child('voltage_settings', 'volt_max').setOpts(limits=[r[1] for r in ranges]) + + DAQ_NIDAQmx_base.commit_settings(self, param) + + def ini_detector(self, controller=None): + """ + Initialisation procedure of the detector. + + See Also + -------- + daq_utils.ThreadCommand + """ + self.status.update(edict(initialized=False, info="", x_axis=None, y_axis=None, controller=None)) + try: + if self.settings['controller_status'] == "Slave": + if controller is None: + raise Exception('no controller has been defined externally while this detector is a slave one') + else: + self.controller = 'A Nidaqmx task' + else: + self.update_task() + + #actions to perform in order to set properly the settings tree options + self.commit_settings(self.settings.child('NIDAQ_type')) + + self.status.info = "Plugin Initialized" + self.status.initialized = True + self.status.controller = controller + return self.status + + except Exception as e: + self.emit_status(ThreadCommand('Update_Status', [str(e), 'log'])) + self.status.info = str(e) + self.status.initialized = False + return self.status + + def grab_data(self, Naverage=1, **kwargs): + """ + | grab the current values with NIDAQ profile procedure. + | + | Send the data_grabed_signal once done. + + =============== ======== =============================================== + **Parameters** **Type** **Description** + *Naverage* int Number of values to average + =============== ======== =============================================== + + See Also + -------- + DAQ_NIDAQ_source + """ + update = False + if 'live' in kwargs: + if kwargs['live'] != self.live: + update = True + self.live = kwargs['live'] + if update: + self.update_task() + + if self.settings['NIDAQ_type'] == DAQ_NIDAQ_source(0).name: #analog input + if self.c_callback is None: + self.register_callback(self.emit_data) + elif self.settings['NIDAQ_type'] == DAQ_NIDAQ_source(1).name: #counter input + self.timer.start(self.settings['counter_settings', 'counting_time']) + self.waitTaskDone() + self.start() + + def emit_data(self, taskhandle, status, callbackdata): + channels_name = [ch.name for ch in self.channels] + + data_tot = [] + data = self.readAnalog(len(self.channels), self.clock_settings) + N = self.clock_settings.Nsamples + if self.control_type == "0D": + for ind in range(len(self.channels)): + data_tot.append(np.array([np.mean(data[ind*N:(ind+1)*N])])) + self.data_grabed_signal.emit([DataFromPlugins(name='NI AI', data=data_tot, dim='Data0D', + labels=channels_name)]) + else: + for ind in range(len(self.channels)): + data_tot.append(data[ind*N:(ind+1)*N]) + self.data_grabed_signal.emit([ + DataFromPlugins(name='NI AI', data=data_tot, dim='Data1D', + labels=channels_name, + axes=[Axis(data=np.linspace(0, N / self.clock_settings.frequency, N), + index=0)])]) + return 0 #mandatory for the PyDAQmx callback + + def counter_done(self): + channels_name = [ch.name for ch in self.channels] + data_counter = self.readCounter(len(self.channels), + self.settings['counter_settings', 'counting_time'] * 1e-3) + self.data_grabed_signal.emit([DataFromPlugins(name='NI Counter', data=[data_counter / 1e-3], dim='Data0D', + labels=channels_name,)]) + #y_axis=Axis(label='Count Number', units='1/s'))]) + self.task.StopTask() + + +class DAQ_NIDAQmx_Actuator(DAQ_Move_base, DAQ_NIDAQmx_base): + """ + Wrapper object to access the Mock fonctionnalities, similar wrapper for all controllers. + + =============== ============== + **Attributes** **Type** + *params* dictionnary + =============== ============== + """ + _controller_units = 'Volts' + is_multiaxes = False # set to True if this plugin is controlled for a multiaxis controller (with a unique communication link) + stage_names = [] # "list of strings of the multiaxes + + params = DAQ_NIDAQmx_base.params +[ + # elements to be added here as dicts in order to control your custom stage + ############ + {'title': 'MultiAxes:', 'name': 'multiaxes', 'type': 'group', 'visible': is_multiaxes, + 'children': [ + {'title': 'is Multiaxes:', 'name': 'ismultiaxes', 'type': 'bool', 'value': is_multiaxes, + 'default': False}, + {'title': 'Status:', 'name': 'multi_status', 'type': 'list', 'value': 'Master', + 'limits': ['Master', 'Slave']}, + {'title': 'Axis:', 'name': 'axis', 'type': 'list', 'limits': stage_names}, + + ]}] + actuator_params + + def __init__(self, parent=None, params_state=None, control_type="Actuator"): + DAQ_Move_base.__init__(self, parent, params_state) # defines settings attribute and various other methods + DAQ_NIDAQmx_base.__init__(self) + + self.control_type = "Actuator" # could be "0D", "1D" or "Actuator" + self.settings.child('NIDAQ_type').setLimits(['Analog_Output', 'Digital_Output']) + + self.settings.child('clock_settings', 'Nsamples').setValue(1) + + def get_actuator_value(self) -> DataActuator: + """Get the current position from the hardware with scaling conversion. + + Returns + ------- + float: The position obtained after scaling conversion. + """ + + pos = self.target_position + ## + + pos = self.get_position_with_scaling(pos) + self.emit_status(ThreadCommand('check_position', [pos])) + return pos + + + def commit_settings(self, param): + """ + | Activate any parameter changes on the PI_GCS2 hardware. + | + | Called after a param_tree_changed signal from DAQ_Move_main. + + """ + + DAQ_NIDAQmx_base.commit_settings(self, param) + if param.name() == 'waveform': + if param.value() == 'DC': + self.settings.child('ao_settings', 'cont_param').setValue('offset') + self.settings.child('ao_settings', 'cont_param').show(not param.value() == 'DC') + self.settings.child('ao_settings', 'waveform_settings').show(not param.value() == 'DC') + + if param.parent() is not None: + if param.parent().name() == 'ao_channels': + device = param.opts['title'].split('/')[0] + self.settings.child('clock_settings', 'frequency').setOpts(max=self.getAOMaxRate(device)) + + ranges = self.getAOVoltageRange(device) + param.child('voltage_settings', 'volt_min').setOpts(limits=[r[0] for r in ranges]) + param.child('voltage_settings', 'volt_max').setOpts(limits=[r[1] for r in ranges]) + + 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 + ------- + self.status (edict): with initialization status: three fields: + * info (str) + * controller (object) initialized controller + *initialized: (bool): False if initialization failed otherwise True + """ + + try: + # initialize the stage and its controller status + # controller is an object that may be passed to other instances of DAQ_Move_Mock in case + # of one controller controlling multiactuators (or detector) + + self.status.update(edict(info="", controller=None, initialized=False)) + + # check whether this stage is controlled by a multiaxe controller (to be defined for each plugin) + # if multiaxes then init the controller here if Master state otherwise use external controller + if self.settings['multiaxes', 'ismultiaxes'] and self.settings['multiaxes', + 'multi_status'] == "Slave": + if controller is None: + raise Exception('no controller has been defined externally while this axe is a slave one') + else: + self.controller = controller + else: + self.controller = 'A Nidaqmx task' + self.update_task() + + # actions to perform in order to set properly the settings tree options + self.commit_settings(self.settings.child('NIDAQ_type')) + + self.status.info = "Plugin Initialized" + self.status.controller = self.controller + self.status.initialized = True + return self.status + + except Exception as e: + self.emit_status(ThreadCommand('Update_Status', [getLineInfo() + str(e), 'log'])) + self.status.info = getLineInfo() + str(e) + self.status.initialized = False + return self.status + + def calulate_waveform(self, value): + waveform = self.settings['ao_settings', 'waveform'] + if waveform == 'DC': + values = np.array([value]) + else: + Nsamples = self.settings['clock_settings', 'Nsamples'] + freq = self.settings['clock_settings', 'frequency'] + time = np.linspace(0, Nsamples / freq, Nsamples, endpoint=False) + + freq0 = self.settings['ao_settings', 'waveform_settings', 'frequency'] + amp = self.settings['ao_settings', 'waveform_settings', 'amplitude'] + offset = self.settings['ao_settings', 'waveform_settings', 'offset'] + if waveform == 'Sinus': + values = offset + amp * np.sin(2*np.pi*freq0*time) + elif waveform == 'Ramp': + values = offset + amp * np.linspace(0, 1, Nsamples) + + return values + + + def move_Abs(self, position): + """ Move the actuator to the absolute target defined by position + + Parameters + ---------- + position: (flaot) value of the absolute target positioning + """ + + position = self.check_bound(position) # if user checked bounds, the defined bounds are applied here + position = self.set_position_with_scaling(position) # apply scaling if the user specified one + if self.settings['NIDAQ_type'] == 'Analog_Output': + self.settings.child('ao_settings', 'waveform_settings', + self.settings['ao_settings', 'cont_param']).setValue(position) + values = self.calulate_waveform(position) + self.target_position = position + + self.stop() + + if len(values) == 1: + self.writeAnalog(len(values), 1, values, autostart=True) + self.current_position = self.check_position() + self.move_done() + else: + if self.c_callback is None: + self.register_callback(self.move_done_callback) + self.writeAnalog(len(values), 1, values, autostart=False) + self.task.StartTask() + elif self.settings['NIDAQ_type'] == 'Digital_Output': + self.writeDigital(1, np.array([position], dtype=np.uint8), autostart=True) + + def move_done_callback(self, taskhandle, status, callbackdata): + self.current_position = self.check_position() + QtWidgets.QApplication.processEvents() + self.move_done() + self.task.StopTask() + return 0 + def move_Rel(self, position): + """ Move the actuator to the relative target actuator value defined by position + Parameters + ---------- + position: (flaot) value of the relative target positioning + """ + + position = self.check_bound(self.current_position + position) - self.current_position + self.target_position = position + self.current_position + if self.settings['NIDAQ_type'] == 'Analog_Output': + self.settings.child('ao_settings', 'waveform_settings', + self.settings['ao_settings', 'cont_param']).setValue(self.target_position) + + values = self.calulate_waveform(self.target_position) + + self.stop() + + if len(values) == 1: + self.writeAnalog(len(values), 1, values, autostart=True) + self.current_position = self.check_position() + self.move_done() + else: + if self.c_callback is None: + self.register_callback(self.move_done_callback) + self.writeAnalog(len(values), 1, values, autostart=False) + self.task.StartTask() + elif self.settings['NIDAQ_type'] == 'Digital_Output': + self.writeDigital(1, np.array([self.target_position], dtype=np.uint8), autostart=True) + + def move_Home(self): + """ + Send the update status thread command. + See Also + -------- + daq_utils.ThreadCommand + """ + + self.stop() + + if self.c_callback is None: + self.register_callback(self.move_done_callback) + self.writeAnalog(1, 1, np.array([0.])) + self.task.StartTask() + + def stop_motion(self): + """ + Call the specific move_done function (depending on the hardware). + + See Also + -------- + move_done + """ + ## TODO for your custom plugin + self.stop() + self.emit_status(ThreadCommand('Update_Status', ['Some info you want to log'])) + self.move_done() # to let the interface know the actuator stopped + ############################## \ No newline at end of file From 056e4c828efcc52b0110efccf5bcd8109d14a48b Mon Sep 17 00:00:00 2001 From: Sebastien Date: Mon, 27 Jan 2025 10:54:00 +0100 Subject: [PATCH 054/140] NIDAQmx_Move not implemented yet --- .../daq_move_plugins/daq_move_DAQmx.py | 2 +- .../national_instruments/NIDAQmx_Move.py | 246 ------------------ 2 files changed, 1 insertion(+), 247 deletions(-) delete mode 100644 src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_Move.py diff --git a/src/pymodaq_plugins_daqmx/daq_move_plugins/daq_move_DAQmx.py b/src/pymodaq_plugins_daqmx/daq_move_plugins/daq_move_DAQmx.py index f627b51..0fb267d 100644 --- a/src/pymodaq_plugins_daqmx/daq_move_plugins/daq_move_DAQmx.py +++ b/src/pymodaq_plugins_daqmx/daq_move_plugins/daq_move_DAQmx.py @@ -1,4 +1,4 @@ -from ..hardware.national_instruments.NIDAQmx_Move import DAQ_NIDAQmx_Actuator +from ..hardware.national_instruments.daq_NIDAQmx import DAQ_NIDAQmx_Actuator class DAQ_Move_DAQmx(DAQ_NIDAQmx_Actuator): diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_Move.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_Move.py deleted file mode 100644 index 3ad598d..0000000 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_Move.py +++ /dev/null @@ -1,246 +0,0 @@ -import numpy as np -from easydict import EasyDict as edict -from qtpy import QtWidgets -from pymodaq.control_modules.move_utility_classes import DAQ_Move_base, comon_parameters as actuator_params -from pymodaq_plugins_daqmx.hardware.national_instruments.NIDAQmx_base import DAQ_NIDAQmx_base -from pymodaq.utils.daq_utils import ThreadCommand, getLineInfo -from pymodaq.utils.data import DataActuator -from pymodaq.utils.logger import set_logger, get_module_name -logger = set_logger(get_module_name(__file__)) - - -class DAQ_NIDAQmx_Actuator(DAQ_Move_base, DAQ_NIDAQmx_base): - """ - Wrapper object to access the Mock fonctionnalities, similar wrapper for all controllers. - - =============== ============== - **Attributes** **Type** - *params* dictionnary - =============== ============== - """ - _controller_units = 'Volts' - is_multiaxes = False # set to True if this plugin is controlled for a multiaxis controller (with a unique communication link) - stage_names = [] # "list of strings of the multiaxes - - params = DAQ_NIDAQmx_base.params + [ - # elements to be added here as dicts in order to control your custom stage - ############ - {'title': 'MultiAxes:', 'name': 'multiaxes', 'type': 'group', 'visible': is_multiaxes, - 'children': [ - {'title': 'is Multiaxes:', 'name': 'ismultiaxes', 'type': 'bool', 'value': is_multiaxes, - 'default': False}, - {'title': 'Status:', 'name': 'multi_status', 'type': 'list', 'value': 'Master', - 'limits': ['Master', 'Slave']}, - {'title': 'Axis:', 'name': 'axis', 'type': 'list', 'limits': stage_names}, - ]}] + actuator_params() - - def __init__(self, parent=None, params_state=None, control_type="Actuator"): - DAQ_Move_base.__init__(self, parent, params_state) # defines settings attribute and various other methods - DAQ_NIDAQmx_base.__init__(self) - - self.control_type = "Actuator" # could be "0D", "1D" or "Actuator" - self.settings.child('NIDAQ_type').setLimits(['Analog_Output', 'Digital_Output']) - - self.settings.child('clock_settings', 'Nsamples').setValue(1) - - def get_actuator_value(self) -> DataActuator: - """Get the current position from the hardware with scaling conversion. - - Returns - ------- - float: The position obtained after scaling conversion. - """ - - pos = self.target_position - ## - - pos = self.get_position_with_scaling(pos) - self.emit_status(ThreadCommand('check_position', [pos])) - return pos - - def commit_settings(self, param): - """ - | Activate any parameter changes on the PI_GCS2 hardware. - | - | Called after a param_tree_changed signal from DAQ_Move_main. - - """ - - DAQ_NIDAQmx_base.commit_settings(self, param) - if param.name() == 'waveform': - if param.value() == 'DC': - self.settings.child('ao_settings', 'cont_param').setValue('offset') - self.settings.child('ao_settings', 'cont_param').show(not param.value() == 'DC') - self.settings.child('ao_settings', 'waveform_settings').show(not param.value() == 'DC') - - if param.parent() is not None: - if param.parent().name() == 'ao_channels': - device = param.opts['title'].split('/')[0] - self.settings.child('clock_settings', 'frequency').setOpts(max=self.getAOMaxRate(device)) - - ranges = self.getAOVoltageRange(device) - param.child('voltage_settings', 'volt_min').setOpts(limits=[r[0] for r in ranges]) - param.child('voltage_settings', 'volt_max').setOpts(limits=[r[1] for r in ranges]) - - 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 - ------- - self.status (easydict): with initialization status: three fields: - * info (str) - * controller (object) initialized controller - *initialized: (bool): False if initialization failed otherwise True - """ - - try: - # initialize the stage and its controller status - # controller is an object that may be passed to other instances of DAQ_Move_Mock in case - # of one controller controlling multiactuators (or detector) - - self.status.update(edict(info="", controller=None, initialized=False)) - - # check whether this stage is controlled by a multiaxe controller (to be defined for each plugin) - # if multiaxes then init the controller here if Master state otherwise use external controller - if self.settings['multiaxes', 'ismultiaxes'] and self.settings['multiaxes', - 'multi_status'] == "Slave": - if controller is None: - raise Exception('no controller has been defined externally while this axe is a slave one') - else: - self.controller = controller - else: - self.controller = 'A Nidaqmx task' - self.update_task() - - # actions to perform in order to set properly the settings tree options - self.commit_settings(self.settings.child('NIDAQ_type')) - - self.status.info = "Plugin Initialized" - self.status.controller = self.controller - self.status.initialized = True - return self.status - - except Exception as e: - self.emit_status(ThreadCommand('Update_Status', [getLineInfo() + str(e), 'log'])) - self.status.info = getLineInfo() + str(e) - self.status.initialized = False - return self.status - - def calulate_waveform(self, value): - waveform = self.settings['ao_settings', 'waveform'] - if waveform == 'DC': - values = np.array([value]) - else: - Nsamples = self.settings['clock_settings', 'Nsamples'] - freq = self.settings['clock_settings', 'frequency'] - time = np.linspace(0, Nsamples / freq, Nsamples, endpoint=False) - - freq0 = self.settings['ao_settings', 'waveform_settings', 'frequency'] - amp = self.settings['ao_settings', 'waveform_settings', 'amplitude'] - offset = self.settings['ao_settings', 'waveform_settings', 'offset'] - if waveform == 'Sinus': - values = offset + amp * np.sin(2 * np.pi * freq0 * time) - elif waveform == 'Ramp': - values = offset + amp * np.linspace(0, 1, Nsamples) - - return values - - def move_abs(self, position): - """ Move the actuator to the absolute target defined by position - - Parameters - ---------- - position: (flaot) value of the absolute target positioning - """ - - position = self.check_bound(position) # if user checked bounds, the defined bounds are applied here - position = self.set_position_with_scaling(position) # apply scaling if the user specified one - if self.settings['NIDAQ_type'] == 'Analog_Output': - self.settings.child('ao_settings', 'waveform_settings', - self.settings['ao_settings', 'cont_param']).setValue(position) - values = self.calulate_waveform(position) - self.target_position = position - - self.stop() - - if len(values) == 1: - self.writeAnalog(len(values), 1, values, autostart=True) - self.current_position = self.check_position() - self.move_done() - else: - if self.c_callback is None: - self.register_callback(self.move_done_callback) - self.writeAnalog(len(values), 1, values, autostart=False) - self.task.StartTask() - elif self.settings['NIDAQ_type'] == 'Digital_Output': - self.writeDigital(1, np.array([position], dtype=np.uint8), autostart=True) - - def move_done_callback(self, taskhandle, status, callbackdata): - self.current_position = self.check_position() - QtWidgets.QApplication.processEvents() - self.move_done() - self.task.StopTask() - return 0 - - def move_rel(self, position): - """ Move the actuator to the relative target actuator value defined by position - - Parameters - ---------- - position: (flaot) value of the relative target positioning - """ - - position = self.check_bound(self.current_position + position) - self.current_position - self.target_position = position + self.current_position - if self.settings['NIDAQ_type'] == 'Analog_Output': - self.settings.child('ao_settings', 'waveform_settings', - self.settings['ao_settings', 'cont_param']).setValue(self.target_position) - - values = self.calulate_waveform(self.target_position) - - self.stop() - - if len(values) == 1: - self.writeAnalog(len(values), 1, values, autostart=True) - self.current_position = self.check_position() - self.move_done() - else: - if self.c_callback is None: - self.register_callback(self.move_done_callback) - self.writeAnalog(len(values), 1, values, autostart=False) - self.task.StartTask() - elif self.settings['NIDAQ_type'] == 'Digital_Output': - self.writeDigital(1, np.array([self.target_position], dtype=np.uint8), autostart=True) - - def move_home(self): - """ - Send the update status thread command. - See Also - -------- - daq_utils.ThreadCommand - """ - - self.stop() - - if self.c_callback is None: - self.register_callback(self.move_done_callback) - self.writeAnalog(1, 1, np.array([0.])) - self.task.StartTask() - - def stop_motion(self): - """ - Call the specific move_done function (depending on the hardware). - - See Also - -------- - move_done - """ - - ## TODO for your custom plugin - self.stop() - self.emit_status(ThreadCommand('Update_Status', ['Some info you want to log'])) - self.move_done() # to let the interface know the actuator stopped From 8d3cb707b99f909ee5666863f7a9295dee1ad494 Mon Sep 17 00:00:00 2001 From: Sebastien Date: Mon, 27 Jan 2025 11:13:46 +0100 Subject: [PATCH 055/140] NIDAQmx_Move not implemented yet --- .../national_instruments/daq_NIDAQmx_Move.py | 246 ------------------ 1 file changed, 246 deletions(-) delete mode 100644 src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx_Move.py diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx_Move.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx_Move.py deleted file mode 100644 index 396e910..0000000 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daq_NIDAQmx_Move.py +++ /dev/null @@ -1,246 +0,0 @@ -import numpy as np -from easydict import EasyDict as edict -from qtpy import QtWidgets -from pymodaq.control_modules.move_utility_classes import DAQ_Move_base, comon_parameters as actuator_params -from pymodaq_plugins_daqmx.hardware.national_instruments.daq_NIDAQmx import DAQ_NIDAQmx_base -from pymodaq.utils.daq_utils import ThreadCommand, getLineInfo -from pymodaq.utils.data import DataActuator -from pymodaq.utils.logger import set_logger, get_module_name -logger = set_logger(get_module_name(__file__)) - - -class DAQ_NIDAQmx_Actuator(DAQ_Move_base, DAQ_NIDAQmx_base): - """ - Wrapper object to access the Mock fonctionnalities, similar wrapper for all controllers. - - =============== ============== - **Attributes** **Type** - *params* dictionnary - =============== ============== - """ - _controller_units = 'Volts' - is_multiaxes = False # set to True if this plugin is controlled for a multiaxis controller (with a unique communication link) - stage_names = [] # "list of strings of the multiaxes - - params = DAQ_NIDAQmx_base.params + [ - # elements to be added here as dicts in order to control your custom stage - ############ - {'title': 'MultiAxes:', 'name': 'multiaxes', 'type': 'group', 'visible': is_multiaxes, - 'children': [ - {'title': 'is Multiaxes:', 'name': 'ismultiaxes', 'type': 'bool', 'value': is_multiaxes, - 'default': False}, - {'title': 'Status:', 'name': 'multi_status', 'type': 'list', 'value': 'Master', - 'limits': ['Master', 'Slave']}, - {'title': 'Axis:', 'name': 'axis', 'type': 'list', 'limits': stage_names}, - ]}] + actuator_params() - - def __init__(self, parent=None, params_state=None, control_type="Actuator"): - DAQ_Move_base.__init__(self, parent, params_state) # defines settings attribute and various other methods - DAQ_NIDAQmx_base.__init__(self) - - self.control_type = "Actuator" # could be "0D", "1D" or "Actuator" - self.settings.child('NIDAQ_type').setLimits(['Analog_Output', 'Digital_Output']) - - self.settings.child('clock_settings', 'Nsamples').setValue(1) - - def get_actuator_value(self) -> DataActuator: - """Get the current position from the hardware with scaling conversion. - - Returns - ------- - float: The position obtained after scaling conversion. - """ - - pos = self.target_position - ## - - pos = self.get_position_with_scaling(pos) - self.emit_status(ThreadCommand('check_position', [pos])) - return pos - - def commit_settings(self, param): - """ - | Activate any parameter changes on the PI_GCS2 hardware. - | - | Called after a param_tree_changed signal from DAQ_Move_main. - - """ - - DAQ_NIDAQmx_base.commit_settings(self, param) - if param.name() == 'waveform': - if param.value() == 'DC': - self.settings.child('ao_settings', 'cont_param').setValue('offset') - self.settings.child('ao_settings', 'cont_param').show(not param.value() == 'DC') - self.settings.child('ao_settings', 'waveform_settings').show(not param.value() == 'DC') - - if param.parent() is not None: - if param.parent().name() == 'ao_channels': - device = param.opts['title'].split('/')[0] - self.settings.child('clock_settings', 'frequency').setOpts(max=self.getAOMaxRate(device)) - - ranges = self.getAOVoltageRange(device) - param.child('voltage_settings', 'volt_min').setOpts(limits=[r[0] for r in ranges]) - param.child('voltage_settings', 'volt_max').setOpts(limits=[r[1] for r in ranges]) - - 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 - ------- - self.status (easydict): with initialization status: three fields: - * info (str) - * controller (object) initialized controller - *initialized: (bool): False if initialization failed otherwise True - """ - - try: - # initialize the stage and its controller status - # controller is an object that may be passed to other instances of DAQ_Move_Mock in case - # of one controller controlling multiactuators (or detector) - - self.status.update(edict(info="", controller=None, initialized=False)) - - # check whether this stage is controlled by a multiaxe controller (to be defined for each plugin) - # if multiaxes then init the controller here if Master state otherwise use external controller - if self.settings['multiaxes', 'ismultiaxes'] and self.settings['multiaxes', - 'multi_status'] == "Slave": - if controller is None: - raise Exception('no controller has been defined externally while this axe is a slave one') - else: - self.controller = controller - else: - self.controller = 'A Nidaqmx task' - self.update_task() - - # actions to perform in order to set properly the settings tree options - self.commit_settings(self.settings.child('NIDAQ_type')) - - self.status.info = "Plugin Initialized" - self.status.controller = self.controller - self.status.initialized = True - return self.status - - except Exception as e: - self.emit_status(ThreadCommand('Update_Status', [getLineInfo() + str(e), 'log'])) - self.status.info = getLineInfo() + str(e) - self.status.initialized = False - return self.status - - def calulate_waveform(self, value): - waveform = self.settings['ao_settings', 'waveform'] - if waveform == 'DC': - values = np.array([value]) - else: - Nsamples = self.settings['clock_settings', 'Nsamples'] - freq = self.settings['clock_settings', 'frequency'] - time = np.linspace(0, Nsamples / freq, Nsamples, endpoint=False) - - freq0 = self.settings['ao_settings', 'waveform_settings', 'frequency'] - amp = self.settings['ao_settings', 'waveform_settings', 'amplitude'] - offset = self.settings['ao_settings', 'waveform_settings', 'offset'] - if waveform == 'Sinus': - values = offset + amp * np.sin(2 * np.pi * freq0 * time) - elif waveform == 'Ramp': - values = offset + amp * np.linspace(0, 1, Nsamples) - - return values - - def move_abs(self, position): - """ Move the actuator to the absolute target defined by position - - Parameters - ---------- - position: (float) value of the absolute target positioning - """ - - position = self.check_bound(position) # if user checked bounds, the defined bounds are applied here - position = self.set_position_with_scaling(position) # apply scaling if the user specified one - if self.settings['NIDAQ_type'] == 'Analog_Output': - self.settings.child('ao_settings', 'waveform_settings', - self.settings['ao_settings', 'cont_param']).setValue(position) - values = self.calulate_waveform(position) - self.target_position = position - - self.stop() - - if len(values) == 1: - self.writeAnalog(len(values), 1, values, autostart=True) - self.current_position = self.check_position() - self.move_done() - else: - if self.c_callback is None: - self.register_callback(self.move_done_callback) - self.writeAnalog(len(values), 1, values, autostart=False) - self.task.StartTask() - elif self.settings['NIDAQ_type'] == 'Digital_Output': - self.writeDigital(1, np.array([position], dtype=np.uint8), autostart=True) - - def move_done_callback(self, taskhandle, status, callbackdata): - self.current_position = self.check_position() - QtWidgets.QApplication.processEvents() - self.move_done() - self.task.StopTask() - return 0 - - def move_rel(self, position): - """ Move the actuator to the relative target actuator value defined by position - - Parameters - ---------- - position: (float) value of the relative target positioning - """ - - position = self.check_bound(self.current_position + position) - self.current_position - self.target_position = position + self.current_position - if self.settings['NIDAQ_type'] == 'Analog_Output': - self.settings.child('ao_settings', 'waveform_settings', - self.settings['ao_settings', 'cont_param']).setValue(self.target_position) - - values = self.calulate_waveform(self.target_position) - - self.stop() - - if len(values) == 1: - self.writeAnalog(len(values), 1, values, autostart=True) - self.current_position = self.check_position() - self.move_done() - else: - if self.c_callback is None: - self.register_callback(self.move_done_callback) - self.writeAnalog(len(values), 1, values, autostart=False) - self.task.StartTask() - elif self.settings['NIDAQ_type'] == 'Digital_Output': - self.writeDigital(1, np.array([self.target_position], dtype=np.uint8), autostart=True) - - def move_home(self): - """ - Send the update status thread command. - See Also - -------- - daq_utils.ThreadCommand - """ - - self.stop() - - if self.c_callback is None: - self.register_callback(self.move_done_callback) - self.writeAnalog(1, 1, np.array([0.])) - self.task.StartTask() - - def stop_motion(self): - """ - Call the specific move_done function (depending on the hardware). - - See Also - -------- - move_done - """ - - ## TODO for your custom plugin - self.stop() - self.emit_status(ThreadCommand('Update_Status', ['Some info you want to log'])) - self.move_done() # to let the interface know the actuator stopped From 47d6e13c6f502b13f957663dab925b15cd2cbd88 Mon Sep 17 00:00:00 2001 From: Sebastien Date: Mon, 27 Jan 2025 16:25:40 +0100 Subject: [PATCH 056/140] UsageTypeCI added for future counter implementation --- .../hardware/national_instruments/daqmxni.py | 60 +++++++++++-------- 1 file changed, 35 insertions(+), 25 deletions(-) diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py index 039fb6e..2955c55 100644 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py @@ -408,22 +408,31 @@ def update_task(self, channels=[], clock_settings=ClockSettings(), trigger_setti if err_code: status = self.DAQmxGetErrorString(err_code) raise IOError(status) - elif channel.source == 'Counter': # counter + elif channel.source == ChannelType.ANALOG_OUTPUT: # Analog_Output + try: + if channel.analog_type == UsageTypeAI.VOLTAGE: + self._task.ao_channels.add_ao_voltage_chan(channel.name, "", + channel.value_min, + channel.value_max, + VoltageUnits.VOLTS, None) + + elif channel.analog_type == UsageTypeAI.CURRENT: + self._task.ao_channels.add_ao_current_chan(channel.name, "", + channel.value_min, + channel.value_max, + VoltageUnits.VOLTS, None) + except DaqError as e: + err_code = e.error_code + if not not err_code: + status = self.DAQmxGetErrorString(err_code) + raise IOError(status) + elif channel.source == ChannelType.COUNTER_INPUT: # counter input try: - if channel.counter_type == "Edge Counter": + if channel.counter_type == UsageTypeCI.COUNT_EDGES: self._task.ci_channels.add_ci_count_edges_chan(channel.name, "", channel.edge, 0, CountDirection.COUNT_UP) - - elif channel.counter_type == "Clock Output": - self._task.co_channels.add_co_pulse_chan_freq(channel.name, "clock task", - FrequencyUnits.HZ, - Level.LOW, - 0, - channel.clock_frequency, - 0.5) - - elif channel.counter_type == "SemiPeriod Input": + elif channel.counter_type == UsageTypeCI.PULSE_WIDTH_DIGITAL_SEMI_PERIOD: self._task.ci_channels.add_ci_semi_period_chan(channel.name, "counter task", 0, # expected min channel.value_max, # expected max @@ -439,21 +448,23 @@ def update_task(self, channels=[], clock_settings=ClockSettings(), trigger_setti if not not err_code: status = self.DAQmxGetErrorString(err_code) raise IOError(status) - elif channel.source == ChannelType.ANALOG_OUTPUT: # Analog_Output + elif channel.source == ChannelType.COUNTER_OUTPUT: # counter output try: - if channel.analog_type == UsageTypeAI.VOLTAGE: - self._task.ao_channels.add_ao_voltage_chan(channel.name, "", - channel.value_min, - channel.value_max, - VoltageUnits.VOLTS, None) + if channel.counter_type == UsageTypeCO.PULSE_FREQUENCY: + self._task.co_channels.add_co_pulse_chan_freq(channel.name, "clock task", + FrequencyUnits.HZ, + Level.LOW, + 0, + channel.clock_frequency, + 0.5) - elif channel.analog_type == UsageTypeAI.CURRENT: - self._task.ao_channels.add_ao_current_chan(channel.name, "", - channel.value_min, - channel.value_max, - VoltageUnits.VOLTS, None) except DaqError as e: err_code = e.error_code + + if not not err_code: + status = self.DAQmxGetErrorString(err_code) + raise IOError(status) + if not not err_code: status = self.DAQmxGetErrorString(err_code) raise IOError(status) @@ -503,7 +514,7 @@ def update_task(self, channels=[], clock_settings=ClockSettings(), trigger_setti for channel in channels: if not trigger_settings.enable: - if channel.source == 'Counter': + if channel.source == ChannelType.COUNTER_INPUT: pass # Maybe here adding the configuration fastCTr0 with Ctr1 etc...? else: pass @@ -529,7 +540,6 @@ def register_callback(self, callback, event='done', nsamples=1): if event == 'done': self._task.register_done_event(callback) - # NOT SURE HERE elif event == 'sample': self._task.register_every_n_samples_acquired_into_buffer_event(1, callback) elif event == 'Nsamples': From 662d2f4a3654a51688768a2786aa92644168d2d7 Mon Sep 17 00:00:00 2001 From: Sebastien Date: Mon, 27 Jan 2025 16:47:49 +0100 Subject: [PATCH 057/140] [completion] --- .../hardware/national_instruments/daqmxni.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py index 2955c55..cf7ebe8 100644 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py @@ -410,13 +410,13 @@ def update_task(self, channels=[], clock_settings=ClockSettings(), trigger_setti raise IOError(status) elif channel.source == ChannelType.ANALOG_OUTPUT: # Analog_Output try: - if channel.analog_type == UsageTypeAI.VOLTAGE: + if channel.analog_type == UsageTypeAO.VOLTAGE: self._task.ao_channels.add_ao_voltage_chan(channel.name, "", channel.value_min, channel.value_max, VoltageUnits.VOLTS, None) - elif channel.analog_type == UsageTypeAI.CURRENT: + elif channel.analog_type == UsageTypeAO.CURRENT: self._task.ao_channels.add_ao_current_chan(channel.name, "", channel.value_min, channel.value_max, From 2e8953a6355de4a8480888064561a23b6c0b2b8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Weber?= Date: Wed, 29 Jan 2025 15:54:32 +0100 Subject: [PATCH 058/140] using scm to get version from github tags --- .github/workflows/python-publish.yml | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml index cc1ffb5..76fca2c 100644 --- a/.github/workflows/python-publish.yml +++ b/.github/workflows/python-publish.yml @@ -13,19 +13,26 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Set up Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: '3.x' - name: Install dependencies run: | python -m pip install --upgrade pip - pip install setuptools wheel twine toml - - name: Build and publish + pip install hatch hatchling toml + - name: Get history and tags for SCM versioning to work + run: | + git branch + git fetch --prune --unshallow + git fetch --depth=1 origin +refs/tags/*:refs/tags/* + hatch version + - name: Build + run: hatch build + - name: publish env: - TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} - TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} + HATCH_INDEX_USER: ${{ secrets.PYPI_USERNAME }} + HATCH_INDEX_AUTH: ${{ secrets.PYPI_PASSWORD }} run: | - python setup.py sdist bdist_wheel - twine upload dist/* + hatch publish From 63d46640fe2bf5d3ea8e1a94929910464d6f55fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Weber?= Date: Wed, 29 Jan 2025 16:00:04 +0100 Subject: [PATCH 059/140] using pyproject and dynamic version tag --- setup.py | 62 --------------------- src/pymodaq_plugins_daqmx/resources/VERSION | 1 - 2 files changed, 63 deletions(-) delete mode 100644 setup.py delete mode 100644 src/pymodaq_plugins_daqmx/resources/VERSION diff --git a/setup.py b/setup.py deleted file mode 100644 index 08568a0..0000000 --- a/setup.py +++ /dev/null @@ -1,62 +0,0 @@ -from setuptools import setup, find_packages -import toml - -config = toml.load('./plugin_info.toml') -SHORT_PLUGIN_NAME = config['plugin-info']['SHORT_PLUGIN_NAME'] -PLUGIN_NAME = f"pymodaq_plugins_{config['plugin-info']['SHORT_PLUGIN_NAME']}" - - -from pathlib import Path - -with open(str(Path(__file__).parent.joinpath(f'src/{PLUGIN_NAME}/resources/VERSION')), 'r') as fvers: - version = fvers.read().strip() - - -with open('README.rst') as fd: - long_description = fd.read() - -setupOpts = dict( - name=PLUGIN_NAME, - description=config['plugin-info']['description'], - long_description=long_description, - license=config['plugin-info']['license'], - url=config['plugin-info']['package-url'], - author=config['plugin-info']['author'], - author_email=config['plugin-info']['author-email'], - classifiers=[ - "Programming Language :: Python :: 3", - "Development Status :: 5 - Production/Stable", - "Environment :: Other Environment", - "Intended Audience :: Science/Research", - "Topic :: Scientific/Engineering :: Human Machine Interfaces", - "Topic :: Scientific/Engineering :: Visualization", - "License :: OSI Approved :: MIT License", - "Operating System :: OS Independent", - "Topic :: Software Development :: Libraries :: Python Modules", - "Topic :: Software Development :: User Interfaces", - ], ) - -entrypoints = {} -if 'features' in config: - if config['features'].get('instruments', False): - entrypoints['pymodaq.instruments'] = f'{SHORT_PLUGIN_NAME} = {PLUGIN_NAME}' - if config['features'].get('extensions', False): - entrypoints['pymodaq.extensions'] = f'{SHORT_PLUGIN_NAME} = {PLUGIN_NAME}' - if config['features'].get('pid_models', False): - entrypoints['pymodaq.pid_models'] = f'{SHORT_PLUGIN_NAME} = {PLUGIN_NAME}' - if config['features'].get('h5exporters', False): - entrypoints['pymodaq.h5exporters'] = f'{SHORT_PLUGIN_NAME} = {PLUGIN_NAME}' - if config['features'].get('scanners', False): - entrypoints['pymodaq.scanners'] = f'{SHORT_PLUGIN_NAME} = {PLUGIN_NAME}' -else: - entrypoints['pymodaq.instruments'] = f'{SHORT_PLUGIN_NAME} = {PLUGIN_NAME}' - -setup( - version=version, - packages=find_packages(where='./src'), - package_dir={'': 'src'}, - include_package_data=True, - entry_points=entrypoints, - install_requires=['toml', ]+config['plugin-install']['packages-required'], - **setupOpts -) diff --git a/src/pymodaq_plugins_daqmx/resources/VERSION b/src/pymodaq_plugins_daqmx/resources/VERSION deleted file mode 100644 index 1d0ba9e..0000000 --- a/src/pymodaq_plugins_daqmx/resources/VERSION +++ /dev/null @@ -1 +0,0 @@ -0.4.0 From 159806ec840b38e44bc0dadf1b651d7b1a18462c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Weber?= Date: Fri, 31 Jan 2025 16:29:29 +0100 Subject: [PATCH 060/140] added enum access --- .../hardware/national_instruments/daqmx.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmx.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmx.py index 3e1f3f1..7b43c41 100644 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmx.py +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmx.py @@ -341,17 +341,17 @@ def get_NIDAQ_channels(cls, devices=None, source_type=None): if not not devices: for device in devices: for source in source_type: - if source == DAQ_NIDAQ_source['Analog_Input'].name: # analog input + if source == DAQ_NIDAQ_source.Analog_Input.name: # analog input string = try_string_buffer(PyDAQmx.DAQmxGetDevAIPhysicalChans, device) - elif source == DAQ_NIDAQ_source['Counter'].name: # counter + elif source == DAQ_NIDAQ_source.Counter.name: # counter string = try_string_buffer(PyDAQmx.DAQmxGetDevCIPhysicalChans, device) - elif source == DAQ_NIDAQ_source['Analog_Output'].name: # analog output + elif source == DAQ_NIDAQ_source.Analog_Output.name: # analog output string = try_string_buffer(PyDAQmx.DAQmxGetDevAOPhysicalChans, device) - elif source == DAQ_NIDAQ_source['Digital_Output'].name: # digital output + elif source == DAQ_NIDAQ_source.Digital_Output.name: # digital output string = try_string_buffer(PyDAQmx.DAQmxGetDevDOLines, device) - elif source == DAQ_NIDAQ_source['Digital_Input'].name: # digital output + elif source == DAQ_NIDAQ_source.Digital_Input.name: # digital output string = try_string_buffer(PyDAQmx.DAQmxGetDevDILines, device) - elif source == DAQ_NIDAQ_source['Terminals'].name: # digital output + elif source == DAQ_NIDAQ_source.Terminals.name: # digital output string = try_string_buffer(PyDAQmx.DAQmxGetDevTerminals, device) channels = string.split(', ') From 733f8ef8e3f3f21322ea255399856e6616c2163f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Weber?= Date: Fri, 31 Jan 2025 16:29:38 +0100 Subject: [PATCH 061/140] remove old style version --- src/pymodaq_plugins_daqmx/__init__.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/pymodaq_plugins_daqmx/__init__.py b/src/pymodaq_plugins_daqmx/__init__.py index ca051e5..62ada89 100644 --- a/src/pymodaq_plugins_daqmx/__init__.py +++ b/src/pymodaq_plugins_daqmx/__init__.py @@ -4,5 +4,3 @@ from .utils import Config config = Config() -with open(str(Path(__file__).parent.joinpath('resources/VERSION')), 'r') as fvers: - __version__ = fvers.read().strip() From 867734487cc4fba040135aaed2854e5c056a221a Mon Sep 17 00:00:00 2001 From: Sebastien Date: Tue, 17 Jun 2025 16:55:58 +0200 Subject: [PATCH 062/140] Old plugin class removed --- .../daq_move_DAQmx_MultipleScannerControl.py | 297 ------------------ .../daq_move_DAQmx_ScannerControl.py | 272 ---------------- .../daq_0Dviewer_DAQmx_PLcounter.py | 143 --------- 3 files changed, 712 deletions(-) delete mode 100644 src/pymodaq_plugins_daqmx/daq_move_plugins/daq_move_DAQmx_MultipleScannerControl.py delete mode 100644 src/pymodaq_plugins_daqmx/daq_move_plugins/daq_move_DAQmx_ScannerControl.py delete mode 100755 src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_DAQmx_PLcounter.py diff --git a/src/pymodaq_plugins_daqmx/daq_move_plugins/daq_move_DAQmx_MultipleScannerControl.py b/src/pymodaq_plugins_daqmx/daq_move_plugins/daq_move_DAQmx_MultipleScannerControl.py deleted file mode 100644 index e95e90e..0000000 --- a/src/pymodaq_plugins_daqmx/daq_move_plugins/daq_move_DAQmx_MultipleScannerControl.py +++ /dev/null @@ -1,297 +0,0 @@ -import numpy as np -from pymodaq.control_modules.move_utility_classes import DAQ_Move_base, comon_parameters_fun, main # common set of -# parameters for all actuators -from pymodaq.utils.daq_utils import ThreadCommand # object used to send info back to the main thread -from pymodaq.utils.parameter import Parameter - -from pymodaq_plugins_daqmx.hardware.national_instruments.daqmx_objects import AO_with_clock_DAQmx - -from pymodaq_plugins_daqmx.hardware.national_instruments.daqmx import DAQmx, AOChannel, \ - ClockSettings, DAQ_analog_types, Edge - -import PyDAQmx - - -class DAQ_Move_DAQmx_MultipleScannerControl(DAQ_Move_base): - """Plugin to control a piezo scanners with a NI card. This modules requires a clock channel to handle the - timing of the movement and display the position, and this clock channel is shared between the master and - the slave daq_move created with this module, to allow smooth movements. - - This object inherits all functionality to communicate with PyMoDAQ Module through inheritance via DAQ_Move_base - It then implements the particular communication with the instrument. - - Attributes: - ----------- - controller: object - The particular object that allow the communication with the hardware, in general a python wrapper around the - hardware library - - """ - _controller_units = 'nm' - is_multiaxes = True - axes_names = ['x', 'y', 'z'] - _epsilon = 10 - - params = [ {"title": "Output channel:", "name": "analog_channel", - "type": "list", "limits": DAQmx.get_NIDAQ_channels(source_type="Analog_Output")}, - {'title': 'Clock channel:', 'name': 'clock_channel', 'type': 'list', - 'limits': DAQmx.get_NIDAQ_channels(source_type='Counter')}, - {"title": "Step size (nm)", "name": "step_size", "type": "float", "value": 100.0}, - {"title": "Step time (ms)", "name": "step_time", "type": "float", "value": 10.0}, - {"title": "Conversion factor (nm/V)", "name": "conv_factor", "type": "float", "value": 7500.0} - ] + comon_parameters_fun(is_multiaxes, axes_names) - - def ini_attributes(self): - self.controller = None - self.step_size = 100.0 # in nm! be careful with the scaling param - self.number_steps = 1 - self.conv_factor = 7500.0 - self.scanner_channel = None - self.voltage_list = np.array([0.0]) - self.init_step_index = 0 - self.waiting_to_move = [False, "abs"] - - def get_actuator_value(self): - """Get the current value from the hardware with scaling conversion. - Reading the actual state is tricky with the NI card because the list of voltages - is sent in the buffer immediately, so reading the last written voltage always - yield the target value, independently of the clock timing. - To bypass this issue, instead of reading the written voltage, we read the current - clock index and use it to retrieve the corresponding voltage from our list. - Beware that the index given by GetCOCount start at a value 2*nb_voltages + 1 - and decreases. Therefore, we store the first value returned in self.init_step_index - and use this as a starting point. - - Returns - ------- - float: The position obtained after scaling conversion. - """ - if len(self.voltage_list) > 1: - try: - current_step_index = PyDAQmx.c_ulong() - self.controller.clock.task.GetCOCount(self.controller.clock_channel_name, - PyDAQmx.byref(current_step_index)) - index = self.init_step_index - current_step_index.value - voltage = self.voltage_list[min(int(index/2), len(self.voltage_list)-1)] - except: # when the task did not start - voltage = self.voltage_list[0] - - # if we do only one step, we do not care, there is no timing anyway. - else: - voltage = self.controller.applied_voltages[self.settings.child('multiaxes', 'axis').value()] - # convert voltage to position - pos = voltage * self.conv_factor - pos = self.get_position_with_scaling(pos) - return pos - - def close(self): - """ Terminate the communication protocol""" - # This might be brutal if we are controlling another axis at the same time - self.controller.clock.close() - self.controller.analog.close() - - 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() == "analog_channel": - self.update_task() - elif param.name() == "clock_channel": - self.controller.clock_channel_name = self.settings.child("clock_channel").value() - self.update_task() - elif param.name() == "step_size": - self.step_size = param.value() - elif param.name() == "step_time": - self.controller.clock_frequency = 1e3 / self.settings.child("step_time").value() # time give in ms - elif param.name() == "conv_factor": - self.conv_factor = param.value() - - 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 - """ - # we need a clock task and analog output task. - # The analog output can handle several channels, for the different axis - self.controller = self.ini_stage_init(old_controller=controller, - new_controller=AO_with_clock_DAQmx()) - - self.step_size = self.settings.child("step_size").value() - self.conv_factor = self.settings.child("conv_factor").value() - - # Step time is given in ms by the user - # Clock channel and step time should only be modified on the master actuator - # because the clock is shared. If not master, these parameters are hidden. - if self.settings.child("multiaxes", "multi_status").value() == "Master": - self.controller.clock_frequency = 1e3 / self.settings.child("step_time").value() # time give in ms - self.controller.clock_channel_name = self.settings.child("clock_channel").value() - else: - self.settings.child("step_time").hide() - self.settings.child("clock_channel").hide() - - self.move_done_signal.connect(self.controller.received_move_done) - self.controller.ni_card_ready_for_moving.connect(self.finish_waiting) - - try: - self.update_task() - initialized = True - info = "NI card based piezo scanner control." - self.move_abs(0.0, init=True) # to avoid bad initial positioning because - # we can't read the actual value from the NI card. - except Exception as e: - print(e) - initialized = False - info = "Error" - - return info, initialized - - def move_abs(self, value, init=False): - """ Move the actuator to the absolute target defined by value - - Parameters - ---------- - value: (float) value of the absolute target positioning - """ - if value == 0.0: - value = 1.0 # using 0.0 creates issues - value = self.check_bound(value) # if user checked bounds, the defined bounds are applied here - self.target_value = value - self.set_position_with_scaling(value) # apply scaling if the user specified one - # check if we are already there - if np.abs(self.current_value - self.target_value) < self._epsilon and not init: - # already there - self.emit_status(ThreadCommand('Update_Status', ['Already there.'])) - return - else: - if not self.controller.locked or init: - self.move_scanner(init=init) - self.emit_status(ThreadCommand('Update_Status', ['Absolute movement.'])) - else: - self.waiting_to_move = [True, "abs"] - - def move_rel(self, value): - """ Move the actuator to the relative target actuator value defined by value - - Parameters - ---------- - value: (float) value of the relative target positioning - """ - if np.abs(value) < self._epsilon: - # already there - self.emit_status(ThreadCommand('Update_Status', ['Already there.'])) - return - else: - value = self.check_bound(self.current_value + value) - self.current_value - self.target_value = value + self.current_value - if self.target_value == 0.0: - self.target_value = 1.0 - self.set_position_relative_with_scaling(value) - if not self.controller.locked: - self.move_scanner() - self.emit_status(ThreadCommand('Update_Status', ['Relative movement.'])) - else: - self.waiting_to_move = [True, "rel"] - - def move_home(self): - """Do nothing""" - self.emit_status(ThreadCommand('Update_Status', ['No home position implemented.'])) - - def stop_motion(self): - """Stop the actuator and emits move_done signal""" - self.controller.locked = False - self.waiting_to_move[0] = False - self.controller.stop() - self.emit_status(ThreadCommand('Update_Status', ['Motion stopped.'])) - - def update_task(self): - """ Set up the analog output task in the NI card, and the clock if necessary.""" - min_voltage = self.settings.child("bounds", "min_bound").value()/self.conv_factor - max_voltage = self.settings.child("bounds", "max_bound").value()/self.conv_factor - self.scanner_channel = AOChannel(name=self.settings.child("analog_channel").value(), - source="Analog_Output", - analog_type=DAQ_analog_types.names()[0], - value_min=min_voltage, - value_max=max_voltage) - - # If we do more than one step, we need the clock for the time sampling - # because we want a smooth and slow movement. - if len(self.voltage_list) > 1: - clock_settings_ao = self.controller.set_up_clock(self.number_steps) - else: - # empty clock settings if we do only one step - clock_settings_ao = ClockSettings(source=None, - frequency=1/self.controller.clock_frequency, - Nsamples=1, - edge=Edge.names()[0], - repetition=False) - - self.controller.update_ao_channels(self.scanner_channel, - self.settings.child('multiaxes', 'axis').value(), - clock_settings_ao) - - def prepare_voltage_list(self): - """Generates the list of voltages to move smoothly the scanner.""" - # if the desired step size is larger than the movement to realize, only one value - if np.abs(self.target_value - self.current_value) <= self.step_size: - self.voltage_list = np.array([self.target_value])/self.conv_factor - # otherwise we do steps - else: - pos_list = np.arange(min(self.current_value, self.target_value), - max(self.current_value, self.target_value)+self.step_size, - self.step_size) - # we need to start from the beginning - if pos_list[0] != self.current_value: - pos_list = pos_list[::-1] - - # we ensure that the last value is the target, otherwise we might get caught in a loop - # if the position goes to target from more than epsilon. - pos_list[-1] = self.target_value - - # convert to voltage - self.voltage_list = pos_list/self.conv_factor - self.number_steps = len(self.voltage_list) - - def move_scanner(self, init=False): - """ Actually moves the scanner. """ - # compute the path - self.prepare_voltage_list() - if not init: - self.controller.locked = True - # fill with zeros according to the other AO channels in the controller - self.controller.set_up_voltage_array(self.voltage_list, - self.settings.child('multiaxes', 'axis').value()) - # prepare the tasks - self.update_task() - if self.number_steps > 1: - self.controller.clock.start() - self.init_step_index = 2*len(self.voltage_list)+1 - - # Actually tells the NI card to send the list of voltages. - self.controller.write_voltages() - - def finish_waiting(self): - if self.waiting_to_move[0]: - self.move_scanner() - if self.waiting_to_move[1] == "abs": - self.emit_status(ThreadCommand('Update_Status', ['Absolute movement.'])) - elif self.waiting_to_move[1] == "rel": - self.emit_status(ThreadCommand('Update_Status', ['Relative movement.'])) - self.waiting_to_move[0] = False - - -if __name__ == '__main__': - main(__file__) diff --git a/src/pymodaq_plugins_daqmx/daq_move_plugins/daq_move_DAQmx_ScannerControl.py b/src/pymodaq_plugins_daqmx/daq_move_plugins/daq_move_DAQmx_ScannerControl.py deleted file mode 100644 index 1a576e6..0000000 --- a/src/pymodaq_plugins_daqmx/daq_move_plugins/daq_move_DAQmx_ScannerControl.py +++ /dev/null @@ -1,272 +0,0 @@ -import numpy as np -from pymodaq.control_modules.move_utility_classes import DAQ_Move_base, comon_parameters_fun, main # common set of -# parameters for all actuators -from pymodaq.utils.daq_utils import ThreadCommand # object used to send info back to the main thread -from pymodaq.utils.parameter import Parameter - -from pymodaq_plugins_daqmx.hardware.national_instruments.daqmx import DAQmx, AOChannel, \ - ClockSettings, DAQ_analog_types, ClockCounter, Edge - -from PyDAQmx import DAQmx_Val_FiniteSamps - -import PyDAQmx - - -class DAQ_Move_DAQmx_ScannerControl(DAQ_Move_base): - """Plugin to control a piezo scanner with a NI card. This modules requires a clock channel to handle the - timing of the movement and display the position. Avoid using several scanners (ie several analog outputs) - with a single clock, this creates conflicts in the use of the clock channel. For this purpose, use the module - MultipleScannerControl. - - This object inherits all functionality to communicate with PyMoDAQ Module through inheritance via DAQ_Move_base - It then implements the particular communication with the instrument - - Attributes: - ----------- - controller: object - The particular object that allow the communication with the hardware, in general a python wrapper around the - hardware library - - """ - _controller_units = 'nm' - is_multiaxes = False - axes_names = [ ] - _epsilon = 10 - - params = [ {"title": "Output channel:", "name": "analog_channel", - "type": "list", "limits": DAQmx.get_NIDAQ_channels(source_type="Analog_Output")}, - {'title': 'Clock channel:', 'name': 'clock_channel', 'type': 'list', - 'limits': DAQmx.get_NIDAQ_channels(source_type='Counter')}, - {"title": "Step size (nm)", "name": "step_size", "type": "float", "value": 100.0}, - {"title": "Step time (ms)", "name": "step_time", "type": "float", "value": 10.0}, - {"title": "Conversion factor (nm/V)", "name": "conv_factor", "type": "float", "value": 7500.0} - ] + comon_parameters_fun(is_multiaxes, axes_names) - - def ini_attributes(self): - self.controller = None - self.clock = None - self.step_size = 100.0 # in nm! be careful with the scaling param - self.step_time = 10e-3 - self.number_steps = 1 - self.conv_factor = 7500.0 - self.clock_channel = None - self.scanner_channel = None - self.voltage_list = np.array([0.0]) - self.init_step_index = 0 - - def get_actuator_value(self): - """Get the current value from the hardware with scaling conversion. - Reading the actual state is tricky with the NI card because the list of voltages - is sent in the buffer immediately, so reading the last written voltage always - yield the target value, independently of the clock timing. - To bypass this issue, instead of reading the written voltage, we read the current - clock index and use it to retrieve the corresponding voltage from our list. - Beware that the index given by GetCOCount start at a value 2*nb_voltages + 1 - and decreases. Therefore, we store the first value returned in self.init_step_index - and use this as a starting point. - - Returns - ------- - float: The position obtained after scaling conversion. - """ - if len(self.voltage_list) > 1: - try: - current_step_index = PyDAQmx.c_ulong() - self.clock.task.GetCOCount(self.clock_channel.name, PyDAQmx.byref(current_step_index)) - index = self.init_step_index - current_step_index.value - voltage = self.voltage_list[min(int(index/2), len(self.voltage_list)-1)] - except: # when the task did not start - voltage = self.voltage_list[0] - - # if we do only one step, we do not care, there is no timing anyway. - else: - voltage = self.controller.get_last_write() - # convert voltage to position - pos = voltage * self.conv_factor - pos = self.get_position_with_scaling(pos) - return pos - - def close(self): - """ Terminate the communication protocol""" - print("move_done received, closing task") - self.clock.close() - self.controller.close() - - 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() == "analog_channel": - self.close() - self.update_task() - elif param.name() == "clock_channel": - self.close() - self.update_task() - elif param.name() == "step_size": - self.step_size = param.value() - elif param.name() == "step_time": - self.step_time = param.value()*1e-3 - elif param.name() == "conv_factor": - self.conv_factor = param.value() - - 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 - """ - self.controller = DAQmx() - self.clock = DAQmx() - self.step_size = self.settings.child("step_size").value() - # Step time is given in ms by the user - self.step_time = self.settings.child("step_time").value()*1e-3 - self.conv_factor = self.settings.child("conv_factor").value() - - # we need to close the clock once the move is done, to free - # the counter resource - self.move_done_signal.connect(self.close) - - try: - self.update_task() - initialized = True - info = "NI card based piezo scanner control." - self.move_abs(0.0) # to avoid bad initial positioning because - # we can't read the actual value from the NI card. - except Exception as e: - print(e) - initialized = False - info = "Error" - - return info, initialized - - def move_abs(self, value): - """ Move the actuator to the absolute target defined by value - - Parameters - ---------- - value: (float) value of the absolute target positioning - """ - if value == 0.0: - value = 1.0 - self.close() - 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 - self.move_scanner() - self.emit_status(ThreadCommand('Update_Status', ['Absolute movement.'])) - - def move_rel(self, value): - """ Move the actuator to the relative target actuator value defined by value - - Parameters - ---------- - value: (float) value of the relative target positioning - """ - self.close() - value = self.check_bound(self.current_value + value) - self.current_value - self.target_value = value + self.current_value - if self.target_value == 0.0: - self.target_value = 1.0 - value = self.set_position_relative_with_scaling(value) - self.move_scanner() - self.emit_status(ThreadCommand('Update_Status', ['Relative movement.'])) - - def move_home(self): - """Do nothing""" - self.emit_status(ThreadCommand('Update_Status', ['No home position implemented.'])) - - def stop_motion(self): - """Stop the actuator and emits move_done signal""" - self.close() - self.emit_status(ThreadCommand('Update_Status', ['Motion stopped.'])) - - def update_task(self): - """ Set up the analog output task in the NI card, and the clock if necessary.""" - min_voltage = self.settings.child("bounds", "min_bound").value()/self.conv_factor - max_voltage = self.settings.child("bounds", "max_bound").value()/self.conv_factor - self.scanner_channel = AOChannel(name=self.settings.child("analog_channel").value(), - source="Analog_Output", - analog_type=DAQ_analog_types.names()[0], - value_min=min_voltage, - value_max=max_voltage) - - # If we do more than one step, we need a clock for the time sampling - # because we want a smooth and slow movement. - if len(self.voltage_list) > 1: - self.clock_channel = ClockCounter(1/self.step_time, - name=self.settings.child("clock_channel").value(), - source="Counter") - self.clock.update_task(channels=[self.clock_channel]) - # we need to set the rate again, I do not understand why - self.clock.task.SetSampClkRate(1/self.step_time) - self.clock.task.CfgImplicitTiming(DAQmx_Val_FiniteSamps, self.number_steps+1) - - clock_settings_ao = ClockSettings(source="/" + self.clock_channel.name + "InternalOutput", - frequency=1/self.step_time, - edge=Edge.names()[0], - Nsamples=len(self.voltage_list)+1, - repetition=False) - else: - # empty clock settings if we do only one step - clock_settings_ao = ClockSettings(source=None, - frequency=1/self.step_time, - Nsamples=1, - edge=Edge.names()[0], - repetition=False) - - self.controller.update_task(channels=[self.scanner_channel], - clock_settings=clock_settings_ao) - - def prepare_voltage_list(self): - """Generates the list of voltages to move smoothly the scanner.""" - # if the desired step size is larger than the movement to realize, only one value - if np.abs(self.target_value - self.current_value) <= self.step_size: - self.voltage_list = np.array([self.target_value])/self.conv_factor - # otherwise we do steps - else: - pos_list = np.arange(min(self.current_value, self.target_value), - max(self.current_value, self.target_value)+self.step_size, - self.step_size) - # we need to start from the beginning - if pos_list[0] != self.current_value: - pos_list = pos_list[::-1] - - # we ensure that the last value is the target, otherwise we might get caught in a loop - # if the position goes to target from more than epsilon. - pos_list[-1] = self.target_value - - # convert to voltage - self.voltage_list = pos_list/self.conv_factor - self.number_steps = len(self.voltage_list) - - def move_scanner(self): - """ Actually moves the scanner. """ - # compute the path - self.prepare_voltage_list() - # prepare the tasks - self.update_task() - if len(self.voltage_list) > 1: - #self.controller.task.SetSampTimingType(DAQmx_Val_SampClk) - self.clock.start() - self.init_step_index = 2*len(self.voltage_list)+1 - - # Actually tells the NI card to send the list of voltages. - self.controller.start() - self.controller.writeAnalog(self.number_steps, 1, self.voltage_list) - - -if __name__ == '__main__': - main(__file__) diff --git a/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_DAQmx_PLcounter.py b/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_DAQmx_PLcounter.py deleted file mode 100755 index 154df24..0000000 --- a/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_DAQmx_PLcounter.py +++ /dev/null @@ -1,143 +0,0 @@ -import numpy as np -from pymodaq.utils.daq_utils import ThreadCommand -from pymodaq.utils.data import DataWithAxes, DataToExport, DataSource -from pymodaq.control_modules.viewer_utility_classes import DAQ_Viewer_base, comon_parameters, main -from pymodaq.utils.parameter import Parameter - -from pymodaq_plugins_daqmx.hardware.national_instruments.daqmx import DAQmx, \ - Edge, ClockSettings, Counter, ClockCounter, TriggerSettings - -from PyDAQmx import DAQmx_Val_ContSamps - -class DAQ_0DViewer_DAQmx_PLcounter(DAQ_Viewer_base): - """ - Plugin for a 0D PL counter, based on a NI card. - """ - params = comon_parameters+[ - {"title": "Counting channel:", "name": "counter_channel", - "type": "list", "limits": DAQmx.get_NIDAQ_channels(source_type="Counter")}, - {"title": "Photon source:", "name": "photon_channel", - "type": "list", "limits": DAQmx.getTriggeringSources()}, - {"title": "Clock frequency (Hz):", "name": "clock_freq", - "type": "float", "value": 100., "default": 100., "min": 1}, - {'title': 'Clock channel:', 'name': 'clock_channel', 'type': 'list', - 'limits': DAQmx.get_NIDAQ_channels(source_type='Counter')} - ] - - def ini_attributes(self): - self.controller = None - self.clock_channel = None - self.counter_channel = None - self.live = False # True during a continuous grab - self.counting_time = 0.1 - - 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() == "clock_freq": - self.counting_time = 1/param.value() - else: - self.stop() - self.update_tasks() - - def ini_detector(self, controller=None): - """Detector communication initialization - - Parameters - ---------- - controller: (object) - custom object of a PyMoDAQ plugin (Slave case). None if only one actuator/detector by controller - (Master case) - - Returns - ------- - info: str - initialized: bool - False if initialization failed otherwise True - """ - self.controller = {"clock": DAQmx(), "counter": DAQmx()} - try: - self.update_tasks() - initialized = True - info = "NI card based PL counter" - self.counting_time = 1 / self.settings.child("clock_freq").value() - except Exception as e: - print(e) - initialized = False - info = "Error" - - self.dte_signal_temp.emit(DataToExport(name='PL', - data=[DataWithAxes(name='PL', data=[np.array([0])], - source=DataSource['raw'], - dim='Data0D', labels=['PL (kcts/s)'])])) - - return info, initialized - - def close(self): - """Terminate the communication protocol""" - self.controller["clock"].close() - self.controller["counter"].close() - - def grab_data(self, Naverage=1, **kwargs): - """Start a grab from the detector - - Parameters - ---------- - Naverage: int - Number of hardware averaging not relevant here. - kwargs: dict - others optionals arguments - """ - update = True # to decide if we do the initial set up or not - - if 'live' in kwargs: - if kwargs['live'] == self.live and self.live: - update = False # we are already live - self.live = kwargs['live'] - - if update: - self.update_tasks() - self.controller["clock"].start() - - read_data = self.controller["counter"].readCounter(1, counting_time=self.counting_time) - data_pl = 1e-3*read_data/self.counting_time # convert to kcts/s - self.dte_signal.emit(DataToExport(name='PL', - data=[DataWithAxes(name='PL', data=[data_pl], - source=DataSource['raw'], - dim='Data0D', labels=['PL (kcts/s)'])])) - - def stop(self): - """Stop the current grab hardware wise if necessary""" - self.close() - self.emit_status(ThreadCommand('Update_Status', ['Acquisition stopped.'])) - return '' - - def update_tasks(self): - """Set up the counting tasks in the NI card.""" - # Create channels - self.clock_channel = ClockCounter(self.settings.child("clock_freq").value(), - name=self.settings.child("clock_channel").value(), - source="Counter") - self.counter_channel = Counter(name=self.settings.child("counter_channel").value(), - source="Counter", edge=Edge.names()[0]) - - self.controller["clock"].update_task(channels=[self.clock_channel], - clock_settings=ClockSettings(), - trigger_settings=TriggerSettings()) - self.controller["clock"].task.CfgImplicitTiming(DAQmx_Val_ContSamps, 1) - - self.controller["counter"].update_task(channels=[self.counter_channel], - clock_settings=ClockSettings(), - trigger_settings=TriggerSettings()) - - # connect the clock to the counter - self.controller["counter"].task.SetSampClkSrc("/" + self.clock_channel.name + "InternalOutput") - - -if __name__ == '__main__': - main(__file__) From d1bd8344a76137f116cf2dc627047ace717006b5 Mon Sep 17 00:00:00 2001 From: Sebastien Date: Mon, 30 Jun 2025 15:04:13 +0200 Subject: [PATCH 063/140] Refactor things for matching nidaqmx library --- ...{daq_move_DAQmx.py => daq_move_NIDAQmx.py} | 4 +- ...iewer_DAQmx.py => daq_1Dviewer_NIDAQmx.py} | 2 +- .../national_instruments/NIDAQmx_Move.py | 181 ++++++++++++++++++ 3 files changed, 184 insertions(+), 3 deletions(-) rename src/pymodaq_plugins_daqmx/daq_move_plugins/{daq_move_DAQmx.py => daq_move_NIDAQmx.py} (77%) rename src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_1D/{daq_1Dviewer_DAQmx.py => daq_1Dviewer_NIDAQmx.py} (93%) create mode 100644 src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_Move.py diff --git a/src/pymodaq_plugins_daqmx/daq_move_plugins/daq_move_DAQmx.py b/src/pymodaq_plugins_daqmx/daq_move_plugins/daq_move_NIDAQmx.py similarity index 77% rename from src/pymodaq_plugins_daqmx/daq_move_plugins/daq_move_DAQmx.py rename to src/pymodaq_plugins_daqmx/daq_move_plugins/daq_move_NIDAQmx.py index 0fb267d..a5fae0d 100644 --- a/src/pymodaq_plugins_daqmx/daq_move_plugins/daq_move_DAQmx.py +++ b/src/pymodaq_plugins_daqmx/daq_move_plugins/daq_move_NIDAQmx.py @@ -1,7 +1,7 @@ -from ..hardware.national_instruments.daq_NIDAQmx import DAQ_NIDAQmx_Actuator +from ..hardware.national_instruments.NIDAQmx_Move import DAQ_NIDAQmx_Actuator -class DAQ_Move_DAQmx(DAQ_NIDAQmx_Actuator): +class DAQ_Move_NIDAQmx(DAQ_NIDAQmx_Actuator): """ ==================== ======================== **Attributes** **Type** diff --git a/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_1D/daq_1Dviewer_DAQmx.py b/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_1D/daq_1Dviewer_NIDAQmx.py similarity index 93% rename from src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_1D/daq_1Dviewer_DAQmx.py rename to src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_1D/daq_1Dviewer_NIDAQmx.py index ebe032f..09c9fd5 100644 --- a/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_1D/daq_1Dviewer_DAQmx.py +++ b/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_1D/daq_1Dviewer_NIDAQmx.py @@ -2,7 +2,7 @@ from pymodaq.control_modules.viewer_utility_classes import main -class DAQ_1DViewer_DAQmx(DAQ_NIDAQmx_Viewer): +class DAQ_1DViewer_NIDAQmx(DAQ_NIDAQmx_Viewer): """ ==================== ======================== **Attributes** **Type** diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_Move.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_Move.py new file mode 100644 index 0000000..dbaf9af --- /dev/null +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_Move.py @@ -0,0 +1,181 @@ +from easydict import EasyDict as edict +from .daqmxni import NIDAQmx +from pymodaq.utils.data import DataActuator +from pymodaq.utils.daq_utils import ThreadCommand, getLineInfo +from pymodaq.control_modules.move_utility_classes import DAQ_Move_base +from pymodaq.control_modules.move_utility_classes import comon_parameters_fun as actuator_params +from pymodaq_plugins_daqmx.hardware.national_instruments.NIDAQmx_base import DAQ_NIDAQmx_base +from pymodaq.utils.logger import set_logger, get_module_name +logger = set_logger(get_module_name(__file__)) + +class DAQ_NIDAQmx_Actuator(DAQ_Move_base, DAQ_NIDAQmx_base): + """ + Wrapper object to access the Mock fonctionnalities, similar wrapper for all controllers. + + =============== ============== + **Attributes** **Type** + *params* dictionnary + =============== ============== + """ + _controller_units = 'Volts' + is_multiaxes = False # set to True if this plugin is controlled for a multiaxis controller (with a unique communication link) + stage_names = [] # "list of strings of the multiaxes + + params = DAQ_NIDAQmx_base.params +[ + # elements to be added here as dicts in order to control your custom stage + ############ + {'title': 'MultiAxes:', 'name': 'multiaxes', 'type': 'group', 'visible': is_multiaxes, + 'children': [ + {'title': 'is Multiaxes:', 'name': 'ismultiaxes', 'type': 'bool', 'value': is_multiaxes, + 'default': False}, + {'title': 'Status:', 'name': 'multi_status', 'type': 'list', 'value': 'Master', + 'limits': ['Master', 'Slave']}, + {'title': 'Axis:', 'name': 'axis', 'type': 'list', 'limits': stage_names}, + + ]}] + actuator_params + + def __init__(self, parent=None, params_state=None, control_type="Actuator"): + DAQ_Move_base.__init__(self, parent, params_state) # defines settings attribute and various other methods + DAQ_NIDAQmx_base.__init__(self) + + self.control_type = "Actuator" # could be "0D", "1D" or "Actuator" + self.settings.child('NIDAQ_type').setLimits(['Analog_Output', 'Digital_Output']) + + self.settings.child('clock_settings', 'Nsamples').setValue(1) + + def get_actuator_value(self) -> DataActuator: + """Get the current position from the hardware with scaling conversion. + + Returns + ------- + float: The position obtained after scaling conversion. + """ + ## TODO: Reimplement in your daq_move with NIDAQmx library + pos = self.target_position + ## + + pos = self.get_position_with_scaling(pos) + self.emit_status(ThreadCommand('check_position', [pos])) + return pos + + + def commit_settings(self, param): + """ + | Activate any parameter changes on the PI_GCS2 hardware. + | + | Called after a param_tree_changed signal from DAQ_Move_main. + + """ + ## TODO: Reimplement in your daq_move with NIDAQmx library + DAQ_NIDAQmx_base.commit_settings(self, param) + if param.parent() is not None: + if param.parent().name() == 'ao_channels': + device = param.opts['title'].split('/')[0] + self.settings.child('clock_settings', 'frequency').setOpts(max=self.getAOMaxRate(device)) + + volt_ranges = self.controller.getAOVoltageRange(device) + curr_ranges = self.controller.getA0CurrentRange(device) + try: + param.child('voltage_settings', 'volt_min').setValue(volt_ranges[0]) + param.child('voltage_settings', 'volt_max').setValue(volt_ranges[1]) + param.child('current_settings', 'curr_min').setValue(curr_ranges[0]) + param.child('current_settings', 'curr_max').setValue(curr_ranges[1]) + except: + pass + + 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 + ------- + self.status (edict): with initialization status: three fields: + * info (str) + * controller (object) initialized controller + *initialized: (bool): False if initialization failed otherwise True + """ + try: + # initialize the stage and its controller status + # controller is an object that may be passed to other instances of DAQ_Move_Mock in case + # of one controller controlling multiactuators (or detector) + + self.status.update(edict(info="", controller=None, initialized=False)) + + # check whether this stage is controlled by a multiaxe controller (to be defined for each plugin) + # if multiaxes then init the controller here if Master state otherwise use external controller + if self.is_multiaxes and not self.is_master: + if controller is None: + raise Exception('no controller has been defined externally while this axe is a slave one') + else: + self.controller = controller + else: + self.controller = NIDAQmx() + + # actions to perform in order to set properly the settings tree options + self.commit_settings(self.settings.child('NIDAQ_type')) + + self.status.info = "Plugin Initialized" + self.status.controller = self.controller + self.status.initialized = True + return self.status + + except Exception as e: + self.emit_status(ThreadCommand('Update_Status', [getLineInfo() + str(e), 'log'])) + self.status.info = getLineInfo() + str(e) + self.status.initialized = False + return self.status + + def move_Abs(self, position): + """ Move the actuator to the absolute target defined by position + + Parameters + ---------- + position: (flaot) value of the absolute target positioning + """ + ## TODO: Reimplement in your daq_move with NIDAQmx library + raise NotImplementedError + + def move_done_callback(self, taskhandle, status, callbackdata): + ## TODO: Reimplement in your daq_move with NIDAQmx library + raise NotImplementedError + + def move_Rel(self, position): + """ Move the actuator to the relative target actuator value defined by position + + Parameters + ---------- + position: (flaot) value of the relative target positioning + """ + ## TODO: Reimplement in your daq_move with NIDAQmx library + raise NotImplementedError + + def move_Home(self): + """ + Send the update status thread command. + See Also + -------- + daq_utils.ThreadCommand + """ + ## TODO: Reimplement in your daq_move with NIDAQmx library + raise NotImplementedError + + def stop_motion(self): + """ + Call the specific move_done function (depending on the hardware). + + See Also + -------- + move_done + """ + """Stop the current grab hardware wise if necessary""" + try: + DAQ_NIDAQmx_base.stop(self) + self.move_done() # to let the interface know the actuator stopped + logger.info("Motion stopped.") + except Exception: + pass + self.emit_status(ThreadCommand('Update_Status', ['Motion stopped.'])) + return '' From e83787dc02acad01fffb57c680871536fef4d63f Mon Sep 17 00:00:00 2001 From: Sebastien Date: Tue, 1 Jul 2025 10:36:14 +0200 Subject: [PATCH 064/140] methods and imports corrections --- .../plugins_0D/daq_0Dviewer_NIDAQmx.py | 2 +- .../national_instruments/NIDAQmx_Viewer.py | 32 ++++--------------- .../national_instruments/NIDAQmx_base.py | 6 ++-- .../hardware/national_instruments/daqmxni.py | 14 +++++--- 4 files changed, 21 insertions(+), 33 deletions(-) diff --git a/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_NIDAQmx.py b/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_NIDAQmx.py index 0c3895b..1c9218c 100644 --- a/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_NIDAQmx.py +++ b/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_NIDAQmx.py @@ -49,7 +49,7 @@ def ini_attributes(self): def close(self): self.live = False - self.controller.stop() + self.stop() self.controller.close() pass diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_Viewer.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_Viewer.py index ba15f27..8ae0a86 100644 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_Viewer.py +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_Viewer.py @@ -57,7 +57,7 @@ def __init__(self, parent=None, params_state=None, control_type="0D"): def stop(self): """Stop the current grab hardware wise if necessary""" try: - self.controller.stop() + DAQ_NIDAQmx_base.stop(self) self.live = False logger.info("Acquisition stopped.") except Exception: @@ -84,31 +84,13 @@ def commit_settings(self, param): device = param.opts['title'].split('/')[0] self.settings.child('clock_settings', 'frequency').setOpts(max=self.controller.getAIMaxRate(device)) - ranges = self.controller.getAIVoltageRange(device) - param.child('voltage_settings', 'volt_min').setOpts(limits=[r[0] for r in ranges]) - param.child('voltage_settings', 'volt_max').setOpts(limits=[r[1] for r in ranges]) - - DAQ_NIDAQmx_base.commit_settings(self, param) - - def ini_detector(self, controller=None): - """ - Initialisation procedure of the detector. - - See Also - -------- - daq_utils.ThreadCommand - """ - try: - self.current_device = nidaqmx.system.Device(self.settings["devices"]) - self.controller = self.ini_detector_init(controller, NIDAQmx()) - self.controller.configuration_sequence(self, self.current_device) - - # actions to perform in order to set properly the settings tree options - self.commit_settings(self.settings.child('NIDAQ_type')) - for ch in self.config_channels: + volt_ranges = self.controller.getAIVoltageRange(device) + curr_ranges = self.controller.getAICurrentRange(device) try: - ch.analog_type = ch.analog_type - ch.termination = ch.termination + param.child('voltage_settings', 'volt_min').setValue(volt_ranges[0]) + param.child('voltage_settings', 'volt_max').setValue(volt_ranges[1]) + param.child('current_settings', 'curr_min').setValue(curr_ranges[0]) + param.child('current_settings', 'curr_max').setValue(curr_ranges[1]) except: pass if self.settings.child("devices").value() in ch.name: diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_base.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_base.py index 7218073..cfd439a 100644 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_base.py +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_base.py @@ -1,8 +1,8 @@ from qtpy import QtWidgets from qtpy.QtCore import Signal -from pymodaq.utils.logger import set_logger, get_module_name -from pymodaq.utils.parameter import Parameter -from pymodaq.utils.parameter.pymodaq_ptypes import registerParameterType, GroupParameter +from pymodaq_utils.logger import set_logger, get_module_name +from pymodaq_gui.parameter import Parameter +from pymodaq_gui.parameter.pymodaq_ptypes import registerParameterType, GroupParameter from pymodaq_plugins_daqmx.hardware.national_instruments.daqmxni import NIDAQmx, Edge, ChannelType, ClockSettings, \ AIChannel, AIThermoChannel, AOChannel, CIChannel, COChannel, DOChannel, DIChannel, UsageTypeAI, UsageTypeAO, \ ThermocoupleType, TerminalConfiguration, TriggerSettings diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py index cf7ebe8..e525190 100644 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py @@ -560,13 +560,19 @@ def readCounter(self): @classmethod def getAIVoltageRange(cls, device='Dev1'): - ret = niSystem.local().devices[device].ai_voltage_rngs # todo self.devices[device].ai_voltage_rngs - return [tuple(ret[6:8])] + return niSystem.local().devices[device].ai_voltage_rngs[6:8] # todo self.devices[device].ai_voltage_rngs + + @classmethod + def getAICurrentRange(cls, device='Dev1'): + return niSystem.local().devices[device].ai_current_rngs[6:8] # todo self.devices[device].ai_current_rngs @classmethod def getAOVoltageRange(cls, device='Dev1'): - ret = niSystem.local().devices[device].ao_voltage_rngs # todo self.devices[device].ao_voltage_rngs - return [tuple(ret)] # [(-10., 10.)] Why this format is needed?? + return niSystem.local().devices[device].ao_voltage_rngs # todo self.devices[device].ao_voltage_rngs + + @classmethod + def getAOCurrentRange(cls, device='Dev1'): + return niSystem.local().devices[device].ao_current_rngs # todo self.devices[device].ao_current_rngs def stop(self): if self._task is not None: From c4cc6277900cdc2229f3310df5a0985b92374c3a Mon Sep 17 00:00:00 2001 From: Sebastien Date: Tue, 1 Jul 2025 10:40:28 +0200 Subject: [PATCH 065/140] Config loading and ini_detector separated --- .../national_instruments/NIDAQmx_Viewer.py | 104 ++++++++++++------ .../national_instruments/NIDAQmx_base.py | 1 + 2 files changed, 69 insertions(+), 36 deletions(-) diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_Viewer.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_Viewer.py index 8ae0a86..c448b6a 100644 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_Viewer.py +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_Viewer.py @@ -78,6 +78,7 @@ def commit_settings(self, param): -------- update_NIDAQ_channels, update_task, refresh_hardware """ + DAQ_NIDAQmx_base.commit_settings(self, param) if param.parent() is not None: if param.parent().name() == 'ai_channels': @@ -93,42 +94,73 @@ def commit_settings(self, param): param.child('current_settings', 'curr_max').setValue(curr_ranges[1]) except: pass - if self.settings.child("devices").value() in ch.name: - self.settings.child('ai_channels').addNew(ch.name) - param = [a for a in self.settings.child('ai_channels').childs if a.opts['title'] == ch.name][0] - self.settings.child("ai_channels", param.opts['name'], "ai_type").setValue(ch.analog_type.name) - param.child("voltage_settings").show(ch.analog_type == UsageTypeAI.VOLTAGE) - param.child("current_settings").show(ch.analog_type == UsageTypeAI.CURRENT) - param.child("thermoc_settings").show(ch.analog_type == UsageTypeAI.TEMPERATURE_THERMOCOUPLE) - if ch.analog_type == UsageTypeAI.VOLTAGE: - self.settings.child("ai_channels", param.opts['name'], "voltage_settings", "volt_min").setValue( - ch.value_min) - self.settings.child("ai_channels", param.opts['name'], "voltage_settings", "volt_max").setValue( - ch.value_max) - self.settings.child("ai_channels", param.opts['name'], "termination").setValue( - ch.termination.name) - elif ch.analog_type == UsageTypeAI.CURRENT: - self.settings.child("ai_channels", param.opts['name'], "current_settings", "curr_min").setValue( - ch.value_min) - self.settings.child("ai_channels", param.opts['name'], "current_settings", "curr_max").setValue( - ch.value_max) - self.settings.child("ai_channels", param.opts['name'], "termination").setValue( - ch.termination.name) - elif ch.analog_type == UsageTypeAI.TEMPERATURE_THERMOCOUPLE: - self.settings.child("ai_channels", - param.opts['name'], - "thermoc_settings", - "thermoc_type").setValue(ch.thermo_type.name) - self.settings.child("ai_channels", - param.opts['name'], - "thermoc_settings", - "T_min").setValue(ch.value_min) - self.settings.child("ai_channels", - param.opts['name'], - "thermoc_settings", - "T_max").setValue(ch.value_max) - self.settings.child("ai_channels", param.opts['name'], "termination").setValue( - TerminalConfiguration.DEFAULT.name) + elif param.name() == 'load_config': + self.controller.configuration_sequence(self, self.current_device) + self.settings.child('load_config').hide() + for ch in self.config_channels: + if self.settings.child("devices").value() in ch.name: + self.settings.child('ai_channels').addNew(ch.name) + param = [a for a in self.settings.child('ai_channels').childs if a.opts['title'] == ch.name][0] + param.child("voltage_settings").show(ch.analog_type == UsageTypeAI.VOLTAGE) + param.child("current_settings").show(ch.analog_type == UsageTypeAI.CURRENT) + param.child("thermoc_settings").show(ch.analog_type == UsageTypeAI.TEMPERATURE_THERMOCOUPLE) + match ch.analog_type: + case UsageTypeAI.VOLTAGE: + self.settings.child("ai_channels", param.opts['name'], "ai_type").setValue("VOLTAGE") + self.settings.child("ai_channels", param.opts['name'], "voltage_settings", + "volt_min").setValue( + ch.value_min) + self.settings.child("ai_channels", param.opts['name'], "voltage_settings", + "volt_max").setValue( + ch.value_max) + self.settings.child("ai_channels", param.opts['name'], "termination").setValue( + ch.termination.name) + case UsageTypeAI.CURRENT: + self.settings.child("ai_channels", param.opts['name'], "ai_type").setValue("CURRENT") + self.settings.child("ai_channels", param.opts['name'], "current_settings", + "curr_min").setValue( + ch.value_min) + self.settings.child("ai_channels", param.opts['name'], "current_settings", + "curr_max").setValue( + ch.value_max) + self.settings.child("ai_channels", param.opts['name'], "termination").setValue( + ch.termination.name) + case UsageTypeAI.TEMPERATURE_THERMOCOUPLE: + self.settings.child("ai_channels", param.opts['name'], "ai_type").setValue( + "TEMPERATURE_THERMOCOUPLE") + self.settings.child("ai_channels", + param.opts['name'], + "thermoc_settings", + "thermoc_type").setValue(ch.thermo_type.name) + self.settings.child("ai_channels", + param.opts['name'], + "thermoc_settings", + "T_min").setValue(ch.value_min) + self.settings.child("ai_channels", + param.opts['name'], + "thermoc_settings", + "T_max").setValue(ch.value_max) + self.settings.child("ai_channels", param.opts['name'], "termination").setValue( + TerminalConfiguration.DEFAULT.name) + + def ini_detector(self, controller=None): + """ + Initialisation procedure of the detector. + + See Also + -------- + daq_utils.ThreadCommand + """ + try: + if self.is_master: + self.controller = NIDAQmx() + else: + self.controller = controller + self.current_device = nidaqmx.system.Device(self.settings["devices"]) + + # actions to perform in order to set properly the settings tree options + self.commit_settings(self.settings.child('NIDAQ_type')) + info = "Plugin Initialized" initialized = True logger.info("Detector 0D initialized") diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_base.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_base.py index cfd439a..b334cd2 100644 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_base.py +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_base.py @@ -255,6 +255,7 @@ class DAQ_NIDAQmx_base: params = [{'title': 'Refresh hardware:', 'name': 'refresh_hardware', 'type': 'bool', 'value': False}, {'title': 'Signal type:', 'name': 'NIDAQ_type', 'type': 'list', 'limits': [Ds.name for Ds in ChannelType]}, + {'title': 'Load configuration', 'name': 'load_config', 'type': 'bool', 'value': False, 'readonly': False}, {'title': 'NSamples To Read', 'name': 'nsamplestoread', 'type': 'int', 'value': 1000, 'default': 1000, 'min': 1}, {'title': 'AO Settings:', 'name': 'ao_settings', 'type': 'group', 'children': [ From 81335e5efd287a256c2dd43a71ddf1aa1109974c Mon Sep 17 00:00:00 2001 From: Sebastien Date: Tue, 1 Jul 2025 10:50:17 +0200 Subject: [PATCH 066/140] Avoiding conflict with more accurate ScalableGroups child's name --- .../national_instruments/NIDAQmx_base.py | 64 +++++-------------- 1 file changed, 17 insertions(+), 47 deletions(-) diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_base.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_base.py index b334cd2..62618fd 100644 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_base.py +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_base.py @@ -26,20 +26,20 @@ class ScalableGroupAI(GroupParameter): hardware.DAQ_Move_Stage_type """ - params = [{'title': 'AI type:', 'name': 'ai_type', 'type': 'list', 'limits': [Uai.name for Uai in UsageTypeAI]}, - {'title': 'Voltage:', 'name': 'voltage_settings', 'type': 'group', 'children': [ - {'title': 'Voltage Min:', 'name': 'volt_min', 'type': 'float', 'value': -10.}, - {'title': 'Voltage Max:', 'name': 'volt_max', 'type': 'float', 'value': 10.}, + params = [{'title': 'AI type:', 'name': 'ai_type', 'type': 'list', 'visible': True, 'limits': [Uai.name for Uai in UsageTypeAI]}, + {'title': 'VOLTAGE:', 'name': 'voltage_settings', 'type': 'group', 'visible': False, 'children': [ + {'title': 'Voltage Min:', 'name': 'volt_min', 'type': 'float', 'value': -5, 'suffix': 'V'}, + {'title': 'Voltage Max:', 'name': 'volt_max', 'type': 'float', 'value': 5, 'suffix': 'V'}, ]}, - {'title': 'Current:', 'name': 'current_settings', 'type': 'group', 'visible': False, 'children': [ + {'title': 'CURRENT:', 'name': 'current_settings', 'type': 'group', 'visible': False, 'children': [ {'title': 'Current Min:', 'name': 'curr_min', 'type': 'float', 'value': -1, 'suffix': 'A'}, {'title': 'Current Max:', 'name': 'curr_max', 'type': 'float', 'value': 1, 'suffix': 'A'}, ]}, - {'title': 'Thermocouple:', 'name': 'thermoc_settings', 'type': 'group', 'visible': False, 'children': [ + {'title': 'TEMPERATURE_THERMOCOUPLE:', 'name': 'thermoc_settings', 'type': 'group', 'visible': False, 'children': [ {'title': 'Thc. type:', 'name': 'thermoc_type', 'type': 'list', 'limits': [Th.name for Th in ThermocoupleType], 'value': 'K'}, - {'title': 'Temp. Min (°C):', 'name': 'T_min', 'type': 'float', 'value': 0, 'suffix': '°C'}, - {'title': 'Temp. Max (°C):', 'name': 'T_max', 'type': 'float', 'value': 50, 'suffix': '°C'}, + {'title': 'Temp. Min:', 'name': 'T_min', 'type': 'float', 'value': 0, 'suffix': '°C'}, + {'title': 'Temp. Max:', 'name': 'T_max', 'type': 'float', 'value': 50, 'suffix': '°C'}, ]}, {'title': 'Termination:', 'name': 'termination', 'type': 'list', 'limits': [Te.name for Te in TerminalConfiguration]}, @@ -60,13 +60,7 @@ def addNew(self, typ=None): *typ* string =============== =========== """ - childnames = [par.name() for par in self.children()] - if childnames == []: - newindex = 0 - else: - newindex = len(childnames) - - child = {'title': typ, 'name': 'ai{:02.0f}'.format(newindex), 'type': 'group', 'children': self.params, + child = {'title': typ, 'name': '{}'.format(typ.replace("/", "_")), 'type': 'group', 'children': self.params, 'removable': True, 'renamable': False} self.addChild(child) @@ -90,11 +84,11 @@ class ScalableGroupAO(GroupParameter): """ params = [{'title': 'AO type:', 'name': 'ao_type', 'type': 'list', 'limits': [Uao.name for Uao in UsageTypeAO]}, - {'title': 'Voltages:', 'name': 'voltage_settings', 'type': 'group', 'children': [ - {'title': 'Voltage Min:', 'name': 'volt_min', 'type': 'list', 'value': -10., }, - {'title': 'Voltage Max:', 'name': 'volt_max', 'type': 'list', 'value': 10., }, + {'title': 'VOLTAGE:', 'name': 'voltage_settings', 'type': 'group', 'children': [ + {'title': 'Voltage Min:', 'name': 'volt_min', 'type': 'list', 'value': -10., 'suffix': 'V'}, + {'title': 'Voltage Max:', 'name': 'volt_max', 'type': 'list', 'value': 10., 'suffix': 'V'}, ]}, - {'title': 'Current:', 'name': 'current_settings', 'type': 'group', 'visible': False, 'children': [ + {'title': 'CURRENT:', 'name': 'current_settings', 'type': 'group', 'visible': False, 'children': [ {'title': 'Current Min:', 'name': 'curr_min', 'type': 'float', 'value': -1, 'suffix': 'A'}, {'title': 'Current Max:', 'name': 'curr_max', 'type': 'float', 'value': 1, 'suffix': 'A'}, ]}, @@ -115,13 +109,7 @@ def addNew(self, typ=None): *typ* string =============== =========== """ - childnames = [par.name() for par in self.children()] - if childnames == []: - newindex = 0 - else: - newindex = len(childnames) - - child = {'title': typ, 'name': 'ao{:02.0f}'.format(newindex), 'type': 'group', 'children': self.params, + child = {'title': typ, 'name': 'ao{:02.0f}'.format(typ.replace("/", "_")), 'type': 'group', 'children': self.params, 'removable': True, 'renamable': False} self.addChild(child) @@ -161,13 +149,7 @@ def addNew(self, typ=None): *typ* string =============== =========== """ - childnames = [par.name() for par in self.children()] - if childnames == []: - newindex = 0 - else: - newindex = len(childnames) - - child = {'title': typ, 'name': 'counter{:02.0f}'.format(newindex), 'type': 'group', 'children': self.params, + child = {'title': typ, 'name': 'counter{:02.0f}'.format(typ.replace("/", "_")), 'type': 'group', 'children': self.params, 'removable': True, 'renamable': False} self.addChild(child) @@ -197,13 +179,7 @@ def addNew(self, typ=None): *typ* string =============== =========== """ - childnames = [par.name() for par in self.children()] - if childnames == []: - newindex = 0 - else: - newindex = len(childnames) - - child = {'title': typ, 'name': 'di{:02.0f}'.format(newindex), 'type': 'group', 'children': self.params, + child = {'title': typ, 'name': 'di{:02.0f}'.format(typ.replace("/", "_")), 'type': 'group', 'children': self.params, 'removable': True, 'renamable': False} self.addChild(child) @@ -232,13 +208,7 @@ def addNew(self, typ=None): *typ* string =============== =========== """ - childnames = [par.name() for par in self.children()] - if childnames == []: - newindex = 0 - else: - newindex = len(childnames) - - child = {'title': typ, 'name': 'counter{:02.0f}'.format(newindex), 'type': 'group', 'children': self.params, + child = {'title': typ, 'name': 'counter{:02.0f}'.format(typ.replace("/", "_")), 'type': 'group', 'children': self.params, 'removable': True, 'renamable': False} self.addChild(child) From b3ca044be1c45c563b61d9c831a1ad25ab671868 Mon Sep 17 00:00:00 2001 From: Sebastien Date: Wed, 2 Jul 2025 17:30:30 +0200 Subject: [PATCH 067/140] Counter stuff --- .../national_instruments/NIDAQmx_Viewer.py | 16 +++++----------- .../national_instruments/NIDAQmx_base.py | 4 ++++ .../hardware/national_instruments/daqmxni.py | 12 +----------- 3 files changed, 10 insertions(+), 22 deletions(-) diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_Viewer.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_Viewer.py index c448b6a..1468cee 100644 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_Viewer.py +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_Viewer.py @@ -200,7 +200,10 @@ def grab_data(self, Naverage=1, **kwargs): if self.controller.task is None: self.update_task() - self.controller.register_callback(self.emit_data, "Nsamples", self.clock_settings.Nsamples) + if self.settings['NIDAQ_type'] == ChannelType.ANALOG_INPUT.name: + self.controller.register_callback(self.emit_data, "Nsamples", self.clock_settings.Nsamples) + elif self.settings['NIDAQ_type'] == ChannelType.COUNTER_INPUT.name: + self.timer.start(self.settings['counter_settings', 'counting_time']) self.controller.start() def emit_data(self, task_handle, every_n_samples_event_type, number_of_samples, callback_data): @@ -220,13 +223,4 @@ def emit_data(self, task_handle, every_n_samples_event_type, number_of_samples, ), ]) self.dte_signal.emit(dte) - return 0 # mandatory for the NIDAQmx callback - - def counter_done(self): - channels_name = [ch.name for ch in self.channels] - data_counter = self.readCounter(len(self.channels), - self.settings['counter_settings', 'counting_time'] * 1e-3) - self.data_grabed_signal.emit([DataFromPlugins(name='NI Counter', data=[data_counter / 1e-3], dim='Data0D', - labels=channels_name, )]) - # y_axis=Axis(label='Count Number', units='1/s'))]) - self.task.StopTask() + return 0 # mandatory for the NIDAQmx callback \ No newline at end of file diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_base.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_base.py index 62618fd..de621cc 100644 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_base.py +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_base.py @@ -323,7 +323,9 @@ def commit_settings(self, param: Parameter): self.settings.child('ai_channels').hide() self.settings.child('ao_channels').hide() self.settings.child('ao_settings').hide() + self.settings.child('counter_settings').show() self.settings.child('counter_settings', 'ci_channels').show() + self.settings.child('counter_settings', 'co_channels').hide() self.settings.child('do_channels').hide() self.settings.child('di_channels').hide() @@ -332,7 +334,9 @@ def commit_settings(self, param: Parameter): self.settings.child('ai_channels').hide() self.settings.child('ao_channels').hide() self.settings.child('ao_settings').hide() + self.settings.child('counter_settings').show() self.settings.child('counter_settings', 'co_channels').show() + self.settings.child('counter_settings', 'ci_channels').hide() self.settings.child('do_channels').hide() self.settings.child('di_channels').hide() diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py index e525190..bc6a8ae 100644 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py @@ -546,17 +546,7 @@ def register_callback(self, callback, event='done', nsamples=1): self._task.register_every_n_samples_acquired_into_buffer_event(nsamples, callback) def readCounter(self): - # return 25 - - time.sleep(1) - self.value = self._task.read() - - # t = Timer(counting_time,self.read) - # self._task.start() - # t.start() - # time.sleep(2*counting_time) - # print(self.value) - return self.value + return self._task.read() @classmethod def getAIVoltageRange(cls, device='Dev1'): From 4df29d60f00fbcdaaa085b9b43f8a7e6da471ebb Mon Sep 17 00:00:00 2001 From: Sebastien Date: Wed, 2 Jul 2025 17:31:26 +0200 Subject: [PATCH 068/140] Documentation & examples improved --- README.rst | 14 +++++- .../hardware/national_instruments/daqmxni.py | 20 ++++++--- .../resources/config_template.toml | 43 ++++++++++++++----- 3 files changed, 58 insertions(+), 19 deletions(-) diff --git a/README.rst b/README.rst index b4f3222..1ae979f 100644 --- a/README.rst +++ b/README.rst @@ -41,6 +41,18 @@ Viewer0D ++++++++ * **DAQmx_PLcounter**: Single photon counting -* **NIDAQmx**: For now (01/2025) Only Analog Input tested and working. (current-voltage-temperature measurements on cDAQ & DAQ-USB) +* **NIDAQmx**: General measurement with National Instrument materials +NIDAQmx Viewer +++++++++++++++ +This viewer aims at being very general and allows measurement with as much different NI modules as possible. +At this point (06/2025), it supports Analog measurement of currents, voltages and temperatures from thermocouples. + +Tests have been running with the following devices: + * Chassis NI cDAQ-9174 & NI USB-9162 + * NI modules 9211 & 9205 + +Acquisition channels can ge generated directly running the viewer, but it is also possible to load channels from the configuration file config_daqmx.toml clicking on "load configuration" in the parameter tree + +To use such configuration, copy paste the content from C:/ProgramData/.pymodaq/config_daqmx.toml to /.pymodaq/config_daqmx.toml and fill it with the configuration of your own experimental setup. \ No newline at end of file diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py index bc6a8ae..7b23e5e 100644 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py @@ -250,6 +250,8 @@ def configuration_sequence(self, viewer, current_device): for dev in config["NIDAQ_Devices"]: if not isinstance(config["NIDAQ_Devices", dev], dict): continue + elif 'Example' in dev: + continue try: device_name = config["NIDAQ_Devices", dev].get('name') if not device_name == current_device.name: @@ -263,6 +265,8 @@ def configuration_sequence(self, viewer, current_device): for mod in config["NIDAQ_Devices", dev]: if not isinstance(config["NIDAQ_Devices", dev, mod], dict): continue + elif 'Example' in mod: + continue try: module_name = config["NIDAQ_Devices", dev, mod].get('name') module_product = config["NIDAQ_Devices", dev, mod].get('product') @@ -278,6 +282,8 @@ def configuration_sequence(self, viewer, current_device): if src == "ai": ai = config["NIDAQ_Devices", dev, mod, src] for ch in ai.keys(): + if 'Example' in ch: + continue name = module_name + "/" + str(ch) source = ChannelType[ai[ch].get("source")] analog_type = UsageTypeAI[ai[ch].get("analog_type")] @@ -294,13 +300,13 @@ def configuration_sequence(self, viewer, current_device): elif analog_type == UsageTypeAI.CURRENT: term = TerminalConfiguration[ai[ch].get("termination")] viewer.config_channels.append(AIChannel - (name=name, - source=source, - analog_type=analog_type, - value_min=float(ai[ch].get("value_min")), - value_max=float(ai[ch].get("value_max")), - termination=term, - )) + (name=name, + source=source, + analog_type=analog_type, + value_min=float(ai[ch].get("value_min")), + value_max=float(ai[ch].get("value_max")), + termination=term, + )) elif analog_type == UsageTypeAI.TEMPERATURE_THERMOCOUPLE: th = ThermocoupleType[ai[ch].get("thermo_type")] viewer.config_channels.append(AIThermoChannel diff --git a/src/pymodaq_plugins_daqmx/resources/config_template.toml b/src/pymodaq_plugins_daqmx/resources/config_template.toml index 4a84012..958c085 100644 --- a/src/pymodaq_plugins_daqmx/resources/config_template.toml +++ b/src/pymodaq_plugins_daqmx/resources/config_template.toml @@ -7,22 +7,43 @@ title = "Configuration entry for a NIDAQmx device" name = "" product = "" +[NIDAQ_Devices.DEVICE02_Example] +title = "Configuration entry for a NIDAQmx device" +name = "cDAQ1" +product = "cDAQ-9174" + [NIDAQ_Devices.DEVICE01.MODULE01] title = "A module plugged in the NIDAQmx device" name = "" product = "" -[NIDAQ_Devices.DEVICE01.MODULE02] +[NIDAQ_Devices.DEVICE01.MODULE02_Example] title = "A module plugged in a NIDAQmx device" -name = "" -product = "" +name = "cDAQMod1" +product = "NI 9211" -[NIDAQ_Devices.DEVICE01.MODULE03] -title = "A module plugged in a NIDAQmx device" -name = "" -product = "" +[NIDAQ_Devices.DEVICE01.MODULE01.ai] + +[NIDAQ_Devices.DEVICE01.MODULE01.ai.ai_current_Example] +info = "Replace ai_current_Example by the channel id such as ai02" +source = 'ANALOG_INPUT' +analog_type = 'CURRENT' +value_min = 0 +value_max = 1 +termination = "RSE" + +[NIDAQ_Devices.DEVICE01.MODULE01.ai.ai_voltage_Example] +info = "Replace ai_voltage_Example by the channel id such as ai02" +source = 'ANALOG_INPUT' +analog_type = 'VOLTAGE' +value_min = -10 +value_max = 10 +termination = "RSE" -[NIDAQ_Devices.DEVICE01.MODULE01.ai.ai0] -current_fields_example = "source, analog_type, value_min, value_max, termination" -voltage_fields_example = "source, analog_type, value_min, value_max, termination" -thrmcpl_fields_example = "source, analog_type, value_min, value_max, thermo_type" \ No newline at end of file +[NIDAQ_Devices.DEVICE01.MODULE01.ai.ai_thrmcpl_Example] +info = "Replace ai_thrmcpl_Example by the channel id such as ai02" +source = 'ANALOG_INPUT' +analog_type = 'TEMPERATURE_THERMOCOUPLE' +thermo_type = "K" +value_min = 0 +value_max = 100 \ No newline at end of file From 8cc57302c0fcc2f0a49a980e2c455559ad53e74f Mon Sep 17 00:00:00 2001 From: Sebastien Date: Wed, 2 Jul 2025 17:32:50 +0200 Subject: [PATCH 069/140] Params small update --- .../hardware/national_instruments/NIDAQmx_base.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_base.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_base.py index de621cc..761e4fa 100644 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_base.py +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_base.py @@ -221,12 +221,11 @@ class DAQ_NIDAQmx_base: Base NIDAQmx class for using DAQmx objects from daqmxni.py in the DAQ_NIDAQmx_Move & DAQ_NIDAQmx_Viewer """ data_grabed_signal = Signal(list) - param_instance = NIDAQmx() params = [{'title': 'Refresh hardware:', 'name': 'refresh_hardware', 'type': 'bool', 'value': False}, {'title': 'Signal type:', 'name': 'NIDAQ_type', 'type': 'list', 'limits': [Ds.name for Ds in ChannelType]}, {'title': 'Load configuration', 'name': 'load_config', 'type': 'bool', 'value': False, 'readonly': False}, - {'title': 'NSamples To Read', 'name': 'nsamplestoread', 'type': 'int', 'value': 1000, 'default': 1000, + {'title': 'NSamples To Read', 'name': 'nsamplestoread', 'type': 'int', 'value': 10, 'default': 10, 'min': 1}, {'title': 'AO Settings:', 'name': 'ao_settings', 'type': 'group', 'children': [ {'title': 'Waveform:', 'name': 'waveform', 'type': 'list', 'value': 'DC', @@ -241,8 +240,8 @@ class DAQ_NIDAQmx_base: ]}, ]}, {'title': 'Clock Settings:', 'name': 'clock_settings', 'type': 'group', 'children': [ - {'title': 'Nsamples:', 'name': 'Nsamples', 'type': 'int', 'value': 1000, 'default': 1000, 'min': 1}, - {'title': 'Frequency:', 'name': 'frequency', 'type': 'float', 'value': 1000., 'default': 1000., + {'title': 'Nsamples:', 'name': 'Nsamples', 'type': 'int', 'value': 10, 'default': 10, 'min': 1}, + {'title': 'Frequency:', 'name': 'frequency', 'type': 'float', 'value': 10., 'default': 10., 'min': 0., 'suffix': 'Hz'}, {'title': 'Repetition?:', 'name': 'repetition', 'type': 'bool', 'value': False, }, ] From fe2ce576bafc2fc0f32be4f26fde56b476cf131b Mon Sep 17 00:00:00 2001 From: Sebastien Date: Wed, 2 Jul 2025 17:36:35 +0200 Subject: [PATCH 070/140] [Errors] Handling errors if no channels added --- .../hardware/national_instruments/NIDAQmx_Viewer.py | 5 ++++- .../hardware/national_instruments/NIDAQmx_base.py | 2 ++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_Viewer.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_Viewer.py index 1468cee..ce3e829 100644 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_Viewer.py +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_Viewer.py @@ -201,7 +201,10 @@ def grab_data(self, Naverage=1, **kwargs): self.update_task() if self.settings['NIDAQ_type'] == ChannelType.ANALOG_INPUT.name: - self.controller.register_callback(self.emit_data, "Nsamples", self.clock_settings.Nsamples) + try: + self.controller.register_callback(self.emit_data, "Nsamples", self.clock_settings.Nsamples) + except AttributeError: + logger.error("Can't find a task to run") elif self.settings['NIDAQ_type'] == ChannelType.COUNTER_INPUT.name: self.timer.start(self.settings['counter_settings', 'counting_time']) self.controller.start() diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_base.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_base.py index 761e4fa..3ce13ce 100644 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_base.py +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_base.py @@ -388,6 +388,8 @@ def update_task(self): level=self.settings['trigger_settings', 'level'], ) if self.channels: self.controller.update_task(self.channels, self.clock_settings, trigger_settings=self.trigger_settings) + else: + logger.warning("No channels assigned, task not created") def get_channels_from_settings(self): channels = [] From 72433a4b90961abdd6abe9e5768942218a5b31eb Mon Sep 17 00:00:00 2001 From: Sebastien Date: Wed, 2 Jul 2025 17:38:56 +0200 Subject: [PATCH 071/140] Remove impossible case (buffer can't get only 1 data) --- .../hardware/national_instruments/daqmxni.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py index 7b23e5e..fa16ff8 100644 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py @@ -543,11 +543,8 @@ def update_task(self, channels=[], clock_settings=ClockSettings(), trigger_setti logger.error(traceback.format_exc()) def register_callback(self, callback, event='done', nsamples=1): - if event == 'done': self._task.register_done_event(callback) - elif event == 'sample': - self._task.register_every_n_samples_acquired_into_buffer_event(1, callback) elif event == 'Nsamples': self._task.register_every_n_samples_acquired_into_buffer_event(nsamples, callback) From 7f979329b827865a899cf5166bf63396d4fb8f5e Mon Sep 17 00:00:00 2001 From: Sebastien Date: Wed, 2 Jul 2025 17:55:37 +0200 Subject: [PATCH 072/140] cleaning old stuff --- .../hardware/national_instruments/NIDAQmx_Viewer.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_Viewer.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_Viewer.py index ce3e829..e04e218 100644 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_Viewer.py +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_Viewer.py @@ -49,11 +49,6 @@ def __init__(self, parent=None, params_state=None, control_type="0D"): self.settings.child('ao_settings').hide() self.settings.child('ao_channels').hide() - # timer used for the counter - self.timer = QtCore.QTimer() - self.timer.setSingleShot(True) - self.timer.timeout.connect(self.counter_done) - def stop(self): """Stop the current grab hardware wise if necessary""" try: @@ -211,7 +206,6 @@ def grab_data(self, Naverage=1, **kwargs): def emit_data(self, task_handle, every_n_samples_event_type, number_of_samples, callback_data): channels_names = [ch.name for ch in self.channels] - # channels_ai_names = [ch.name for ch in self.channels if ch.source == 'Analog_Input'] data_from_task = self.controller.task.read(self.settings['nsamplestoread'], timeout=20.0) if self.control_type == "0D": if not len(self.controller.task.channels.channel_names) != 1: From 754909466105d55f1c3a5be19470ffb6faeb87ef Mon Sep 17 00:00:00 2001 From: Sebastien Date: Wed, 2 Jul 2025 18:20:46 +0200 Subject: [PATCH 073/140] [Cleaning] General attributes & methods put in general NIDAQmx_Viewer class and unused imports & attributes removed --- .../plugins_0D/daq_0Dviewer_NIDAQmx.py | 153 +----------------- .../national_instruments/NIDAQmx_Viewer.py | 38 ++++- .../hardware/national_instruments/daqmxni.py | 4 +- 3 files changed, 34 insertions(+), 161 deletions(-) diff --git a/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_NIDAQmx.py b/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_NIDAQmx.py index 1c9218c..e0bd337 100644 --- a/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_NIDAQmx.py +++ b/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_NIDAQmx.py @@ -1,160 +1,9 @@ -from pymodaq.control_modules.viewer_utility_classes import main -from pymodaq.control_modules.viewer_utility_classes import comon_parameters as viewer_params -from pymodaq_plugins_daqmx import config -from pymodaq_plugins_daqmx.hardware.national_instruments.daqmxni import AIChannel, AIThermoChannel, NIDAQmx, niTask, \ - niDevice, TemperatureUnits, CJCSource, VoltageUnits, AcquisitionType -from pymodaq_plugins_daqmx.hardware.national_instruments.daqmxni import UsageTypeAI, ThermocoupleType, \ - TerminalConfiguration, Edge -from pymodaq_plugins_daqmx.hardware.national_instruments.NIDAQmx_base import DAQ_NIDAQmx_base from pymodaq_plugins_daqmx.hardware.national_instruments.NIDAQmx_Viewer import DAQ_NIDAQmx_Viewer -from pymodaq.utils.logger import set_logger, get_module_name -logger = set_logger(get_module_name(__file__)) class DAQ_0DViewer_NIDAQmx(DAQ_NIDAQmx_Viewer): """ Plugin for a 0D data visualization & acquisition with various NI modules plugged in a NI cDAQ. """ - - config_channels: list - channels_ai: list - config: config # todo review Useful/Unused - controller: NIDAQmx - config_devices: list - config_modules: list - current_device: niDevice - live: bool - Naverage: int - - param_devices = NIDAQmx.get_NIDAQ_devices().device_names - params = viewer_params + [ - {'title': 'Display type:', 'name': 'display', 'type': 'list', 'limits': ['0D', '1D']}, - {'title': 'Devices :', 'name': 'devices', 'type': 'list', 'limits': param_devices, - 'value': param_devices[0] - }, - ] + DAQ_NIDAQmx_base.params - def __init__(self, parent=None, params_state=None): - super().__init__(parent, params_state) - - def ini_attributes(self): - super().ini_attributes() - self.channels_ai = [] - self.config = config - self.config_channels = [] - self.config_devices = [] - self.config_modules = [] - self.live = False - self.Naverage = 1 - - def close(self): - self.live = False - self.stop() - self.controller.close() - pass - - -if __name__ == '__main__': - """Main section used during development tests""" - main_file = False - if main_file: - main(__file__) - else: - try: - print("In main") - from pymodaq_plugins_daqmx.hardware.national_instruments.daqmxni import niSystem - - # EXPLORE DEVICES - devices = niSystem.local().devices - print("devices {}".format(devices)) - print("devices names {}".format(devices.device_names)) - print("devices types {}".format([dev.product_type for dev in devices])) - cdaq = devices[0] - mod1 = cdaq.chassis_module_devices[0] # Equivalent devices[1] - mod2 = devices[2] - mod3 = devices[3] - try: - usb1 = devices[4] - except Exception as e: - pass - print("cDAQ modules: {}".format(mod.compact_daq_chassis_device.product_type for mod in [mod1, mod2, mod3])) - - # TEST RESOURCES - try: - for device in devices: - device.self_test_device() - except Exception as e: - print("Resources test failed: {}" .format(e)) - - # CREATE CHANNELS - channels_th = [AIThermoChannel(name="cDAQ1Mod1/ai0", - source='Analog_Input', - analog_type='Thermocouple', - value_min=-100, - value_max=1000, - thermo_type=ThermocoupleType.K), - ] - channels_voltage = [AIChannel(name="cDAQ1Mod3/ai0", - source='Analog_Input', - analog_type='voltage', - value_min=-80.0e-3, - value_max=80.0e-3, - termination=TerminalConfiguration.DEFAULT, - ), - AIChannel(name="cDAQ1Mod3/ai1", - source='Analog_Input', - analog_type='voltage', - value_min=-80.0e-3, - value_max=80.0e-3, - termination=TerminalConfiguration.DEFAULT, - ), - ] - # CREATE TASK - task_9211 = niTask() - task_9205 = niTask() - - def callback_9211(task_handle, every_n_samples_event_type, number_of_samples, callback_data): - data9211 = task_9211.read(5) - print(data9211) - - def callback_9205(task_handle, every_n_samples_event_type, number_of_samples, callback_data): - data9205 = task_9205.read(5) - print(data9205) - - for channel in channels_th: - task_9211.ai_channels.add_ai_thrmcpl_chan(channel.name, - "", - channel.value_min, - channel.value_max, - TemperatureUnits.DEG_C, - channel.thermo_type, - CJCSource.BUILT_IN, - 0., - "") - for channel in channels_voltage: - task_9205.ai_channels.add_ai_voltage_chan(channel.name, - "", - channel.termination, - channel.value_min, - channel.value_max, - VoltageUnits.VOLTS, - "") - task_9211.timing.cfg_samp_clk_timing(5.0, None, Edge.RISING, - AcquisitionType.CONTINUOUS, 5) - task_9211.register_every_n_samples_acquired_into_buffer_event(10, callback_9211) - - task_9205.timing.cfg_samp_clk_timing(10, None, Edge.RISING, - AcquisitionType.CONTINUOUS, 10) - task_9205.register_every_n_samples_acquired_into_buffer_event(2, callback_9205) - - task_9211.start() - task_9205.start() - - print("Acquisition in progress... Press enter to stop") - input() - - task_9211.close() - task_9205.close() - - except Exception as e: - print("Exception ({}): {}".format(type(e), str(e))) + super().__init__(parent, params_state, control_type='0D') diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_Viewer.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_Viewer.py index e04e218..ba54a75 100644 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_Viewer.py +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_Viewer.py @@ -1,8 +1,7 @@ import nidaqmx import numpy as np import traceback -from qtpy import QtCore -from .daqmxni import NIDAQmx +from .daqmxni import NIDAQmx, niDevice from pymodaq_plugins_daqmx.hardware.national_instruments.NIDAQmx_base import DAQ_NIDAQmx_base, TerminalConfiguration, \ UsageTypeAI, ChannelType from pymodaq.control_modules.viewer_utility_classes import DAQ_Viewer_base, comon_parameters as viewer_params @@ -25,9 +24,22 @@ class DAQ_NIDAQmx_Viewer(DAQ_Viewer_base, DAQ_NIDAQmx_base): -------- refresh_hardware """ - + config_channels: list + channels_ai: list + controller: NIDAQmx + config_devices: list + config_modules: list + current_device: niDevice + live: bool + Naverage: int live_mode_available = True - params = viewer_params + DAQ_NIDAQmx_base.params + param_devices = NIDAQmx.get_NIDAQ_devices().device_names + params = viewer_params + [ + {'title': 'Display type:', 'name': 'display', 'type': 'list', 'limits': ['0D', '1D']}, + {'title': 'Devices :', 'name': 'devices', 'type': 'list', 'limits': param_devices, + 'value': param_devices[0] + }, + ] + DAQ_NIDAQmx_base.params def __init__(self, parent=None, params_state=None, control_type="0D"): DAQ_Viewer_base.__init__(self, parent, params_state) # defines settings attribute and various other methods @@ -40,7 +52,7 @@ def __init__(self, parent=None, params_state=None, control_type="0D"): if self.control_type == "0D": self.settings.child('NIDAQ_type').setLimits([ChannelType.ANALOG_INPUT.name, ChannelType.COUNTER_INPUT.name, - ChannelType.DIGITAL_INPUT.name]) # analog & digital input + counter + ChannelType.DIGITAL_INPUT.name]) elif self.control_type == "1D": self.settings.child('NIDAQ_type').setLimits(ChannelType.ANALOG_INPUT.name) elif self.control_type == "Actuator": @@ -49,6 +61,15 @@ def __init__(self, parent=None, params_state=None, control_type="0D"): self.settings.child('ao_settings').hide() self.settings.child('ao_channels').hide() + def ini_attributes(self): + super().ini_attributes() + self.channels_ai = [] + self.config_channels = [] + self.config_devices = [] + self.config_modules = [] + self.live = False + self.Naverage = 1 + def stop(self): """Stop the current grab hardware wise if necessary""" try: @@ -220,4 +241,9 @@ def emit_data(self, task_handle, every_n_samples_event_type, number_of_samples, ), ]) self.dte_signal.emit(dte) - return 0 # mandatory for the NIDAQmx callback \ No newline at end of file + return 0 # mandatory for the NIDAQmx callback + + def close(self): + self.live = False + self.stop() + self.controller.close() diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py index fa16ff8..fe362eb 100644 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py @@ -1,6 +1,4 @@ -import time import traceback -from enum import Enum import numpy as np from pymodaq.utils.logger import set_logger, get_module_name @@ -245,7 +243,7 @@ def configuration_sequence(self, viewer, current_device): devices_info = [dev.name + ': ' + dev.product_type for dev in self.devices] logger.info("Detected devices: {}".format(devices_info)) try: - viewer.config_devices = [config["NIDAQ_Devices", dev].get('name') for dev in viewer.config["NIDAQ_Devices"] + viewer.config_devices = [config["NIDAQ_Devices", dev].get('name') for dev in config["NIDAQ_Devices"] if "Mod" not in config["NIDAQ_Devices", dev].get('name')] for dev in config["NIDAQ_Devices"]: if not isinstance(config["NIDAQ_Devices", dev], dict): From 25f15a8ee34c7c5e96f183ee8a56c46554a1e017 Mon Sep 17 00:00:00 2001 From: Sebastien Date: Thu, 3 Jul 2025 18:12:40 +0200 Subject: [PATCH 074/140] Rework of examples (avoid conflicts if MODULE01 doesn't support analog input as written previously) --- .../hardware/national_instruments/daqmxni.py | 4 -- .../resources/config_template.toml | 39 ++++++++++++------- 2 files changed, 25 insertions(+), 18 deletions(-) diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py index fe362eb..c4780b5 100644 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py @@ -263,8 +263,6 @@ def configuration_sequence(self, viewer, current_device): for mod in config["NIDAQ_Devices", dev]: if not isinstance(config["NIDAQ_Devices", dev, mod], dict): continue - elif 'Example' in mod: - continue try: module_name = config["NIDAQ_Devices", dev, mod].get('name') module_product = config["NIDAQ_Devices", dev, mod].get('product') @@ -280,8 +278,6 @@ def configuration_sequence(self, viewer, current_device): if src == "ai": ai = config["NIDAQ_Devices", dev, mod, src] for ch in ai.keys(): - if 'Example' in ch: - continue name = module_name + "/" + str(ch) source = ChannelType[ai[ch].get("source")] analog_type = UsageTypeAI[ai[ch].get("analog_type")] diff --git a/src/pymodaq_plugins_daqmx/resources/config_template.toml b/src/pymodaq_plugins_daqmx/resources/config_template.toml index 958c085..bfcb402 100644 --- a/src/pymodaq_plugins_daqmx/resources/config_template.toml +++ b/src/pymodaq_plugins_daqmx/resources/config_template.toml @@ -7,41 +7,52 @@ title = "Configuration entry for a NIDAQmx device" name = "" product = "" -[NIDAQ_Devices.DEVICE02_Example] -title = "Configuration entry for a NIDAQmx device" -name = "cDAQ1" -product = "cDAQ-9174" - [NIDAQ_Devices.DEVICE01.MODULE01] title = "A module plugged in the NIDAQmx device" name = "" product = "" -[NIDAQ_Devices.DEVICE01.MODULE02_Example] -title = "A module plugged in a NIDAQmx device" +[NIDAQ_Devices.DEVICE02_Example] +title = "Example of configuration entry for a NIDAQmx device" +name = "cDAQ1" +product = "cDAQ-9174" + +[NIDAQ_Devices.DEVICE02_Example.MODULE01] +title = "Example of module plugged in a NIDAQmx device for current measurement" +name = "cDAQMod1" +product = "NI 9208" + +[NIDAQ_Devices.DEVICE02_Example.MODULE02] +title = "Example of module plugged in a NIDAQmx device for voltage measurement" name = "cDAQMod1" product = "NI 9211" -[NIDAQ_Devices.DEVICE01.MODULE01.ai] +[NIDAQ_Devices.DEVICE02_Example.MODULE03] +title = "Example of module plugged in a NIDAQmx device for thermocouple measurement" +name = "cDAQMod1" +product = "NI 9205" -[NIDAQ_Devices.DEVICE01.MODULE01.ai.ai_current_Example] -info = "Replace ai_current_Example by the channel id such as ai02" +[NIDAQ_Devices.DEVICE02_Example.MODULE01.ai] +[NIDAQ_Devices.DEVICE02_Example.MODULE01.ai.ai00] +info = "Example of AI current channel" source = 'ANALOG_INPUT' analog_type = 'CURRENT' value_min = 0 value_max = 1 termination = "RSE" -[NIDAQ_Devices.DEVICE01.MODULE01.ai.ai_voltage_Example] -info = "Replace ai_voltage_Example by the channel id such as ai02" +[NIDAQ_Devices.DEVICE02_Example.MODULE02.ai] +[NIDAQ_Devices.DEVICE02_Example.MODULE02.ai.ai01] +info = "Example of AI voltage channel" source = 'ANALOG_INPUT' analog_type = 'VOLTAGE' value_min = -10 value_max = 10 termination = "RSE" -[NIDAQ_Devices.DEVICE01.MODULE01.ai.ai_thrmcpl_Example] -info = "Replace ai_thrmcpl_Example by the channel id such as ai02" +[NIDAQ_Devices.DEVICE02_Example.MODULE03.ai] +[NIDAQ_Devices.DEVICE02_Example.MODULE03.ai.ai02] +info = "Example of AI thermocouple channel" source = 'ANALOG_INPUT' analog_type = 'TEMPERATURE_THERMOCOUPLE' thermo_type = "K" From bff9a8d6cdeff82d5b434b0c5fb4fa3ff2b7c79a Mon Sep 17 00:00:00 2001 From: Sebastien Date: Fri, 4 Jul 2025 15:25:51 +0200 Subject: [PATCH 075/140] [Bug] when loading config from current module --- .../hardware/national_instruments/daqmxni.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py index c4780b5..34ecc75 100644 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py @@ -252,7 +252,7 @@ def configuration_sequence(self, viewer, current_device): continue try: device_name = config["NIDAQ_Devices", dev].get('name') - if not device_name == current_device.name: + if device_name not in current_device.name: continue device_product = config["NIDAQ_Devices", dev].get('product') device = niDevice(device_name) From ef54d4218ef3b462c4007d74dbdef65624bb5a61 Mon Sep 17 00:00:00 2001 From: Sebastien Date: Fri, 4 Jul 2025 15:33:16 +0200 Subject: [PATCH 076/140] Viewer 0D and 1D split and defined more safely (changing display into the loaded viewer is not possible anymore) --- .../plugins_0D/daq_0Dviewer_NIDAQmx.py | 23 +++++++++++ .../plugins_1D/daq_1Dviewer_NIDAQmx.py | 39 ++++++++++--------- .../national_instruments/NIDAQmx_Viewer.py | 27 +++---------- 3 files changed, 49 insertions(+), 40 deletions(-) diff --git a/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_NIDAQmx.py b/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_NIDAQmx.py index e0bd337..8ef5b75 100644 --- a/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_NIDAQmx.py +++ b/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_NIDAQmx.py @@ -1,9 +1,32 @@ +import numpy as np +from pymodaq_data import DataToExport +from pymodaq.utils.data import DataFromPlugins from pymodaq_plugins_daqmx.hardware.national_instruments.NIDAQmx_Viewer import DAQ_NIDAQmx_Viewer class DAQ_0DViewer_NIDAQmx(DAQ_NIDAQmx_Viewer): """ Plugin for a 0D data visualization & acquisition with various NI modules plugged in a NI cDAQ. + Warning: This 0D viewer is a quick solution for sending 0D data and keeping track of history, + Since it recombines arrays from buffer to send it in 0D, it only supports low acquisition rate """ def __init__(self, parent=None, params_state=None): super().__init__(parent, params_state, control_type='0D') + + def emit_data(self, task_handle, every_n_samples_event_type, number_of_samples, callback_data): + channels_names = [ch.name for ch in self.channels] + data_from_task = self.controller.task.read(self.settings['nsamplestoread'], timeout=20.0) + for i in range(self.settings['nsamplestoread']): + if len(self.controller.task.channels.channel_names) == 1: + data_dfp = np.array([data_from_task[i]]) + else: + data_dfp = np.array([data[i] for data in data_from_task]) + self.dte_signal.emit(DataToExport(name='NIDAQmx', + data=[DataFromPlugins(name='NI Analog Input', + data=[np.array([data_dfp[i]]) for i in + range(len(data_dfp))], + dim=f'Data{self.control_type}', + labels=channels_names + ), + ])) + return 0 # mandatory for the NIDAQmx callback diff --git a/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_1D/daq_1Dviewer_NIDAQmx.py b/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_1D/daq_1Dviewer_NIDAQmx.py index 09c9fd5..17cf163 100644 --- a/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_1D/daq_1Dviewer_NIDAQmx.py +++ b/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_1D/daq_1Dviewer_NIDAQmx.py @@ -1,25 +1,28 @@ +import numpy as np +from pymodaq_data import DataToExport +from pymodaq.utils.data import DataFromPlugins from pymodaq_plugins_daqmx.hardware.national_instruments.NIDAQmx_Viewer import DAQ_NIDAQmx_Viewer -from pymodaq.control_modules.viewer_utility_classes import main class DAQ_1DViewer_NIDAQmx(DAQ_NIDAQmx_Viewer): """ - ==================== ======================== - **Attributes** **Type** - *data_grabed_signal* instance of Signal - *params* dictionnary list - *task* - ==================== ======================== - - See Also - -------- - refresh_hardware + Plugin for a 0D data visualization & acquisition with various NI modules plugged in a NI cDAQ. """ - control_type = "1D" # could be "0D", "1D" - - def __init__(self, *args, **kwargs): - super().__init__(*args, control_type=self.control_type, **kwargs) - + def __init__(self, parent=None, params_state=None): + super().__init__(parent, params_state, control_type='1D') -if __name__ == '__main__': - main(__file__) \ No newline at end of file + def emit_data(self, task_handle, every_n_samples_event_type, number_of_samples, callback_data): + channels_names = [ch.name for ch in self.channels] + data_from_task = self.controller.task.read(self.settings['nsamplestoread'], timeout=20.0) + if not len(self.controller.task.channels.channel_names) != 1: + data_dfp = [np.array(data_from_task)] + else: + data_dfp = list(map(np.array, data_from_task)) + self.dte_signal.emit(DataToExport(name='NIDAQmx', + data=[DataFromPlugins(name='NI Analog Input', + data=data_dfp, + dim=f'Data{self.control_type}', + labels=channels_names, + ), + ])) + return 0 # mandatory for the NIDAQmx callback \ No newline at end of file diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_Viewer.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_Viewer.py index ba54a75..b0b68a8 100644 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_Viewer.py +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_Viewer.py @@ -35,7 +35,6 @@ class DAQ_NIDAQmx_Viewer(DAQ_Viewer_base, DAQ_NIDAQmx_base): live_mode_available = True param_devices = NIDAQmx.get_NIDAQ_devices().device_names params = viewer_params + [ - {'title': 'Display type:', 'name': 'display', 'type': 'list', 'limits': ['0D', '1D']}, {'title': 'Devices :', 'name': 'devices', 'type': 'list', 'limits': param_devices, 'value': param_devices[0] }, @@ -48,15 +47,13 @@ def __init__(self, parent=None, params_state=None, control_type="0D"): self.current_device = None self.Naverage = None self.live = False - self.control_type = control_type # could be "0D", "1D" or "Actuator" + self.control_type = control_type # could be "0D", "1D" if self.control_type == "0D": self.settings.child('NIDAQ_type').setLimits([ChannelType.ANALOG_INPUT.name, ChannelType.COUNTER_INPUT.name, ChannelType.DIGITAL_INPUT.name]) elif self.control_type == "1D": - self.settings.child('NIDAQ_type').setLimits(ChannelType.ANALOG_INPUT.name) - elif self.control_type == "Actuator": - self.settings.child('NIDAQ_type').setLimits(ChannelType.ANALOG_OUTPUT.name, ChannelType.COUNTER_OUTPUT.name) + self.settings.child('NIDAQ_type').setLimits([ChannelType.ANALOG_INPUT.name]) self.settings.child('ao_settings').hide() self.settings.child('ao_channels').hide() @@ -179,7 +176,7 @@ def ini_detector(self, controller=None): info = "Plugin Initialized" initialized = True - logger.info("Detector 0D initialized") + logger.info("Detector {} initialized".format(self.control_type)) return info, initialized except Exception as e: @@ -226,22 +223,8 @@ def grab_data(self, Naverage=1, **kwargs): self.controller.start() def emit_data(self, task_handle, every_n_samples_event_type, number_of_samples, callback_data): - channels_names = [ch.name for ch in self.channels] - data_from_task = self.controller.task.read(self.settings['nsamplestoread'], timeout=20.0) - if self.control_type == "0D": - if not len(self.controller.task.channels.channel_names) != 1: - data_dfp = [np.array(data_from_task)] - else: - data_dfp = list(map(np.array, data_from_task)) - dte = DataToExport(name='NIDAQmx', - data=[DataFromPlugins(name='NI Analog Input', - data=data_dfp, - dim=f'Data{self.settings.child("display").value()}', - labels=channels_names - ), - ]) - self.dte_signal.emit(dte) - return 0 # mandatory for the NIDAQmx callback + """Data emission to reimplement in the viewer""" + raise NotImplementedError def close(self): self.live = False From fae1576e15469fd29a3248ae10664cd253f7ce8c Mon Sep 17 00:00:00 2001 From: Sebastien Date: Mon, 7 Jul 2025 16:22:02 +0200 Subject: [PATCH 077/140] [BUG] Get the last couple of values whatever the case (some modules have 1 range and other several ranges) --- .../hardware/national_instruments/daqmxni.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py index 34ecc75..c7edeb0 100644 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py @@ -547,19 +547,19 @@ def readCounter(self): @classmethod def getAIVoltageRange(cls, device='Dev1'): - return niSystem.local().devices[device].ai_voltage_rngs[6:8] # todo self.devices[device].ai_voltage_rngs + return niSystem.local().devices[device].ai_voltage_rngs[-2:] # todo self.devices[device].ai_voltage_rngs @classmethod def getAICurrentRange(cls, device='Dev1'): - return niSystem.local().devices[device].ai_current_rngs[6:8] # todo self.devices[device].ai_current_rngs + return niSystem.local().devices[device].ai_current_rngs[-2:] # todo self.devices[device].ai_current_rngs @classmethod def getAOVoltageRange(cls, device='Dev1'): - return niSystem.local().devices[device].ao_voltage_rngs # todo self.devices[device].ao_voltage_rngs + return niSystem.local().devices[device].ao_voltage_rngs[-2:] # todo self.devices[device].ao_voltage_rngs @classmethod def getAOCurrentRange(cls, device='Dev1'): - return niSystem.local().devices[device].ao_current_rngs # todo self.devices[device].ao_current_rngs + return niSystem.local().devices[device].ao_current_rngs[-2:] # todo self.devices[device].ao_current_rngs def stop(self): if self._task is not None: From 2218d7fe5dded2ab82395a8f873a6cb9f62723bd Mon Sep 17 00:00:00 2001 From: Sebastien Date: Fri, 25 Jul 2025 14:30:05 +0200 Subject: [PATCH 078/140] 0D viewer deprecated since buffer return 1D array --- .../plugins_0D/daq_0Dviewer_NIDAQmx.py | 32 ------------------- 1 file changed, 32 deletions(-) delete mode 100644 src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_NIDAQmx.py diff --git a/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_NIDAQmx.py b/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_NIDAQmx.py deleted file mode 100644 index 8ef5b75..0000000 --- a/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_NIDAQmx.py +++ /dev/null @@ -1,32 +0,0 @@ -import numpy as np -from pymodaq_data import DataToExport -from pymodaq.utils.data import DataFromPlugins -from pymodaq_plugins_daqmx.hardware.national_instruments.NIDAQmx_Viewer import DAQ_NIDAQmx_Viewer - - -class DAQ_0DViewer_NIDAQmx(DAQ_NIDAQmx_Viewer): - """ - Plugin for a 0D data visualization & acquisition with various NI modules plugged in a NI cDAQ. - Warning: This 0D viewer is a quick solution for sending 0D data and keeping track of history, - Since it recombines arrays from buffer to send it in 0D, it only supports low acquisition rate - """ - def __init__(self, parent=None, params_state=None): - super().__init__(parent, params_state, control_type='0D') - - def emit_data(self, task_handle, every_n_samples_event_type, number_of_samples, callback_data): - channels_names = [ch.name for ch in self.channels] - data_from_task = self.controller.task.read(self.settings['nsamplestoread'], timeout=20.0) - for i in range(self.settings['nsamplestoread']): - if len(self.controller.task.channels.channel_names) == 1: - data_dfp = np.array([data_from_task[i]]) - else: - data_dfp = np.array([data[i] for data in data_from_task]) - self.dte_signal.emit(DataToExport(name='NIDAQmx', - data=[DataFromPlugins(name='NI Analog Input', - data=[np.array([data_dfp[i]]) for i in - range(len(data_dfp))], - dim=f'Data{self.control_type}', - labels=channels_names - ), - ])) - return 0 # mandatory for the NIDAQmx callback From 0ccfda1da38241b534bed1c42e600f3715d5b8e0 Mon Sep 17 00:00:00 2001 From: Sebastien Date: Fri, 25 Jul 2025 14:38:54 +0200 Subject: [PATCH 079/140] grab & emit data centralized within NIDQmx_Viewer --- .../plugins_1D/daq_1Dviewer_NIDAQmx.py | 16 ----------- .../national_instruments/NIDAQmx_Viewer.py | 27 +++++++++++++++++-- 2 files changed, 25 insertions(+), 18 deletions(-) diff --git a/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_1D/daq_1Dviewer_NIDAQmx.py b/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_1D/daq_1Dviewer_NIDAQmx.py index 17cf163..e3c21de 100644 --- a/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_1D/daq_1Dviewer_NIDAQmx.py +++ b/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_1D/daq_1Dviewer_NIDAQmx.py @@ -10,19 +10,3 @@ class DAQ_1DViewer_NIDAQmx(DAQ_NIDAQmx_Viewer): """ def __init__(self, parent=None, params_state=None): super().__init__(parent, params_state, control_type='1D') - - def emit_data(self, task_handle, every_n_samples_event_type, number_of_samples, callback_data): - channels_names = [ch.name for ch in self.channels] - data_from_task = self.controller.task.read(self.settings['nsamplestoread'], timeout=20.0) - if not len(self.controller.task.channels.channel_names) != 1: - data_dfp = [np.array(data_from_task)] - else: - data_dfp = list(map(np.array, data_from_task)) - self.dte_signal.emit(DataToExport(name='NIDAQmx', - data=[DataFromPlugins(name='NI Analog Input', - data=data_dfp, - dim=f'Data{self.control_type}', - labels=channels_names, - ), - ])) - return 0 # mandatory for the NIDAQmx callback \ No newline at end of file diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_Viewer.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_Viewer.py index b0b68a8..d6b58e5 100644 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_Viewer.py +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_Viewer.py @@ -223,8 +223,31 @@ def grab_data(self, Naverage=1, **kwargs): self.controller.start() def emit_data(self, task_handle, every_n_samples_event_type, number_of_samples, callback_data): - """Data emission to reimplement in the viewer""" - raise NotImplementedError + channels_names = [ch.name for ch in self.channels] + data_from_task = self.controller.task.read(self.settings['nsamplestoread'], timeout=20.0) + if not len(self.controller.task.channels.channel_names) != 1: + data_dfp = [np.array(data_from_task)] + else: + data_dfp = list(map(np.array, data_from_task)) + self.dte_signal.emit(DataToExport(name='NIDAQmx', + data=[DataFromPlugins(name='Data from ' + self.current_device.name, + data=data_dfp, + dim=f'Data{self.control_type}', + labels=channels_names, + ), + ])) + return 0 # mandatory for the NIDAQmx callback + + def stop(self): + """Stop the current grab hardware wise if necessary""" + try: + DAQ_NIDAQmx_base.stop(self) + self.live = False + logger.info("Acquisition stopped.") + except Exception: + pass + self.emit_status(ThreadCommand('Update_Status', ['Acquisition stopped.'])) + return '' def close(self): self.live = False From 71b4e4075429fd73be107f4a3db929100ab91748 Mon Sep 17 00:00:00 2001 From: Sebastien Date: Fri, 25 Jul 2025 14:46:39 +0200 Subject: [PATCH 080/140] Cleanning old attributes --- .../hardware/national_instruments/NIDAQmx_Viewer.py | 3 --- .../hardware/national_instruments/daqmxni.py | 10 +++------- 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_Viewer.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_Viewer.py index d6b58e5..bb1c478 100644 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_Viewer.py +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_Viewer.py @@ -25,7 +25,6 @@ class DAQ_NIDAQmx_Viewer(DAQ_Viewer_base, DAQ_NIDAQmx_base): refresh_hardware """ config_channels: list - channels_ai: list controller: NIDAQmx config_devices: list config_modules: list @@ -44,7 +43,6 @@ def __init__(self, parent=None, params_state=None, control_type="0D"): DAQ_Viewer_base.__init__(self, parent, params_state) # defines settings attribute and various other methods DAQ_NIDAQmx_base.__init__(self) - self.current_device = None self.Naverage = None self.live = False self.control_type = control_type # could be "0D", "1D" @@ -60,7 +58,6 @@ def __init__(self, parent=None, params_state=None, control_type="0D"): def ini_attributes(self): super().ini_attributes() - self.channels_ai = [] self.config_channels = [] self.config_devices = [] self.config_modules = [] diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py index c7edeb0..45e9c7e 100644 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py @@ -146,10 +146,8 @@ def __init__(self): self._task = None self.update_NIDAQ_devices() self.update_NIDAQ_channels() - self.c_callback = None # A qui servent ces callback ?? - self.callback_data = None - self.is_scalar = True - self.write_buffer = np.array([0.]) # ou est utilisé ce buffer?? + devices_info = [dev.name + ': ' + dev.product_type for dev in self.devices] + logger.info("Detected devices: {}".format(devices_info)) @property def task(self): @@ -161,7 +159,7 @@ def device(self): @device.setter def device(self, device): - if device not in self.devices.device_names: + if device not in self.devices: raise IOError(f'your device: {device} is not known or connected') self._device = device @@ -318,7 +316,6 @@ def configuration_sequence(self, viewer, current_device): logger.error("Configuration entries <{}> does not match the hardware ".format(err)) except Exception as err: logger.info("Configuration sequence error, verify if your config matches the hardware: {}".format(err)) - pass logger.info(" ********** CONFIGURATION SEQUENCE SUCCESSFULLY ENDED **********") @classmethod @@ -363,7 +360,6 @@ def update_task(self, channels=[], clock_settings=ClockSettings(), trigger_setti self._task.close() self._task = None - self.c_callback = None self._task = niTask() logger.info("TASK: {}".format(self._task)) From 7cfdf9996c7e1bf1c144d75b89c4281827bf9613 Mon Sep 17 00:00:00 2001 From: Sebastien Date: Fri, 25 Jul 2025 14:47:27 +0200 Subject: [PATCH 081/140] Load config not automatic anymore (more convenient + prevent from conflicts) --- .../national_instruments/NIDAQmx_Viewer.py | 96 ++++++++++--------- .../national_instruments/NIDAQmx_base.py | 5 +- .../hardware/national_instruments/daqmxni.py | 4 +- 3 files changed, 55 insertions(+), 50 deletions(-) diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_Viewer.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_Viewer.py index bb1c478..072ba31 100644 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_Viewer.py +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_Viewer.py @@ -105,53 +105,57 @@ def commit_settings(self, param): except: pass elif param.name() == 'load_config': - self.controller.configuration_sequence(self, self.current_device) + self.controller.configuration_sequence(self, self.controller.device) self.settings.child('load_config').hide() - for ch in self.config_channels: - if self.settings.child("devices").value() in ch.name: - self.settings.child('ai_channels').addNew(ch.name) - param = [a for a in self.settings.child('ai_channels').childs if a.opts['title'] == ch.name][0] - param.child("voltage_settings").show(ch.analog_type == UsageTypeAI.VOLTAGE) - param.child("current_settings").show(ch.analog_type == UsageTypeAI.CURRENT) - param.child("thermoc_settings").show(ch.analog_type == UsageTypeAI.TEMPERATURE_THERMOCOUPLE) - match ch.analog_type: - case UsageTypeAI.VOLTAGE: - self.settings.child("ai_channels", param.opts['name'], "ai_type").setValue("VOLTAGE") - self.settings.child("ai_channels", param.opts['name'], "voltage_settings", - "volt_min").setValue( - ch.value_min) - self.settings.child("ai_channels", param.opts['name'], "voltage_settings", - "volt_max").setValue( - ch.value_max) - self.settings.child("ai_channels", param.opts['name'], "termination").setValue( - ch.termination.name) - case UsageTypeAI.CURRENT: - self.settings.child("ai_channels", param.opts['name'], "ai_type").setValue("CURRENT") - self.settings.child("ai_channels", param.opts['name'], "current_settings", - "curr_min").setValue( - ch.value_min) - self.settings.child("ai_channels", param.opts['name'], "current_settings", - "curr_max").setValue( - ch.value_max) - self.settings.child("ai_channels", param.opts['name'], "termination").setValue( - ch.termination.name) - case UsageTypeAI.TEMPERATURE_THERMOCOUPLE: - self.settings.child("ai_channels", param.opts['name'], "ai_type").setValue( - "TEMPERATURE_THERMOCOUPLE") - self.settings.child("ai_channels", - param.opts['name'], - "thermoc_settings", - "thermoc_type").setValue(ch.thermo_type.name) - self.settings.child("ai_channels", - param.opts['name'], - "thermoc_settings", - "T_min").setValue(ch.value_min) - self.settings.child("ai_channels", - param.opts['name'], - "thermoc_settings", - "T_max").setValue(ch.value_max) - self.settings.child("ai_channels", param.opts['name'], "termination").setValue( - TerminalConfiguration.DEFAULT.name) + config_ai_channels = [ch for ch in self.config_channels if ch.source == ChannelType.ANALOG_INPUT] + for ch in config_ai_channels: # Browse configuration analog channels + self.settings.child('ai_channels').addNew(ch.name) + ch_par = [a for a in self.settings.child('ai_channels').childs if a.opts['title'] == ch.name][0] + ch_par.child("voltage_settings").show(ch.analog_type == UsageTypeAI.VOLTAGE) + ch_par.child("current_settings").show(ch.analog_type == UsageTypeAI.CURRENT) + ch_par.child("thermoc_settings").show(ch.analog_type == UsageTypeAI.TEMPERATURE_THERMOCOUPLE) + match ch.analog_type: + case UsageTypeAI.VOLTAGE: + self.settings.child("ai_channels", ch_par.opts['name'], "ai_type").setValue("VOLTAGE") + self.settings.child("ai_channels", ch_par.opts['name'], "voltage_settings", + "volt_min").setValue( + ch.value_min) + self.settings.child("ai_channels", ch_par.opts['name'], "voltage_settings", + "volt_max").setValue( + ch.value_max) + self.settings.child("ai_channels", ch_par.opts['name'], "termination").setValue( + ch.termination.name) + case UsageTypeAI.CURRENT: + self.settings.child("ai_channels", ch_par.opts['name'], "ai_type").setValue("CURRENT") + self.settings.child("ai_channels", ch_par.opts['name'], "current_settings", + "curr_min").setValue( + ch.value_min) + self.settings.child("ai_channels", ch_par.opts['name'], "current_settings", + "curr_max").setValue( + ch.value_max) + self.settings.child("ai_channels", ch_par.opts['name'], "termination").setValue( + ch.termination.name) + case UsageTypeAI.TEMPERATURE_THERMOCOUPLE: + self.settings.child("ai_channels", ch_par.opts['name'], "ai_type").setValue( + "TEMPERATURE_THERMOCOUPLE") + self.settings.child("ai_channels", + ch_par.opts['name'], + "thermoc_settings", + "thermoc_type").setValue(ch.thermo_type.name) + self.settings.child("ai_channels", + ch_par.opts['name'], + "thermoc_settings", + "T_min").setValue(ch.value_min) + self.settings.child("ai_channels", + ch_par.opts['name'], + "thermoc_settings", + "T_max").setValue(ch.value_max) + self.settings.child("ai_channels", ch_par.opts['name'], "termination").setValue( + TerminalConfiguration.DEFAULT.name) + self.channels = self.get_channels_from_settings() + self.set_max_frequency() # Set the acquisition frequency to the device maximum frequency + self.channels = self.get_channels_from_settings() + self.get_max_frequency() # Set the frequency 'max' option to the device maximum frequency and display it def ini_detector(self, controller=None): """ diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_base.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_base.py index 3ce13ce..0bed795 100644 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_base.py +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_base.py @@ -224,8 +224,9 @@ class DAQ_NIDAQmx_base: params = [{'title': 'Refresh hardware:', 'name': 'refresh_hardware', 'type': 'bool', 'value': False}, {'title': 'Signal type:', 'name': 'NIDAQ_type', 'type': 'list', 'limits': [Ds.name for Ds in ChannelType]}, - {'title': 'Load configuration', 'name': 'load_config', 'type': 'bool', 'value': False, 'readonly': False}, - {'title': 'NSamples To Read', 'name': 'nsamplestoread', 'type': 'int', 'value': 10, 'default': 10, + {'title': 'Plugin config:', 'name': 'load_config', 'type': 'bool_push', 'value': False, + 'readonly': False, 'label': 'Load Config'}, + {'title': 'NSamples To Read:', 'name': 'nsamplestoread', 'type': 'int', 'value': 10, 'default': 10, 'min': 1}, {'title': 'AO Settings:', 'name': 'ao_settings', 'type': 'group', 'children': [ {'title': 'Waveform:', 'name': 'waveform', 'type': 'list', 'value': 'DC', diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py index 45e9c7e..6c43f1b 100644 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py @@ -238,8 +238,6 @@ def configuration_sequence(self, viewer, current_device): and send the nidaqmx a sequence which set up each channel. """ logger.info("********** CONFIGURATION SEQUENCE INITIALIZED **********") - devices_info = [dev.name + ': ' + dev.product_type for dev in self.devices] - logger.info("Detected devices: {}".format(devices_info)) try: viewer.config_devices = [config["NIDAQ_Devices", dev].get('name') for dev in config["NIDAQ_Devices"] if "Mod" not in config["NIDAQ_Devices", dev].get('name')] @@ -263,6 +261,8 @@ def configuration_sequence(self, viewer, current_device): continue try: module_name = config["NIDAQ_Devices", dev, mod].get('name') + if current_device.name not in module_name: + continue module_product = config["NIDAQ_Devices", dev, mod].get('product') module = niDevice(module_name) assert module in self.devices and module.product_type == module_product, module.name From d466d3bf1b9a5b98f00f24176c3dc211e2855a99 Mon Sep 17 00:00:00 2001 From: Sebastien Date: Fri, 25 Jul 2025 14:49:10 +0200 Subject: [PATCH 082/140] Add frequency management depending on the material used --- .../national_instruments/NIDAQmx_Viewer.py | 63 +++++++++---------- .../national_instruments/NIDAQmx_base.py | 5 +- .../hardware/national_instruments/daqmxni.py | 6 +- 3 files changed, 39 insertions(+), 35 deletions(-) diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_Viewer.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_Viewer.py index 072ba31..3a6ac70 100644 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_Viewer.py +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_Viewer.py @@ -93,17 +93,16 @@ def commit_settings(self, param): if param.parent() is not None: if param.parent().name() == 'ai_channels': device = param.opts['title'].split('/')[0] - self.settings.child('clock_settings', 'frequency').setOpts(max=self.controller.getAIMaxRate(device)) - + self.settings.child('clock_settings', 'frequency').setOpts(max=self.controller.getAIMaxSingleRate(device)) volt_ranges = self.controller.getAIVoltageRange(device) - curr_ranges = self.controller.getAICurrentRange(device) - try: + if volt_ranges: param.child('voltage_settings', 'volt_min').setValue(volt_ranges[0]) param.child('voltage_settings', 'volt_max').setValue(volt_ranges[1]) + curr_ranges = self.controller.getAICurrentRange(device) + if curr_ranges: param.child('current_settings', 'curr_min').setValue(curr_ranges[0]) param.child('current_settings', 'curr_max').setValue(curr_ranges[1]) - except: - pass + elif param.name() == 'load_config': self.controller.configuration_sequence(self, self.controller.device) self.settings.child('load_config').hide() @@ -157,35 +156,35 @@ def commit_settings(self, param): self.channels = self.get_channels_from_settings() self.get_max_frequency() # Set the frequency 'max' option to the device maximum frequency and display it - def ini_detector(self, controller=None): - """ - Initialisation procedure of the detector. - - See Also - -------- - daq_utils.ThreadCommand - """ + def get_max_frequency(self): + # Destined to be removed when set_max_frequency will work correctly try: - if self.is_master: - self.controller = NIDAQmx() + if self.settings.child('ai_channels').children() or self.settings.child('di_channels').children(): + max_freq = self.controller.getAIMaxSingleRate(self.controller.device.name)/len(self.channels) + self.settings.child('clock_settings', 'max_freq').setValue(max_freq) else: - self.controller = controller - self.current_device = nidaqmx.system.Device(self.settings["devices"]) - - # actions to perform in order to set properly the settings tree options - self.commit_settings(self.settings.child('NIDAQ_type')) - - info = "Plugin Initialized" - initialized = True - logger.info("Detector {} initialized".format(self.control_type)) - return info, initialized + max_freq = self.controller.getAIMaxSingleRate(self.controller.device.name) + self.settings.child('clock_settings', 'max_freq').setValue(max_freq) + except: + pass - except Exception as e: - logger.info(traceback.format_exc()) - self.emit_status(ThreadCommand('Update_Status', [str(e), 'log'])) - info = str(e) - initialized = False - return info, initialized + def set_max_frequency(self): + # Opts 'max' get the right value but doesn't update the viewer which keep as max the initial max value + try: + if self.settings.child('ai_channels').children() or self.settings.child('di_channels').children(): + max_freq = self.controller.getAIMaxSingleRate(self.controller.device.name) / len(self.channels) + self.settings.child('clock_settings', 'frequency').setOpts(max=max_freq) + self.settings.child('clock_settings', 'frequency').emitStateChanged('max', max_freq) + self.settings.child('clock_settings', 'frequency')._emitOptionsChanged( + self.settings.child('clock_settings', 'frequency'), {'max': max_freq}) + else: + max_freq = self.controller.getAIMaxSingleRate(self.controller.device.name) + self.settings.child('clock_settings', 'frequency').setOpts(max=max_freq) + self.settings.child('clock_settings', 'frequency').emitStateChanged('max', max_freq) + self.settings.child('clock_settings', 'frequency')._emitOptionsChanged( + self.settings.child('clock_settings', 'frequency'), {'max': max_freq}) + except: + pass def grab_data(self, Naverage=1, **kwargs): """ diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_base.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_base.py index 0bed795..ee1a5d1 100644 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_base.py +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_base.py @@ -242,9 +242,10 @@ class DAQ_NIDAQmx_base: ]}, {'title': 'Clock Settings:', 'name': 'clock_settings', 'type': 'group', 'children': [ {'title': 'Nsamples:', 'name': 'Nsamples', 'type': 'int', 'value': 10, 'default': 10, 'min': 1}, + {'title': 'Max frequency supported:', 'name': 'max_freq', 'type': 'float', 'value': 10., + 'readonly': True, 'suffix': 'Hz'}, {'title': 'Frequency:', 'name': 'frequency', 'type': 'float', 'value': 10., 'default': 10., - 'min': 0., 'suffix': 'Hz'}, - {'title': 'Repetition?:', 'name': 'repetition', 'type': 'bool', 'value': False, }, + 'min': 0., 'max': 10e5, 'suffix': 'Hz'}, ] }, {'title': 'AI Channels:', 'name': 'ai_channels', 'type': 'groupai', diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py index 6c43f1b..b00980a 100644 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py @@ -323,9 +323,13 @@ def getAOMaxRate(cls, device): return niDevice(device).ao_max_rate @classmethod - def getAIMaxRate(cls, device): + def getAIMaxSingleRate(cls, device): return niDevice(device).ai_max_single_chan_rate + @classmethod + def getAIMaxMultiRate(cls, device): + return niDevice(device).ai_max_multi_chan_rate + @classmethod def isAnalogTriggeringSupported(cls, device): return niDevice(device).anlg_trig_supported From 65410955f428c9967786317e2f87fa4abd48e3f7 Mon Sep 17 00:00:00 2001 From: Sebastien Date: Fri, 25 Jul 2025 14:50:34 +0200 Subject: [PATCH 083/140] ini_detector use new master/slave management with self.is_master --- .../national_instruments/NIDAQmx_Viewer.py | 37 ++++++++++++++----- 1 file changed, 28 insertions(+), 9 deletions(-) diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_Viewer.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_Viewer.py index 3a6ac70..642b374 100644 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_Viewer.py +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_Viewer.py @@ -64,16 +64,35 @@ def ini_attributes(self): self.live = False self.Naverage = 1 - def stop(self): - """Stop the current grab hardware wise if necessary""" + def ini_detector(self, controller=None): + """ + Initialisation procedure of the detector. + + See Also + -------- + daq_utils.ThreadCommand + """ try: - DAQ_NIDAQmx_base.stop(self) - self.live = False - logger.info("Acquisition stopped.") - except Exception: - pass - self.emit_status(ThreadCommand('Update_Status', ['Acquisition stopped.'])) - return '' + if self.is_master: + self.controller = NIDAQmx() + else: + self.controller = controller + self.controller.device = nidaqmx.system.Device(self.settings["devices"]) + + # actions to perform in order to set properly the settings tree options + self.commit_settings(self.settings.child('NIDAQ_type')) + + info = "Plugin Initialized" + initialized = True + logger.info("Detector {} initialized".format(self.control_type)) + return info, initialized + + except Exception as e: + logger.info(traceback.format_exc()) + self.emit_status(ThreadCommand('Update_Status', [str(e), 'log'])) + info = str(e) + initialized = False + return info, initialized def commit_settings(self, param): """ From fc7de56c69e2310fb84037e2b71d84d392a8f15f Mon Sep 17 00:00:00 2001 From: Sebastien Date: Fri, 25 Jul 2025 15:08:01 +0200 Subject: [PATCH 084/140] Update pyproject.toml & README.rst --- README.rst | 11 ++++++----- pyproject.toml | 4 ++-- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/README.rst b/README.rst index 1ae979f..be6cb3b 100644 --- a/README.rst +++ b/README.rst @@ -35,24 +35,25 @@ Below is the list of instruments included in this plugin Actuators +++++++++ -* **DAQmx_MultipleScannerControl**: Control of piezo scanners with an analog output. +* **NIDAQmx**: General generation (voltage / current) with National Instrument materials Viewer0D ++++++++ -* **DAQmx_PLcounter**: Single photon counting * **NIDAQmx**: General measurement with National Instrument materials NIDAQmx Viewer ++++++++++++++ This viewer aims at being very general and allows measurement with as much different NI modules as possible. -At this point (06/2025), it supports Analog measurement of currents, voltages and temperatures from thermocouples. +At this point (07/2025), it supports Analog measurement of currents, voltages and temperatures from thermocouples. Tests have been running with the following devices: * Chassis NI cDAQ-9174 & NI USB-9162 * NI modules 9211 & 9205 -Acquisition channels can ge generated directly running the viewer, but it is also possible to load channels from the configuration file config_daqmx.toml clicking on "load configuration" in the parameter tree +Acquisition channels can ge generated directly running the viewer, but it is also possible to load channels from the configuration file config_daqmx.toml clicking on "load configuration" in the parameter tree. -To use such configuration, copy paste the content from C:/ProgramData/.pymodaq/config_daqmx.toml to /.pymodaq/config_daqmx.toml and fill it with the configuration of your own experimental setup. \ No newline at end of file +To use such configuration, copy paste the content from C:/ProgramData/.pymodaq/config_daqmx.toml to /.pymodaq/config_daqmx.toml and fill it with the configuration of your own experimental setup. + +It is recommended to create a preset with one detector for each module used (using the current device parameter). It allows to control the frequency for each module and make the "Load config" button only catch channels corresponding to the selected module. \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 3726a60..bbc2c29 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,9 +10,9 @@ package-url = 'https://github.com/PyMoDAQ/pymodaq_plugins_daqmx' [project] name = "pymodaq_plugins_daqmx" -description = 'Hardware plugins for PyMoDAQ using the NiDAQmx framework (nidaq wrapper)' +description = 'Hardware plugins for PyMoDAQ using the NiDAQmx framework (nidaqmx wrapper)' dependencies = [ - "pymodaq>=4.4", + "pymodaq>=5.0", "pydaqmx", "nidaqmx", "aenum" From 8061ff305b185abb776230994449b3676e09987c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Guerrero?= Date: Tue, 14 Oct 2025 17:17:25 +0200 Subject: [PATCH 085/140] correct device attribute --- .../hardware/national_instruments/NIDAQmx_Viewer.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_Viewer.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_Viewer.py index 642b374..7997daf 100644 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_Viewer.py +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_Viewer.py @@ -28,7 +28,6 @@ class DAQ_NIDAQmx_Viewer(DAQ_Viewer_base, DAQ_NIDAQmx_base): controller: NIDAQmx config_devices: list config_modules: list - current_device: niDevice live: bool Naverage: int live_mode_available = True @@ -121,7 +120,8 @@ def commit_settings(self, param): if curr_ranges: param.child('current_settings', 'curr_min').setValue(curr_ranges[0]) param.child('current_settings', 'curr_max').setValue(curr_ranges[1]) - + elif param.name() == 'devices': + self.controller.device = self.settings.child('devices') elif param.name() == 'load_config': self.controller.configuration_sequence(self, self.controller.device) self.settings.child('load_config').hide() @@ -249,7 +249,7 @@ def emit_data(self, task_handle, every_n_samples_event_type, number_of_samples, else: data_dfp = list(map(np.array, data_from_task)) self.dte_signal.emit(DataToExport(name='NIDAQmx', - data=[DataFromPlugins(name='Data from ' + self.current_device.name, + data=[DataFromPlugins(name='Data from ' + self.controller.device.name, data=data_dfp, dim=f'Data{self.control_type}', labels=channels_names, From 8c6896e19f203a8385113f6e277ecb17e6c59a43 Mon Sep 17 00:00:00 2001 From: CyrilAUCLAIR-fr Date: Wed, 22 Oct 2025 15:15:15 +0200 Subject: [PATCH 086/140] Add of viewer parameter menu related to RTD temperature measurements --- .../national_instruments/NIDAQmx_base.py | 39 ++++++++++++++++++- .../hardware/national_instruments/daqmxni.py | 14 ++++++- 2 files changed, 50 insertions(+), 3 deletions(-) diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_base.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_base.py index ee1a5d1..034a6ad 100644 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_base.py +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_base.py @@ -5,7 +5,7 @@ from pymodaq_gui.parameter.pymodaq_ptypes import registerParameterType, GroupParameter from pymodaq_plugins_daqmx.hardware.national_instruments.daqmxni import NIDAQmx, Edge, ChannelType, ClockSettings, \ AIChannel, AIThermoChannel, AOChannel, CIChannel, COChannel, DOChannel, DIChannel, UsageTypeAI, UsageTypeAO, \ - ThermocoupleType, TerminalConfiguration, TriggerSettings + ThermocoupleType, TerminalConfiguration, TriggerSettings, RTDType, TemperatureUnits, ResistanceConfiguration, ExcitationSource logger = set_logger(get_module_name(__file__)) @@ -41,6 +41,32 @@ class ScalableGroupAI(GroupParameter): {'title': 'Temp. Min:', 'name': 'T_min', 'type': 'float', 'value': 0, 'suffix': '°C'}, {'title': 'Temp. Max:', 'name': 'T_max', 'type': 'float', 'value': 50, 'suffix': '°C'}, ]}, + {'title': 'TEMPERATURE_RTD:', 'name': 'rtd_settings', 'type': 'group', 'visible': False, + 'children': [ + {'title': 'Max. value in:', 'name': 'max_value_in', 'type': 'float', 'value': '0'}, + {'title': 'Max. value out:', 'name': 'max_value_out', 'type': 'float', 'value': '0'}, + {'title': 'Temp. unit:', 'name': 'temp_unit', 'type': 'list', + 'limits': [TemperatureUnits.DEG_C.name, TemperatureUnits.DEG_F.name], + 'value': TemperatureUnits.DEG_C.name}, + {'title': 'r0', 'name': 'r0', 'type': 'float', 'value': 100, 'suffix': 'Ω'}, + {'title': 'RTD type:', 'name': 'rtd_type', 'type': 'list', + 'limits': [rtd_type.name for rtd_type in RTDType], 'value': RTDType.PT_3750.name}, + {'title': 'Callendar-Van Dusen coefficients:', 'name': 'c-vd_coeff.', 'type': 'group', + 'visible': False, 'children': [ + # These coefficients are supposed to used in a °C Callendar-Van Dusen equation, + # cf. https://www.ni.com/docs/fr-FR/bundle/ni-daqmx/page/callendarvandusen.html (Oct. 2025) + {'title': 'A', 'name': 'a_c-vd_coeff', 'type': 'float', 'suffix': 'Ω/°C'}, + {'title': 'B', 'name': 'b_c-vd_coeff', 'type': 'float', 'suffix': 'Ω/(°C²)'}, + {'title': 'C', 'name': 'c_c-vd_coeff', 'type': 'float', 'suffix': 'Ω/(°C⁴)'}, + ]}, + {'title': 'Resistance config.', 'name': 'resistance_config', 'type': 'list', + 'limits': [rc.name for rc in ResistanceConfiguration], + 'value': ResistanceConfiguration.FOUR_WIRE.name}, + {'title' : 'Curr. excit. src', 'name': 'current_excit_src', 'type': 'list', + 'limits' : [excit_src.name for excit_src in ExcitationSource], + 'value': ExcitationSource.INTERNAL.name}, + {'title': 'Iex value', 'name': 'i_ex_value', 'type': 'float', 'value': 0.00100, 'suffix': 'A'}, + ]}, {'title': 'Termination:', 'name': 'termination', 'type': 'list', 'limits': [Te.name for Te in TerminalConfiguration]}, ] @@ -369,6 +395,10 @@ def commit_settings(self, param: Parameter): param.parent().child('voltage_settings').show(param.value() == UsageTypeAI.VOLTAGE.name) param.parent().child('current_settings').show(param.value() == UsageTypeAI.CURRENT.name) param.parent().child('thermoc_settings').show(param.value() == UsageTypeAI.TEMPERATURE_THERMOCOUPLE.name) + param.parent().child('rtd_settings').show(param.value() == UsageTypeAI.TEMPERATURE_RTD.name) + + elif param.name() == 'rtd_type': + param.parent().child('c-vd_coeff.').show(param.value() == RTDType.CUSTOM.name) elif param.name() == 'ao_type': param.parent().child('voltage_settings').show(param.value() == UsageTypeAI.VOLTAGE.name) @@ -418,6 +448,13 @@ def get_channels_from_settings(self): value_max=channel['thermoc_settings', 'T_max'], thermo_type=ThermocoupleType[ channel['thermoc_settings', 'thermoc_type']], )) + elif analog_type == UsageTypeAI.TEMPERATURE_RTD: + channels.append(AIThermoChannel(name=channel.opts['title'], + source=source, analog_type=analog_type, + value_min=channel['thermoc_settings', 'T_min'], + value_max=channel['thermoc_settings', 'T_max'], + thermo_type=ThermocoupleType[ + channel['thermoc_settings', 'thermoc_type']], )) elif self.settings['NIDAQ_type'] == ChannelType.ANALOG_OUTPUT.name: # analog output source = ChannelType.ANALOG_OUTPUT diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py index b00980a..b7677ee 100644 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py @@ -2,10 +2,11 @@ import numpy as np from pymodaq.utils.logger import set_logger, get_module_name -from nidaqmx.constants import AcquisitionType, VoltageUnits, CurrentUnits, CurrentShuntResistorLocation, \ +from nidaqmx.constants import (AcquisitionType, VoltageUnits, CurrentUnits, CurrentShuntResistorLocation, \ TemperatureUnits, CJCSource, CountDirection, Level, FrequencyUnits, TimeUnits, \ LineGrouping, UsageTypeAI, UsageTypeAO, UsageTypeCI, UsageTypeCO, Edge, \ - TerminalConfiguration, ThermocoupleType, ChannelType + TerminalConfiguration, ThermocoupleType, ChannelType, RTDType, TemperatureUnits, + ResistanceConfiguration, ExcitationSource) from nidaqmx.system import System as niSystem from nidaqmx.system.device import Device as niDevice @@ -90,6 +91,13 @@ def __init__(self, thermo_type=ThermocoupleType.K, **kwargs): assert thermo_type in ThermocoupleType self.thermo_type = thermo_type +# class AI_RTD_Channel(AIChannel): +# def __init__(self, rtd_type=RTDType.Pt_3750, **kwargs): +# super().__init__(**kwargs) +# assert rtd_type in RTDType +# self.rtd_type = rtd_type +# à compléter !!!!!! + class AOChannel(AChannel): def __init__(self, **kwargs): @@ -403,6 +411,8 @@ def update_task(self, channels=[], clock_settings=ClockSettings(), trigger_setti CJCSource.BUILT_IN, 0., "") + # elif channel.analog_type == UsageTypeAI.TEMPERATURE_RTD: + # self._task.ai_channels.add_ai except DaqError as e: err_code = e.error_code if err_code: From 2da531ff7742b9b60d9ca13600166008900083cc Mon Sep 17 00:00:00 2001 From: CyrilAUCLAIR-fr Date: Wed, 22 Oct 2025 18:25:25 +0200 Subject: [PATCH 087/140] RTD measurements integration --- .../national_instruments/NIDAQmx_base.py | 23 +++++++----- .../hardware/national_instruments/daqmxni.py | 35 ++++++++++++++----- 2 files changed, 42 insertions(+), 16 deletions(-) diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_base.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_base.py index 034a6ad..6987cdd 100644 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_base.py +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_base.py @@ -5,7 +5,7 @@ from pymodaq_gui.parameter.pymodaq_ptypes import registerParameterType, GroupParameter from pymodaq_plugins_daqmx.hardware.national_instruments.daqmxni import NIDAQmx, Edge, ChannelType, ClockSettings, \ AIChannel, AIThermoChannel, AOChannel, CIChannel, COChannel, DOChannel, DIChannel, UsageTypeAI, UsageTypeAO, \ - ThermocoupleType, TerminalConfiguration, TriggerSettings, RTDType, TemperatureUnits, ResistanceConfiguration, ExcitationSource + ThermocoupleType, TerminalConfiguration, TriggerSettings, RTDType, TemperatureUnits, ResistanceConfiguration, ExcitationSource , AI_RTD_Channel logger = set_logger(get_module_name(__file__)) @@ -43,8 +43,8 @@ class ScalableGroupAI(GroupParameter): ]}, {'title': 'TEMPERATURE_RTD:', 'name': 'rtd_settings', 'type': 'group', 'visible': False, 'children': [ - {'title': 'Max. value in:', 'name': 'max_value_in', 'type': 'float', 'value': '0'}, - {'title': 'Max. value out:', 'name': 'max_value_out', 'type': 'float', 'value': '0'}, + {'title': 'Min. value in:', 'name': 'min_value_in', 'type': 'float', 'value': '0'}, + {'title': 'Max. value in:', 'name': 'max_value_in', 'type': 'float', 'value': '100'}, {'title': 'Temp. unit:', 'name': 'temp_unit', 'type': 'list', 'limits': [TemperatureUnits.DEG_C.name, TemperatureUnits.DEG_F.name], 'value': TemperatureUnits.DEG_C.name}, @@ -449,12 +449,19 @@ def get_channels_from_settings(self): thermo_type=ThermocoupleType[ channel['thermoc_settings', 'thermoc_type']], )) elif analog_type == UsageTypeAI.TEMPERATURE_RTD: - channels.append(AIThermoChannel(name=channel.opts['title'], + channels.append(AI_RTD_Channel(name=channel.opts['title'], source=source, analog_type=analog_type, - value_min=channel['thermoc_settings', 'T_min'], - value_max=channel['thermoc_settings', 'T_max'], - thermo_type=ThermocoupleType[ - channel['thermoc_settings', 'thermoc_type']], )) + value_min=channel['rtd_settings', 'min_value_in'], + value_max=channel['rtd_settings', 'max_value_in'], + units=TemperatureUnits[channel['rtd_settings', 'temp_unit']], + rtd_type=RTDType[channel['rtd_settings', 'rtd_type']], + resistance_config= + ResistanceConfiguration[channel['rtd_settings', 'resistance_config']], + current_excit_source= + ExcitationSource[channel['rtd_settings', 'current_excit_src']], + current_excit_val=channel['rtd_settings', 'i_ex_value'], + r_0=channel['rtd_settings', 'r0'], + )) elif self.settings['NIDAQ_type'] == ChannelType.ANALOG_OUTPUT.name: # analog output source = ChannelType.ANALOG_OUTPUT diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py index b7677ee..21d4488 100644 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py @@ -91,12 +91,22 @@ def __init__(self, thermo_type=ThermocoupleType.K, **kwargs): assert thermo_type in ThermocoupleType self.thermo_type = thermo_type -# class AI_RTD_Channel(AIChannel): -# def __init__(self, rtd_type=RTDType.Pt_3750, **kwargs): -# super().__init__(**kwargs) -# assert rtd_type in RTDType -# self.rtd_type = rtd_type -# à compléter !!!!!! +class AI_RTD_Channel(AIChannel): + def __init__(self, units = TemperatureUnits.DEG_C, resistance_config=ResistanceConfiguration.TWO_WIRE, r_0=100, rtd_type=RTDType.PT_3750, + current_excit_source = ExcitationSource.INTERNAL, current_excit_val = 0.01, **kwargs): + super().__init__(**kwargs) + assert units in TemperatureUnits + self.units = units + assert resistance_config in ResistanceConfiguration + self.resistance_config = resistance_config + assert type(r_0) in [float, int] + self.r_0 = r_0 + assert rtd_type in RTDType + self.rtd_type = rtd_type + assert current_excit_source in ExcitationSource + self.current_excit_source = current_excit_source + assert type(current_excit_val) in [float, int] + self.current_excit_val = current_excit_val class AOChannel(AChannel): @@ -411,8 +421,17 @@ def update_task(self, channels=[], clock_settings=ClockSettings(), trigger_setti CJCSource.BUILT_IN, 0., "") - # elif channel.analog_type == UsageTypeAI.TEMPERATURE_RTD: - # self._task.ai_channels.add_ai + elif channel.analog_type == UsageTypeAI.TEMPERATURE_RTD: + self._task.ai_channels.add_ai_rtd_chan(channel.name, + "", + channel.value_min, + channel.value_max, + units=channel.units, + rtd_type=channel.rtd_type , + resistance_config=channel.resistance_config, + current_excit_source=channel.current_excit_source, + current_excit_val=channel.current_excit_val, + r_0=channel.r_0) except DaqError as e: err_code = e.error_code if err_code: From efe8f339a5002313f22e637dc55247ea20b27100 Mon Sep 17 00:00:00 2001 From: CyrilAUCLAIR-fr Date: Thu, 23 Oct 2025 16:00:49 +0200 Subject: [PATCH 088/140] add of RTD file config loading --- .../national_instruments/NIDAQmx_Viewer.py | 38 +++++++++++++++++++ .../national_instruments/NIDAQmx_base.py | 2 +- .../hardware/national_instruments/daqmxni.py | 22 ++++++++++- 3 files changed, 60 insertions(+), 2 deletions(-) diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_Viewer.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_Viewer.py index 7997daf..2e8dfbe 100644 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_Viewer.py +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_Viewer.py @@ -132,6 +132,7 @@ def commit_settings(self, param): ch_par.child("voltage_settings").show(ch.analog_type == UsageTypeAI.VOLTAGE) ch_par.child("current_settings").show(ch.analog_type == UsageTypeAI.CURRENT) ch_par.child("thermoc_settings").show(ch.analog_type == UsageTypeAI.TEMPERATURE_THERMOCOUPLE) + ch_par.child("rtd_settings").show(ch.analog_type == UsageTypeAI.TEMPERATURE_RTD) match ch.analog_type: case UsageTypeAI.VOLTAGE: self.settings.child("ai_channels", ch_par.opts['name'], "ai_type").setValue("VOLTAGE") @@ -170,6 +171,43 @@ def commit_settings(self, param): "T_max").setValue(ch.value_max) self.settings.child("ai_channels", ch_par.opts['name'], "termination").setValue( TerminalConfiguration.DEFAULT.name) + case UsageTypeAI.TEMPERATURE_RTD: + self.settings.child("ai_channels", ch_par.opts['name'], "ai_type").setValue( + "TEMPERATURE_RTD") + self.settings.child("ai_channels", + ch_par.opts['name'], + "rtd_settings", + "min_value_in").setValue(ch.value_min) + self.settings.child("ai_channels", + ch_par.opts['name'], + "rtd_settings", + "max_value_in").setValue(ch.value_max) + self.settings.child("ai_channels", + ch_par.opts['name'], + "rtd_settings", + "temp_unit").setValue(ch.units.name) + self.settings.child("ai_channels", + ch_par.opts['name'], + "rtd_settings", + "resistance_config").setValue(ch.resistance_config.name) + self.settings.child("ai_channels", + ch_par.opts['name'], + "rtd_settings", + "r0").setValue(ch.r_0.name) + self.settings.child("ai_channels", + ch_par.opts['name'], + "rtd_settings", + "rtd_type").setValue(ch.rtd_type.name) + self.settings.child("ai_channels", + ch_par.opts['name'], + "rtd_settings", + "current_excit_src").setValue(ch.current_excit_source.name) + self.settings.child("ai_channels", + ch_par.opts['name'], + "rtd_settings", + "i_ex_value").setValue(ch.current_excit_val.name) + self.settings.child("ai_channels", ch_par.opts['name'], "termination").setValue( + TerminalConfiguration.DEFAULT.name) self.channels = self.get_channels_from_settings() self.set_max_frequency() # Set the acquisition frequency to the device maximum frequency self.channels = self.get_channels_from_settings() diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_base.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_base.py index 6987cdd..4764325 100644 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_base.py +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_base.py @@ -53,7 +53,7 @@ class ScalableGroupAI(GroupParameter): 'limits': [rtd_type.name for rtd_type in RTDType], 'value': RTDType.PT_3750.name}, {'title': 'Callendar-Van Dusen coefficients:', 'name': 'c-vd_coeff.', 'type': 'group', 'visible': False, 'children': [ - # These coefficients are supposed to used in a °C Callendar-Van Dusen equation, + # These coefficients are supposed to be used in a Callendar-Van Dusen equation in °C, # cf. https://www.ni.com/docs/fr-FR/bundle/ni-daqmx/page/callendarvandusen.html (Oct. 2025) {'title': 'A', 'name': 'a_c-vd_coeff', 'type': 'float', 'suffix': 'Ω/°C'}, {'title': 'B', 'name': 'b_c-vd_coeff', 'type': 'float', 'suffix': 'Ω/(°C²)'}, diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py index 21d4488..92ebf32 100644 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py @@ -92,7 +92,7 @@ def __init__(self, thermo_type=ThermocoupleType.K, **kwargs): self.thermo_type = thermo_type class AI_RTD_Channel(AIChannel): - def __init__(self, units = TemperatureUnits.DEG_C, resistance_config=ResistanceConfiguration.TWO_WIRE, r_0=100, rtd_type=RTDType.PT_3750, + def __init__(self, units = TemperatureUnits.DEG_C, resistance_config=ResistanceConfiguration.TWO_WIRE, r_0=float(100), rtd_type=RTDType.PT_3750, current_excit_source = ExcitationSource.INTERNAL, current_excit_val = 0.01, **kwargs): super().__init__(**kwargs) assert units in TemperatureUnits @@ -327,6 +327,26 @@ def configuration_sequence(self, viewer, current_device): value_max=float(ai[ch].get("value_max")), thermo_type=th, )) + elif analog_type == UsageTypeAI.TEMPERATURE_RTD: + units = TemperatureUnits[ai[ch].get("units")] + resistance_config = ResistanceConfiguration[ai[ch].get("resistance_config")] + r_0 = float(ai[ch].get("r_0")) + rtd_type = RTDType[ai[ch].get("rtd_type")] + current_excit_source = ExcitationSource[ai[ch].get("current_excit_source")] + current_excit_val = float(ai[ch].get("current_excit_val")) + viewer.config_channels.append(AI_RTD_Channel + (name=name, + source=source, + analog_type=analog_type, + value_min=float(ai[ch].get("value_min")), + value_max=float(ai[ch].get("value_max")), + units=units, + resistance_config=resistance_config, + r_0=r_0, + rtd_type=rtd_type, + current_excit_source=current_excit_source, + current_excit_val=current_excit_val, + )) logger.info("Devices from config: {}".format(viewer.config_devices)) logger.info("Modules from config: {}".format(viewer.config_modules)) logger.info("Channels from config: {}".format([ch.name for ch in viewer.config_channels])) From aba9c3d4fcb42bf6a8df8ec0f1570693f2eb132d Mon Sep 17 00:00:00 2001 From: CyrilAUCLAIR-fr Date: Thu, 23 Oct 2025 17:28:37 +0200 Subject: [PATCH 089/140] update of contributions and NI-modules informations --- README.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index be6cb3b..2031333 100644 --- a/README.rst +++ b/README.rst @@ -27,6 +27,7 @@ Contributors * Amelie Jarnac * Aurore Finco * Sébastien Guerrero (sebastien.guerrero@insa-lyon.fr) +* Cyril Auclair (cyril.auclair@insa-lyon.fr) Instruments =========== @@ -46,11 +47,11 @@ NIDAQmx Viewer ++++++++++++++ This viewer aims at being very general and allows measurement with as much different NI modules as possible. -At this point (07/2025), it supports Analog measurement of currents, voltages and temperatures from thermocouples. +At this point (10/2025), it supports Analog measurement of currents, voltages and temperatures from thermocouples and RTD. Tests have been running with the following devices: * Chassis NI cDAQ-9174 & NI USB-9162 - * NI modules 9211 & 9205 + * NI modules 9211, 9205 & 9217 (for RTD measurements) Acquisition channels can ge generated directly running the viewer, but it is also possible to load channels from the configuration file config_daqmx.toml clicking on "load configuration" in the parameter tree. From fd242105ff2e9e0a0329629739f0bc7c1a50c7e5 Mon Sep 17 00:00:00 2001 From: CyrilAUCLAIR-fr Date: Fri, 24 Oct 2025 11:51:30 +0200 Subject: [PATCH 090/140] Integration of Callendar - Van Dusen coefficients configuration --- .../national_instruments/NIDAQmx_Viewer.py | 19 +++++++++++--- .../national_instruments/NIDAQmx_base.py | 9 ++++--- .../hardware/national_instruments/daqmxni.py | 26 ++++++++++++++++--- 3 files changed, 44 insertions(+), 10 deletions(-) diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_Viewer.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_Viewer.py index 2e8dfbe..adb9706 100644 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_Viewer.py +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_Viewer.py @@ -3,7 +3,7 @@ import traceback from .daqmxni import NIDAQmx, niDevice from pymodaq_plugins_daqmx.hardware.national_instruments.NIDAQmx_base import DAQ_NIDAQmx_base, TerminalConfiguration, \ - UsageTypeAI, ChannelType + UsageTypeAI, ChannelType,RTDType from pymodaq.control_modules.viewer_utility_classes import DAQ_Viewer_base, comon_parameters as viewer_params from pymodaq.utils.daq_utils import ThreadCommand from pymodaq.utils.data import DataFromPlugins, DataToExport @@ -172,6 +172,7 @@ def commit_settings(self, param): self.settings.child("ai_channels", ch_par.opts['name'], "termination").setValue( TerminalConfiguration.DEFAULT.name) case UsageTypeAI.TEMPERATURE_RTD: + ch_par.child("rtd_settings", "c-vd_coeff.").show(ch.rtd_type == RTDType.CUSTOM) self.settings.child("ai_channels", ch_par.opts['name'], "ai_type").setValue( "TEMPERATURE_RTD") self.settings.child("ai_channels", @@ -193,11 +194,23 @@ def commit_settings(self, param): self.settings.child("ai_channels", ch_par.opts['name'], "rtd_settings", - "r0").setValue(ch.r_0.name) + "r0").setValue(ch.r_0) self.settings.child("ai_channels", ch_par.opts['name'], "rtd_settings", "rtd_type").setValue(ch.rtd_type.name) + self.settings.child("ai_channels", + ch_par.opts['name'], + "rtd_settings", "c-vd_coeff.", + "a_c-vd_coeff").setValue(ch.a_cvd_coeff) + self.settings.child("ai_channels", + ch_par.opts['name'], + "rtd_settings", "c-vd_coeff.", + "b_c-vd_coeff").setValue(ch.b_cvd_coeff) + self.settings.child("ai_channels", + ch_par.opts['name'], + "rtd_settings", "c-vd_coeff.", + "c_c-vd_coeff").setValue(ch.c_cvd_coeff) self.settings.child("ai_channels", ch_par.opts['name'], "rtd_settings", @@ -205,7 +218,7 @@ def commit_settings(self, param): self.settings.child("ai_channels", ch_par.opts['name'], "rtd_settings", - "i_ex_value").setValue(ch.current_excit_val.name) + "i_ex_value").setValue(ch.current_excit_val) self.settings.child("ai_channels", ch_par.opts['name'], "termination").setValue( TerminalConfiguration.DEFAULT.name) self.channels = self.get_channels_from_settings() diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_base.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_base.py index 4764325..e2919e2 100644 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_base.py +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_base.py @@ -55,9 +55,9 @@ class ScalableGroupAI(GroupParameter): 'visible': False, 'children': [ # These coefficients are supposed to be used in a Callendar-Van Dusen equation in °C, # cf. https://www.ni.com/docs/fr-FR/bundle/ni-daqmx/page/callendarvandusen.html (Oct. 2025) - {'title': 'A', 'name': 'a_c-vd_coeff', 'type': 'float', 'suffix': 'Ω/°C'}, - {'title': 'B', 'name': 'b_c-vd_coeff', 'type': 'float', 'suffix': 'Ω/(°C²)'}, - {'title': 'C', 'name': 'c_c-vd_coeff', 'type': 'float', 'suffix': 'Ω/(°C⁴)'}, + {'title': 'A', 'name': 'a_c-vd_coeff', 'type': 'float', 'suffix': '°C⁻¹'}, + {'title': 'B', 'name': 'b_c-vd_coeff', 'type': 'float', 'suffix': '°C⁻²'}, + {'title': 'C', 'name': 'c_c-vd_coeff', 'type': 'float', 'suffix': '°C⁻⁴'}, ]}, {'title': 'Resistance config.', 'name': 'resistance_config', 'type': 'list', 'limits': [rc.name for rc in ResistanceConfiguration], @@ -455,6 +455,9 @@ def get_channels_from_settings(self): value_max=channel['rtd_settings', 'max_value_in'], units=TemperatureUnits[channel['rtd_settings', 'temp_unit']], rtd_type=RTDType[channel['rtd_settings', 'rtd_type']], + a_cvd_coeff=channel['rtd_settings', 'c-vd_coeff.', 'a_c-vd_coeff'], + b_cvd_coeff=channel['rtd_settings', 'c-vd_coeff.', 'b_c-vd_coeff'], + c_cvd_coeff=channel['rtd_settings', 'c-vd_coeff.', 'c_c-vd_coeff'], resistance_config= ResistanceConfiguration[channel['rtd_settings', 'resistance_config']], current_excit_source= diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py index 92ebf32..9bbe925 100644 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py @@ -92,8 +92,9 @@ def __init__(self, thermo_type=ThermocoupleType.K, **kwargs): self.thermo_type = thermo_type class AI_RTD_Channel(AIChannel): - def __init__(self, units = TemperatureUnits.DEG_C, resistance_config=ResistanceConfiguration.TWO_WIRE, r_0=float(100), rtd_type=RTDType.PT_3750, - current_excit_source = ExcitationSource.INTERNAL, current_excit_val = 0.01, **kwargs): + def __init__(self, units = TemperatureUnits.DEG_C, resistance_config=ResistanceConfiguration.TWO_WIRE, r_0=float(100), + rtd_type=RTDType.PT_3750, a_cvd_coeff=float(0), b_cvd_coeff=float(0), c_cvd_coeff=float(0), + current_excit_source = ExcitationSource.INTERNAL, current_excit_val = 0.01, **kwargs): super().__init__(**kwargs) assert units in TemperatureUnits self.units = units @@ -103,6 +104,12 @@ def __init__(self, units = TemperatureUnits.DEG_C, resistance_config=ResistanceC self.r_0 = r_0 assert rtd_type in RTDType self.rtd_type = rtd_type + assert type(a_cvd_coeff) in [float, int] + self.a_cvd_coeff = a_cvd_coeff + assert type(b_cvd_coeff) in [float, int] + self.b_cvd_coeff = b_cvd_coeff + assert type(c_cvd_coeff) in [float, int] + self.c_cvd_coeff = c_cvd_coeff assert current_excit_source in ExcitationSource self.current_excit_source = current_excit_source assert type(current_excit_val) in [float, int] @@ -332,6 +339,9 @@ def configuration_sequence(self, viewer, current_device): resistance_config = ResistanceConfiguration[ai[ch].get("resistance_config")] r_0 = float(ai[ch].get("r_0")) rtd_type = RTDType[ai[ch].get("rtd_type")] + a_cvd_coeff = float(ai[ch].get("a_cvd_coeff")) + b_cvd_coeff = float(ai[ch].get("b_cvd_coeff")) + c_cvd_coeff = float(ai[ch].get("c_cvd_coeff")) current_excit_source = ExcitationSource[ai[ch].get("current_excit_source")] current_excit_val = float(ai[ch].get("current_excit_val")) viewer.config_channels.append(AI_RTD_Channel @@ -344,6 +354,9 @@ def configuration_sequence(self, viewer, current_device): resistance_config=resistance_config, r_0=r_0, rtd_type=rtd_type, + a_cvd_coeff=a_cvd_coeff, + b_cvd_coeff=b_cvd_coeff, + c_cvd_coeff=c_cvd_coeff, current_excit_source=current_excit_source, current_excit_val=current_excit_val, )) @@ -442,16 +455,21 @@ def update_task(self, channels=[], clock_settings=ClockSettings(), trigger_setti 0., "") elif channel.analog_type == UsageTypeAI.TEMPERATURE_RTD: - self._task.ai_channels.add_ai_rtd_chan(channel.name, + ai_rtd_chan = self._task.ai_channels.add_ai_rtd_chan(channel.name, "", channel.value_min, channel.value_max, units=channel.units, - rtd_type=channel.rtd_type , + rtd_type=channel.rtd_type, resistance_config=channel.resistance_config, current_excit_source=channel.current_excit_source, current_excit_val=channel.current_excit_val, r_0=channel.r_0) + if ai_rtd_chan.ai_rtd_type == RTDType["CUSTOM"]: + # configuration of callendar-van dusen coefficients + ai_rtd_chan.ai_rtd_a = channel.a_cvd_coeff + ai_rtd_chan.ai_rtd_b = channel.b_cvd_coeff + ai_rtd_chan.ai_rtd_c = channel.c_cvd_coeff except DaqError as e: err_code = e.error_code if err_code: From 6f3b70c2343dc3c0da5e1628e4ad02b3554a3256 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Guerrero?= Date: Fri, 24 Oct 2025 17:01:21 +0200 Subject: [PATCH 091/140] Data dimension --- .../daq_viewer_plugins/plugins_1D/daq_1Dviewer_NIDAQmx.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_1D/daq_1Dviewer_NIDAQmx.py b/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_1D/daq_1Dviewer_NIDAQmx.py index e3c21de..a5d900e 100644 --- a/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_1D/daq_1Dviewer_NIDAQmx.py +++ b/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_1D/daq_1Dviewer_NIDAQmx.py @@ -6,7 +6,7 @@ class DAQ_1DViewer_NIDAQmx(DAQ_NIDAQmx_Viewer): """ - Plugin for a 0D data visualization & acquisition with various NI modules plugged in a NI cDAQ. + Plugin for bufferized 1D data visualization & acquisition with NI modules (plugged in NI cDAQ or NI-USB). """ def __init__(self, parent=None, params_state=None): super().__init__(parent, params_state, control_type='1D') From f4fdd3bfe61788e8960322a58807abbdbfcc2ae0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Guerrero?= Date: Fri, 24 Oct 2025 17:20:28 +0200 Subject: [PATCH 092/140] add nidaqmx documentation files --- docs/source/user_guide/nidaqmx_viewer_details.rst | 0 docs/source/user_guide/nidaqmx_viewer_use.rst | 0 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 docs/source/user_guide/nidaqmx_viewer_details.rst create mode 100644 docs/source/user_guide/nidaqmx_viewer_use.rst diff --git a/docs/source/user_guide/nidaqmx_viewer_details.rst b/docs/source/user_guide/nidaqmx_viewer_details.rst new file mode 100644 index 0000000..e69de29 diff --git a/docs/source/user_guide/nidaqmx_viewer_use.rst b/docs/source/user_guide/nidaqmx_viewer_use.rst new file mode 100644 index 0000000..e69de29 From 12a689a4bf926b57db59721cb1899ce3e06703aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Guerrero?= Date: Fri, 24 Oct 2025 18:11:04 +0200 Subject: [PATCH 093/140] Update documentation for nidaqmx 1D viewer --- README.rst | 2 +- docs/source/images/parameters.jpg | Bin 0 -> 77516 bytes docs/source/user_guide/nidaqmx_viewer_use.rst | 46 ++++++++++++++++++ 3 files changed, 47 insertions(+), 1 deletion(-) create mode 100644 docs/source/images/parameters.jpg diff --git a/README.rst b/README.rst index be6cb3b..d7878c3 100644 --- a/README.rst +++ b/README.rst @@ -37,7 +37,7 @@ Actuators * **NIDAQmx**: General generation (voltage / current) with National Instrument materials -Viewer0D +Viewer1D ++++++++ * **NIDAQmx**: General measurement with National Instrument materials diff --git a/docs/source/images/parameters.jpg b/docs/source/images/parameters.jpg new file mode 100644 index 0000000000000000000000000000000000000000..9ba1b65ae810b319ecbfcfc54bbf3d516ccaaa4f GIT binary patch literal 77516 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\.pymodaq.\config_daqmx.toml`` with your own material (syntax needs to be respected to prevent from errors). + +* **Clock Settings - Nsamples**: The number of samples requested to the material for each channel + +* **Clock Settings - Frequency**: the acquisition rate, which is common for each acquisition channel. + +* **AI Channels**: the channel(s) you will use for your analog measurements, something like ``cDAQ[X]Mod[Y]/ai[Z]``. You can generate it directly or load it automatically using the plugin config. Pay attention not creating the same channel twice! + + .. image:: /images/parameters.png + :width: 800 + +This plugin is not intended to be used with a "Slave" configuration, but you can definitely use the same NI card device as hardware for other plugins at the same time. + +Be careful to use as photon source the default input terminal corresponding to the counting channel, check the documentation of your NI device about this. + + +Use +--- + +It works as a usual 1D viewer plugin. + +If you encounter some problems, you might get some warnings or errors in the log, check it to get some hints to solve it. +You can run ``Get-Content .pymodaq\log\pymodaq.log -Tail 10 -Wait`` in the PowerShell to display the log in real time. From 8361c6edca53a97029d210792e45f0e24eff7199 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Guerrero?= Date: Mon, 27 Oct 2025 14:22:38 +0100 Subject: [PATCH 094/140] Update GitHub actions versions --- .github/workflows/python-publish.yml | 4 ++-- .github/workflows/sphinx.yml | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml index 76fca2c..8e5c0e3 100644 --- a/.github/workflows/python-publish.yml +++ b/.github/workflows/python-publish.yml @@ -13,9 +13,9 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v4.2.2 - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5.4.0 with: python-version: '3.x' - name: Install dependencies diff --git a/.github/workflows/sphinx.yml b/.github/workflows/sphinx.yml index 4678bcc..104fdcb 100644 --- a/.github/workflows/sphinx.yml +++ b/.github/workflows/sphinx.yml @@ -9,8 +9,8 @@ jobs: docs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - uses: actions/setup-python@v3 + - uses: actions/checkout@v4.2.2 + - uses: actions/setup-python@v5.4.0 - name: Install dependencies run: | pip install sphinx sphinx_rtd_theme myst_parser @@ -18,14 +18,14 @@ jobs: run: | sphinx-build docs/source docs/build/html - name: Upload artifacts - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: html-docs path: docs/build/html - name: Create .nojekyll file run: touch docs/build/html/.nojekyll - name: Deploy to GitHub Pages - uses: peaceiris/actions-gh-pages@v3 + uses: peaceiris/actions-gh-pages@v4 if: ${{ github.event_name == 'push'}} with: github_token: ${{ secrets.GITHUB_TOKEN }} From 3e72535b79ccc6db642525c3f3e0fc0ab8b5591b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Guerrero?= Date: Tue, 4 Nov 2025 10:51:47 +0100 Subject: [PATCH 095/140] Add 0D viewer --- .../plugins_0D/daq_0Dviewer_NIDAQmx.py | 50 ++++++++++++++++ .../plugins_1D/daq_1Dviewer_NIDAQmx.py | 58 ++++++++++++++++++- .../national_instruments/NIDAQmx_Viewer.py | 53 ----------------- .../national_instruments/NIDAQmx_base.py | 7 +++ 4 files changed, 114 insertions(+), 54 deletions(-) create mode 100644 src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_NIDAQmx.py diff --git a/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_NIDAQmx.py b/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_NIDAQmx.py new file mode 100644 index 0000000..f0fc835 --- /dev/null +++ b/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_NIDAQmx.py @@ -0,0 +1,50 @@ +import numpy as np +from pymodaq_data import DataToExport +from pymodaq.utils.data import DataFromPlugins +from pymodaq_plugins_daqmx.hardware.national_instruments.NIDAQmx_Viewer import DAQ_NIDAQmx_Viewer +from pymodaq_plugins_daqmx.hardware.national_instruments.daqmxni import Edge, ChannelType, ClockSettings, \ + TriggerSettings +from pymodaq.utils.logger import set_logger, get_module_name +logger = set_logger(get_module_name(__file__)) + +class DAQ_0DViewer_NIDAQmx(DAQ_NIDAQmx_Viewer): + """ + Plugin for bufferized 0D data visualization & acquisition with NI modules (plugged in NI cDAQ or NI-USB). + """ + live_mode_available = False + + def __init__(self, parent=None, params_state=None): + super().__init__(parent, params_state, control_type='0D') + + def grab_data(self, Naverage=1, **kwargs): + """ + | grab the current values with NIDAQ profile procedure. + | + | Send the data_grabed_signal once done. + + =============== ======== =============================================== + **Parameters** **Type** **Description** + *Naverage* int Number of values to average + =============== ======== =============================================== + """ + if self.controller.task is None: + # In synchronous mode, acquisition rate does not come from clock settings but from each grab + self.clock_settings = ClockSettings(frequency=self.settings['clock_settings', 'frequency'], + Nsamples=self.settings['clock_settings', 'Nsamples'], + edge=Edge.RISING, + repetition=False, ) + self.controller.update_task(self.get_channels_from_settings(), self.clock_settings) + + if self.settings['NIDAQ_type'] == ChannelType.COUNTER_INPUT.name: + self.timer.start(self.settings['counter_settings', 'counting_time']) + + channels_names = [ch.name for ch in self.channels] + data_from_task = self.controller.task.read(timeout=20.0) + data_dfp = list(map(lambda f: np.array([f]), data_from_task)) + self.dte_signal.emit(DataToExport(name='NIDAQmx', + data=[DataFromPlugins(name='Data from ' + self.controller.device.name, + data=data_dfp, + dim=f'Data{self.control_type}', + labels=channels_names, + ), + ])) diff --git a/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_1D/daq_1Dviewer_NIDAQmx.py b/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_1D/daq_1Dviewer_NIDAQmx.py index a5d900e..b82203d 100644 --- a/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_1D/daq_1Dviewer_NIDAQmx.py +++ b/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_1D/daq_1Dviewer_NIDAQmx.py @@ -2,11 +2,67 @@ from pymodaq_data import DataToExport from pymodaq.utils.data import DataFromPlugins from pymodaq_plugins_daqmx.hardware.national_instruments.NIDAQmx_Viewer import DAQ_NIDAQmx_Viewer - +from pymodaq_plugins_daqmx.hardware.national_instruments.NIDAQmx_base import ChannelType +from pymodaq.utils.logger import set_logger, get_module_name +logger = set_logger(get_module_name(__file__)) class DAQ_1DViewer_NIDAQmx(DAQ_NIDAQmx_Viewer): """ Plugin for bufferized 1D data visualization & acquisition with NI modules (plugged in NI cDAQ or NI-USB). """ + live_mode_available = True + def __init__(self, parent=None, params_state=None): super().__init__(parent, params_state, control_type='1D') + + def grab_data(self, Naverage=1, **kwargs): + """ + | grab the current values with NIDAQ profile procedure. + | + | Send the data_grabed_signal once done. + + =============== ======== =============================================== + **Parameters** **Type** **Description** + *Naverage* int Number of values to average + =============== ======== =============================================== + """ + update = False + + if 'live' in kwargs: + if kwargs['live'] != self.live: + update = True + self.live = kwargs['live'] + + if Naverage != self.Naverage: + self.Naverage = Naverage + update = True + if update: + self.update_task() + + if self.controller.task is None: + self.update_task() + + if self.settings['NIDAQ_type'] == ChannelType.ANALOG_INPUT.name: + try: + self.controller.register_callback(self.emit_data, "Nsamples", self.clock_settings.Nsamples) + except AttributeError: + logger.error("Can't find a task to run") + elif self.settings['NIDAQ_type'] == ChannelType.COUNTER_INPUT.name: + self.timer.start(self.settings['counter_settings', 'counting_time']) + self.controller.start() + + def emit_data(self, task_handle, every_n_samples_event_type, number_of_samples, callback_data): + channels_names = [ch.name for ch in self.channels] + data_from_task = self.controller.task.read(self.settings['nsamplestoread'], timeout=20.0) + if not len(self.controller.task.channels.channel_names) != 1: + data_dfp = [np.array(data_from_task)] + else: + data_dfp = list(map(np.array, data_from_task)) + self.dte_signal.emit(DataToExport(name='NIDAQmx', + data=[DataFromPlugins(name='Data from ' + self.controller.device.name, + data=data_dfp, + dim=f'Data{self.control_type}', + labels=channels_names, + ), + ])) + return 0 # mandatory for the NIDAQmx callback \ No newline at end of file diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_Viewer.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_Viewer.py index 7997daf..8ec95b9 100644 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_Viewer.py +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_Viewer.py @@ -30,7 +30,6 @@ class DAQ_NIDAQmx_Viewer(DAQ_Viewer_base, DAQ_NIDAQmx_base): config_modules: list live: bool Naverage: int - live_mode_available = True param_devices = NIDAQmx.get_NIDAQ_devices().device_names params = viewer_params + [ {'title': 'Devices :', 'name': 'devices', 'type': 'list', 'limits': param_devices, @@ -205,58 +204,6 @@ def set_max_frequency(self): except: pass - def grab_data(self, Naverage=1, **kwargs): - """ - | grab the current values with NIDAQ profile procedure. - | - | Send the data_grabed_signal once done. - - =============== ======== =============================================== - **Parameters** **Type** **Description** - *Naverage* int Number of values to average - =============== ======== =============================================== - """ - update = False - - if 'live' in kwargs: - if kwargs['live'] != self.live: - update = True - self.live = kwargs['live'] - - if Naverage != self.Naverage: - self.Naverage = Naverage - update = True - if update: - self.update_task() - - if self.controller.task is None: - self.update_task() - - if self.settings['NIDAQ_type'] == ChannelType.ANALOG_INPUT.name: - try: - self.controller.register_callback(self.emit_data, "Nsamples", self.clock_settings.Nsamples) - except AttributeError: - logger.error("Can't find a task to run") - elif self.settings['NIDAQ_type'] == ChannelType.COUNTER_INPUT.name: - self.timer.start(self.settings['counter_settings', 'counting_time']) - self.controller.start() - - def emit_data(self, task_handle, every_n_samples_event_type, number_of_samples, callback_data): - channels_names = [ch.name for ch in self.channels] - data_from_task = self.controller.task.read(self.settings['nsamplestoread'], timeout=20.0) - if not len(self.controller.task.channels.channel_names) != 1: - data_dfp = [np.array(data_from_task)] - else: - data_dfp = list(map(np.array, data_from_task)) - self.dte_signal.emit(DataToExport(name='NIDAQmx', - data=[DataFromPlugins(name='Data from ' + self.controller.device.name, - data=data_dfp, - dim=f'Data{self.control_type}', - labels=channels_names, - ), - ])) - return 0 # mandatory for the NIDAQmx callback - def stop(self): """Stop the current grab hardware wise if necessary""" try: diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_base.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_base.py index ee1a5d1..61a5b70 100644 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_base.py +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_base.py @@ -377,6 +377,13 @@ def commit_settings(self, param: Parameter): elif param.name() == 'trigger_channel': param.parent().child('level').show('PF' not in param.opts['title']) + if self.control_type == "0D": + self.settings.child('nsamplestoread').hide() + self.settings.child('clock_settings', 'Nsamples').setValue(1) + self.settings.child('clock_settings', 'Nsamples').setOpts(readonly=True) + self.settings.child('clock_settings', 'frequency').hide() + self.settings.child('clock_settings', 'max_freq').hide() + def update_task(self): self.channels = self.get_channels_from_settings() self.clock_settings = ClockSettings(frequency=self.settings['clock_settings', 'frequency'], From 634fa30c3a12e0c71d2975f2979421d2edb53edd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Guerrero?= Date: Tue, 4 Nov 2025 11:35:21 +0100 Subject: [PATCH 096/140] [BUG] Update buffer size with respect to frequency --- .../hardware/national_instruments/daqmxni.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py index b00980a..1d853ab 100644 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py @@ -500,6 +500,7 @@ def update_task(self, channels=[], clock_settings=ClockSettings(), trigger_setti clock_settings.edge, mode, clock_settings.Nsamples) + self._task.in_stream.input_buf_size = int(clock_settings.frequency)*10 elif isinstance(clock_settings, ChangeDetectionSettings): self._task.timing.cfg_change_detection_timing(clock_settings.rising_channel, clock_settings.falling_channel, From f078ef6693f675d6a1e0473e4ee7e750fd1b2a31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Guerrero?= Date: Tue, 4 Nov 2025 11:36:02 +0100 Subject: [PATCH 097/140] Give material max frequency --- .../national_instruments/NIDAQmx_Viewer.py | 25 ++++++------------- 1 file changed, 7 insertions(+), 18 deletions(-) diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_Viewer.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_Viewer.py index 8ec95b9..7364224 100644 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_Viewer.py +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_Viewer.py @@ -177,30 +177,19 @@ def commit_settings(self, param): def get_max_frequency(self): # Destined to be removed when set_max_frequency will work correctly try: - if self.settings.child('ai_channels').children() or self.settings.child('di_channels').children(): - max_freq = self.controller.getAIMaxSingleRate(self.controller.device.name)/len(self.channels) - self.settings.child('clock_settings', 'max_freq').setValue(max_freq) - else: - max_freq = self.controller.getAIMaxSingleRate(self.controller.device.name) - self.settings.child('clock_settings', 'max_freq').setValue(max_freq) + max_freq = int(self.controller.getAIMaxSingleRate(self.controller.device.name)) + self.settings.child('clock_settings', 'max_freq').setValue(max_freq) except: pass def set_max_frequency(self): # Opts 'max' get the right value but doesn't update the viewer which keep as max the initial max value try: - if self.settings.child('ai_channels').children() or self.settings.child('di_channels').children(): - max_freq = self.controller.getAIMaxSingleRate(self.controller.device.name) / len(self.channels) - self.settings.child('clock_settings', 'frequency').setOpts(max=max_freq) - self.settings.child('clock_settings', 'frequency').emitStateChanged('max', max_freq) - self.settings.child('clock_settings', 'frequency')._emitOptionsChanged( - self.settings.child('clock_settings', 'frequency'), {'max': max_freq}) - else: - max_freq = self.controller.getAIMaxSingleRate(self.controller.device.name) - self.settings.child('clock_settings', 'frequency').setOpts(max=max_freq) - self.settings.child('clock_settings', 'frequency').emitStateChanged('max', max_freq) - self.settings.child('clock_settings', 'frequency')._emitOptionsChanged( - self.settings.child('clock_settings', 'frequency'), {'max': max_freq}) + max_freq = int(self.controller.getAIMaxSingleRate(self.controller.device.name)) + self.settings.child('clock_settings', 'frequency').setOpts(max=max_freq) + self.settings.child('clock_settings', 'frequency').emitStateChanged('max', max_freq) + self.settings.child('clock_settings', 'frequency')._emitOptionsChanged( + self.settings.child('clock_settings', 'frequency'), {'max': max_freq}) except: pass From a0604dfd7f72ccd7c05e0ecb08412c6b10f7ccaf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Guerrero?= Date: Wed, 5 Nov 2025 10:55:17 +0100 Subject: [PATCH 098/140] Update daqmx to nidaqmx --- docs/source/conf.py | 8 ++++---- docs/source/index.rst | 8 ++++---- docs/source/introduction.rst | 2 +- docs/source/{daqmx.rst => nidaqmx.rst} | 18 +++++++++--------- 4 files changed, 18 insertions(+), 18 deletions(-) rename docs/source/{daqmx.rst => nidaqmx.rst} (51%) diff --git a/docs/source/conf.py b/docs/source/conf.py index 399bbdb..f3ff4a8 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -7,9 +7,9 @@ # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information project = 'pymodaq_plugins_daqmx' -copyright = '2023, Sébastien Weber, Aurore Finco' -author = 'Sébastien Weber, Aurore Finco' -release = '0.3.0' +copyright = '2025, Sébastien Weber, Aurore Finco, Sébastien Guerrero' +author = 'Sébastien Weber, Aurore Finco, Sébastien Guerrero' +release = '1.1.0' # -- General configuration --------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration @@ -26,4 +26,4 @@ html_theme = 'sphinx_rtd_theme' html_static_path = ['_static'] -html_logo = 'images/splash.png' +html_logo = 'images/splash.png' \ No newline at end of file diff --git a/docs/source/index.rst b/docs/source/index.rst index 0aad1e0..d538b22 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -11,16 +11,16 @@ In addition, and they are also meant as examples, it provides actuator plugins w This documentation is written as a tutorial to guide you when setting up your NI acquisition devices with PyMoDAQ. -**Caution! The 0D and 1D Viewers DAQmx and the Move DAQmx are not working properly. Write your own plugin for your specific needs.** +**Caution! The 0D and 1D Viewers DAQmx and the Move DAQmx deliver basic solutions. Write your own plugin for your specific needs.** -This plugin is compatible with PymoDAQ 4. +This plugin is compatible with PymoDAQ 5. .. toctree:: :maxdepth: 1 :caption: Contents: - introduction + introduction user_guide - daqmx + nidaqmx creating_plugin diff --git a/docs/source/introduction.rst b/docs/source/introduction.rst index 8b9b1be..e17360a 100644 --- a/docs/source/introduction.rst +++ b/docs/source/introduction.rst @@ -11,5 +11,5 @@ Installation Of course, you need a working PyMoDAQ installation, see `here `_ the installation procedure. You also need to have the NIDAQmx driver on your computer, together with the acquisition device. Use the NI MAX software to test your hardware. Then you can install the *pymodaq_plugins_daqmx* module, from the `plugin manager `_ or with ``pip install pymodaq_plugins_daqmx``. -The interface between the NIDAQmx driver and python (and thus PyMoDAQ) is done by the package `PyDAQmx `_. +The interface between the NIDAQmx driver and python (and thus PyMoDAQ) is done by the package `NIDAQmx `_. diff --git a/docs/source/daqmx.rst b/docs/source/nidaqmx.rst similarity index 51% rename from docs/source/daqmx.rst rename to docs/source/nidaqmx.rst index cb75e05..69c9008 100644 --- a/docs/source/daqmx.rst +++ b/docs/source/nidaqmx.rst @@ -1,29 +1,29 @@ -The DAQmx object +The NIDAQmx object ================ -You will find its definition in the file ``hardware/national_instruments/daqmx.py``. This is building block for this plugin, this is the object that you will use as a controller inside your custom plugin. +You will find its definition in the file ``hardware/national_instruments/daqmxni.py``. This is building block for this plugin, this is the object that you will use as a controller inside your custom plugin. -The ``DAQmx`` object has an attribute ``task``, which corresponds to a Task for the NIDAQmx driver. Each ``DAQmx`` can only handle a single task, but a task can be executed on several channels. You will find in the same file the definition of many different Channel classes, which are all the channel types that are available physically on such NI devices. +The ``NIDAQmx`` object has an attribute ``task``, which corresponds to a Task for the NIDAQmx driver. Each ``DAQmx`` can only handle a single task, but a task can be executed on several channels. You will find in the same file the definition of many different Channel classes, which are all the channel types that are available physically on such NI devices. The list of these channels is: - AChannel (Analog Channel) - + - AIChannel: Analog Input - AIThermoChannel: Specific analog input for thermocouples - AOChannel: Analog Output - + - Counter (Regular edge counter) - + - ClockCounter: a counter that you use as a clock - SemiPeriodCounter: another specific type of counter which might be useful to count between state transitions of a signal - + - DigitalChannel - + - DOChannel: Digital Output - DIChannel: Digital Input -To use the ``DAQmx``, you therefore need to create Channels of the type you need, specifying the appropriate ``ClockSettings`` and ``TriggerSettings`` (these are also defined in the same file). Then, you pass these channels to the ``update_task`` method of the ``DAQmx``. Be careful that a task can only handle one channel type, if you need several, you need several ``DAQmx``. The ``update_task`` method sets everything up, but if you are using channels as trigger or clock for other channels, you will need to connects them yourself after calling ``update_task``. +To use the ``NIDAQmx``, you therefore need to create Channels of the type you need, specifying the appropriate ``ClockSettings`` and ``TriggerSettings`` (these are also defined in the same file). Then, you pass these channels to the ``update_task`` method of the ``DAQmx``. Be careful that a task can only handle one channel type, if you need several, you need several ``DAQmx``. The ``update_task`` method sets everything up, but if you are using channels as trigger or clock for other channels, you will need to connects them yourself after calling ``update_task``. Once the task is properly set up, you can start it with the method ``start()`` of the ``DAQmx`` object. Several convenient methods of the ``DAQmx`` object are available to read or write data, depending on the task you are performing. They have self-explaining names: From 54ddf8d4aa8b33d8c4429fc4ad3904c6ebe9cd4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Guerrero?= Date: Wed, 5 Nov 2025 12:35:24 +0100 Subject: [PATCH 099/140] [BUG] correct device type --- .../hardware/national_instruments/NIDAQmx_Viewer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_Viewer.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_Viewer.py index a3f55fa..abadfa9 100644 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_Viewer.py +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_Viewer.py @@ -120,7 +120,7 @@ def commit_settings(self, param): param.child('current_settings', 'curr_min').setValue(curr_ranges[0]) param.child('current_settings', 'curr_max').setValue(curr_ranges[1]) elif param.name() == 'devices': - self.controller.device = self.settings.child('devices') + self.controller.device = nidaqmx.system.Device(self.settings["devices"]) elif param.name() == 'load_config': self.controller.configuration_sequence(self, self.controller.device) self.settings.child('load_config').hide() From 8c877abab009e402bc1286d488e2aa3697478258 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Guerrero?= Date: Wed, 5 Nov 2025 15:56:09 +0100 Subject: [PATCH 100/140] clear imports --- .../plugins_0D/daq_0Dviewer_NIDAQmx.py | 4 ++-- .../hardware/national_instruments/NIDAQmx_Viewer.py | 13 +++++-------- .../hardware/national_instruments/NIDAQmx_base.py | 3 ++- 3 files changed, 9 insertions(+), 11 deletions(-) diff --git a/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_NIDAQmx.py b/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_NIDAQmx.py index f0fc835..597156e 100644 --- a/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_NIDAQmx.py +++ b/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_NIDAQmx.py @@ -2,11 +2,11 @@ from pymodaq_data import DataToExport from pymodaq.utils.data import DataFromPlugins from pymodaq_plugins_daqmx.hardware.national_instruments.NIDAQmx_Viewer import DAQ_NIDAQmx_Viewer -from pymodaq_plugins_daqmx.hardware.national_instruments.daqmxni import Edge, ChannelType, ClockSettings, \ - TriggerSettings +from pymodaq_plugins_daqmx.hardware.national_instruments.daqmxni import ChannelType from pymodaq.utils.logger import set_logger, get_module_name logger = set_logger(get_module_name(__file__)) + class DAQ_0DViewer_NIDAQmx(DAQ_NIDAQmx_Viewer): """ Plugin for bufferized 0D data visualization & acquisition with NI modules (plugged in NI cDAQ or NI-USB). diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_Viewer.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_Viewer.py index abadfa9..cd2578d 100644 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_Viewer.py +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_Viewer.py @@ -1,12 +1,9 @@ -import nidaqmx -import numpy as np import traceback -from .daqmxni import NIDAQmx, niDevice -from pymodaq_plugins_daqmx.hardware.national_instruments.NIDAQmx_base import DAQ_NIDAQmx_base, TerminalConfiguration, \ - UsageTypeAI, ChannelType,RTDType +from pymodaq_plugins_daqmx.hardware.national_instruments.daqmxni import niDevice +from pymodaq_plugins_daqmx.hardware.national_instruments.NIDAQmx_base import NIDAQmx, DAQ_NIDAQmx_base, \ + TerminalConfiguration, UsageTypeAI, ChannelType, RTDType from pymodaq.control_modules.viewer_utility_classes import DAQ_Viewer_base, comon_parameters as viewer_params from pymodaq.utils.daq_utils import ThreadCommand -from pymodaq.utils.data import DataFromPlugins, DataToExport from pymodaq.utils.logger import set_logger, get_module_name logger = set_logger(get_module_name(__file__)) @@ -75,7 +72,7 @@ def ini_detector(self, controller=None): self.controller = NIDAQmx() else: self.controller = controller - self.controller.device = nidaqmx.system.Device(self.settings["devices"]) + self.controller.device = niDevice(self.settings["devices"]) # actions to perform in order to set properly the settings tree options self.commit_settings(self.settings.child('NIDAQ_type')) @@ -120,7 +117,7 @@ def commit_settings(self, param): param.child('current_settings', 'curr_min').setValue(curr_ranges[0]) param.child('current_settings', 'curr_max').setValue(curr_ranges[1]) elif param.name() == 'devices': - self.controller.device = nidaqmx.system.Device(self.settings["devices"]) + self.controller.device = niDevice(self.settings["devices"]) elif param.name() == 'load_config': self.controller.configuration_sequence(self, self.controller.device) self.settings.child('load_config').hide() diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_base.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_base.py index 18ab9cd..011d137 100644 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_base.py +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_base.py @@ -5,7 +5,8 @@ from pymodaq_gui.parameter.pymodaq_ptypes import registerParameterType, GroupParameter from pymodaq_plugins_daqmx.hardware.national_instruments.daqmxni import NIDAQmx, Edge, ChannelType, ClockSettings, \ AIChannel, AIThermoChannel, AOChannel, CIChannel, COChannel, DOChannel, DIChannel, UsageTypeAI, UsageTypeAO, \ - ThermocoupleType, TerminalConfiguration, TriggerSettings, RTDType, TemperatureUnits, ResistanceConfiguration, ExcitationSource , AI_RTD_Channel + ThermocoupleType, TerminalConfiguration, TriggerSettings, RTDType, TemperatureUnits, ResistanceConfiguration,\ + ExcitationSource , AI_RTD_Channel logger = set_logger(get_module_name(__file__)) From b9c56f727993547015b7369df080f82a91b68df6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Guerrero?= Date: Wed, 5 Nov 2025 15:59:02 +0100 Subject: [PATCH 101/140] Semi-live mode for frequency optimisation (grab data trigger a read of a continuous task in background) --- .../plugins_0D/daq_0Dviewer_NIDAQmx.py | 28 +++++++++++++++---- 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_NIDAQmx.py b/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_NIDAQmx.py index 597156e..4f9bfe2 100644 --- a/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_NIDAQmx.py +++ b/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_NIDAQmx.py @@ -12,6 +12,7 @@ class DAQ_0DViewer_NIDAQmx(DAQ_NIDAQmx_Viewer): Plugin for bufferized 0D data visualization & acquisition with NI modules (plugged in NI cDAQ or NI-USB). """ live_mode_available = False + acquisition_state = False def __init__(self, parent=None, params_state=None): super().__init__(parent, params_state, control_type='0D') @@ -29,18 +30,21 @@ def grab_data(self, Naverage=1, **kwargs): """ if self.controller.task is None: # In synchronous mode, acquisition rate does not come from clock settings but from each grab - self.clock_settings = ClockSettings(frequency=self.settings['clock_settings', 'frequency'], - Nsamples=self.settings['clock_settings', 'Nsamples'], - edge=Edge.RISING, - repetition=False, ) - self.controller.update_task(self.get_channels_from_settings(), self.clock_settings) + self.update_task() if self.settings['NIDAQ_type'] == ChannelType.COUNTER_INPUT.name: self.timer.start(self.settings['counter_settings', 'counting_time']) + if not self.acquisition_state: + self.controller.task.start() + self.acquisition_state = True + channels_names = [ch.name for ch in self.channels] data_from_task = self.controller.task.read(timeout=20.0) - data_dfp = list(map(lambda f: np.array([f]), data_from_task)) + if isinstance(data_from_task, float): + data_dfp = np.array([data_from_task]) + else: + data_dfp = list(map(lambda f: np.array([f]), data_from_task)) self.dte_signal.emit(DataToExport(name='NIDAQmx', data=[DataFromPlugins(name='Data from ' + self.controller.device.name, data=data_dfp, @@ -48,3 +52,15 @@ def grab_data(self, Naverage=1, **kwargs): labels=channels_names, ), ])) + + def stop(self): + """Stop the current grab hardware wise if necessary""" + self.acquisition_state = False + try: + self.controller.task.stop() + self.controller.task.close() + self.controller._task = None + except Exception as e: + logger.error(e) + finally: + DAQ_NIDAQmx_Viewer.stop(self) From ab7bd0544633b02f33f8e76f7ef688662042851e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Guerrero?= Date: Fri, 14 Nov 2025 17:06:46 +0100 Subject: [PATCH 102/140] Update pymodaq doc links --- docs/source/introduction.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/source/introduction.rst b/docs/source/introduction.rst index e17360a..1008839 100644 --- a/docs/source/introduction.rst +++ b/docs/source/introduction.rst @@ -8,8 +8,8 @@ NI acquisition and signal generation devices are very versatile instruments, whi Installation ------------ -Of course, you need a working PyMoDAQ installation, see `here `_ the installation procedure. You also need to have the NIDAQmx driver on your computer, together with the acquisition device. Use the NI MAX software to test your hardware. +Of course, you need a working PyMoDAQ installation, see `here `_ the installation procedure. You also need to have the NIDAQmx driver on your computer, together with the acquisition device. Use the NI MAX software to test your hardware. -Then you can install the *pymodaq_plugins_daqmx* module, from the `plugin manager `_ or with ``pip install pymodaq_plugins_daqmx``. +Then you can install the *pymodaq_plugins_daqmx* module, from the `plugin manager `_ or with ``pip install pymodaq_plugins_daqmx``. The interface between the NIDAQmx driver and python (and thus PyMoDAQ) is done by the package `NIDAQmx `_. From a781851a5f5b68f7e493a7cf59f529a972a1da7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Guerrero?= Date: Fri, 14 Nov 2025 17:07:11 +0100 Subject: [PATCH 103/140] Add deprecated note --- docs/source/user_guide/counter_details.rst | 3 +++ docs/source/user_guide/counter_use.rst | 7 +++++-- docs/source/user_guide/scanner_details.rst | 3 +++ docs/source/user_guide/scanner_use.rst | 7 +++++-- 4 files changed, 16 insertions(+), 4 deletions(-) diff --git a/docs/source/user_guide/counter_details.rst b/docs/source/user_guide/counter_details.rst index 91a516f..d689cd7 100644 --- a/docs/source/user_guide/counter_details.rst +++ b/docs/source/user_guide/counter_details.rst @@ -1,6 +1,9 @@ Details about the implementation of the OD viewer PLcounter =========================================================== +.. note:: + This viewer is deprecated since using the old PYDAQmx library. Install an anterior version to use it (<1.1.0). + As you have seen from the configuration of the plugin, we use two counter channels to perform the measurement: a first one which is counting and a second one to handle the measurement timing. These are 2 separate tasks, although the channels will be connected together. As a result, we cannot use a single ``DAQmx`` object as hardware controller. The controller here is a dict containing two ``DAQmx``, one of them is the "clock" and the other the "counter". Each of them contains a single task. diff --git a/docs/source/user_guide/counter_use.rst b/docs/source/user_guide/counter_use.rst index 2ba1caf..f2c21e3 100644 --- a/docs/source/user_guide/counter_use.rst +++ b/docs/source/user_guide/counter_use.rst @@ -1,5 +1,8 @@ -Using the 0D viewer PLcounter -============================= +0D viewer PLcounter +=================== + +.. note:: + This viewer is deprecated since using the old PYDAQmx library. Install an anterior version to use it (<1.1.0). This plugin is meant to be used to count single photons with a NI card. The signal consists of TTL pulses sent by a detector (mainly an avalanche photodiode (APD)). diff --git a/docs/source/user_guide/scanner_details.rst b/docs/source/user_guide/scanner_details.rst index 051f5e0..de0804c 100644 --- a/docs/source/user_guide/scanner_details.rst +++ b/docs/source/user_guide/scanner_details.rst @@ -1,6 +1,9 @@ Details about the implementation of the MultipleScannerControl move =================================================================== +.. note:: + This move is deprecated since using the old PYDAQmx library. Install an anterior version to use it (<1.1.0). + Similarly to the case of the PLcounter, we need to use several channels together to make the scanner work, a clock and an analog output. In principle, we can use the same trick as for the PLcounter and define the controller as a dict containing 2 ``DAQmx`` objects. This is what is done in the ScannerControl plugin, but its means that you need one clock channel for each analog output channel, so if you have many scanners, it will very quickly become an issue. In addition, with a scanner, you most probably want to use the Scan extension. For each movement, the extension sends the command to move to every actuator roughly simultaneously: if your scanners share the same clock, you will get into trouble. Introduction of a new object to use as controller diff --git a/docs/source/user_guide/scanner_use.rst b/docs/source/user_guide/scanner_use.rst index e08b350..50eb5d0 100644 --- a/docs/source/user_guide/scanner_use.rst +++ b/docs/source/user_guide/scanner_use.rst @@ -1,5 +1,8 @@ -Using the ScannerControl and MultipleScannerControl moves -========================================================= +ScannerControl and MultipleScannerControl moves +=============================================== + +.. note:: + This move is deprecated since using the old PYDAQmx library. Install an anterior version to use it (<1.1.0). These plugins allow you to use an analog output of a NI device to control a piezoelectric scanner. It was made for use with Attocube scanners, with an amplifier between the NI card and the scanner, but it should work with any type of scanner that takes a voltage as input. The main difference between the two is that ``ScannerControl`` cannot reliably be used with the Scan extension. It is therefore recommended to use MultipleScannerControl, which can also control a single scanner. This page is thus focussing on the latter. From 0e33e41859d0e846858324e07af6ede91806eae3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Guerrero?= Date: Fri, 14 Nov 2025 17:08:05 +0100 Subject: [PATCH 104/140] Update daqmx to nidaqmx --- docs/source/creating_plugin.rst | 6 +++--- docs/source/nidaqmx.rst | 17 +++++++---------- 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/docs/source/creating_plugin.rst b/docs/source/creating_plugin.rst index 6cae923..b5140bd 100644 --- a/docs/source/creating_plugin.rst +++ b/docs/source/creating_plugin.rst @@ -7,10 +7,10 @@ In order to create a plugin using a NI card to acquire some data or control an a Then you have to analyze the experiment that you want to perform and find the best type of controller that you should use: -* a single ``DAQmx`` object, if you need to perform only one type of task. +* a single ``NIDAQmx`` object, if you need to perform only one type of task. -* a dict of several ``DAQmx`` objects, like in the PLcounter example, if you will only use the NI card but need to combine several tasks. +* a dict of several ``NIDAQmx`` objects, like in the PLcounter example, if you will only use the NI card but need to combine several tasks. -* a dict of ``DAQmx`` objects and other hardware controllers, for example if you want to synchronise your acquisition with an actuator which will be triggered by the NI card. +* a dict of ``NIDAQmx`` objects and other hardware controllers, for example if you want to synchronise your acquisition with an actuator which will be triggered by the NI card. * Another more complicated object (that you will need to write!), like in the MultipleScannerControl example, if you need to share some resources of the NI card between different instrument plugins which control independent parameters/measurements. Beware in particular of the Scan extension which sends all the move commands at the same time. diff --git a/docs/source/nidaqmx.rst b/docs/source/nidaqmx.rst index 69c9008..48ce959 100644 --- a/docs/source/nidaqmx.rst +++ b/docs/source/nidaqmx.rst @@ -1,9 +1,9 @@ The NIDAQmx object -================ +================== You will find its definition in the file ``hardware/national_instruments/daqmxni.py``. This is building block for this plugin, this is the object that you will use as a controller inside your custom plugin. -The ``NIDAQmx`` object has an attribute ``task``, which corresponds to a Task for the NIDAQmx driver. Each ``DAQmx`` can only handle a single task, but a task can be executed on several channels. You will find in the same file the definition of many different Channel classes, which are all the channel types that are available physically on such NI devices. +The ``NIDAQmx`` object has an attribute ``task``, which corresponds to a Task for the NIDAQmx driver. Each ``NIDAQmx`` can only handle a single task, but a task can be executed on several channels. You will find in the same file the definition of many different Channel classes, which are all the channel types that are available physically on such NI devices. The list of these channels is: @@ -23,14 +23,11 @@ The list of these channels is: - DOChannel: Digital Output - DIChannel: Digital Input -To use the ``NIDAQmx``, you therefore need to create Channels of the type you need, specifying the appropriate ``ClockSettings`` and ``TriggerSettings`` (these are also defined in the same file). Then, you pass these channels to the ``update_task`` method of the ``DAQmx``. Be careful that a task can only handle one channel type, if you need several, you need several ``DAQmx``. The ``update_task`` method sets everything up, but if you are using channels as trigger or clock for other channels, you will need to connects them yourself after calling ``update_task``. +To use the ``NIDAQmx``, you therefore need to create Channels of the type you need, specifying the appropriate ``ClockSettings`` and ``TriggerSettings`` (these are also defined in the same file). Then, you pass these channels to the ``update_task`` method of the ``NIDAQmx``. The ``update_task`` method sets everything up, but if you are using channels as trigger or clock for other channels, you will need to connects them yourself after calling ``update_task``. -Once the task is properly set up, you can start it with the method ``start()`` of the ``DAQmx`` object. Several convenient methods of the ``DAQmx`` object are available to read or write data, depending on the task you are performing. They have self-explaining names: - - - ``writeAnalog(Nsamples, Nchannels, values, autostart=False)`` - - ``readAnalog(Nchannels)`` - - ``readCounter(Nchannels, counting_time=10., read_function='Ex')`` - - ``readDigital(Nchannels)`` - - ``writeDigital(Nchannes, values, autostart=False)`` +Once the task is properly set up, you can start it with the method ``start()`` of the ``NIDAQmx`` object. Depending on the task you are performing, several convenient methods with self-explaining names are available to read or write data: + - ``read(number_of_samples_per_channel)``: direct measurement (such as in the 0Dviewer) + - ``register_callback(callback, event, nsamples)``: Measurement using the NI card's buffer (such as in the 1Dviewer) + - ``write(data, auto_start)`` You can also stop the task with the ``stop()`` method. From ec04a22628cd6b19c559b8886c3463eb1b2ab336 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Guerrero?= Date: Fri, 14 Nov 2025 17:09:08 +0100 Subject: [PATCH 105/140] Add nidaqmx viewers to user_guide --- docs/source/user_guide.rst | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/docs/source/user_guide.rst b/docs/source/user_guide.rst index fc1afaa..6de12a3 100644 --- a/docs/source/user_guide.rst +++ b/docs/source/user_guide.rst @@ -4,27 +4,33 @@ User guide Now that you have a working installation, two options are possible: either there is already a plugin doing exactly what you need, or most probably, you need to build your own daq_move or daq_viewer. The available specific functions are: -* counting single photons (``DAQmx_PL_counter``). -* using an analog output to move a piezoelectric scanner at a desired (slow) speed (``DAQmx_ScannerControl`` and ``DAQmx_MultipleScannerControl``). +* counting single photons (``DAQmx_PL_counter``). (deprecated) +* using an analog output to move a piezoelectric scanner at a desired (slow) speed (``DAQmx_ScannerControl`` and ``DAQmx_MultipleScannerControl``). (deprecated) +* 0D data visualization & acquisition +* bufferized 1D data visualization & acquisition -This user guide will first explain how to use these two plugins, and then detail how they work. +This user guide will first explain how to use these plugins, and then detail how they work. .. toctree:: :maxdepth: 1 user_guide/counter_use user_guide/scanner_use + user_guide/0Dviewer_use + user_guide/1Dviewer_use For any other use, you will need to create your own move or viewer based on this plugin. See the :ref:`create_plugin` guide for a detailed procedure. Working principle of the plugins -------------------------------- -The controller object, which is usually the wrapper to your hardware, will always be a ``DAQmx`` object. These are defined in ``hardware\national_instruments\daqmx.py``. Such an object contains a **task**, which is configured and then sent to the NI device. Beware that a task can only contain one channel type. The channel types are Analog Output, Analog Input, Digital Output, Digital Input or Counter. As a consequence, if you need to combine several of them, your controller will be a bit more complicated. It might be a dict containing several ``DAQmx`` objects, as in the photon counter example, or you might even need to create a specific object to handle these ``DAQmx`` objects, as in the scanner control example. More details about these two situations are given in the following sections. +The controller object, which is usually the wrapper to your hardware, will always be a ``NIDAQmx`` object. These are defined in ``hardware\national_instruments\daqmxni.py``. Such an object contains a **task**, which is configured and then sent to the NI device. Beware that a task can only contain one channel type. The channel types are Analog Output, Analog Input, Digital Output, Digital Input or Counter. As a consequence, if you need to combine several of them, your controller will be a bit more complicated. It might be a dict containing several ``NIDAQmx`` objects, as in the photon counter example, or you might even need to create a specific object to handle these ``NIDAQmx`` objects, as in the scanner control example. More details about these two situations are given in the following sections. .. toctree:: :maxdepth: 1 - user_guide/counter_details - user_guide/scanner_details + user_guide/counter_details (deprecated) + user_guide/scanner_details (deprecated) + user_guide/0Dviewer_details + user_guide/1Dviewer_details From 9818acf4bdc2f84762feebd8d06aa8582b2594b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Guerrero?= Date: Fri, 14 Nov 2025 17:10:06 +0100 Subject: [PATCH 106/140] Prepare rst files for 1D viewers --- docs/source/user_guide/1Dviewer_details.rst | 0 docs/source/user_guide/1Dviewer_use.rst | 0 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 docs/source/user_guide/1Dviewer_details.rst create mode 100644 docs/source/user_guide/1Dviewer_use.rst diff --git a/docs/source/user_guide/1Dviewer_details.rst b/docs/source/user_guide/1Dviewer_details.rst new file mode 100644 index 0000000..e69de29 diff --git a/docs/source/user_guide/1Dviewer_use.rst b/docs/source/user_guide/1Dviewer_use.rst new file mode 100644 index 0000000..e69de29 From 94d77a7395174837b6dfe9dd72ce5b3cea9c598c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Guerrero?= Date: Fri, 14 Nov 2025 17:10:26 +0100 Subject: [PATCH 107/140] Update viewer guide --- .../{nidaqmx_viewer_details.rst => 0Dviewer_details.rst} | 0 .../user_guide/{nidaqmx_viewer_use.rst => 0Dviewer_use.rst} | 6 +++--- 2 files changed, 3 insertions(+), 3 deletions(-) rename docs/source/user_guide/{nidaqmx_viewer_details.rst => 0Dviewer_details.rst} (100%) rename docs/source/user_guide/{nidaqmx_viewer_use.rst => 0Dviewer_use.rst} (95%) diff --git a/docs/source/user_guide/nidaqmx_viewer_details.rst b/docs/source/user_guide/0Dviewer_details.rst similarity index 100% rename from docs/source/user_guide/nidaqmx_viewer_details.rst rename to docs/source/user_guide/0Dviewer_details.rst diff --git a/docs/source/user_guide/nidaqmx_viewer_use.rst b/docs/source/user_guide/0Dviewer_use.rst similarity index 95% rename from docs/source/user_guide/nidaqmx_viewer_use.rst rename to docs/source/user_guide/0Dviewer_use.rst index 996e22d..197cfb5 100644 --- a/docs/source/user_guide/nidaqmx_viewer_use.rst +++ b/docs/source/user_guide/0Dviewer_use.rst @@ -1,12 +1,12 @@ -Using the 1D viewer Nidaqmx -=========================== +0Dviewer NIDAQmx +================ This plugin is meant to be used to make measurement and acquire analog data with a NI card. According to the material used, a lot of physical quantity can be detected. Until August 2025, you can measure temperatures, currents or voltages. .. note:: Stay aware of the maximum quantity measurable which depends on what can supports your device. -It was tested only with a cDAQ-9174 with modules NI-9205 and NI-9211 devices. +It was tested with a cDAQ-9174 and a USB-6341 with modules NI-9205, NI-9211 and NI 9217. Configuration ------------- From 462d668992b067751bd71b9b8ea2519a04fa3a50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Guerrero?= Date: Tue, 18 Nov 2025 17:12:34 +0100 Subject: [PATCH 108/140] Add parameters pictures --- docs/source/images/params_0D.jpg | Bin 0 -> 42329 bytes docs/source/images/params_1D.jpg | Bin 0 -> 58363 bytes docs/source/images/params_AIchannels.jpg | Bin 0 -> 72439 bytes 3 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 docs/source/images/params_0D.jpg create mode 100644 docs/source/images/params_1D.jpg create mode 100644 docs/source/images/params_AIchannels.jpg diff --git a/docs/source/images/params_0D.jpg b/docs/source/images/params_0D.jpg new file mode 100644 index 0000000000000000000000000000000000000000..d1a4a2e3c26857859bc6d889f91e60d42d26260f GIT binary patch literal 42329 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

+#{sZQE-6 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 Bf{OqE literal 0 HcmV?d00001 diff --git a/docs/source/images/params_1D.jpg b/docs/source/images/params_1D.jpg new file mode 100644 index 0000000000000000000000000000000000000000..85f28c2547f7dcd19638a5beb45fad6ba6af3371 GIT binary patch literal 58363 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

FHBB_7(6vP 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

puU(44{2 z!_qJI>kWq4sE!k}!VgEiD`-`%5+x`V5*D6*b8w#pR+Cx8f`l0W;&_H3hK?muY^W1? z;qp@ucSY9wi9!VtxzG067Lttj~;>I>LtsaEDiUf94RaWCA>Lw9RRyN|LP<_Cy^C3yNszH@-eek zncSwk^~pRPuiY=hKBcQtGP!JTy}HNguJ}@ApLOqd5TI>EoL6oG^HeHx1I90HA&bvT zj0J|iC5V?s%Jj*(+U`DZr<>!PZS{=+vw_CLjb>;;bA7x#lwi6HKkOY5=Fqb-%JRlJ zn))n(7iyO(T2qD;#|pm`PNx{vi{b&8(W5gJw@=q+8;G299Y1zi^(Lgf!H~=YuxRHt zk}HdP)=EvyYZDh|!(%gD%UUz43P7%jH$nTKpba6)bCWFCqJBLKi<)As{VO%dpUv&x J{R#d)_#frNz*+zR literal 0 HcmV?d00001 diff --git a/docs/source/images/params_AIchannels.jpg b/docs/source/images/params_AIchannels.jpg new file mode 100644 index 0000000000000000000000000000000000000000..7f9025aaa72aa603348fe8d12ee99852dec79b68 GIT binary patch literal 72439 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 Date: Fri, 28 Nov 2025 15:54:08 +0100 Subject: [PATCH 109/140] [BUG] list + list concatenation --- .../hardware/national_instruments/NIDAQmx_Move.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_Move.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_Move.py index dbaf9af..2d953dc 100644 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_Move.py +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_Move.py @@ -32,7 +32,7 @@ class DAQ_NIDAQmx_Actuator(DAQ_Move_base, DAQ_NIDAQmx_base): 'limits': ['Master', 'Slave']}, {'title': 'Axis:', 'name': 'axis', 'type': 'list', 'limits': stage_names}, - ]}] + actuator_params + ]}] + actuator_params() def __init__(self, parent=None, params_state=None, control_type="Actuator"): DAQ_Move_base.__init__(self, parent, params_state) # defines settings attribute and various other methods From 6e63387210ae785592e7584ef01735813aa06706 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Guerrero?= Date: Thu, 4 Dec 2025 16:14:35 +0100 Subject: [PATCH 110/140] [DOC] Add library reference with a class diagram --- docs/source/conf.py | 32 ++++++++++++++++++++++++++--- docs/source/docstring/classdiag.rst | 6 ++++++ docs/source/index.rst | 3 ++- docs/source/library.rst | 10 +++++++++ 4 files changed, 47 insertions(+), 4 deletions(-) create mode 100644 docs/source/docstring/classdiag.rst create mode 100644 docs/source/library.rst diff --git a/docs/source/conf.py b/docs/source/conf.py index f3ff4a8..b8de822 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -2,6 +2,18 @@ # # For the full list of built-in configuration values, see the documentation: # https://www.sphinx-doc.org/en/master/usage/configuration.html +import os +import sys +sys.path.insert(0, os.path.abspath('./')) +sys.path.insert(0, os.path.abspath('../')) +sys.path.insert(0, os.path.abspath('../src')) +sys.path.insert(0, os.path.abspath('../src/pymodaq_plugins_daqmx')) +sys.path.insert(0, os.path.abspath('../src/pymodaq_plugins_daqmx/hardware')) +sys.path.insert(0, os.path.abspath('../src/pymodaq_plugins_daqmx/hardware/national_instruments')) +sys.path.insert(0, os.path.abspath('../src/pymodaq_plugins_daqmx/daq_move_plugins')) +sys.path.insert(0, os.path.abspath('../src/pymodaq_plugins_daqmx/daq_viewer_plugins')) +sys.path.insert(0, os.path.abspath('../src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D')) +sys.path.insert(0, os.path.abspath('../src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_1D')) # -- Project information ----------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information @@ -14,10 +26,24 @@ # -- General configuration --------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration -extensions = ['sphinx_rtd_theme'] - +extensions = ['sphinx_rtd_theme', + "sphinx.ext.autodoc", + "sphinx.ext.viewcode", + "sphinx.ext.graphviz", + "sphinx.ext.inheritance_diagram" + ] + +autodoc_default_flags = ['members', 'inherited-members', 'show-inheritance'] +autodoc_default_options = { + "members": True, + "special-members": "__init__", + "inherited-members": False, + "show-inheritance": False, +} +inheritance_graph_attrs = dict(rankdir="TB", size='""') +graphviz_dot = "C:/Program Files (x86)/Graphviz/bin/dot.exe" templates_path = ['_templates'] -exclude_patterns = [] +exclude_patterns = ['_build'] diff --git a/docs/source/docstring/classdiag.rst b/docs/source/docstring/classdiag.rst new file mode 100644 index 0000000..43dcc05 --- /dev/null +++ b/docs/source/docstring/classdiag.rst @@ -0,0 +1,6 @@ +Class Inheritance +================= + +.. inheritance-diagram:: pymodaq_plugins_daqmx.daq_move_plugins.daq_move_NIDAQmx.DAQ_Move_NIDAQmx pymodaq_plugins_daqmx.daq_viewer_plugins.plugins_0D.daq_0Dviewer_NIDAQmx.DAQ_0DViewer_NIDAQmx pymodaq_plugins_daqmx.daq_viewer_plugins.plugins_1D.daq_1Dviewer_NIDAQmx.DAQ_1DViewer_NIDAQmx pymodaq_plugins_daqmx.hardware.national_instruments.daqmxni.NIDAQmx pymodaq_plugins_daqmx.hardware.national_instruments.NIDAQmx_base.DAQ_NIDAQmx_base pymodaq_plugins_daqmx.hardware.national_instruments.NIDAQmx_base.ScalableGroupAI pymodaq_plugins_daqmx.hardware.national_instruments.NIDAQmx_Move.DAQ_NIDAQmx_Actuator pymodaq_plugins_daqmx.hardware.national_instruments.NIDAQmx_Viewer.DAQ_NIDAQmx_Viewer + :parts: 1 + :top-classes: PyQt5.QtCore.QObject \ No newline at end of file diff --git a/docs/source/index.rst b/docs/source/index.rst index d538b22..cdfdeba 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -16,11 +16,12 @@ This documentation is written as a tutorial to guide you when setting up your NI This plugin is compatible with PymoDAQ 5. .. toctree:: - :maxdepth: 1 + :maxdepth: 4 :caption: Contents: introduction user_guide nidaqmx + library creating_plugin diff --git a/docs/source/library.rst b/docs/source/library.rst new file mode 100644 index 0000000..caedc13 --- /dev/null +++ b/docs/source/library.rst @@ -0,0 +1,10 @@ +Library Reference +================= + +.. toctree:: + :maxdepth: 4 + + docstring/daq_move_plugins.rst + docstring/daq_viewer_plugins.rst + docstring/hardware.rst + docstring/classdiag.rst \ No newline at end of file From c75f6d7f36675eba2512550d65887a621de42b98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Guerrero?= Date: Thu, 4 Dec 2025 16:16:21 +0100 Subject: [PATCH 111/140] [DOC] Update documentation --- README.rst | 17 +---- docs/source/introduction.rst | 2 +- docs/source/user_guide.rst | 10 ++- docs/source/user_guide/0Dviewer_details.rst | 0 docs/source/user_guide/0Dviewer_use.rst | 46 -------------- docs/source/user_guide/1Dviewer_details.rst | 0 docs/source/user_guide/1Dviewer_use.rst | 0 docs/source/user_guide/nidaqmx_viewer_use.rst | 62 +++++++++++++++++++ 8 files changed, 70 insertions(+), 67 deletions(-) delete mode 100644 docs/source/user_guide/0Dviewer_details.rst delete mode 100644 docs/source/user_guide/0Dviewer_use.rst delete mode 100644 docs/source/user_guide/1Dviewer_details.rst delete mode 100644 docs/source/user_guide/1Dviewer_use.rst create mode 100644 docs/source/user_guide/nidaqmx_viewer_use.rst diff --git a/README.rst b/README.rst index 37c542b..d448358 100644 --- a/README.rst +++ b/README.rst @@ -43,18 +43,7 @@ Viewer1D * **NIDAQmx**: General measurement with National Instrument materials -NIDAQmx Viewer -++++++++++++++ +Documentation ++++++++++++++ -This viewer aims at being very general and allows measurement with as much different NI modules as possible. -At this point (10/2025), it supports Analog measurement of currents, voltages and temperatures from thermocouples and RTD. - -Tests have been running with the following devices: - * Chassis NI cDAQ-9174 & NI USB-9162 - * NI modules 9211, 9205 & 9217 (for RTD measurements) - -Acquisition channels can ge generated directly running the viewer, but it is also possible to load channels from the configuration file config_daqmx.toml clicking on "load configuration" in the parameter tree. - -To use such configuration, copy paste the content from C:/ProgramData/.pymodaq/config_daqmx.toml to /.pymodaq/config_daqmx.toml and fill it with the configuration of your own experimental setup. - -It is recommended to create a preset with one detector for each module used (using the current device parameter). It allows to control the frequency for each module and make the "Load config" button only catch channels corresponding to the selected module. \ No newline at end of file +Refer to the `documentation `_ to use this plugin or discover its functionalities. \ No newline at end of file diff --git a/docs/source/introduction.rst b/docs/source/introduction.rst index 1008839..7a9a2eb 100644 --- a/docs/source/introduction.rst +++ b/docs/source/introduction.rst @@ -3,7 +3,7 @@ Introduction .. _introduction: -NI acquisition and signal generation devices are very versatile instruments, which can be used both as actuators and detectors. They offer the possibility to generate or acquire both analog and digital signals, with external or internal timing of the tasks. As a consequence, one can hardly provide a generic PyMoDAQ plugin to operate a NI acquisition card, and you will most probably need to build your own module to perform the precise task that you have in mind. +NI acquisition and signal generation devices are very versatile instruments, which can be used both as actuators and detectors. They offer the possibility to generate or acquire both analog and digital signals, with external or internal timing of the tasks. As a consequence, one can hardly provide a generic PyMoDAQ plugin to operate a NI acquisition card, and you will most probably need to build your own module to perform the precise task that you have in mind. However, if your goal is to perform basics analog measurement, you can begin with the nidaqmx viewer which enable acquisition of various physical quantity. Installation ------------ diff --git a/docs/source/user_guide.rst b/docs/source/user_guide.rst index 6de12a3..ddedd79 100644 --- a/docs/source/user_guide.rst +++ b/docs/source/user_guide.rst @@ -16,8 +16,7 @@ This user guide will first explain how to use these plugins, and then detail how user_guide/counter_use user_guide/scanner_use - user_guide/0Dviewer_use - user_guide/1Dviewer_use + user_guide/nidaqmx_viewer_use For any other use, you will need to create your own move or viewer based on this plugin. See the :ref:`create_plugin` guide for a detailed procedure. @@ -29,8 +28,7 @@ The controller object, which is usually the wrapper to your hardware, will alway .. toctree:: :maxdepth: 1 - user_guide/counter_details (deprecated) - user_guide/scanner_details (deprecated) - user_guide/0Dviewer_details - user_guide/1Dviewer_details + user_guide/counter_details + user_guide/scanner_details + diff --git a/docs/source/user_guide/0Dviewer_details.rst b/docs/source/user_guide/0Dviewer_details.rst deleted file mode 100644 index e69de29..0000000 diff --git a/docs/source/user_guide/0Dviewer_use.rst b/docs/source/user_guide/0Dviewer_use.rst deleted file mode 100644 index 197cfb5..0000000 --- a/docs/source/user_guide/0Dviewer_use.rst +++ /dev/null @@ -1,46 +0,0 @@ -0Dviewer NIDAQmx -================ - -This plugin is meant to be used to make measurement and acquire analog data with a NI card. According to the material used, a lot of physical quantity can be detected. Until August 2025, you can measure temperatures, currents or voltages. - -.. note:: - Stay aware of the maximum quantity measurable which depends on what can supports your device. - -It was tested with a cDAQ-9174 and a USB-6341 with modules NI-9205, NI-9211 and NI 9217. - -Configuration -------------- - -You need to provide those parameters (or at least be aware of the default settings), as shown in the screenshot below. - -* **Devices**: The device you want to use among each detected device. It can be the whole cDAQ, such as ``/cDAQ[X]``, or a specific module, such as ``/cDAQ[X]Mod[Y]``. - -* **Signal type**: the type of measurement you want to make (mostly ANALOG_INPUT since digital measurements are not supported yet and output signals do not concern viewers but daq_move). - -* **NSamples To Read**: the number of samples displayed by the viewer - -* **Plugin config**: A push button loading a specific configuration set by the user. - It aims to prevent from multiple channel entry each time the plugin is loaded. It is very useful in the case of setups including many modules / channels, for exemple with one experimental bench being used for multiples measurement campaigns. - To make your configuration, refer to the example shown in the file ``C:\ProgramData\.pymodaq.\config_daqmx.toml`` and fill the file ``C:\Users\\.pymodaq.\config_daqmx.toml`` with your own material (syntax needs to be respected to prevent from errors). - -* **Clock Settings - Nsamples**: The number of samples requested to the material for each channel - -* **Clock Settings - Frequency**: the acquisition rate, which is common for each acquisition channel. - -* **AI Channels**: the channel(s) you will use for your analog measurements, something like ``cDAQ[X]Mod[Y]/ai[Z]``. You can generate it directly or load it automatically using the plugin config. Pay attention not creating the same channel twice! - - .. image:: /images/parameters.png - :width: 800 - -This plugin is not intended to be used with a "Slave" configuration, but you can definitely use the same NI card device as hardware for other plugins at the same time. - -Be careful to use as photon source the default input terminal corresponding to the counting channel, check the documentation of your NI device about this. - - -Use ---- - -It works as a usual 1D viewer plugin. - -If you encounter some problems, you might get some warnings or errors in the log, check it to get some hints to solve it. -You can run ``Get-Content .pymodaq\log\pymodaq.log -Tail 10 -Wait`` in the PowerShell to display the log in real time. diff --git a/docs/source/user_guide/1Dviewer_details.rst b/docs/source/user_guide/1Dviewer_details.rst deleted file mode 100644 index e69de29..0000000 diff --git a/docs/source/user_guide/1Dviewer_use.rst b/docs/source/user_guide/1Dviewer_use.rst deleted file mode 100644 index e69de29..0000000 diff --git a/docs/source/user_guide/nidaqmx_viewer_use.rst b/docs/source/user_guide/nidaqmx_viewer_use.rst new file mode 100644 index 0000000..9c95869 --- /dev/null +++ b/docs/source/user_guide/nidaqmx_viewer_use.rst @@ -0,0 +1,62 @@ +NIDAQmx viewer +============== + +This plugin is meant to be used to make measurement and acquire analog data with a NI card. According to the material used, a lot of physical quantity can be detected. Until August 2025, you can measure temperatures, currents or voltages. + +.. note:: + Stay aware of the maximum quantity measurable which depends on what can supports your device. + +It was tested with a cDAQ-9174 and a USB-6341 with modules NI-9205, NI-9211 and NI 9217. + +Global Configuration +-------------------- + +To use this plugin, you need to provide some parameters, or at least be aware of the default settings. Refer to their description below and the examples given for both 0D and 1D configuration. + +* **Devices**: The device you want to use among each detected device. It can be the whole cDAQ, such as ``cDAQ[X]``, or a specific module, such as ``cDAQ[X]Mod[Y]``. + +* **Signal type**: the type of measurement you want to make (mostly ANALOG_INPUT since digital measurements are not supported yet and output signals do not concern viewers but daq_move). + +* **Plugin config**: A push button (which disappear when used) loading a specific configuration set by the user. It aims to prevent from multiple channel entry each time the plugin is loaded. It is very useful in the case of setups including many modules / channels, for exemple with one experimental bench being used for multiples measurement campaigns. To make your configuration, refer to the example shown in the file ``C:\ProgramData\.pymodaq.\config_daqmx.toml`` and fill the file ``C:\Users\\.pymodaq.\config_daqmx.toml`` with your own material (syntax needs to be respected to prevent from errors). + +0D Configuration +---------------- + +The goal of 0D acquisition is to display each data in live and keep the graphical history. It is sometimes necessary to control visually some quantity, for example for security reasons. + +* **Clock Settings - Nsamples**: Fixed to 1 + +.. image:: /images/params_0D.jpg + :width: 800 + +1D Configuration +---------------- + +The goal of 1D acquisition is to get data with an optimal use of the hardware capacity. Data are acquired in the hardware buffer first and then transmitted to PyMoDAQ, which display each array of data received, hence the use of the 1D viewer. + +* **NSamples To Read**: the number of samples displayed by the viewer + +* **Clock Settings - Nsamples**: The number of samples requested to the material for each channel + +* **Clock Settings - Frequency**: the acquisition rate, which is common for each acquisition channel. + +.. image:: /images/params_1D.jpg + :width: 800 + +Channels Configuration +---------------------- + +Last but not least, the channels section of the parameters tree contains each analog measurement you will process. If you clicked on the **load config** button, the channels from the .toml configuration file are displayed here. You can also generate directly the channels you want to use, selecting the id corresponding the the physical channel your sensor is plugged-in, and choosing the type of your analog input. Below are some examples of channels that can be used with this plugin. + +* **AI Channels**: the channel(s) you will use for your analog measurements, something like ``cDAQ[X]Mod[Y]/ai[Z]``. You can generate it directly or load it automatically using the plugin config. Pay attention not creating the same channel twice! + +.. image:: /images/params_AIchannels.jpg + :width: 800 + +Use +--- + +It works as a usual viewer plugin, acquiring data on each channels at the configured rate. + +If you encounter some problems, you might get some warnings or errors in the log, check it to get some hints to solve it. +You can run ``Get-Content .pymodaq\log\pymodaq.log -Tail 10 -Wait`` in the PowerShell to display the log in real time. From e8d352b1ca2f6fb5d89f4a2749429835d8324cef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Guerrero?= Date: Thu, 4 Dec 2025 16:16:38 +0100 Subject: [PATCH 112/140] [DOC] Update library ref --- docs/source/docstring/daq_move_plugins.rst | 4 ++++ docs/source/docstring/daq_viewer_plugins.rst | 12 ++++++++++++ docs/source/docstring/hardware.rst | 7 +++++++ 3 files changed, 23 insertions(+) create mode 100644 docs/source/docstring/daq_move_plugins.rst create mode 100644 docs/source/docstring/daq_viewer_plugins.rst create mode 100644 docs/source/docstring/hardware.rst diff --git a/docs/source/docstring/daq_move_plugins.rst b/docs/source/docstring/daq_move_plugins.rst new file mode 100644 index 0000000..1e69bbf --- /dev/null +++ b/docs/source/docstring/daq_move_plugins.rst @@ -0,0 +1,4 @@ +daq_move_plugins +================ + +.. automodule:: pymodaq_plugins_daqmx.daq_move_plugins.daq_move_NIDAQmx \ No newline at end of file diff --git a/docs/source/docstring/daq_viewer_plugins.rst b/docs/source/docstring/daq_viewer_plugins.rst new file mode 100644 index 0000000..6e6d5f4 --- /dev/null +++ b/docs/source/docstring/daq_viewer_plugins.rst @@ -0,0 +1,12 @@ +daq_viewer_plugins +================== + +plugins_0D +---------- + +.. automodule:: pymodaq_plugins_daqmx.daq_viewer_plugins.plugins_0D.daq_0Dviewer_NIDAQmx + +plugins_1D +---------- + +.. automodule:: pymodaq_plugins_daqmx.daq_viewer_plugins.plugins_1D.daq_1Dviewer_NIDAQmx diff --git a/docs/source/docstring/hardware.rst b/docs/source/docstring/hardware.rst new file mode 100644 index 0000000..142d4ae --- /dev/null +++ b/docs/source/docstring/hardware.rst @@ -0,0 +1,7 @@ +hardware +======== + +national_instruments +-------------------- + +.. automodule:: pymodaq_plugins_daqmx.hardware.national_instruments.daqmxni From 42454d1f3326252572d1d8cd715c405745b4b909 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Guerrero?= Date: Thu, 4 Dec 2025 16:18:33 +0100 Subject: [PATCH 113/140] [DOC] correct unindent --- docs/source/nidaqmx.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/source/nidaqmx.rst b/docs/source/nidaqmx.rst index 48ce959..58d61ef 100644 --- a/docs/source/nidaqmx.rst +++ b/docs/source/nidaqmx.rst @@ -30,4 +30,5 @@ Once the task is properly set up, you can start it with the method ``start()`` o - ``read(number_of_samples_per_channel)``: direct measurement (such as in the 0Dviewer) - ``register_callback(callback, event, nsamples)``: Measurement using the NI card's buffer (such as in the 1Dviewer) - ``write(data, auto_start)`` + You can also stop the task with the ``stop()`` method. From 94b48fad256e2e4f175e41236c60c6b7d5e1ecee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Guerrero?= Date: Thu, 4 Dec 2025 16:36:47 +0100 Subject: [PATCH 114/140] [DOC] Add all NIDAQmx module --- docs/source/docstring/hardware.rst | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/docs/source/docstring/hardware.rst b/docs/source/docstring/hardware.rst index 142d4ae..aef7485 100644 --- a/docs/source/docstring/hardware.rst +++ b/docs/source/docstring/hardware.rst @@ -1,7 +1,26 @@ -hardware -======== +hardware - National Instruments +=============================== -national_instruments --------------------- +NIDAQmx object +-------------- .. automodule:: pymodaq_plugins_daqmx.hardware.national_instruments.daqmxni + :members: NIDAQmx + +NIDAQmx Base module +------------------- + +.. automodule:: pymodaq_plugins_daqmx.hardware.national_instruments.NIDAQmx_base + :members: ScalableGroupAI, ScalableGroupAO, ScalableGroupCounter, ScalableGroupDI, ScalableGroupDO, DAQ_NIDAQmx_base + +NIDAQmx Actuator module +----------------------- + +.. automodule:: pymodaq_plugins_daqmx.hardware.national_instruments.NIDAQmx_Move + :members: DAQ_NIDAQmx_Actuator + +NIDAQmx Viewer module +--------------------- + +.. automodule:: pymodaq_plugins_daqmx.hardware.national_instruments.NIDAQmx_Viewer + :members: DAQ_NIDAQmxViewer From 094e8bf99bd805e23413e1f6a5a6291109b1c9c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Guerrero?= Date: Thu, 4 Dec 2025 16:58:08 +0100 Subject: [PATCH 115/140] [DOC] Add docstring to base modules --- .../national_instruments/NIDAQmx_Viewer.py | 7 +++ .../national_instruments/NIDAQmx_base.py | 46 ++++++------------- 2 files changed, 21 insertions(+), 32 deletions(-) diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_Viewer.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_Viewer.py index cd2578d..8ff24e8 100644 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_Viewer.py +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_Viewer.py @@ -223,6 +223,10 @@ def commit_settings(self, param): self.get_max_frequency() # Set the frequency 'max' option to the device maximum frequency and display it def get_max_frequency(self): + """ + Get the maximum frequency according to the material's limits and display the information. + This method is destined to be removed once set_max_frequency will work correctly. + """ # Destined to be removed when set_max_frequency will work correctly try: max_freq = int(self.controller.getAIMaxSingleRate(self.controller.device.name)) @@ -231,6 +235,9 @@ def get_max_frequency(self): pass def set_max_frequency(self): + """ + Set the maximum frequency applicable to the frequency parameter according to material's limits. + """ # Opts 'max' get the right value but doesn't update the viewer which keep as max the initial max value try: max_freq = int(self.controller.getAIMaxSingleRate(self.controller.device.name)) diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_base.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_base.py index 011d137..8360d31 100644 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_base.py +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_base.py @@ -15,16 +15,7 @@ class ScalableGroupAI(GroupParameter): """ - | - - ================ ============= - **Attributes** **Type** - *opts* dictionnary - ================ ============= - - See Also - -------- - hardware.DAQ_Move_Stage_type + Generic parameter items holding Analog Input parameters """ params = [{'title': 'AI type:', 'name': 'ai_type', 'type': 'list', 'visible': True, 'limits': [Uai.name for Uai in UsageTypeAI]}, @@ -63,8 +54,8 @@ class ScalableGroupAI(GroupParameter): {'title': 'Resistance config.', 'name': 'resistance_config', 'type': 'list', 'limits': [rc.name for rc in ResistanceConfiguration], 'value': ResistanceConfiguration.FOUR_WIRE.name}, - {'title' : 'Curr. excit. src', 'name': 'current_excit_src', 'type': 'list', - 'limits' : [excit_src.name for excit_src in ExcitationSource], + {'title': 'Curr. excit. src', 'name': 'current_excit_src', 'type': 'list', + 'limits': [excit_src.name for excit_src in ExcitationSource], 'value': ExcitationSource.INTERNAL.name}, {'title': 'Iex value', 'name': 'i_ex_value', 'type': 'float', 'value': 0.00100, 'suffix': 'A'}, ]}, @@ -98,16 +89,7 @@ def addNew(self, typ=None): class ScalableGroupAO(GroupParameter): """ - | - - ================ ============= - **Attributes** **Type** - *opts* dictionnary - ================ ============= - - See Also - -------- - hardware.DAQ_Move_Stage_type + Generic parameter items holding Analog Output parameters """ params = [{'title': 'AO type:', 'name': 'ao_type', 'type': 'list', 'limits': [Uao.name for Uao in UsageTypeAO]}, @@ -147,16 +129,7 @@ def addNew(self, typ=None): class ScalableGroupCounter(GroupParameter): """ - | - - ================ ============= - **Attributes** **Type** - *opts* dictionnary - ================ ============= - - See Also - -------- - hardware.DAQ_Move_Stage_type + Generic parameter items holding Counter parameters """ params = [{'title': 'Edge type:', 'name': 'edge', 'type': 'list', 'limits': [e.name for e in Edge]}, ] @@ -187,6 +160,7 @@ def addNew(self, typ=None): class ScalableGroupDI(GroupParameter): """ + Generic parameter items holding Digital Input parameters """ params = [] @@ -216,6 +190,7 @@ def addNew(self, typ=None): class ScalableGroupDO(GroupParameter): """ + Generic parameter items holding Digital Output parameters """ params = [] @@ -416,6 +391,9 @@ def commit_settings(self, param: Parameter): self.settings.child('clock_settings', 'max_freq').hide() def update_task(self): + """ + Update task according to channels from settings. + """ self.channels = self.get_channels_from_settings() self.clock_settings = ClockSettings(frequency=self.settings['clock_settings', 'frequency'], Nsamples=self.settings['clock_settings', 'Nsamples'], @@ -432,6 +410,9 @@ def update_task(self): logger.warning("No channels assigned, task not created") def get_channels_from_settings(self): + """ + Browse the viewer settings to gather all the channels that will be used. + """ channels = [] if self.settings['NIDAQ_type'] == ChannelType.ANALOG_INPUT.name: # analog input source = ChannelType.ANALOG_INPUT @@ -513,6 +494,7 @@ def get_channels_from_settings(self): def stop(self): """ + Stop the acquisition. """ if not not self.timer: self.timer.stop() From b0d32ad0f67f3f99ff486dfeabbbf84a4b966855 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Guerrero?= Date: Thu, 4 Dec 2025 18:06:14 +0100 Subject: [PATCH 116/140] [DOC] Install graphviz --- .github/workflows/sphinx.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/sphinx.yml b/.github/workflows/sphinx.yml index 104fdcb..990ff44 100644 --- a/.github/workflows/sphinx.yml +++ b/.github/workflows/sphinx.yml @@ -13,6 +13,7 @@ jobs: - uses: actions/setup-python@v5.4.0 - name: Install dependencies run: | + sudo apt install graphviz pip install sphinx sphinx_rtd_theme myst_parser - name: Sphinx build run: | From b63a9ccafdc3f1e29fdf1a8da5b6c6ddac3faa8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Guerrero?= Date: Fri, 5 Dec 2025 14:19:16 +0100 Subject: [PATCH 117/140] [DOC] Add requirements --- .github/workflows/sphinx.yml | 2 +- docs/requirements.txt | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 docs/requirements.txt diff --git a/.github/workflows/sphinx.yml b/.github/workflows/sphinx.yml index 990ff44..6006fae 100644 --- a/.github/workflows/sphinx.yml +++ b/.github/workflows/sphinx.yml @@ -14,7 +14,7 @@ jobs: - name: Install dependencies run: | sudo apt install graphviz - pip install sphinx sphinx_rtd_theme myst_parser + pip install -r docs/requirements.txt - name: Sphinx build run: | sphinx-build docs/source docs/build/html diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 0000000..0af7b3b --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1,4 @@ +. +sphinx +sphinx_rtd_theme +myst_parser \ No newline at end of file From c2148d6379d08da96cfcb77e2caa4a35a8803f18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Guerrero?= Date: Fri, 5 Dec 2025 14:22:33 +0100 Subject: [PATCH 118/140] [DOC] Add pyqt6 to requirements --- docs/requirements.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/requirements.txt b/docs/requirements.txt index 0af7b3b..94709a1 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,4 +1,5 @@ . +pyqt6 sphinx sphinx_rtd_theme -myst_parser \ No newline at end of file +myst_parser From 31c991857e44ee6786495f19386ff454fa3952a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Guerrero?= Date: Fri, 5 Dec 2025 14:38:06 +0100 Subject: [PATCH 119/140] [DOC] Replace pyqt6 by pyqt5 --- docs/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/requirements.txt b/docs/requirements.txt index 94709a1..a275e0d 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,5 +1,5 @@ . -pyqt6 +pyqt5 sphinx sphinx_rtd_theme myst_parser From 2f5cbca19956c25d063fde5d419aa02a36f083ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Guerrero?= Date: Fri, 5 Dec 2025 14:46:42 +0100 Subject: [PATCH 120/140] [DOC] Add plugin dependencies to requirements.txt --- docs/requirements.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/requirements.txt b/docs/requirements.txt index a275e0d..0b96774 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,5 +1,7 @@ . +pymodaq pyqt5 +nidaqmx sphinx sphinx_rtd_theme myst_parser From c8dec9597598f931a9b14a9941e0bf31b8eee06a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Guerrero?= Date: Thu, 11 Dec 2025 10:14:32 +0100 Subject: [PATCH 121/140] [DOC] Correct docstring --- .../daq_move_plugins/daq_move_NIDAQmx.py | 11 +---------- .../plugins_0D/daq_0Dviewer_NIDAQmx.py | 2 +- .../hardware/national_instruments/NIDAQmx_Move.py | 3 ++- 3 files changed, 4 insertions(+), 12 deletions(-) diff --git a/src/pymodaq_plugins_daqmx/daq_move_plugins/daq_move_NIDAQmx.py b/src/pymodaq_plugins_daqmx/daq_move_plugins/daq_move_NIDAQmx.py index a5fae0d..2f082b2 100644 --- a/src/pymodaq_plugins_daqmx/daq_move_plugins/daq_move_NIDAQmx.py +++ b/src/pymodaq_plugins_daqmx/daq_move_plugins/daq_move_NIDAQmx.py @@ -3,16 +3,7 @@ class DAQ_Move_NIDAQmx(DAQ_NIDAQmx_Actuator): """ - ==================== ======================== - **Attributes** **Type** - *data_grabed_signal* instance of Signal - *params* dictionnary list - *task* - ==================== ======================== - - See Also - -------- - refresh_hardware + Plugin for basic current or voltage control with NI modules (plugged in NI cDAQ or NI-USB). (in process) """ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) diff --git a/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_NIDAQmx.py b/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_NIDAQmx.py index 4f9bfe2..6ced253 100644 --- a/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_NIDAQmx.py +++ b/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_NIDAQmx.py @@ -9,7 +9,7 @@ class DAQ_0DViewer_NIDAQmx(DAQ_NIDAQmx_Viewer): """ - Plugin for bufferized 0D data visualization & acquisition with NI modules (plugged in NI cDAQ or NI-USB). + Plugin for 0D data visualization & acquisition with NI modules (plugged in NI cDAQ or NI-USB). """ live_mode_available = False acquisition_state = False diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_Move.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_Move.py index 2d953dc..c4720be 100644 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_Move.py +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_Move.py @@ -95,7 +95,8 @@ def ini_stage(self, controller=None): self.status (edict): with initialization status: three fields: * info (str) * controller (object) initialized controller - *initialized: (bool): False if initialization failed otherwise True + * initialized: (bool): False if initialization failed otherwise True + """ try: # initialize the stage and its controller status From 16f52c43ec096f23c8f6ee3691657fe3e6935bb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Guerrero?= Date: Fri, 12 Dec 2025 10:21:17 +0100 Subject: [PATCH 122/140] [DOC] Raw import of library.html --- .github/workflows/sphinx.yml | 1 + docs/source/docstring/library.html | 232 +++++++++++++++++++++++++++++ 2 files changed, 233 insertions(+) create mode 100644 docs/source/docstring/library.html diff --git a/.github/workflows/sphinx.yml b/.github/workflows/sphinx.yml index 6006fae..3b699cf 100644 --- a/.github/workflows/sphinx.yml +++ b/.github/workflows/sphinx.yml @@ -18,6 +18,7 @@ jobs: - name: Sphinx build run: | sphinx-build docs/source docs/build/html + cp docs/source/docstring/library.html docs/build/html/library.html - name: Upload artifacts uses: actions/upload-artifact@v4 with: diff --git a/docs/source/docstring/library.html b/docs/source/docstring/library.html new file mode 100644 index 0000000..bc6b271 --- /dev/null +++ b/docs/source/docstring/library.html @@ -0,0 +1,232 @@ + + + + + + + + + Library Reference — pymodaq_plugins_daqmx 1.1.0 documentation + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file From 9e359b994ba882d1137882d91a3cb5d144a505a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Guerrero?= Date: Fri, 12 Dec 2025 10:21:17 +0100 Subject: [PATCH 123/140] [DOC] Raw import of library.html --- .github/workflows/sphinx.yml | 1 + docs/source/docstring/classdiag.rst | 4 - docs/source/docstring/daq_move_plugins.rst | 2 - docs/source/docstring/daq_viewer_plugins.rst | 4 - docs/source/docstring/hardware.rst | 12 - docs/source/docstring/library.html | 232 +++++++++++++++++++ 6 files changed, 233 insertions(+), 22 deletions(-) create mode 100644 docs/source/docstring/library.html diff --git a/.github/workflows/sphinx.yml b/.github/workflows/sphinx.yml index 6006fae..3b699cf 100644 --- a/.github/workflows/sphinx.yml +++ b/.github/workflows/sphinx.yml @@ -18,6 +18,7 @@ jobs: - name: Sphinx build run: | sphinx-build docs/source docs/build/html + cp docs/source/docstring/library.html docs/build/html/library.html - name: Upload artifacts uses: actions/upload-artifact@v4 with: diff --git a/docs/source/docstring/classdiag.rst b/docs/source/docstring/classdiag.rst index 43dcc05..c19c85c 100644 --- a/docs/source/docstring/classdiag.rst +++ b/docs/source/docstring/classdiag.rst @@ -1,6 +1,2 @@ Class Inheritance ================= - -.. inheritance-diagram:: pymodaq_plugins_daqmx.daq_move_plugins.daq_move_NIDAQmx.DAQ_Move_NIDAQmx pymodaq_plugins_daqmx.daq_viewer_plugins.plugins_0D.daq_0Dviewer_NIDAQmx.DAQ_0DViewer_NIDAQmx pymodaq_plugins_daqmx.daq_viewer_plugins.plugins_1D.daq_1Dviewer_NIDAQmx.DAQ_1DViewer_NIDAQmx pymodaq_plugins_daqmx.hardware.national_instruments.daqmxni.NIDAQmx pymodaq_plugins_daqmx.hardware.national_instruments.NIDAQmx_base.DAQ_NIDAQmx_base pymodaq_plugins_daqmx.hardware.national_instruments.NIDAQmx_base.ScalableGroupAI pymodaq_plugins_daqmx.hardware.national_instruments.NIDAQmx_Move.DAQ_NIDAQmx_Actuator pymodaq_plugins_daqmx.hardware.national_instruments.NIDAQmx_Viewer.DAQ_NIDAQmx_Viewer - :parts: 1 - :top-classes: PyQt5.QtCore.QObject \ No newline at end of file diff --git a/docs/source/docstring/daq_move_plugins.rst b/docs/source/docstring/daq_move_plugins.rst index 1e69bbf..89e8de8 100644 --- a/docs/source/docstring/daq_move_plugins.rst +++ b/docs/source/docstring/daq_move_plugins.rst @@ -1,4 +1,2 @@ daq_move_plugins ================ - -.. automodule:: pymodaq_plugins_daqmx.daq_move_plugins.daq_move_NIDAQmx \ No newline at end of file diff --git a/docs/source/docstring/daq_viewer_plugins.rst b/docs/source/docstring/daq_viewer_plugins.rst index 6e6d5f4..c53ed9e 100644 --- a/docs/source/docstring/daq_viewer_plugins.rst +++ b/docs/source/docstring/daq_viewer_plugins.rst @@ -4,9 +4,5 @@ daq_viewer_plugins plugins_0D ---------- -.. automodule:: pymodaq_plugins_daqmx.daq_viewer_plugins.plugins_0D.daq_0Dviewer_NIDAQmx - plugins_1D ---------- - -.. automodule:: pymodaq_plugins_daqmx.daq_viewer_plugins.plugins_1D.daq_1Dviewer_NIDAQmx diff --git a/docs/source/docstring/hardware.rst b/docs/source/docstring/hardware.rst index aef7485..7b4a565 100644 --- a/docs/source/docstring/hardware.rst +++ b/docs/source/docstring/hardware.rst @@ -4,23 +4,11 @@ hardware - National Instruments NIDAQmx object -------------- -.. automodule:: pymodaq_plugins_daqmx.hardware.national_instruments.daqmxni - :members: NIDAQmx - NIDAQmx Base module ------------------- -.. automodule:: pymodaq_plugins_daqmx.hardware.national_instruments.NIDAQmx_base - :members: ScalableGroupAI, ScalableGroupAO, ScalableGroupCounter, ScalableGroupDI, ScalableGroupDO, DAQ_NIDAQmx_base - NIDAQmx Actuator module ----------------------- -.. automodule:: pymodaq_plugins_daqmx.hardware.national_instruments.NIDAQmx_Move - :members: DAQ_NIDAQmx_Actuator - NIDAQmx Viewer module --------------------- - -.. automodule:: pymodaq_plugins_daqmx.hardware.national_instruments.NIDAQmx_Viewer - :members: DAQ_NIDAQmxViewer diff --git a/docs/source/docstring/library.html b/docs/source/docstring/library.html new file mode 100644 index 0000000..bc6b271 --- /dev/null +++ b/docs/source/docstring/library.html @@ -0,0 +1,232 @@ + + + + + + + + + Library Reference — pymodaq_plugins_daqmx 1.1.0 documentation + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +
+

Library Reference

+
+ +
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file From 14928a38702c5e7bac545c0338f1e4e02afad344 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Guerrero?= Date: Fri, 12 Dec 2025 11:12:32 +0100 Subject: [PATCH 124/140] [DOC] Raw import of docstring html files --- .github/workflows/sphinx.yml | 6 +- docs/source/_static/classdiag.html | 120 +++ docs/source/_static/daq_move_plugins.html | 122 +++ docs/source/_static/daq_viewer_plugins.html | 190 ++++ docs/source/_static/hardware.html | 914 ++++++++++++++++++ .../{docstring => _static}/library.html | 0 6 files changed, 1351 insertions(+), 1 deletion(-) create mode 100644 docs/source/_static/classdiag.html create mode 100644 docs/source/_static/daq_move_plugins.html create mode 100644 docs/source/_static/daq_viewer_plugins.html create mode 100644 docs/source/_static/hardware.html rename docs/source/{docstring => _static}/library.html (100%) diff --git a/.github/workflows/sphinx.yml b/.github/workflows/sphinx.yml index 3b699cf..9af1f0e 100644 --- a/.github/workflows/sphinx.yml +++ b/.github/workflows/sphinx.yml @@ -18,7 +18,11 @@ jobs: - name: Sphinx build run: | sphinx-build docs/source docs/build/html - cp docs/source/docstring/library.html docs/build/html/library.html + cp docs/source/_static/classdiag.html docs/build/html/classdiag.html + cp docs/source/_static/daq_move_plugins.html docs/build/html/daq_move_plugins.html + cp docs/source/_static/daq_viewer_plugins.html docs/build/html/daq_viewer_plugins.html + cp docs/source/_static/hardware.html docs/build/html/hardware.html + cp docs/source/_static/library.html docs/build/html/library.html - name: Upload artifacts uses: actions/upload-artifact@v4 with: diff --git a/docs/source/_static/classdiag.html b/docs/source/_static/classdiag.html new file mode 100644 index 0000000..e03f803 --- /dev/null +++ b/docs/source/_static/classdiag.html @@ -0,0 +1,120 @@ + + + + + + + + + Class Inheritance — pymodaq_plugins_daqmx 1.1.0 documentation + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +
+

Class Inheritance

+
Inheritance diagram of pymodaq_plugins_daqmx.daq_move_plugins.daq_move_NIDAQmx.DAQ_Move_NIDAQmx, pymodaq_plugins_daqmx.daq_viewer_plugins.plugins_0D.daq_0Dviewer_NIDAQmx.DAQ_0DViewer_NIDAQmx, pymodaq_plugins_daqmx.daq_viewer_plugins.plugins_1D.daq_1Dviewer_NIDAQmx.DAQ_1DViewer_NIDAQmx, pymodaq_plugins_daqmx.hardware.national_instruments.daqmxni.NIDAQmx, pymodaq_plugins_daqmx.hardware.national_instruments.NIDAQmx_base.DAQ_NIDAQmx_base, pymodaq_plugins_daqmx.hardware.national_instruments.NIDAQmx_base.ScalableGroupAI, pymodaq_plugins_daqmx.hardware.national_instruments.NIDAQmx_Move.DAQ_NIDAQmx_Actuator, pymodaq_plugins_daqmx.hardware.national_instruments.NIDAQmx_Viewer.DAQ_NIDAQmx_Viewer
+ + + + + + + + + +
+ + +
+
+
+ +
+ +
+

© Copyright 2025, Sébastien Weber, Aurore Finco, Sébastien Guerrero.

+
+ + Built with Sphinx using a + theme + provided by Read the Docs. + + +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/docs/source/_static/daq_move_plugins.html b/docs/source/_static/daq_move_plugins.html new file mode 100644 index 0000000..9003ebf --- /dev/null +++ b/docs/source/_static/daq_move_plugins.html @@ -0,0 +1,122 @@ + + + + + + + + + daq_move_plugins — pymodaq_plugins_daqmx 1.1.0 documentation + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +
+

daq_move_plugins

+
+
+class pymodaq_plugins_daqmx.daq_move_plugins.daq_move_NIDAQmx.DAQ_Move_NIDAQmx(*args, **kwargs)[source]
+

Bases: DAQ_NIDAQmx_Actuator

+

Plugin for basic current or voltage control with NI modules (plugged in NI cDAQ or NI-USB). (in process)

+
+
+__init__(*args, **kwargs)[source]
+
+ +
+ +
+ + +
+
+
+ +
+ +
+

© Copyright 2025, Sébastien Weber, Aurore Finco, Sébastien Guerrero.

+
+ + Built with Sphinx using a + theme + provided by Read the Docs. + + +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/docs/source/_static/daq_viewer_plugins.html b/docs/source/_static/daq_viewer_plugins.html new file mode 100644 index 0000000..7f7a062 --- /dev/null +++ b/docs/source/_static/daq_viewer_plugins.html @@ -0,0 +1,190 @@ + + + + + + + + + daq_viewer_plugins — pymodaq_plugins_daqmx 1.1.0 documentation + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +
+

daq_viewer_plugins

+
+

plugins_0D

+
+
+class pymodaq_plugins_daqmx.daq_viewer_plugins.plugins_0D.daq_0Dviewer_NIDAQmx.DAQ_0DViewer_NIDAQmx(parent=None, params_state=None)[source]
+

Bases: DAQ_NIDAQmx_Viewer

+

Plugin for 0D data visualization & acquisition with NI modules (plugged in NI cDAQ or NI-USB).

+
+
+__init__(parent=None, params_state=None)[source]
+
+ +
+
+grab_data(Naverage=1, **kwargs)[source]
+
+
grab the current values with NIDAQ profile procedure.
+

+
Send the data_grabed_signal once done.
+
+ + + + + + + + + + + +

Parameters

Type

Description

Naverage

int

Number of values to average

+
+ +
+
+stop()[source]
+

Stop the current grab hardware wise if necessary

+
+ +
+ +
+
+

plugins_1D

+
+
+class pymodaq_plugins_daqmx.daq_viewer_plugins.plugins_1D.daq_1Dviewer_NIDAQmx.DAQ_1DViewer_NIDAQmx(parent=None, params_state=None)[source]
+

Bases: DAQ_NIDAQmx_Viewer

+

Plugin for bufferized 1D data visualization & acquisition with NI modules (plugged in NI cDAQ or NI-USB).

+
+
+__init__(parent=None, params_state=None)[source]
+
+ +
+
+grab_data(Naverage=1, **kwargs)[source]
+
+
grab the current values with NIDAQ profile procedure.
+

+
Send the data_grabed_signal once done.
+
+ + + + + + + + + + + +

Parameters

Type

Description

Naverage

int

Number of values to average

+
+ +
+ +
+
+ + +
+
+
+ +
+ +
+

© Copyright 2025, Sébastien Weber, Aurore Finco, Sébastien Guerrero.

+
+ + Built with Sphinx using a + theme + provided by Read the Docs. + + +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/docs/source/_static/hardware.html b/docs/source/_static/hardware.html new file mode 100644 index 0000000..29b2e42 --- /dev/null +++ b/docs/source/_static/hardware.html @@ -0,0 +1,914 @@ + + + + + + + + + hardware - National Instruments — pymodaq_plugins_daqmx 1.1.0 documentation + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +
+

hardware - National Instruments

+
+

NIDAQmx object

+
+
+class pymodaq_plugins_daqmx.hardware.national_instruments.daqmxni.NIDAQmx[source]
+

Bases: object

+

Wrapper around the NIDAQmx package giving an easy-to-use object to instantiate channels and tasks

+
+
+__init__()[source]
+
+ +
+
+close()[source]
+

close the current task.

+
+ +
+
+configuration_sequence(viewer, current_device)[source]
+

Configure each / modules / channels as giver by the user in the configuration file

+

Read the .toml file to get the desired hardware configuration, +and send the nidaqmx a sequence which set up each channel.

+
+ +
+
+classmethod get_NIDAQ_channels(devices=None, source_type=None)[source]
+

Get the list of available channels for all NiDAq connected devices

+
+

Parameters

+
+
devices: list

list of strings, each one being a connected device

+
+
source_type: str

Channels possible types

+
+
+
+
+

Returns

+

List of str containing device and channel names

+
+
+ +
+
+classmethod get_NIDAQ_devices()[source]
+

Get list of NI connected devices

+
+

Returns

+
+
list

list of devices as strings to be used in subsequent commands

+
+
+
+
+ +
+
+refresh_hardware()[source]
+

Refresh the NIDAQ hardware from settings values.

+
+

See Also

+

update_NIDAQ_devices, update_NIDAQ_channels

+
+
+ +
+ +
+
+

NIDAQmx Base module

+
+
+class pymodaq_plugins_daqmx.hardware.national_instruments.NIDAQmx_base.DAQ_NIDAQmx_base[source]
+

Bases: object

+

Base NIDAQmx class for using DAQmx objects from daqmxni.py in the DAQ_NIDAQmx_Move & DAQ_NIDAQmx_Viewer

+
+
+__init__()[source]
+
+ +
+
+commit_settings(param: Parameter)[source]
+

Activate the parameters changes in the hardware.

+ + + + + + + + + + + +

Parameters

Type

Description

param

instance of pyqtgraph.parameter

the parameter to activate

+
+

See Also

+

update_NIDAQ_channels, update_task, DAQ_NIDAQ_source, refresh_hardware

+
+
+ +
+
+get_channels_from_settings()[source]
+

Browse the viewer settings to gather all the channels that will be used.

+
+ +
+
+stop()[source]
+

Stop the acquisition.

+
+ +
+
+update_task()[source]
+

Update task according to channels from settings.

+
+ +
+ +
+
+class pymodaq_plugins_daqmx.hardware.national_instruments.NIDAQmx_base.ScalableGroupAI(**opts)[source]
+

Bases: GroupParameter

+

Generic parameter items holding Analog Input parameters

+
+
+__init__(**opts)[source]
+

Initialize a Parameter object. Although it is rare to directly create a +Parameter instance, the options available to this method are also allowed +by most Parameter subclasses.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

Keyword Arguments:

name

The name to give this Parameter. This is the name that +will appear in the left-most column of a ParameterTree +for this Parameter.

value

The value to initially assign to this Parameter.

default

The default value for this Parameter (most Parameters +provide an option to ‘reset to default’).

children

A list of children for this Parameter. Children +may be given either as a Parameter instance or as a +dictionary to pass to Parameter.create(). In this way, +it is possible to specify complex hierarchies of +Parameters from a single nested data structure.

readonly

If True, the user will not be allowed to edit this +Parameter. (default=False)

enabled

If False, any widget(s) for this parameter will appear +disabled. (default=True)

visible

If False, the Parameter will not appear when displayed +in a ParameterTree. (default=True)

renamable

If True, the user may rename this Parameter. +(default=False)

removable

If True, the user may remove this Parameter. +(default=False)

expanded

If True, the Parameter will initially be expanded in +ParameterTrees: Its children will be visible. +(default=True)

syncExpanded

If True, the expanded state of this Parameter is +synchronized with all ParameterTrees it is displayed in. +(default=False)

title

(str or None) If specified, then the parameter will be +displayed to the user using this string as its name. +However, the parameter will still be referred to +internally using the name specified above. Note that +this option is not compatible with renamable=True. +(default=None; added in version 0.9.9)

+
+ +
+
+addNew(typ=None)[source]
+

Add a child.

+ + + + + + + + + +

Parameters

Type

typ

string

+
+ +
+ +
+
+class pymodaq_plugins_daqmx.hardware.national_instruments.NIDAQmx_base.ScalableGroupAO(**opts)[source]
+

Bases: GroupParameter

+

Generic parameter items holding Analog Output parameters

+
+
+__init__(**opts)[source]
+

Initialize a Parameter object. Although it is rare to directly create a +Parameter instance, the options available to this method are also allowed +by most Parameter subclasses.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

Keyword Arguments:

name

The name to give this Parameter. This is the name that +will appear in the left-most column of a ParameterTree +for this Parameter.

value

The value to initially assign to this Parameter.

default

The default value for this Parameter (most Parameters +provide an option to ‘reset to default’).

children

A list of children for this Parameter. Children +may be given either as a Parameter instance or as a +dictionary to pass to Parameter.create(). In this way, +it is possible to specify complex hierarchies of +Parameters from a single nested data structure.

readonly

If True, the user will not be allowed to edit this +Parameter. (default=False)

enabled

If False, any widget(s) for this parameter will appear +disabled. (default=True)

visible

If False, the Parameter will not appear when displayed +in a ParameterTree. (default=True)

renamable

If True, the user may rename this Parameter. +(default=False)

removable

If True, the user may remove this Parameter. +(default=False)

expanded

If True, the Parameter will initially be expanded in +ParameterTrees: Its children will be visible. +(default=True)

syncExpanded

If True, the expanded state of this Parameter is +synchronized with all ParameterTrees it is displayed in. +(default=False)

title

(str or None) If specified, then the parameter will be +displayed to the user using this string as its name. +However, the parameter will still be referred to +internally using the name specified above. Note that +this option is not compatible with renamable=True. +(default=None; added in version 0.9.9)

+
+ +
+
+addNew(typ=None)[source]
+

Add a child.

+ + + + + + + + + +

Parameters

Type

typ

string

+
+ +
+ +
+
+class pymodaq_plugins_daqmx.hardware.national_instruments.NIDAQmx_base.ScalableGroupCounter(**opts)[source]
+

Bases: GroupParameter

+

Generic parameter items holding Counter parameters

+
+
+__init__(**opts)[source]
+

Initialize a Parameter object. Although it is rare to directly create a +Parameter instance, the options available to this method are also allowed +by most Parameter subclasses.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

Keyword Arguments:

name

The name to give this Parameter. This is the name that +will appear in the left-most column of a ParameterTree +for this Parameter.

value

The value to initially assign to this Parameter.

default

The default value for this Parameter (most Parameters +provide an option to ‘reset to default’).

children

A list of children for this Parameter. Children +may be given either as a Parameter instance or as a +dictionary to pass to Parameter.create(). In this way, +it is possible to specify complex hierarchies of +Parameters from a single nested data structure.

readonly

If True, the user will not be allowed to edit this +Parameter. (default=False)

enabled

If False, any widget(s) for this parameter will appear +disabled. (default=True)

visible

If False, the Parameter will not appear when displayed +in a ParameterTree. (default=True)

renamable

If True, the user may rename this Parameter. +(default=False)

removable

If True, the user may remove this Parameter. +(default=False)

expanded

If True, the Parameter will initially be expanded in +ParameterTrees: Its children will be visible. +(default=True)

syncExpanded

If True, the expanded state of this Parameter is +synchronized with all ParameterTrees it is displayed in. +(default=False)

title

(str or None) If specified, then the parameter will be +displayed to the user using this string as its name. +However, the parameter will still be referred to +internally using the name specified above. Note that +this option is not compatible with renamable=True. +(default=None; added in version 0.9.9)

+
+ +
+
+addNew(typ=None)[source]
+

Add a child.

+ + + + + + + + + +

Parameters

Type

typ

string

+
+ +
+ +
+
+class pymodaq_plugins_daqmx.hardware.national_instruments.NIDAQmx_base.ScalableGroupDI(**opts)[source]
+

Bases: GroupParameter

+

Generic parameter items holding Digital Input parameters

+
+
+__init__(**opts)[source]
+

Initialize a Parameter object. Although it is rare to directly create a +Parameter instance, the options available to this method are also allowed +by most Parameter subclasses.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

Keyword Arguments:

name

The name to give this Parameter. This is the name that +will appear in the left-most column of a ParameterTree +for this Parameter.

value

The value to initially assign to this Parameter.

default

The default value for this Parameter (most Parameters +provide an option to ‘reset to default’).

children

A list of children for this Parameter. Children +may be given either as a Parameter instance or as a +dictionary to pass to Parameter.create(). In this way, +it is possible to specify complex hierarchies of +Parameters from a single nested data structure.

readonly

If True, the user will not be allowed to edit this +Parameter. (default=False)

enabled

If False, any widget(s) for this parameter will appear +disabled. (default=True)

visible

If False, the Parameter will not appear when displayed +in a ParameterTree. (default=True)

renamable

If True, the user may rename this Parameter. +(default=False)

removable

If True, the user may remove this Parameter. +(default=False)

expanded

If True, the Parameter will initially be expanded in +ParameterTrees: Its children will be visible. +(default=True)

syncExpanded

If True, the expanded state of this Parameter is +synchronized with all ParameterTrees it is displayed in. +(default=False)

title

(str or None) If specified, then the parameter will be +displayed to the user using this string as its name. +However, the parameter will still be referred to +internally using the name specified above. Note that +this option is not compatible with renamable=True. +(default=None; added in version 0.9.9)

+
+ +
+
+addNew(typ=None)[source]
+

Add a child.

+ + + + + + + + + +

Parameters

Type

typ

string

+
+ +
+ +
+
+class pymodaq_plugins_daqmx.hardware.national_instruments.NIDAQmx_base.ScalableGroupDO(**opts)[source]
+

Bases: GroupParameter

+

Generic parameter items holding Digital Output parameters

+
+
+__init__(**opts)[source]
+

Initialize a Parameter object. Although it is rare to directly create a +Parameter instance, the options available to this method are also allowed +by most Parameter subclasses.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

Keyword Arguments:

name

The name to give this Parameter. This is the name that +will appear in the left-most column of a ParameterTree +for this Parameter.

value

The value to initially assign to this Parameter.

default

The default value for this Parameter (most Parameters +provide an option to ‘reset to default’).

children

A list of children for this Parameter. Children +may be given either as a Parameter instance or as a +dictionary to pass to Parameter.create(). In this way, +it is possible to specify complex hierarchies of +Parameters from a single nested data structure.

readonly

If True, the user will not be allowed to edit this +Parameter. (default=False)

enabled

If False, any widget(s) for this parameter will appear +disabled. (default=True)

visible

If False, the Parameter will not appear when displayed +in a ParameterTree. (default=True)

renamable

If True, the user may rename this Parameter. +(default=False)

removable

If True, the user may remove this Parameter. +(default=False)

expanded

If True, the Parameter will initially be expanded in +ParameterTrees: Its children will be visible. +(default=True)

syncExpanded

If True, the expanded state of this Parameter is +synchronized with all ParameterTrees it is displayed in. +(default=False)

title

(str or None) If specified, then the parameter will be +displayed to the user using this string as its name. +However, the parameter will still be referred to +internally using the name specified above. Note that +this option is not compatible with renamable=True. +(default=None; added in version 0.9.9)

+
+ +
+
+addNew(typ=None)[source]
+

Add a child.

+ + + + + + + + + +

Parameters

Type

typ

string

+
+ +
+ +
+
+

NIDAQmx Actuator module

+
+
+class pymodaq_plugins_daqmx.hardware.national_instruments.NIDAQmx_Move.DAQ_NIDAQmx_Actuator(parent=None, params_state=None, control_type='Actuator')[source]
+

Bases: DAQ_Move_base, DAQ_NIDAQmx_base

+

Wrapper object to access the Mock fonctionnalities, similar wrapper for all controllers.

+ + + + + + + + + +

Attributes

Type

params

dictionnary

+
+
+__init__(parent=None, params_state=None, control_type='Actuator')[source]
+
+ +
+
+commit_settings(param)[source]
+
+
Activate any parameter changes on the PI_GCS2 hardware.
+

+
Called after a param_tree_changed signal from DAQ_Move_main.
+
+
+ +
+
+get_actuator_value() → DataActuator[source]
+

Get the current position from the hardware with scaling conversion.

+
+

Returns

+

float: The position obtained after scaling conversion.

+
+
+ +
+
+ini_stage(controller=None)[source]
+

Actuator communication initialization

+
+

Parameters

+

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

+
+
+

Returns

+
+
self.status (edict): with initialization status: three fields:
    +
  • info (str)

  • +
  • controller (object) initialized controller

  • +
  • initialized: (bool): False if initialization failed otherwise True

  • +
+
+
+
+
+ +
+
+move_Abs(position)[source]
+

Move the actuator to the absolute target defined by position

+
+

Parameters

+

position: (flaot) value of the absolute target positioning

+
+
+ +
+
+move_Home()[source]
+
+
Send the update status thread command.

daq_utils.ThreadCommand

+
+
+
+ +
+
+move_Rel(position)[source]
+

Move the actuator to the relative target actuator value defined by position

+
+

Parameters

+

position: (flaot) value of the relative target positioning

+
+
+ +
+
+stop_motion()[source]
+

Call the specific move_done function (depending on the hardware).

+
+

See Also

+

move_done

+
+
+ +
+ +
+
+

NIDAQmx Viewer module

+
+
+class pymodaq_plugins_daqmx.hardware.national_instruments.NIDAQmx_Viewer.DAQ_NIDAQmx_Viewer(parent=None, params_state=None, control_type='0D')[source]
+

Bases: DAQ_Viewer_base, DAQ_NIDAQmx_base

+ + + + + + + + + + + + + + + +

Attributes

Type

data_grabed_signal

instance of Signal

params

dictionary list

task

+
+

See Also

+

refresh_hardware

+
+
+__init__(parent=None, params_state=None, control_type='0D')[source]
+
+ +
+
+close()[source]
+

Mandatory +To be reimplemented in subclass

+
+ +
+
+commit_settings(param)[source]
+

Activate the parameters changes in the hardware.

+ + + + + + + + + + + +

Parameters

Type

Description

param

instance of pyqtgraph.parameter

the parameter to activate

+
+

See Also

+

update_NIDAQ_channels, update_task, refresh_hardware

+
+
+ +
+
+get_max_frequency()[source]
+

Get the maximum frequency according to the material’s limits and display the information. +This method is destined to be removed once set_max_frequency will work correctly.

+
+ +
+
+ini_attributes()[source]
+

To be reimplemented in subclass

+
+ +
+
+ini_detector(controller=None)[source]
+

Initialisation procedure of the detector.

+
+

See Also

+

daq_utils.ThreadCommand

+
+
+ +
+
+set_max_frequency()[source]
+

Set the maximum frequency applicable to the frequency parameter according to material’s limits.

+
+ +
+
+stop()[source]
+

Stop the current grab hardware wise if necessary

+
+ +
+
+ +
+
+ + +
+
+
+ +
+ +
+

© Copyright 2025, Sébastien Weber, Aurore Finco, Sébastien Guerrero.

+
+ + Built with Sphinx using a + theme + provided by Read the Docs. + + +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/docs/source/docstring/library.html b/docs/source/_static/library.html similarity index 100% rename from docs/source/docstring/library.html rename to docs/source/_static/library.html From fe61e096a80bf5abc171a30996baa44bfcbd4270 Mon Sep 17 00:00:00 2001 From: cauclair Date: Wed, 18 Feb 2026 18:03:57 +0100 Subject: [PATCH 125/140] Add "save config" interface button --- .../hardware/national_instruments/NIDAQmx_base.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_base.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_base.py index ee1a5d1..fa853c6 100644 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_base.py +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_base.py @@ -224,6 +224,8 @@ class DAQ_NIDAQmx_base: params = [{'title': 'Refresh hardware:', 'name': 'refresh_hardware', 'type': 'bool', 'value': False}, {'title': 'Signal type:', 'name': 'NIDAQ_type', 'type': 'list', 'limits': [Ds.name for Ds in ChannelType]}, + {'title': 'Save plugin config:', 'name': 'save_config', 'type': 'bool_push', 'value': False, + 'readonly': False, 'label': 'Save Config'}, {'title': 'Plugin config:', 'name': 'load_config', 'type': 'bool_push', 'value': False, 'readonly': False, 'label': 'Load Config'}, {'title': 'NSamples To Read:', 'name': 'nsamplestoread', 'type': 'int', 'value': 10, 'default': 10, From fb556434d1f19c27312b5ce802e93660122e1ab7 Mon Sep 17 00:00:00 2001 From: cauclair Date: Fri, 20 Feb 2026 12:32:13 +0100 Subject: [PATCH 126/140] =?UTF-8?q?Ecriture=20en=20fichier=20config=20user?= =?UTF-8?q?=20d'une=20partie=20des=20donn=C3=A9es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../national_instruments/NIDAQmx_Viewer.py | 53 +++++++++++ .../hardware/national_instruments/daqmxni.py | 87 +++++++++++++++++++ 2 files changed, 140 insertions(+) diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_Viewer.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_Viewer.py index 7997daf..8495236 100644 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_Viewer.py +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_Viewer.py @@ -8,6 +8,11 @@ from pymodaq.utils.daq_utils import ThreadCommand from pymodaq.utils.data import DataFromPlugins, DataToExport from pymodaq.utils.logger import set_logger, get_module_name + +from pymodaq_utils import config as utils_config +from pymodaq_plugins_daqmx import config as daqmx_config +from pathlib import WindowsPath + logger = set_logger(get_module_name(__file__)) @@ -172,6 +177,54 @@ def commit_settings(self, param): TerminalConfiguration.DEFAULT.name) self.channels = self.get_channels_from_settings() self.set_max_frequency() # Set the acquisition frequency to the device maximum frequency + elif param.name() == 'save_config' and param.value(): + self.controller.configuration_backing_up_sequence(self, self.controller.device) + config_dict = {} + cfs_list = self.get_channels_from_settings() + for cfs in cfs_list: # self.get_channels_from_settings(): + n = cfs_list.index(cfs) + toml_section_name = "MODULE" + str(n) + config_string_to_write = "" + cfs_config_dict = {} + cfs_name = cfs.name + cfs_source = cfs.source.name + if cfs.source in [ChannelType.ANALOG_INPUT, ChannelType.ANALOG_OUTPUT]: + cfs_analog_type = cfs.analog_type.name + cfs_value_min = cfs.value_min + cfs_value_max = cfs.value_max + config_string_to_write += 'name = "' + str(cfs_name) + '"\n' + "" \ + 'source = "' + str(cfs_source) + '"\n' + "" \ + 'analog_type = "' + str(cfs_analog_type) + '"\n' + "" \ + 'value_min = ' + str(cfs_value_min) + '\n' + "" \ + 'value_max = ' + str(cfs_value_max) + '\n' + cfs_config_dict['name'] = cfs_name + cfs_config_dict['source'] = cfs_source + cfs_config_dict['analog_type'] = cfs_analog_type + cfs_config_dict['value_min'] = cfs_value_min + cfs_config_dict['value_max'] = cfs_value_max + if cfs.source == ChannelType.ANALOG_INPUT: + cfs_termination = cfs.termination.name + match cfs.analog_type: + case UsageTypeAI.VOLTAGE: + cfs_info = "Example of AI voltage channel" + config_string_to_write = config_string_to_write + ' termination = "' + str(cfs_termination) + '"\n' + cfs_config_dict['termination'] = cfs_termination + case UsageTypeAI.TEMPERATURE_THERMOCOUPLE: + cfs_info = "Example of AI thermocouple channel" + cfs_thermo_type = cfs.thermo_type.name + cfs_config_dict['thermo_type'] = cfs_thermo_type + config_string_to_write += 'thermo_type = "' + str(cfs_thermo_type) + '"\n' + # The other AI type cases are still to implement + else: + pass # to complete + config_string_to_write = 'info = "' + cfs_info + '"\n' + config_string_to_write + cfs_config_dict['info'] = cfs_info + print(config_string_to_write + "\n\n") + config_dict[toml_section_name] = cfs_config_dict + param.setToDefault() + print("config_dict = ", config_dict) + config_file_path = daqmx_config.config_path + utils_config.create_toml_from_dict(config_dict, config_file_path) self.channels = self.get_channels_from_settings() self.get_max_frequency() # Set the frequency 'max' option to the device maximum frequency and display it diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py index b00980a..6c6d1ad 100644 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py @@ -318,6 +318,93 @@ def configuration_sequence(self, viewer, current_device): logger.info("Configuration sequence error, verify if your config matches the hardware: {}".format(err)) logger.info(" ********** CONFIGURATION SEQUENCE SUCCESSFULLY ENDED **********") + def configuration_backing_up_sequence(self, viewer, current_device): + """Save in user the configuration file the configuration of each / modules / channels as giver in the parameters menu + + Write in the .toml file to get the hardware configuration as configured in the nidaqmx. + """ + logger.info("********** CONFIGURATION BACKING UP SEQUENCE INITIALIZED **********") + print("viewer.config_modules : " + str(viewer.config_modules)) + # try: + # viewer.config_devices = [config["NIDAQ_Devices", dev].get('name') for dev in config["NIDAQ_Devices"] + # if "Mod" not in config["NIDAQ_Devices", dev].get('name')] + # for dev in config["NIDAQ_Devices"]: + # if not isinstance(config["NIDAQ_Devices", dev], dict): + # continue + # elif 'Example' in dev: + # continue + # try: + # device_name = config["NIDAQ_Devices", dev].get('name') + # if device_name not in current_device.name: + # continue + # device_product = config["NIDAQ_Devices", dev].get('product') + # device = niDevice(device_name) + # assert device in self.devices and device.product_type == device_product, device.name + # except AssertionError as err: + # logger.error("Device {} not detected: {}".format(device_name, err)) + # continue + # for mod in config["NIDAQ_Devices", dev]: + # if not isinstance(config["NIDAQ_Devices", dev, mod], dict): + # continue + # try: + # module_name = config["NIDAQ_Devices", dev, mod].get('name') + # if current_device.name not in module_name: + # continue + # module_product = config["NIDAQ_Devices", dev, mod].get('product') + # module = niDevice(module_name) + # assert module in self.devices and module.product_type == module_product, module.name + # viewer.config_modules.append(config["NIDAQ_Devices", dev, mod].get('name')) + # except AssertionError as err: + # logger.error("Module {} not detected: {}".format(module_name, err)) + # continue + # for src in config["NIDAQ_Devices", dev, mod]: + # if not isinstance(config["NIDAQ_Devices", dev, mod, src], dict): + # continue + # if src == "ai": + # ai = config["NIDAQ_Devices", dev, mod, src] + # for ch in ai.keys(): + # name = module_name + "/" + str(ch) + # source = ChannelType[ai[ch].get("source")] + # analog_type = UsageTypeAI[ai[ch].get("analog_type")] + # if analog_type == UsageTypeAI.VOLTAGE: + # term = TerminalConfiguration[ai[ch].get("termination")] + # viewer.config_channels.append(AIChannel + # (name=name, + # source=source, + # analog_type=analog_type, + # value_min=float(ai[ch].get("value_min")), + # value_max=float(ai[ch].get("value_max")), + # termination=term, + # )) + # elif analog_type == UsageTypeAI.CURRENT: + # term = TerminalConfiguration[ai[ch].get("termination")] + # viewer.config_channels.append(AIChannel + # (name=name, + # source=source, + # analog_type=analog_type, + # value_min=float(ai[ch].get("value_min")), + # value_max=float(ai[ch].get("value_max")), + # termination=term, + # )) + # elif analog_type == UsageTypeAI.TEMPERATURE_THERMOCOUPLE: + # th = ThermocoupleType[ai[ch].get("thermo_type")] + # viewer.config_channels.append(AIThermoChannel + # (name=name, + # source=source, + # analog_type=analog_type, + # value_min=float(ai[ch].get("value_min")), + # value_max=float(ai[ch].get("value_max")), + # thermo_type=th, + # )) + # logger.info("Devices from config: {}".format(viewer.config_devices)) + # logger.info("Modules from config: {}".format(viewer.config_modules)) + # logger.info("Channels from config: {}".format([ch.name for ch in viewer.config_channels])) + # except AssertionError as err: + # logger.error("Configuration entries <{}> does not match the hardware ".format(err)) + # except Exception as err: + # logger.info("Configuration sequence error, verify if your config matches the hardware: {}".format(err)) + logger.info(" ********** CONFIGURATION BACKING UP SEQUENCE SUCCESSFULLY ENDED **********") + @classmethod def getAOMaxRate(cls, device): return niDevice(device).ao_max_rate From 628370c4608531f516279afab5fd571e4b6b0c1f Mon Sep 17 00:00:00 2001 From: Auclair Cyril Date: Thu, 26 Feb 2026 12:17:15 +0100 Subject: [PATCH 127/140] Add_save_config : add configuration of every present (on local system) CompactDAQ chassis --- .../national_instruments/NIDAQmx_Viewer.py | 19 ++++++++++++++++--- .../national_instruments/NIDAQmx_base.py | 2 +- .../hardware/national_instruments/daqmxni.py | 2 +- 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_Viewer.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_Viewer.py index 8495236..967d39f 100644 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_Viewer.py +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_Viewer.py @@ -3,7 +3,7 @@ import traceback from .daqmxni import NIDAQmx, niDevice from pymodaq_plugins_daqmx.hardware.national_instruments.NIDAQmx_base import DAQ_NIDAQmx_base, TerminalConfiguration, \ - UsageTypeAI, ChannelType + UsageTypeAI, ChannelType, ProductCategory from pymodaq.control_modules.viewer_utility_classes import DAQ_Viewer_base, comon_parameters as viewer_params from pymodaq.utils.daq_utils import ThreadCommand from pymodaq.utils.data import DataFromPlugins, DataToExport @@ -179,11 +179,24 @@ def commit_settings(self, param): self.set_max_frequency() # Set the acquisition frequency to the device maximum frequency elif param.name() == 'save_config' and param.value(): self.controller.configuration_backing_up_sequence(self, self.controller.device) - config_dict = {} + config_dict = {'title' : 'Configuration file of the DAQmx plugin', 'NIDAQ_Devices' : {}} + devices_collection = nidaqmx.system.System.local().devices + for device in devices_collection: + if device.product_category == ProductCategory.COMPACT_DAQ_CHASSIS: + device_name = device.name + device_product = device.product_type + dev_dict_len = len(config_dict['NIDAQ_Devices']) + if dev_dict_len < 9: + toml_subsection_extension_name = "DEVICE0" + str(dev_dict_len + 1) + elif dev_dict_len < 99: + toml_subsection_extension_name = "DEVICE" + str(dev_dict_len + 1) + config_dict['NIDAQ_Devices'][toml_subsection_extension_name] = \ + {'title': "Configuration entry for a NIDAQmx device", 'name': device_name, + 'product': device_product} cfs_list = self.get_channels_from_settings() for cfs in cfs_list: # self.get_channels_from_settings(): n = cfs_list.index(cfs) - toml_section_name = "MODULE" + str(n) + toml_section_name = "CHAN" + str(n) config_string_to_write = "" cfs_config_dict = {} cfs_name = cfs.name diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_base.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_base.py index fa853c6..37155fc 100644 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_base.py +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_base.py @@ -5,7 +5,7 @@ from pymodaq_gui.parameter.pymodaq_ptypes import registerParameterType, GroupParameter from pymodaq_plugins_daqmx.hardware.national_instruments.daqmxni import NIDAQmx, Edge, ChannelType, ClockSettings, \ AIChannel, AIThermoChannel, AOChannel, CIChannel, COChannel, DOChannel, DIChannel, UsageTypeAI, UsageTypeAO, \ - ThermocoupleType, TerminalConfiguration, TriggerSettings + ThermocoupleType, TerminalConfiguration, TriggerSettings, ProductCategory logger = set_logger(get_module_name(__file__)) diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py index 6c6d1ad..4b2aee4 100644 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py @@ -5,7 +5,7 @@ from nidaqmx.constants import AcquisitionType, VoltageUnits, CurrentUnits, CurrentShuntResistorLocation, \ TemperatureUnits, CJCSource, CountDirection, Level, FrequencyUnits, TimeUnits, \ LineGrouping, UsageTypeAI, UsageTypeAO, UsageTypeCI, UsageTypeCO, Edge, \ - TerminalConfiguration, ThermocoupleType, ChannelType + TerminalConfiguration, ThermocoupleType, ChannelType, ProductCategory from nidaqmx.system import System as niSystem from nidaqmx.system.device import Device as niDevice From eadb801f1fd4d0c30485f4dad8db9469ef6dfa99 Mon Sep 17 00:00:00 2001 From: Auclair Cyril Date: Thu, 26 Feb 2026 15:34:50 +0100 Subject: [PATCH 128/140] Add_save_config : add configuration of every present (on local system) C-series modules (on CompactDAQ chassis) --- .../national_instruments/NIDAQmx_Viewer.py | 14 +++++++++++++- .../hardware/national_instruments/NIDAQmx_base.py | 6 +++--- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_Viewer.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_Viewer.py index 967d39f..5bb887d 100644 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_Viewer.py +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_Viewer.py @@ -193,7 +193,19 @@ def commit_settings(self, param): config_dict['NIDAQ_Devices'][toml_subsection_extension_name] = \ {'title': "Configuration entry for a NIDAQmx device", 'name': device_name, 'product': device_product} - cfs_list = self.get_channels_from_settings() + temporary_NIDAQ_devices_length = len(config_dict['NIDAQ_Devices']['DEVICE01']) # there is supposed at least one chassis to be plugged + for device in devices_collection: + if device.product_category == ProductCategory.C_SERIES_MODULE: + cDAQ_chassis_name = device.compact_daq_chassis_device.name + for ni_daq_device in config_dict['NIDAQ_Devices']: + if config_dict['NIDAQ_Devices'][ni_daq_device]['name'] == cDAQ_chassis_name: + module_number = len(config_dict['NIDAQ_Devices'][ni_daq_device]) - temporary_NIDAQ_devices_length + toml_subsection_extension_name = "MODULE0" + str(module_number + 1) # there is supposed to be no more than 9 modules on a device + config_dict['NIDAQ_Devices'][ni_daq_device][toml_subsection_extension_name] = \ + {'title': 'Example of module plugged in a NIDAQmx device', + 'name': device.name, + 'product': device.product_type} + cfs_list = self.get_channels_from_settings(from_all_devices=True) # "cfs" for "channels from settings" for cfs in cfs_list: # self.get_channels_from_settings(): n = cfs_list.index(cfs) toml_section_name = "CHAN" + str(n) diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_base.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_base.py index 37155fc..2e209f1 100644 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_base.py +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_base.py @@ -395,7 +395,7 @@ def update_task(self): else: logger.warning("No channels assigned, task not created") - def get_channels_from_settings(self): + def get_channels_from_settings(self, from_all_devices=False): channels = [] if self.settings['NIDAQ_type'] == ChannelType.ANALOG_INPUT.name: # analog input source = ChannelType.ANALOG_INPUT @@ -454,8 +454,8 @@ def get_channels_from_settings(self): for channel in self.settings.child('do_channels').children(): channels.append(DOChannel(name=channel.opts['title'], source=source)) - - channels = [ch for ch in channels if self.settings.child("devices").value() in ch.name] + if not from_all_devices: + channels = [ch for ch in channels if self.settings.child("devices").value() in ch.name] return channels def stop(self): From 03bf733b1bdf02fbea9e68f77bec0165399dc37b Mon Sep 17 00:00:00 2001 From: Auclair Cyril Date: Fri, 27 Feb 2026 14:55:41 +0100 Subject: [PATCH 129/140] Add_save_config : add configuration of channels --- .../national_instruments/NIDAQmx_Viewer.py | 28 +++++++++++++++++-- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_Viewer.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_Viewer.py index 5bb887d..3c49fc3 100644 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_Viewer.py +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_Viewer.py @@ -194,6 +194,7 @@ def commit_settings(self, param): {'title': "Configuration entry for a NIDAQmx device", 'name': device_name, 'product': device_product} temporary_NIDAQ_devices_length = len(config_dict['NIDAQ_Devices']['DEVICE01']) # there is supposed at least one chassis to be plugged + modules_dict = {} for device in devices_collection: if device.product_category == ProductCategory.C_SERIES_MODULE: cDAQ_chassis_name = device.compact_daq_chassis_device.name @@ -205,10 +206,16 @@ def commit_settings(self, param): {'title': 'Example of module plugged in a NIDAQmx device', 'name': device.name, 'product': device.product_type} + modules_dict[device.name] = {'chassis_id' : ni_daq_device, 'module_id': toml_subsection_extension_name} # <- à utiliser pour suites dev cfs_list = self.get_channels_from_settings(from_all_devices=True) # "cfs" for "channels from settings" - for cfs in cfs_list: # self.get_channels_from_settings(): + abbrev_chan_type_dict = {ChannelType.ANALOG_INPUT : 'ai', + ChannelType.ANALOG_OUTPUT : 'ao', + ChannelType.COUNTER_INPUT : 'ci', + ChannelType.COUNTER_OUTPUT : 'co', + ChannelType.DIGITAL_INPUT : 'di', + ChannelType.DIGITAL_OUTPUT : 'do'} + for cfs in cfs_list: n = cfs_list.index(cfs) - toml_section_name = "CHAN" + str(n) config_string_to_write = "" cfs_config_dict = {} cfs_name = cfs.name @@ -245,7 +252,22 @@ def commit_settings(self, param): config_string_to_write = 'info = "' + cfs_info + '"\n' + config_string_to_write cfs_config_dict['info'] = cfs_info print(config_string_to_write + "\n\n") - config_dict[toml_section_name] = cfs_config_dict + [module_name, physical_chan_name] = cfs_name.split('/') + chassis_id = modules_dict[module_name]['chassis_id'] + module_id = modules_dict[module_name]['module_id'] + abbrev_chan_type = abbrev_chan_type_dict[cfs.source] + if abbrev_chan_type not in config_dict['NIDAQ_Devices'][chassis_id][module_id]: + config_dict['NIDAQ_Devices'][chassis_id][module_id][abbrev_chan_type] = {} + chan_type_dict_len = len(config_dict['NIDAQ_Devices'][chassis_id][module_id][abbrev_chan_type]) + chan_num_str = physical_chan_name.strip('ai') + if int(chan_num_str) < 10: + chan_id_num = "0" + chan_num_str + # if chan_type_dict_len < 9: + # chan_id_num = "0" + str(chan_type_dict_len + 1) + else : + chan_id_num = chan_num_str # there is supposed to be no more than 99 channels on one module + chan_id = abbrev_chan_type+chan_id_num + config_dict['NIDAQ_Devices'][chassis_id][module_id][abbrev_chan_type][physical_chan_name] = cfs_config_dict param.setToDefault() print("config_dict = ", config_dict) config_file_path = daqmx_config.config_path From 3dedb1b4e87c8ec9a0827ee1a03c6ea698c4ccc4 Mon Sep 17 00:00:00 2001 From: Auclair Cyril Date: Fri, 27 Feb 2026 16:59:57 +0100 Subject: [PATCH 130/140] Logger display correction ("load config" sequences) --- .../hardware/national_instruments/daqmxni.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py index 4b2aee4..084bd2f 100644 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py @@ -312,11 +312,11 @@ def configuration_sequence(self, viewer, current_device): logger.info("Devices from config: {}".format(viewer.config_devices)) logger.info("Modules from config: {}".format(viewer.config_modules)) logger.info("Channels from config: {}".format([ch.name for ch in viewer.config_channels])) + logger.info(" ********** CONFIGURATION SEQUENCE SUCCESSFULLY ENDED **********") except AssertionError as err: logger.error("Configuration entries <{}> does not match the hardware ".format(err)) except Exception as err: logger.info("Configuration sequence error, verify if your config matches the hardware: {}".format(err)) - logger.info(" ********** CONFIGURATION SEQUENCE SUCCESSFULLY ENDED **********") def configuration_backing_up_sequence(self, viewer, current_device): """Save in user the configuration file the configuration of each / modules / channels as giver in the parameters menu From 3ef152bb3bc12f7ea1a226e569e0909ab34057be Mon Sep 17 00:00:00 2001 From: Auclair Cyril Date: Fri, 27 Feb 2026 17:12:37 +0100 Subject: [PATCH 131/140] Logger display code move and correction ("save config" sequences) --- .../national_instruments/NIDAQmx_Viewer.py | 193 +++++++++--------- .../hardware/national_instruments/daqmxni.py | 6 +- 2 files changed, 103 insertions(+), 96 deletions(-) diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_Viewer.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_Viewer.py index 3c49fc3..edc7a73 100644 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_Viewer.py +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_Viewer.py @@ -177,101 +177,108 @@ def commit_settings(self, param): TerminalConfiguration.DEFAULT.name) self.channels = self.get_channels_from_settings() self.set_max_frequency() # Set the acquisition frequency to the device maximum frequency + elif param.name() == 'save_config' and param.value(): - self.controller.configuration_backing_up_sequence(self, self.controller.device) - config_dict = {'title' : 'Configuration file of the DAQmx plugin', 'NIDAQ_Devices' : {}} - devices_collection = nidaqmx.system.System.local().devices - for device in devices_collection: - if device.product_category == ProductCategory.COMPACT_DAQ_CHASSIS: - device_name = device.name - device_product = device.product_type - dev_dict_len = len(config_dict['NIDAQ_Devices']) - if dev_dict_len < 9: - toml_subsection_extension_name = "DEVICE0" + str(dev_dict_len + 1) - elif dev_dict_len < 99: - toml_subsection_extension_name = "DEVICE" + str(dev_dict_len + 1) - config_dict['NIDAQ_Devices'][toml_subsection_extension_name] = \ - {'title': "Configuration entry for a NIDAQmx device", 'name': device_name, - 'product': device_product} - temporary_NIDAQ_devices_length = len(config_dict['NIDAQ_Devices']['DEVICE01']) # there is supposed at least one chassis to be plugged - modules_dict = {} - for device in devices_collection: - if device.product_category == ProductCategory.C_SERIES_MODULE: - cDAQ_chassis_name = device.compact_daq_chassis_device.name - for ni_daq_device in config_dict['NIDAQ_Devices']: - if config_dict['NIDAQ_Devices'][ni_daq_device]['name'] == cDAQ_chassis_name: - module_number = len(config_dict['NIDAQ_Devices'][ni_daq_device]) - temporary_NIDAQ_devices_length - toml_subsection_extension_name = "MODULE0" + str(module_number + 1) # there is supposed to be no more than 9 modules on a device - config_dict['NIDAQ_Devices'][ni_daq_device][toml_subsection_extension_name] = \ - {'title': 'Example of module plugged in a NIDAQmx device', - 'name': device.name, - 'product': device.product_type} - modules_dict[device.name] = {'chassis_id' : ni_daq_device, 'module_id': toml_subsection_extension_name} # <- à utiliser pour suites dev - cfs_list = self.get_channels_from_settings(from_all_devices=True) # "cfs" for "channels from settings" - abbrev_chan_type_dict = {ChannelType.ANALOG_INPUT : 'ai', - ChannelType.ANALOG_OUTPUT : 'ao', - ChannelType.COUNTER_INPUT : 'ci', - ChannelType.COUNTER_OUTPUT : 'co', - ChannelType.DIGITAL_INPUT : 'di', - ChannelType.DIGITAL_OUTPUT : 'do'} - for cfs in cfs_list: - n = cfs_list.index(cfs) - config_string_to_write = "" - cfs_config_dict = {} - cfs_name = cfs.name - cfs_source = cfs.source.name - if cfs.source in [ChannelType.ANALOG_INPUT, ChannelType.ANALOG_OUTPUT]: - cfs_analog_type = cfs.analog_type.name - cfs_value_min = cfs.value_min - cfs_value_max = cfs.value_max - config_string_to_write += 'name = "' + str(cfs_name) + '"\n' + "" \ - 'source = "' + str(cfs_source) + '"\n' + "" \ - 'analog_type = "' + str(cfs_analog_type) + '"\n' + "" \ - 'value_min = ' + str(cfs_value_min) + '\n' + "" \ - 'value_max = ' + str(cfs_value_max) + '\n' - cfs_config_dict['name'] = cfs_name - cfs_config_dict['source'] = cfs_source - cfs_config_dict['analog_type'] = cfs_analog_type - cfs_config_dict['value_min'] = cfs_value_min - cfs_config_dict['value_max'] = cfs_value_max - if cfs.source == ChannelType.ANALOG_INPUT: - cfs_termination = cfs.termination.name - match cfs.analog_type: - case UsageTypeAI.VOLTAGE: - cfs_info = "Example of AI voltage channel" - config_string_to_write = config_string_to_write + ' termination = "' + str(cfs_termination) + '"\n' - cfs_config_dict['termination'] = cfs_termination - case UsageTypeAI.TEMPERATURE_THERMOCOUPLE: - cfs_info = "Example of AI thermocouple channel" - cfs_thermo_type = cfs.thermo_type.name - cfs_config_dict['thermo_type'] = cfs_thermo_type - config_string_to_write += 'thermo_type = "' + str(cfs_thermo_type) + '"\n' - # The other AI type cases are still to implement - else: - pass # to complete - config_string_to_write = 'info = "' + cfs_info + '"\n' + config_string_to_write - cfs_config_dict['info'] = cfs_info - print(config_string_to_write + "\n\n") - [module_name, physical_chan_name] = cfs_name.split('/') - chassis_id = modules_dict[module_name]['chassis_id'] - module_id = modules_dict[module_name]['module_id'] - abbrev_chan_type = abbrev_chan_type_dict[cfs.source] - if abbrev_chan_type not in config_dict['NIDAQ_Devices'][chassis_id][module_id]: - config_dict['NIDAQ_Devices'][chassis_id][module_id][abbrev_chan_type] = {} - chan_type_dict_len = len(config_dict['NIDAQ_Devices'][chassis_id][module_id][abbrev_chan_type]) - chan_num_str = physical_chan_name.strip('ai') - if int(chan_num_str) < 10: - chan_id_num = "0" + chan_num_str - # if chan_type_dict_len < 9: - # chan_id_num = "0" + str(chan_type_dict_len + 1) - else : - chan_id_num = chan_num_str # there is supposed to be no more than 99 channels on one module - chan_id = abbrev_chan_type+chan_id_num - config_dict['NIDAQ_Devices'][chassis_id][module_id][abbrev_chan_type][physical_chan_name] = cfs_config_dict + logger.info("********** CONFIGURATION BACKING UP SEQUENCE INITIALIZED **********") + try: + config_dict = {'title' : 'Configuration file of the DAQmx plugin', 'NIDAQ_Devices': {}} + devices_collection = nidaqmx.system.System.local().devices + + # CONFIGURATION OF THE "DEVICES" + for device in devices_collection: + if device.product_category == ProductCategory.COMPACT_DAQ_CHASSIS: + device_name = device.name + device_product = device.product_type + dev_dict_len = len(config_dict['NIDAQ_Devices']) + if dev_dict_len < 9: + toml_subsection_extension_name = "DEVICE0" + str(dev_dict_len + 1) + elif dev_dict_len < 99: + toml_subsection_extension_name = "DEVICE" + str(dev_dict_len + 1) + config_dict['NIDAQ_Devices'][toml_subsection_extension_name] = \ + {'title': "Configuration entry for a NIDAQmx device", 'name': device_name, + 'product': device_product} + + # CONFIGURATION OF THE MODULES + temporary_NIDAQ_devices_length = len(config_dict['NIDAQ_Devices']['DEVICE01']) # there is supposed at least one chassis to be plugged + modules_dict = {} + for device in devices_collection: + if device.product_category == ProductCategory.C_SERIES_MODULE: + cDAQ_chassis_name = device.compact_daq_chassis_device.name + for ni_daq_device in config_dict['NIDAQ_Devices']: + if config_dict['NIDAQ_Devices'][ni_daq_device]['name'] == cDAQ_chassis_name: + module_number = len(config_dict['NIDAQ_Devices'][ni_daq_device]) - temporary_NIDAQ_devices_length + toml_subsection_extension_name = "MODULE0" + str(module_number + 1) # there is supposed to be no more than 9 modules on a device + config_dict['NIDAQ_Devices'][ni_daq_device][toml_subsection_extension_name] = \ + {'title': 'Example of module plugged in a NIDAQmx device', + 'name': device.name, + 'product': device.product_type} + modules_dict[device.name] = {'chassis_id' : ni_daq_device, 'module_id': toml_subsection_extension_name} # <- à utiliser pour suites dev + + # CONFIGURATION OF THE DAQmx CHANNELS + cfs_list = self.get_channels_from_settings(from_all_devices=True) # "cfs" for "channels from settings" + abbrev_chan_type_dict = {ChannelType.ANALOG_INPUT : 'ai', + ChannelType.ANALOG_OUTPUT : 'ao', + ChannelType.COUNTER_INPUT : 'ci', + ChannelType.COUNTER_OUTPUT : 'co', + ChannelType.DIGITAL_INPUT : 'di', + ChannelType.DIGITAL_OUTPUT : 'do'} + for cfs in cfs_list: + config_string_to_write = "" + cfs_config_dict = {} + cfs_name = cfs.name + cfs_source = cfs.source.name + if cfs.source in [ChannelType.ANALOG_INPUT, ChannelType.ANALOG_OUTPUT]: + cfs_analog_type = cfs.analog_type.name + cfs_value_min = cfs.value_min + cfs_value_max = cfs.value_max + config_string_to_write += 'name = "' + str(cfs_name) + '"\n' + "" \ + 'source = "' + str(cfs_source) + '"\n' + "" \ + 'analog_type = "' + str(cfs_analog_type) + '"\n' + "" \ + 'value_min = ' + str(cfs_value_min) + '\n' + "" \ + 'value_max = ' + str(cfs_value_max) + '\n' + cfs_config_dict['name'] = cfs_name + cfs_config_dict['source'] = cfs_source + cfs_config_dict['analog_type'] = cfs_analog_type + cfs_config_dict['value_min'] = cfs_value_min + cfs_config_dict['value_max'] = cfs_value_max + if cfs.source == ChannelType.ANALOG_INPUT: + cfs_termination = cfs.termination.name + match cfs.analog_type: + case UsageTypeAI.VOLTAGE: + cfs_info = "Example of AI voltage channel" + config_string_to_write = config_string_to_write + ' termination = "' + str(cfs_termination) + '"\n' + cfs_config_dict['termination'] = cfs_termination + case UsageTypeAI.TEMPERATURE_THERMOCOUPLE: + cfs_info = "Example of AI thermocouple channel" + cfs_thermo_type = cfs.thermo_type.name + cfs_config_dict['thermo_type'] = cfs_thermo_type + config_string_to_write += 'thermo_type = "' + str(cfs_thermo_type) + '"\n' + # The other AI type cases are still to implement + else: + pass # to complete + cfs_config_dict['info'] = cfs_info + [module_name, physical_chan_name] = cfs_name.split('/') + chassis_id = modules_dict[module_name]['chassis_id'] + module_id = modules_dict[module_name]['module_id'] + abbrev_chan_type = abbrev_chan_type_dict[cfs.source] + if abbrev_chan_type not in config_dict['NIDAQ_Devices'][chassis_id][module_id]: + config_dict['NIDAQ_Devices'][chassis_id][module_id][abbrev_chan_type] = {} + chan_num_str = physical_chan_name.strip('ai') + if int(chan_num_str) < 10: + chan_id_num = "0" + chan_num_str + # if chan_type_dict_len < 9: + # chan_id_num = "0" + str(chan_type_dict_len + 1) + else : + chan_id_num = chan_num_str # there is supposed to be no more than 99 channels on one module + chan_id = abbrev_chan_type+chan_id_num + config_dict['NIDAQ_Devices'][chassis_id][module_id][abbrev_chan_type][physical_chan_name] = cfs_config_dict + + config_file_path = daqmx_config.config_path + utils_config.create_toml_from_dict(config_dict, config_file_path) + logger.info("********** CONFIGURATION BACKING UP SEQUENCE SUCCESSFULLY ENDED **********") + except Exception as err: + logger.info("Configuration sequence error, verify if your config matches the hardware: {}".format(err)) param.setToDefault() - print("config_dict = ", config_dict) - config_file_path = daqmx_config.config_path - utils_config.create_toml_from_dict(config_dict, config_file_path) self.channels = self.get_channels_from_settings() self.get_max_frequency() # Set the frequency 'max' option to the device maximum frequency and display it diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py index 084bd2f..9521bbf 100644 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py @@ -323,8 +323,8 @@ def configuration_backing_up_sequence(self, viewer, current_device): Write in the .toml file to get the hardware configuration as configured in the nidaqmx. """ - logger.info("********** CONFIGURATION BACKING UP SEQUENCE INITIALIZED **********") - print("viewer.config_modules : " + str(viewer.config_modules)) + # logger.info("********** CONFIGURATION BACKING UP SEQUENCE INITIALIZED **********") + # print("viewer.config_modules : " + str(viewer.config_modules)) # try: # viewer.config_devices = [config["NIDAQ_Devices", dev].get('name') for dev in config["NIDAQ_Devices"] # if "Mod" not in config["NIDAQ_Devices", dev].get('name')] @@ -403,7 +403,7 @@ def configuration_backing_up_sequence(self, viewer, current_device): # logger.error("Configuration entries <{}> does not match the hardware ".format(err)) # except Exception as err: # logger.info("Configuration sequence error, verify if your config matches the hardware: {}".format(err)) - logger.info(" ********** CONFIGURATION BACKING UP SEQUENCE SUCCESSFULLY ENDED **********") + # logger.info(" ********** CONFIGURATION BACKING UP SEQUENCE SUCCESSFULLY ENDED **********") @classmethod def getAOMaxRate(cls, device): From 412ef2f9505986b5c23fd534057df872b83b111c Mon Sep 17 00:00:00 2001 From: Auclair Cyril Date: Fri, 27 Feb 2026 17:16:32 +0100 Subject: [PATCH 132/140] Unuseful code suppression --- .../national_instruments/NIDAQmx_Viewer.py | 1 - .../hardware/national_instruments/daqmxni.py | 87 ------------------- 2 files changed, 88 deletions(-) diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_Viewer.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_Viewer.py index edc7a73..5c44c54 100644 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_Viewer.py +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_Viewer.py @@ -246,7 +246,6 @@ def commit_settings(self, param): match cfs.analog_type: case UsageTypeAI.VOLTAGE: cfs_info = "Example of AI voltage channel" - config_string_to_write = config_string_to_write + ' termination = "' + str(cfs_termination) + '"\n' cfs_config_dict['termination'] = cfs_termination case UsageTypeAI.TEMPERATURE_THERMOCOUPLE: cfs_info = "Example of AI thermocouple channel" diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py index 9521bbf..72859a7 100644 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py @@ -318,93 +318,6 @@ def configuration_sequence(self, viewer, current_device): except Exception as err: logger.info("Configuration sequence error, verify if your config matches the hardware: {}".format(err)) - def configuration_backing_up_sequence(self, viewer, current_device): - """Save in user the configuration file the configuration of each / modules / channels as giver in the parameters menu - - Write in the .toml file to get the hardware configuration as configured in the nidaqmx. - """ - # logger.info("********** CONFIGURATION BACKING UP SEQUENCE INITIALIZED **********") - # print("viewer.config_modules : " + str(viewer.config_modules)) - # try: - # viewer.config_devices = [config["NIDAQ_Devices", dev].get('name') for dev in config["NIDAQ_Devices"] - # if "Mod" not in config["NIDAQ_Devices", dev].get('name')] - # for dev in config["NIDAQ_Devices"]: - # if not isinstance(config["NIDAQ_Devices", dev], dict): - # continue - # elif 'Example' in dev: - # continue - # try: - # device_name = config["NIDAQ_Devices", dev].get('name') - # if device_name not in current_device.name: - # continue - # device_product = config["NIDAQ_Devices", dev].get('product') - # device = niDevice(device_name) - # assert device in self.devices and device.product_type == device_product, device.name - # except AssertionError as err: - # logger.error("Device {} not detected: {}".format(device_name, err)) - # continue - # for mod in config["NIDAQ_Devices", dev]: - # if not isinstance(config["NIDAQ_Devices", dev, mod], dict): - # continue - # try: - # module_name = config["NIDAQ_Devices", dev, mod].get('name') - # if current_device.name not in module_name: - # continue - # module_product = config["NIDAQ_Devices", dev, mod].get('product') - # module = niDevice(module_name) - # assert module in self.devices and module.product_type == module_product, module.name - # viewer.config_modules.append(config["NIDAQ_Devices", dev, mod].get('name')) - # except AssertionError as err: - # logger.error("Module {} not detected: {}".format(module_name, err)) - # continue - # for src in config["NIDAQ_Devices", dev, mod]: - # if not isinstance(config["NIDAQ_Devices", dev, mod, src], dict): - # continue - # if src == "ai": - # ai = config["NIDAQ_Devices", dev, mod, src] - # for ch in ai.keys(): - # name = module_name + "/" + str(ch) - # source = ChannelType[ai[ch].get("source")] - # analog_type = UsageTypeAI[ai[ch].get("analog_type")] - # if analog_type == UsageTypeAI.VOLTAGE: - # term = TerminalConfiguration[ai[ch].get("termination")] - # viewer.config_channels.append(AIChannel - # (name=name, - # source=source, - # analog_type=analog_type, - # value_min=float(ai[ch].get("value_min")), - # value_max=float(ai[ch].get("value_max")), - # termination=term, - # )) - # elif analog_type == UsageTypeAI.CURRENT: - # term = TerminalConfiguration[ai[ch].get("termination")] - # viewer.config_channels.append(AIChannel - # (name=name, - # source=source, - # analog_type=analog_type, - # value_min=float(ai[ch].get("value_min")), - # value_max=float(ai[ch].get("value_max")), - # termination=term, - # )) - # elif analog_type == UsageTypeAI.TEMPERATURE_THERMOCOUPLE: - # th = ThermocoupleType[ai[ch].get("thermo_type")] - # viewer.config_channels.append(AIThermoChannel - # (name=name, - # source=source, - # analog_type=analog_type, - # value_min=float(ai[ch].get("value_min")), - # value_max=float(ai[ch].get("value_max")), - # thermo_type=th, - # )) - # logger.info("Devices from config: {}".format(viewer.config_devices)) - # logger.info("Modules from config: {}".format(viewer.config_modules)) - # logger.info("Channels from config: {}".format([ch.name for ch in viewer.config_channels])) - # except AssertionError as err: - # logger.error("Configuration entries <{}> does not match the hardware ".format(err)) - # except Exception as err: - # logger.info("Configuration sequence error, verify if your config matches the hardware: {}".format(err)) - # logger.info(" ********** CONFIGURATION BACKING UP SEQUENCE SUCCESSFULLY ENDED **********") - @classmethod def getAOMaxRate(cls, device): return niDevice(device).ao_max_rate From 057de40cc471f06ca538f42451f62da8a0990f51 Mon Sep 17 00:00:00 2001 From: Auclair Cyril Date: Fri, 27 Feb 2026 17:34:03 +0100 Subject: [PATCH 133/140] Add_save_config : button mechanical action correction --- .../hardware/national_instruments/NIDAQmx_Viewer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_Viewer.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_Viewer.py index 5c44c54..2e51723 100644 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_Viewer.py +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_Viewer.py @@ -254,7 +254,7 @@ def commit_settings(self, param): config_string_to_write += 'thermo_type = "' + str(cfs_thermo_type) + '"\n' # The other AI type cases are still to implement else: - pass # to complete + pass # to complete (for analog outputs, and counter or digital channels) cfs_config_dict['info'] = cfs_info [module_name, physical_chan_name] = cfs_name.split('/') chassis_id = modules_dict[module_name]['chassis_id'] @@ -277,7 +277,7 @@ def commit_settings(self, param): logger.info("********** CONFIGURATION BACKING UP SEQUENCE SUCCESSFULLY ENDED **********") except Exception as err: logger.info("Configuration sequence error, verify if your config matches the hardware: {}".format(err)) - param.setToDefault() + param.setValue(not param.value()) # button mechanical action : it turns back to its value before user click self.channels = self.get_channels_from_settings() self.get_max_frequency() # Set the frequency 'max' option to the device maximum frequency and display it From 322d5bd53b6c306b5c4d74b690c7e4d9e0235983 Mon Sep 17 00:00:00 2001 From: Auclair Cyril Date: Fri, 27 Feb 2026 18:15:44 +0100 Subject: [PATCH 134/140] Add_save_config : add current measurement DAQmw "usage type" --- .../hardware/national_instruments/NIDAQmx_Viewer.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_Viewer.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_Viewer.py index 2e51723..ddee908 100644 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_Viewer.py +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_Viewer.py @@ -244,6 +244,9 @@ def commit_settings(self, param): if cfs.source == ChannelType.ANALOG_INPUT: cfs_termination = cfs.termination.name match cfs.analog_type: + case UsageTypeAI.CURRENT: + cfs_info = "Example of AI current channel" + cfs_config_dict['termination'] = cfs_termination case UsageTypeAI.VOLTAGE: cfs_info = "Example of AI voltage channel" cfs_config_dict['termination'] = cfs_termination From 8b8d508085db7887c9e873573d8d60e5cb37d369 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Guerrero?= Date: Thu, 3 Sep 2026 15:27:45 +0200 Subject: [PATCH 135/140] [warning] fix warning on data type to wrap in a list --- .../daq_viewer_plugins/plugins_0D/daq_0Dviewer_NIDAQmx.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_NIDAQmx.py b/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_NIDAQmx.py index 6ced253..8e1e8f7 100644 --- a/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_NIDAQmx.py +++ b/src/pymodaq_plugins_daqmx/daq_viewer_plugins/plugins_0D/daq_0Dviewer_NIDAQmx.py @@ -42,7 +42,7 @@ def grab_data(self, Naverage=1, **kwargs): channels_names = [ch.name for ch in self.channels] data_from_task = self.controller.task.read(timeout=20.0) if isinstance(data_from_task, float): - data_dfp = np.array([data_from_task]) + data_dfp = [np.array([data_from_task])] else: data_dfp = list(map(lambda f: np.array([f]), data_from_task)) self.dte_signal.emit(DataToExport(name='NIDAQmx', From adf68ff08995ade27d6f65f2f079544c9e063f51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Guerrero?= Date: Thu, 3 Sep 2026 15:28:25 +0200 Subject: [PATCH 136/140] [bug] fix method failure when trying to reach not available device --- .../hardware/national_instruments/daqmxni.py | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py index 49dcd92..da47031 100644 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py @@ -396,15 +396,18 @@ def getTriggeringSources(cls, devices=None): devices = cls.get_NIDAQ_devices().device_names for device in devices: - if cls.isDigitalTriggeringSupported(device): - string = niDevice(device).terminals - channels = [chan for chan in string if 'PFI' in chan] - if channels != ['']: - sources.extend(channels) - if cls.isAnalogTriggeringSupported(device): - channels = niDevice(device).ai_physical_chans.channel_names - if channels != ['']: - sources.extend(channels) + try: + if cls.isDigitalTriggeringSupported(device): + string = niDevice(device).terminals + channels = [chan for chan in string if 'PFI' in chan] + if channels != ['']: + sources.extend(channels) + if cls.isAnalogTriggeringSupported(device): + channels = niDevice(device).ai_physical_chans.channel_names + if channels != ['']: + sources.extend(channels) + except DaqError as err: + logger.error("Daq error with device {}: {}".format(device, err)) return sources def update_task(self, channels=[], clock_settings=ClockSettings(), trigger_settings=TriggerSettings()): From 55f1cf2dfb1d031d68c5405aaa19321e94527856 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Guerrero?= Date: Mon, 5 Oct 2026 17:02:27 +0200 Subject: [PATCH 137/140] Fix imports after merge --- .../hardware/national_instruments/NIDAQmx_Viewer.py | 6 +++--- .../hardware/national_instruments/NIDAQmx_base.py | 4 ++-- .../hardware/national_instruments/daqmxni.py | 11 ++++------- 3 files changed, 9 insertions(+), 12 deletions(-) diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_Viewer.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_Viewer.py index 618ce7b..4e16cb9 100644 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_Viewer.py +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_Viewer.py @@ -1,8 +1,8 @@ import traceback -from pymodaq_plugins_daqmx.hardware.national_instruments.daqmxni import niDevice -from pymodaq_plugins_daqmx.hardware.national_instruments.NIDAQmx_base import NIDAQmx, DAQ_NIDAQmx_base, TerminalConfiguration, \ - +from pymodaq_plugins_daqmx.hardware.national_instruments.daqmxni import niDevice, niSystem, ProductCategory +from pymodaq_plugins_daqmx.hardware.national_instruments.NIDAQmx_base import NIDAQmx, DAQ_NIDAQmx_base, \ + TerminalConfiguration, UsageTypeAI, ChannelType, RTDType from pymodaq.control_modules.viewer_utility_classes import DAQ_Viewer_base, comon_parameters as viewer_params from pymodaq.utils.daq_utils import ThreadCommand from pymodaq.utils.logger import set_logger, get_module_name diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_base.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_base.py index db12178..da4a222 100644 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_base.py +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_base.py @@ -5,8 +5,8 @@ from pymodaq_gui.parameter.pymodaq_ptypes import registerParameterType, GroupParameter from pymodaq_plugins_daqmx.hardware.national_instruments.daqmxni import NIDAQmx, Edge, ChannelType, ClockSettings, \ AIChannel, AIThermoChannel, AOChannel, CIChannel, COChannel, DOChannel, DIChannel, UsageTypeAI, UsageTypeAO, \ - ThermocoupleType, TerminalConfiguration, TriggerSettings, ProductCategory, RTDType, TemperatureUnits, ResistanceConfiguration,\ - ExcitationSource , AI_RTD_Channel + ThermocoupleType, TerminalConfiguration, TriggerSettings, RTDType, TemperatureUnits, \ + ResistanceConfiguration, ExcitationSource , AI_RTD_Channel logger = set_logger(get_module_name(__file__)) diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py index 32b19dd..97b0f24 100644 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py @@ -1,13 +1,10 @@ import traceback import numpy as np from pymodaq.utils.logger import set_logger, get_module_name - -from nidaqmx.constants import (AcquisitionType, VoltageUnits, CurrentUnits, CurrentShuntResistorLocation, \ - TemperatureUnits, CJCSource, CountDirection, Level, FrequencyUnits, TimeUnits, \ - LineGrouping, UsageTypeAI, UsageTypeAO, UsageTypeCI, UsageTypeCO, Edge, \ - TerminalConfiguration, ThermocoupleType, ChannelType, ProductCategory, \ - RTDType, TemperatureUnits, ResistanceConfiguration, ExcitationSource) - +from nidaqmx.constants import AcquisitionType, VoltageUnits, CurrentUnits, CurrentShuntResistorLocation, \ + TemperatureUnits, CJCSource, CountDirection, Level, FrequencyUnits, TimeUnits, LineGrouping, UsageTypeAI, \ + UsageTypeAO, UsageTypeCI, UsageTypeCO, Edge, TerminalConfiguration, ThermocoupleType, ChannelType, \ + ProductCategory, RTDType, TemperatureUnits, ResistanceConfiguration, ExcitationSource from nidaqmx.system import System as niSystem from nidaqmx.system.device import Device as niDevice from nidaqmx import Task as niTask From 9dfe948d09dd4735c68b41683c6b0c820e29fffb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Guerrero?= Date: Mon, 5 Oct 2026 17:02:54 +0200 Subject: [PATCH 138/140] typo --- .../national_instruments/NIDAQmx_Viewer.py | 4 ++-- .../hardware/national_instruments/daqmxni.py | 18 +++++++++--------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_Viewer.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_Viewer.py index 4e16cb9..61b3372 100644 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_Viewer.py +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_Viewer.py @@ -229,8 +229,8 @@ def commit_settings(self, param): elif param.name() == 'save_config' and param.value(): logger.info("********** CONFIGURATION BACKING UP SEQUENCE INITIALIZED **********") try: - config_dict = {'title' : 'Configuration file of the DAQmx plugin', 'NIDAQ_Devices': {}} - devices_collection = nidaqmx.system.System.local().devices + config_dict = {'title': 'Configuration file of the DAQmx plugin', 'NIDAQ_Devices': {}} + devices_collection = niSystem.local().devices # CONFIGURATION OF THE "DEVICES" for device in devices_collection: diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py index 97b0f24..944f7e0 100644 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/daqmxni.py @@ -456,15 +456,15 @@ def update_task(self, channels=[], clock_settings=ClockSettings(), trigger_setti "") elif channel.analog_type == UsageTypeAI.TEMPERATURE_RTD: ai_rtd_chan = self._task.ai_channels.add_ai_rtd_chan(channel.name, - "", - channel.value_min, - channel.value_max, - units=channel.units, - rtd_type=channel.rtd_type, - resistance_config=channel.resistance_config, - current_excit_source=channel.current_excit_source, - current_excit_val=channel.current_excit_val, - r_0=channel.r_0) + "", + channel.value_min, + channel.value_max, + channel.units, + channel.rtd_type, + channel.resistance_config, + channel.current_excit_source, + channel.current_excit_val, + channel.r_0) if ai_rtd_chan.ai_rtd_type == RTDType["CUSTOM"]: # configuration of callendar-van dusen coefficients ai_rtd_chan.ai_rtd_a = channel.a_cvd_coeff From 112958cf51ad483ed4bce358e05521f17c82d6ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Guerrero?= Date: Tue, 6 Oct 2026 18:36:53 +0200 Subject: [PATCH 139/140] [BUG FIX] save config apply changes only to related config without erasing not related fields --- .../national_instruments/NIDAQmx_Viewer.py | 28 ++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_Viewer.py b/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_Viewer.py index 61b3372..6818f9e 100644 --- a/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_Viewer.py +++ b/src/pymodaq_plugins_daqmx/hardware/national_instruments/NIDAQmx_Viewer.py @@ -14,6 +14,29 @@ logger = set_logger(get_module_name(__file__)) +def merge_dicts_recursive(original: dict, update: dict) -> dict: + """ + Recursively merges two dictionaries. + - Keys/values from `update` add or modify those in `original`. + - Empty values (None, {}, [], etc.) in `update` are ignored. + - Nested dictionaries are merged recursively. + """ + merged = original.copy() # Start with a copy of the original to avoid mutation + for key, value in update.items(): + # Skip empty values (None, empty dict, empty list, etc.) + if value is None or (isinstance(value, (dict, list, set)) and not value): + continue + + # If the key exists in both dictionaries and both values are dictionaries, + # recursively merge them + if key in merged and isinstance(merged[key], dict) and isinstance(value, dict): + merged[key] = merge_dicts_recursive(merged[key], value) + else: + # Otherwise, overwrite or add the value + merged[key] = value + return merged + + class DAQ_NIDAQmx_Viewer(DAQ_Viewer_base, DAQ_NIDAQmx_base): """ ==================== ======================== @@ -323,8 +346,11 @@ def commit_settings(self, param): chan_id = abbrev_chan_type+chan_id_num config_dict['NIDAQ_Devices'][chassis_id][module_id][abbrev_chan_type][physical_chan_name] = cfs_config_dict + merged_config = merge_dicts_recursive(original=daqmx_config(), update=config_dict) + daqmx_config().update(merged_config) + daqmx_config.save() config_file_path = daqmx_config.config_path - utils_config.create_toml_from_dict(config_dict, config_file_path) + utils_config.create_toml_from_dict(daqmx_config(), config_file_path) logger.info("********** CONFIGURATION BACKING UP SEQUENCE SUCCESSFULLY ENDED **********") except Exception as err: logger.info("Configuration sequence error, verify if your config matches the hardware: {}".format(err)) From b8c0fab06d9a1ac5e74308aa749c57fc29e83395 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Guerrero?= Date: Tue, 6 Oct 2026 18:39:55 +0200 Subject: [PATCH 140/140] deprecated config examples, config entirely set through the user interface without need to open the .toml file anymore --- .../resources/config_template.toml | 60 ------------------- 1 file changed, 60 deletions(-) diff --git a/src/pymodaq_plugins_daqmx/resources/config_template.toml b/src/pymodaq_plugins_daqmx/resources/config_template.toml index bfcb402..e69de29 100644 --- a/src/pymodaq_plugins_daqmx/resources/config_template.toml +++ b/src/pymodaq_plugins_daqmx/resources/config_template.toml @@ -1,60 +0,0 @@ -title = 'Configuration file of the DAQmx plugin' - -[NIDAQ_Devices] - -[NIDAQ_Devices.DEVICE01] -title = "Configuration entry for a NIDAQmx device" -name = "" -product = "" - -[NIDAQ_Devices.DEVICE01.MODULE01] -title = "A module plugged in the NIDAQmx device" -name = "" -product = "" - -[NIDAQ_Devices.DEVICE02_Example] -title = "Example of configuration entry for a NIDAQmx device" -name = "cDAQ1" -product = "cDAQ-9174" - -[NIDAQ_Devices.DEVICE02_Example.MODULE01] -title = "Example of module plugged in a NIDAQmx device for current measurement" -name = "cDAQMod1" -product = "NI 9208" - -[NIDAQ_Devices.DEVICE02_Example.MODULE02] -title = "Example of module plugged in a NIDAQmx device for voltage measurement" -name = "cDAQMod1" -product = "NI 9211" - -[NIDAQ_Devices.DEVICE02_Example.MODULE03] -title = "Example of module plugged in a NIDAQmx device for thermocouple measurement" -name = "cDAQMod1" -product = "NI 9205" - -[NIDAQ_Devices.DEVICE02_Example.MODULE01.ai] -[NIDAQ_Devices.DEVICE02_Example.MODULE01.ai.ai00] -info = "Example of AI current channel" -source = 'ANALOG_INPUT' -analog_type = 'CURRENT' -value_min = 0 -value_max = 1 -termination = "RSE" - -[NIDAQ_Devices.DEVICE02_Example.MODULE02.ai] -[NIDAQ_Devices.DEVICE02_Example.MODULE02.ai.ai01] -info = "Example of AI voltage channel" -source = 'ANALOG_INPUT' -analog_type = 'VOLTAGE' -value_min = -10 -value_max = 10 -termination = "RSE" - -[NIDAQ_Devices.DEVICE02_Example.MODULE03.ai] -[NIDAQ_Devices.DEVICE02_Example.MODULE03.ai.ai02] -info = "Example of AI thermocouple channel" -source = 'ANALOG_INPUT' -analog_type = 'TEMPERATURE_THERMOCOUPLE' -thermo_type = "K" -value_min = 0 -value_max = 100 \ No newline at end of file
+ + +
+ +
+
+
+ +
+
+
+
+ +
+

Library Reference

+
+ +
+
+ + +
+
+ +
+
+
+