Skip to content

Commit b07a8d1

Browse files
Add AnalogSingleChannelWriter.write_waveform() (#828)
* start with a stub and some tests * Implement write_analog_waveform method in interpreters and update AnalogSingleChannelWriter to use it * Add support for writing AnalogWaveform in Task class and implement related tests * cleanup * cleanup * Refactor counter output data validation to check for Iterable type * misc feedback * Refactor write_analog_waveform method to use write_analog_f64 and add tests for scaling and noncontiguous arrays * test improvements * use AnalogWaveform[Any] for all write() type annotations
1 parent 922dce2 commit b07a8d1

19 files changed

+593
-23
lines changed

generated/nidaqmx/_base_interpreter.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import abc
55
import numpy
66
from nitypes.waveform import AnalogWaveform, DigitalWaveform
7-
from typing import Sequence
7+
from typing import Any, Sequence
88
from nidaqmx.constants import WaveformAttributeMode
99

1010

@@ -1909,3 +1909,13 @@ def read_new_digital_waveforms(
19091909
waveform_attribute_mode: WaveformAttributeMode,
19101910
) -> Sequence[DigitalWaveform[numpy.uint8]]:
19111911
raise NotImplementedError
1912+
1913+
@abc.abstractmethod
1914+
def write_analog_waveform(
1915+
self,
1916+
task_handle: object,
1917+
waveform: AnalogWaveform[Any],
1918+
auto_start: bool,
1919+
timeout: float
1920+
) -> int:
1921+
raise NotImplementedError

generated/nidaqmx/_feature_toggles.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,8 @@ def is_enabled(self) -> bool:
121121
return self._is_enabled_override
122122
return self.readiness <= get_code_readiness_level()
123123

124-
def _raise_if_disabled(self) -> None:
124+
def raise_if_disabled(self) -> None:
125+
"""Raises an error if the feature is disabled."""
125126
if self.is_enabled:
126127
return
127128

@@ -143,7 +144,7 @@ def requires_feature(
143144
def decorator(func: Callable[_P, _T]) -> Callable[_P, _T]:
144145
@functools.wraps(func)
145146
def wrapper(*args: _P.args, **kwargs: _P.kwargs) -> _T:
146-
feature_toggle._raise_if_disabled()
147+
feature_toggle.raise_if_disabled()
147148
return func(*args, **kwargs)
148149

149150
return wrapper

generated/nidaqmx/_grpc_interpreter.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3655,6 +3655,15 @@ def read_new_digital_waveforms(
36553655
) -> Sequence[DigitalWaveform[numpy.uint8]]:
36563656
raise NotImplementedError
36573657

3658+
def write_analog_waveform(
3659+
self,
3660+
task_handle: object,
3661+
waveform: AnalogWaveform[typing.Any],
3662+
auto_start: bool,
3663+
timeout: float
3664+
) -> int:
3665+
raise NotImplementedError
3666+
36583667
def _assign_numpy_array(numpy_array, grpc_array):
36593668
"""
36603669
Assigns grpc array to numpy array maintaining the original shape.

generated/nidaqmx/_library_interpreter.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from datetime import timezone
1212
from hightime import datetime as ht_datetime
1313
from hightime import timedelta as ht_timedelta
14-
from typing import Callable, List, Sequence, Tuple, TYPE_CHECKING, Union
14+
from typing import Any, Callable, List, Sequence, Tuple, TYPE_CHECKING, Union
1515

1616
from nidaqmx._base_interpreter import BaseEventHandler, BaseInterpreter
1717
from nidaqmx._lib import lib_importer, ctypes_byte_str, c_bool32, wrapped_ndpointer, TaskHandle
@@ -6967,6 +6967,28 @@ def read_raw(self, task, num_samps_per_chan, timeout, read_array):
69676967

69686968
return read_array, samples_read.value, number_of_bytes_per_sample.value
69696969

6970+
def write_analog_waveform(
6971+
self,
6972+
task_handle: object,
6973+
waveform: AnalogWaveform[Any],
6974+
auto_start: bool,
6975+
timeout: float
6976+
) -> int:
6977+
"""Write an analog waveform."""
6978+
array = waveform.scaled_data
6979+
if not array.flags.c_contiguous:
6980+
array = array.copy(order="C")
6981+
6982+
return self.write_analog_f64(
6983+
task_handle,
6984+
waveform.sample_count,
6985+
auto_start,
6986+
timeout,
6987+
FillMode.GROUP_BY_CHANNEL.value,
6988+
array,
6989+
)
6990+
6991+
69706992
def write_raw(
69716993
self, task_handle, num_samps_per_chan, auto_start, timeout, numpy_array):
69726994
samps_per_chan_written = ctypes.c_int()

generated/nidaqmx/stream_writers/_analog_single_channel_writer.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
1+
from __future__ import annotations
2+
3+
import numpy
4+
from typing import Any
5+
6+
from nidaqmx._feature_toggles import WAVEFORM_SUPPORT, requires_feature
17
from nidaqmx.constants import FillMode
8+
from nitypes.waveform import AnalogWaveform
29

310
from nidaqmx.stream_writers._channel_writer_base import ChannelWriterBase, AUTO_START_UNSET
411

@@ -80,3 +87,45 @@ def write_one_sample(self, data, timeout=10):
8087
return self._interpreter.write_analog_scalar_f64(
8188
self._handle, auto_start, timeout, data)
8289

90+
@requires_feature(WAVEFORM_SUPPORT)
91+
def write_waveform(
92+
self,
93+
waveform: AnalogWaveform[Any],
94+
timeout: float = 10.0
95+
) -> int:
96+
"""
97+
Writes a waveform to a single analog output channel in a task.
98+
99+
If the task uses on-demand timing, this method returns only
100+
after the device generates all samples. On-demand is the default
101+
timing type if you do not use the timing property on the task to
102+
configure a sample timing type. If the task uses any timing type
103+
other than on-demand, this method returns immediately and does
104+
not wait for the device to generate all samples. Your
105+
application must determine if the task is done to ensure that
106+
the device generated all samples.
107+
108+
Args:
109+
waveform (AnalogWaveform[Any]): Specifies the
110+
waveform to write to the task.
111+
timeout (Optional[float]): Specifies the amount of time in
112+
seconds to wait for the method to write all samples.
113+
NI-DAQmx performs a timeout check only if the method
114+
must wait before it writes data. This method returns an
115+
error if the time elapses. The default timeout is 10
116+
seconds. If you set timeout to
117+
nidaqmx.constants.WAIT_INFINITELY, the method waits
118+
indefinitely. If you set timeout to 0, the method tries
119+
once to write the submitted samples. If the method could
120+
not write all the submitted samples, it returns an error
121+
and the number of samples successfully written.
122+
Returns:
123+
int: Specifies the actual number of samples this method
124+
successfully wrote.
125+
"""
126+
auto_start = (self._auto_start if self._auto_start is not
127+
AUTO_START_UNSET else False)
128+
129+
return self._interpreter.write_analog_waveform(
130+
self._handle, waveform, auto_start, timeout)
131+

generated/nidaqmx/task/_task.py

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import threading
44
import warnings
5+
from collections.abc import Iterable
56
from enum import Enum
67

78
import numpy
@@ -1240,6 +1241,7 @@ def write(self, data, auto_start=AUTO_START_UNSET, timeout=10.0):
12401241
sample for multiple channels.
12411242
- List of lists/2D numpy.ndarray: Multiple samples for multiple
12421243
channels.
1244+
- AnalogWaveform: Waveform data for a single analog output channel.
12431245
12441246
The data type of the samples passed in must be appropriate for
12451247
the channel type of the task.
@@ -1316,6 +1318,14 @@ def write(self, data, auto_start=AUTO_START_UNSET, timeout=10.0):
13161318
number_of_samples_per_channel = len(data)
13171319
element = data[0]
13181320

1321+
elif isinstance(data, AnalogWaveform):
1322+
WAVEFORM_SUPPORT.raise_if_disabled()
1323+
if number_of_channels != 1:
1324+
self._raise_invalid_write_num_chans_error(
1325+
number_of_channels, 1)
1326+
number_of_samples_per_channel = data.sample_count
1327+
element = data.raw_data[0]
1328+
13191329
else:
13201330
number_of_samples_per_channel = 1
13211331
element = data
@@ -1356,10 +1366,14 @@ def write(self, data, auto_start=AUTO_START_UNSET, timeout=10.0):
13561366
auto_start = True
13571367

13581368
if write_chan_type == ChannelType.ANALOG_OUTPUT:
1359-
data = numpy.asarray(data, dtype=numpy.float64)
1360-
return self._interpreter.write_analog_f64(
1361-
self._handle, number_of_samples_per_channel, auto_start,
1362-
timeout, FillMode.GROUP_BY_CHANNEL.value, data)
1369+
if isinstance(data, AnalogWaveform):
1370+
return self._interpreter.write_analog_waveform(
1371+
self._handle, data, auto_start, timeout)
1372+
else:
1373+
data = numpy.asarray(data, dtype=numpy.float64)
1374+
return self._interpreter.write_analog_f64(
1375+
self._handle, number_of_samples_per_channel, auto_start,
1376+
timeout, FillMode.GROUP_BY_CHANNEL.value, data)
13631377

13641378
elif write_chan_type == ChannelType.DIGITAL_OUTPUT:
13651379
if self.out_stream.do_num_booleans_per_chan == 1:
@@ -1396,6 +1410,11 @@ def write(self, data, auto_start=AUTO_START_UNSET, timeout=10.0):
13961410

13971411
if number_of_samples_per_channel == 1:
13981412
data = [data]
1413+
elif not isinstance(data, Iterable):
1414+
raise DaqError(
1415+
'Write failed, because the provided data type is not supported '
1416+
'for counter output channels.',
1417+
DAQmxErrors.UNKNOWN, task_name=self.name)
13991418

14001419
if output_type == UsageTypeCO.PULSE_FREQUENCY:
14011420
if not all(isinstance(sample, CtrFreq) for sample in data):

src/codegen/templates/_base_interpreter.py.mako

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ from __future__ import annotations
1515
import abc
1616
import numpy
1717
from nitypes.waveform import AnalogWaveform, DigitalWaveform
18-
from typing import Sequence
18+
from typing import Any, Sequence
1919
from nidaqmx.constants import WaveformAttributeMode
2020

2121

@@ -118,3 +118,13 @@ class BaseInterpreter(abc.ABC):
118118
waveform_attribute_mode: WaveformAttributeMode,
119119
) -> Sequence[DigitalWaveform[numpy.uint8]]:
120120
raise NotImplementedError
121+
122+
@abc.abstractmethod
123+
def write_analog_waveform(
124+
self,
125+
task_handle: object,
126+
waveform: AnalogWaveform[Any],
127+
auto_start: bool,
128+
timeout: float
129+
) -> int:
130+
raise NotImplementedError

src/codegen/templates/_grpc_interpreter.py.mako

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,15 @@ class GrpcStubInterpreter(BaseInterpreter):
300300
) -> Sequence[DigitalWaveform[numpy.uint8]]:
301301
raise NotImplementedError
302302

303+
def write_analog_waveform(
304+
self,
305+
task_handle: object,
306+
waveform: AnalogWaveform[typing.Any],
307+
auto_start: bool,
308+
timeout: float
309+
) -> int:
310+
raise NotImplementedError
311+
303312
def _assign_numpy_array(numpy_array, grpc_array):
304313
"""
305314
Assigns grpc array to numpy array maintaining the original shape.

src/codegen/templates/_library_interpreter.py.mako

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ from enum import Enum
2828
from datetime import timezone
2929
from hightime import datetime as ht_datetime
3030
from hightime import timedelta as ht_timedelta
31-
from typing import Callable, List, Sequence, Tuple, TYPE_CHECKING, Union
31+
from typing import Any, Callable, List, Sequence, Tuple, TYPE_CHECKING, Union
3232

3333
from nidaqmx._base_interpreter import BaseEventHandler, BaseInterpreter
3434
from nidaqmx._lib import lib_importer, ctypes_byte_str, c_bool32, wrapped_ndpointer, TaskHandle
@@ -793,6 +793,29 @@ class LibraryInterpreter(BaseInterpreter):
793793

794794
return read_array, samples_read.value, number_of_bytes_per_sample.value
795795

796+
## write_analog_waveform has special handling
797+
def write_analog_waveform(
798+
self,
799+
task_handle: object,
800+
waveform: AnalogWaveform[Any],
801+
auto_start: bool,
802+
timeout: float
803+
) -> int:
804+
"""Write an analog waveform."""
805+
array = waveform.scaled_data
806+
if not array.flags.c_contiguous:
807+
array = array.copy(order="C")
808+
809+
return self.write_analog_f64(
810+
task_handle,
811+
waveform.sample_count,
812+
auto_start,
813+
timeout,
814+
FillMode.GROUP_BY_CHANNEL.value,
815+
array,
816+
)
817+
818+
796819
## The datatype of 'write_array' is incorrect in daqmxAPISharp.json file.
797820
def write_raw(
798821
self, task_handle, num_samps_per_chan, auto_start, timeout, numpy_array):

src/handwritten/_feature_toggles.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,8 @@ def is_enabled(self) -> bool:
121121
return self._is_enabled_override
122122
return self.readiness <= get_code_readiness_level()
123123

124-
def _raise_if_disabled(self) -> None:
124+
def raise_if_disabled(self) -> None:
125+
"""Raises an error if the feature is disabled."""
125126
if self.is_enabled:
126127
return
127128

@@ -143,7 +144,7 @@ def requires_feature(
143144
def decorator(func: Callable[_P, _T]) -> Callable[_P, _T]:
144145
@functools.wraps(func)
145146
def wrapper(*args: _P.args, **kwargs: _P.kwargs) -> _T:
146-
feature_toggle._raise_if_disabled()
147+
feature_toggle.raise_if_disabled()
147148
return func(*args, **kwargs)
148149

149150
return wrapper

0 commit comments

Comments
 (0)