Skip to content

Commit 80aa980

Browse files
mikeprosserniMike Prosserbkeryan
authored
Add AnalogMultiChannelReader.read_waveforms() (#801)
* Add AnalogMultiChannelReader.read_waveforms() * cleanup * cleanup --------- Co-authored-by: Mike Prosser <Mike.Prosser@emerson.com> Co-authored-by: Brad Keryan <brad.keryan@ni.com>
1 parent 4451d98 commit 80aa980

File tree

9 files changed

+606
-12
lines changed

9 files changed

+606
-12
lines changed

generated/nidaqmx/_base_interpreter.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import abc
33
import numpy
44
from nitypes.waveform import AnalogWaveform
5+
from typing import Sequence
56

67

78
class BaseEventHandler(abc.ABC):
@@ -1855,5 +1856,15 @@ def read_analog_waveform(
18551856
number_of_samples_per_channel: int,
18561857
timeout: float,
18571858
waveform: AnalogWaveform[numpy.float64]
1859+
) -> None:
1860+
raise NotImplementedError
1861+
1862+
@abc.abstractmethod
1863+
def read_analog_waveforms(
1864+
self,
1865+
task_handle: object,
1866+
number_of_samples_per_channel: int,
1867+
timeout: float,
1868+
waveforms: Sequence[AnalogWaveform[numpy.float64]]
18581869
) -> None:
18591870
raise NotImplementedError

generated/nidaqmx/_grpc_interpreter.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import typing
77
import warnings
88
from nitypes.waveform import AnalogWaveform
9-
from typing import Callable, Generic, TypeVar
9+
from typing import Callable, Generic, Sequence, TypeVar
1010

1111
import google.protobuf.message
1212
from google.protobuf.timestamp_pb2 import Timestamp as GrpcTimestamp
@@ -3610,6 +3610,15 @@ def read_analog_waveform(
36103610
) -> None:
36113611
raise NotImplementedError
36123612

3613+
def read_analog_waveforms(
3614+
self,
3615+
task_handle: object,
3616+
number_of_samples_per_channel: int,
3617+
timeout: float,
3618+
waveforms: Sequence[AnalogWaveform[numpy.float64]]
3619+
) -> None:
3620+
raise NotImplementedError
3621+
36133622
def _assign_numpy_array(numpy_array, grpc_array):
36143623
"""
36153624
Assigns grpc array to numpy array maintaining the original shape.

generated/nidaqmx/_library_interpreter.py

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6398,6 +6398,32 @@ def read_analog_waveform(
63986398
# TODO: AB#3228924 - if the read was short, set waveform.sample_count before throwing the exception
63996399
self.check_for_error(error_code, samps_per_chan_read=samples_read)
64006400

6401+
def read_analog_waveforms(
6402+
self,
6403+
task_handle: object,
6404+
number_of_samples_per_channel: int,
6405+
timeout: float,
6406+
waveforms: Sequence[AnalogWaveform[numpy.float64]]
6407+
) -> None:
6408+
"""Read a set of analog waveforms with timing and attributes. All of the waveforms must be the same size."""
6409+
error_code, samples_read, timestamps, sample_intervals = self.internal_read_analog_waveform_per_chan(
6410+
task_handle,
6411+
number_of_samples_per_channel,
6412+
timeout,
6413+
[waveform.raw_data for waveform in waveforms],
6414+
[waveform.extended_properties for waveform in waveforms]
6415+
)
6416+
6417+
for i, waveform in enumerate(waveforms):
6418+
waveform.timing = Timing(
6419+
sample_interval_mode=SampleIntervalMode.REGULAR,
6420+
timestamp=timestamps[i],
6421+
sample_interval=sample_intervals[i],
6422+
)
6423+
6424+
# TODO: AB#3228924 - if the read was short, set waveform.sample_count before throwing the exception
6425+
self.check_for_error(error_code, samps_per_chan_read=samples_read)
6426+
64016427
def _internal_read_analog_waveform_ex(
64026428
self,
64036429
task_handle: object,
@@ -6470,6 +6496,87 @@ def set_wfm_attr_callback(
64706496

64716497
return error_code, samps_per_chan_read.value, timestamps, sample_intervals
64726498

6499+
def internal_read_analog_waveform_per_chan(
6500+
self,
6501+
task_handle: object,
6502+
num_samps_per_chan: int,
6503+
timeout: float,
6504+
read_arrays: Sequence[numpy.typing.NDArray[numpy.float64]],
6505+
properties: Sequence[ExtendedPropertyDictionary]
6506+
) -> Tuple[
6507+
int, # error code
6508+
int, # The number of samples per channel that were read
6509+
Sequence[ht_datetime], # The timestamps for each sample, indexed by channel
6510+
Sequence[ht_timedelta], # The sample intervals, indexed by channel
6511+
]:
6512+
assert isinstance(task_handle, TaskHandle)
6513+
samps_per_chan_read = ctypes.c_int()
6514+
6515+
channel_count = len(read_arrays)
6516+
assert channel_count > 0
6517+
array_size = read_arrays[0].size
6518+
assert all(read_array.size == array_size for read_array in read_arrays)
6519+
6520+
cfunc = lib_importer.windll.DAQmxInternalReadAnalogWaveformPerChan
6521+
if cfunc.argtypes is None:
6522+
with cfunc.arglock:
6523+
if cfunc.argtypes is None:
6524+
cfunc.argtypes = [
6525+
TaskHandle,
6526+
ctypes.c_int,
6527+
ctypes.c_double,
6528+
wrapped_ndpointer(dtype=numpy.int64, flags=("C", "W")),
6529+
wrapped_ndpointer(dtype=numpy.int64, flags=("C", "W")),
6530+
ctypes.c_uint,
6531+
CSetWfmAttrCallbackPtr,
6532+
ctypes.c_void_p,
6533+
ctypes.POINTER(ctypes.POINTER(ctypes.c_double)),
6534+
ctypes.c_uint,
6535+
ctypes.c_uint,
6536+
ctypes.POINTER(ctypes.c_int),
6537+
ctypes.POINTER(c_bool32),
6538+
]
6539+
6540+
t0_array = numpy.zeros(channel_count, dtype=numpy.int64)
6541+
dt_array = numpy.zeros(channel_count, dtype=numpy.int64)
6542+
6543+
read_array_ptrs = (ctypes.POINTER(ctypes.c_double) * channel_count)()
6544+
for i, read_array in enumerate(read_arrays):
6545+
read_array_ptrs[i] = read_array.ctypes.data_as(ctypes.POINTER(ctypes.c_double))
6546+
6547+
def set_wfm_attr_callback(
6548+
channel_index: int,
6549+
attribute_name: str,
6550+
attribute_type: WfmAttrType,
6551+
value: ExtendedPropertyValue,
6552+
callback_data: object,
6553+
) -> int:
6554+
properties[channel_index][attribute_name] = value
6555+
return 0
6556+
6557+
error_code = cfunc(
6558+
task_handle,
6559+
num_samps_per_chan,
6560+
timeout,
6561+
t0_array,
6562+
dt_array,
6563+
0 if t0_array is None else t0_array.size,
6564+
self._get_wfm_attr_callback_ptr(set_wfm_attr_callback),
6565+
None,
6566+
read_array_ptrs,
6567+
channel_count,
6568+
array_size,
6569+
ctypes.byref(samps_per_chan_read),
6570+
None,
6571+
)
6572+
self.check_for_error(error_code, samps_per_chan_read=samps_per_chan_read.value)
6573+
6574+
timestamps = [_T0_EPOCH + ht_timedelta(seconds=t0 * _INT64_WFM_SEC_PER_TICK) for t0 in t0_array]
6575+
sample_intervals = [ht_timedelta(seconds=dt * _INT64_WFM_SEC_PER_TICK) for dt in dt_array]
6576+
6577+
return error_code, samps_per_chan_read.value, timestamps, sample_intervals
6578+
6579+
64736580
def _get_wfm_attr_value(
64746581
self, attribute_type: int, value: ctypes.c_void_p, value_size_in_bytes: int
64756582
) -> ExtendedPropertyValue:

generated/nidaqmx/stream_readers.py

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -449,6 +449,104 @@ def read_one_sample(self, data, timeout=10):
449449

450450
self._interpreter.read_analog_f64(self._handle, 1, timeout, FillMode.GROUP_BY_CHANNEL.value, data)
451451

452+
@requires_feature(WAVEFORM_SUPPORT)
453+
def read_waveforms(
454+
self,
455+
number_of_samples_per_channel: int = READ_ALL_AVAILABLE,
456+
timeout: int = 10,
457+
waveforms: list[AnalogWaveform[numpy.float64]] | None = None
458+
) -> list[AnalogWaveform[numpy.float64]]:
459+
"""
460+
Reads one or more floating-point samples from one or more analog
461+
input channels into a list of waveforms.
462+
463+
This read method optionally accepts a preallocated list of waveforms to hold
464+
the samples requested, which can be advantageous for performance and
465+
interoperability with NumPy and SciPy.
466+
467+
Passing in a preallocated list of waveforms is valuable in continuous
468+
acquisition scenarios, where the same waveforms can be used
469+
repeatedly in each call to the method.
470+
471+
Args:
472+
number_of_samples_per_channel (Optional[int]): Specifies the
473+
number of samples to read.
474+
475+
If you set this input to nidaqmx.constants.
476+
READ_ALL_AVAILABLE, NI-DAQmx determines how many samples
477+
to read based on if the task acquires samples
478+
continuously or acquires a finite number of samples.
479+
480+
If the task acquires samples continuously and you set
481+
this input to nidaqmx.constants.READ_ALL_AVAILABLE, this
482+
method reads all the samples currently available in the
483+
buffer.
484+
485+
If the task acquires a finite number of samples and you
486+
set this input to nidaqmx.constants.READ_ALL_AVAILABLE,
487+
the method waits for the task to acquire all requested
488+
samples, then reads those samples. If you set the
489+
"read_all_avail_samp" property to True, the method reads
490+
the samples currently available in the buffer and does
491+
not wait for the task to acquire all requested samples.
492+
timeout (Optional[float]): Specifies the amount of time in
493+
seconds to wait for samples to become available. If the
494+
time elapses, the method returns an error and any
495+
samples read before the timeout elapsed. The default
496+
timeout is 10 seconds. If you set timeout to
497+
nidaqmx.constants.WAIT_INFINITELY, the method waits
498+
indefinitely. If you set timeout to 0, the method tries
499+
once to read the requested samples and returns an error
500+
if it is unable to.
501+
waveforms (Optional[list[AnalogWaveform]]): Specifies an existing
502+
list of AnalogWaveform objects to use for reading samples into.
503+
If provided, the raw_data array of each waveform will be
504+
used to store the samples. The list must contain one waveform
505+
for each channel in the task. If not provided, new
506+
AnalogWaveform objects will be created.
507+
Returns:
508+
list[AnalogWaveform]:
509+
510+
A list of waveform objects containing the acquired samples,
511+
one for each channel in the task.
512+
"""
513+
number_of_channels = self._in_stream.num_chans
514+
number_of_samples_per_channel = (
515+
self._task._calculate_num_samps_per_chan(
516+
number_of_samples_per_channel))
517+
518+
519+
if waveforms is None:
520+
waveforms = [
521+
AnalogWaveform(raw_data=numpy.zeros(number_of_samples_per_channel, dtype=numpy.float64))
522+
for _ in range(number_of_channels)
523+
]
524+
else:
525+
if len(waveforms) != number_of_channels:
526+
raise DaqError(
527+
f'The number of waveforms provided ({len(waveforms)}) does not match '
528+
f'the number of channels in the task ({number_of_channels}). Please provide '
529+
'one waveform for each channel.',
530+
DAQmxErrors.MISMATCHED_INPUT_ARRAY_SIZES, task_name=self._task.name)
531+
532+
for i, waveform in enumerate(waveforms):
533+
if number_of_samples_per_channel > waveform.sample_count:
534+
# TODO: AB#3228924 - if allowed by the caller, increase the sample count of the waveform
535+
raise DaqError(
536+
f'The waveform at index {i} does not have enough space ({waveform.sample_count}) to hold '
537+
f'the requested number of samples ({number_of_samples_per_channel}). Please provide larger '
538+
'waveforms or adjust the number of samples requested.',
539+
DAQmxErrors.READ_BUFFER_TOO_SMALL, task_name=self._task.name)
540+
541+
self._interpreter.read_analog_waveforms(
542+
self._handle,
543+
number_of_samples_per_channel,
544+
timeout,
545+
waveforms,
546+
)
547+
548+
return waveforms
549+
452550

453551
class AnalogUnscaledReader(ChannelReaderBase):
454552
"""

src/codegen/templates/_base_interpreter.py.mako

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import abc
1414
import numpy
1515
from nitypes.waveform import AnalogWaveform
16+
from typing import Sequence
1617

1718

1819
class BaseEventHandler(abc.ABC):
@@ -64,5 +65,15 @@ class BaseInterpreter(abc.ABC):
6465
number_of_samples_per_channel: int,
6566
timeout: float,
6667
waveform: AnalogWaveform[numpy.float64]
68+
) -> None:
69+
raise NotImplementedError
70+
71+
@abc.abstractmethod
72+
def read_analog_waveforms(
73+
self,
74+
task_handle: object,
75+
number_of_samples_per_channel: int,
76+
timeout: float,
77+
waveforms: Sequence[AnalogWaveform[numpy.float64]]
6778
) -> None:
6879
raise NotImplementedError

src/codegen/templates/_grpc_interpreter.py.mako

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import threading
2323
import typing
2424
import warnings
2525
from nitypes.waveform import AnalogWaveform
26-
from typing import Callable, Generic, TypeVar
26+
from typing import Callable, Generic, Sequence, TypeVar
2727

2828
import google.protobuf.message
2929
from google.protobuf.timestamp_pb2 import Timestamp as GrpcTimestamp
@@ -255,6 +255,15 @@ class GrpcStubInterpreter(BaseInterpreter):
255255
) -> None:
256256
raise NotImplementedError
257257

258+
def read_analog_waveforms(
259+
self,
260+
task_handle: object,
261+
number_of_samples_per_channel: int,
262+
timeout: float,
263+
waveforms: Sequence[AnalogWaveform[numpy.float64]]
264+
) -> None:
265+
raise NotImplementedError
266+
258267
def _assign_numpy_array(numpy_array, grpc_array):
259268
"""
260269
Assigns grpc array to numpy array maintaining the original shape.

0 commit comments

Comments
 (0)