Skip to content

Commit d81eccf

Browse files
Add digital multiple waveform writer (#838)
* first draft * multi-channel implementation and a few tests * test___digital_multi_channel_writer___write_waveforms_mixed_lines___outputs_match_final_values * test___digital_multi_channel_writer___write_waveforms_ports___outputs_match_final_values * test___digital_multi_channel_writer___write_waveforms_port_and_lines___outputs_match_final_values * test___digital_multi_channel_writer___write_waveforms_with_non_contiguous_data___outputs_match_final_values * tests for channel count, sample count, and line count mismatches * task tests * cleanup comment * brad's feedback * better type annotations
1 parent ac8a24e commit d81eccf

15 files changed

Lines changed: 922 additions & 75 deletions

generated/nidaqmx/_base_interpreter.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1938,4 +1938,13 @@ def write_digital_waveform(
19381938
auto_start: bool,
19391939
timeout: float,
19401940
) -> int:
1941-
raise NotImplementedError
1941+
raise NotImplementedError
1942+
1943+
def write_digital_waveforms(
1944+
self,
1945+
task_handle: object,
1946+
waveform: Sequence[DigitalWaveform[Any]],
1947+
auto_start: bool,
1948+
timeout: float,
1949+
) -> int:
1950+
raise NotImplementedError

generated/nidaqmx/_grpc_interpreter.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3682,6 +3682,15 @@ def write_digital_waveform(
36823682
) -> int:
36833683
raise NotImplementedError
36843684

3685+
def write_digital_waveforms(
3686+
self,
3687+
task_handle: object,
3688+
waveform: Sequence[DigitalWaveform[Any]],
3689+
auto_start: bool,
3690+
timeout: float,
3691+
) -> int:
3692+
raise NotImplementedError
3693+
36853694
def _assign_numpy_array(numpy_array, grpc_array):
36863695
"""
36873696
Assigns grpc array to numpy array maintaining the original shape.

generated/nidaqmx/_library_interpreter.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7108,6 +7108,50 @@ def _get_digital_write_array(self, waveform: DigitalWaveform[Any]) -> numpy.typi
71087108
return data
71097109
return data.copy(order="C")
71107110

7111+
def write_digital_waveforms(
7112+
self,
7113+
task_handle: object,
7114+
waveforms: Sequence[DigitalWaveform[Any]],
7115+
auto_start: bool,
7116+
timeout: float,
7117+
) -> int:
7118+
"""Write digital waveforms."""
7119+
channel_count = len(waveforms)
7120+
assert channel_count > 0
7121+
sample_count = waveforms[0].sample_count
7122+
7123+
for waveform in waveforms:
7124+
if waveform.sample_count != sample_count:
7125+
raise DaqError(
7126+
"The waveforms must all have the same sample count.",
7127+
DAQmxErrors.UNKNOWN
7128+
)
7129+
7130+
bytes_per_chan_array = numpy.array([wf.signal_count for wf in waveforms], dtype=numpy.uint32)
7131+
7132+
# build a temporary contiguous array to write the data from multiple channels into.
7133+
# write_array must be in the format (numChans x numSampsPerChan x maxDataWidth)
7134+
write_array = numpy.zeros(
7135+
(channel_count, sample_count, max(bytes_per_chan_array)),
7136+
dtype=numpy.uint8,
7137+
)
7138+
for i, waveform in enumerate(waveforms):
7139+
signal_count = waveform.signal_count
7140+
write_array[i, :, :signal_count] = waveform.data
7141+
7142+
error_code, samples_written = self._internal_write_digital_waveform(
7143+
task_handle,
7144+
sample_count,
7145+
auto_start,
7146+
timeout,
7147+
FillMode.GROUP_BY_CHANNEL.value,
7148+
write_array,
7149+
bytes_per_chan_array,
7150+
)
7151+
7152+
self.check_for_error(error_code, samps_per_chan_written=samples_written)
7153+
return samples_written
7154+
71117155
def _internal_write_digital_waveform(
71127156
self,
71137157
task_handle: object,

generated/nidaqmx/stream_writers/_digital_multi_channel_writer.py

Lines changed: 59 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
from __future__ import annotations
2+
3+
from typing import Any, Sequence
4+
5+
from nitypes.waveform import DigitalWaveform
6+
7+
from nidaqmx._feature_toggles import WAVEFORM_SUPPORT, requires_feature
18
from nidaqmx.constants import FillMode
29
from nidaqmx.stream_writers._channel_writer_base import (
310
AUTO_START_UNSET,
@@ -9,7 +16,7 @@ class DigitalMultiChannelWriter(ChannelWriterBase):
916
"""Writes samples to one or more digital output channels in an NI-DAQmx task."""
1017

1118
def write_many_sample_port_byte(self, data, timeout=10.0):
12-
"""Writes one or more 8-bit unsigned integer samples to one or more digital output channels in a task.
19+
"""Writes 8-bit unsigned integer samples to one or more digital output channels in a task.
1320
1421
Use this method for devices with up to 8 lines per port.
1522
@@ -47,7 +54,7 @@ def write_many_sample_port_byte(self, data, timeout=10.0):
4754
4855
Specifies the actual number of samples this method
4956
successfully wrote to each channel in the task.
50-
""" # noqa: W505 - doc line too long (110 > 100 characters) (auto-generated noqa)
57+
"""
5158
self._verify_array(data, True, True)
5259

5360
auto_start = self._auto_start if self._auto_start is not AUTO_START_UNSET else False
@@ -57,7 +64,7 @@ def write_many_sample_port_byte(self, data, timeout=10.0):
5764
)
5865

5966
def write_many_sample_port_uint16(self, data, timeout=10.0):
60-
"""Writes one or more 16-bit unsigned integer samples to one or more digital output channels in a task.
67+
"""Writes 16-bit unsigned integer samples to one or more digital output channels in a task.
6168
6269
Use this method for devices with up to 16 lines per port.
6370
@@ -95,7 +102,7 @@ def write_many_sample_port_uint16(self, data, timeout=10.0):
95102
96103
Specifies the actual number of samples this method
97104
successfully wrote to each channel in the task.
98-
""" # noqa: W505 - doc line too long (111 > 100 characters) (auto-generated noqa)
105+
"""
99106
self._verify_array(data, True, True)
100107

101108
auto_start = self._auto_start if self._auto_start is not AUTO_START_UNSET else False
@@ -105,7 +112,7 @@ def write_many_sample_port_uint16(self, data, timeout=10.0):
105112
)
106113

107114
def write_many_sample_port_uint32(self, data, timeout=10.0):
108-
"""Writes one or more 32-bit unsigned integer samples to one or more digital output channels in a task.
115+
"""Writes 32-bit unsigned integer samples to one or more digital output channels in a task.
109116
110117
Use this method for devices with up to 32 lines per port.
111118
@@ -143,7 +150,7 @@ def write_many_sample_port_uint32(self, data, timeout=10.0):
143150
144151
Specifies the actual number of samples this method
145152
successfully wrote to each channel in the task.
146-
""" # noqa: W505 - doc line too long (111 > 100 characters) (auto-generated noqa)
153+
"""
147154
self._verify_array(data, True, True)
148155

149156
auto_start = self._auto_start if self._auto_start is not AUTO_START_UNSET else False
@@ -219,7 +226,7 @@ def write_one_sample_one_line(self, data, timeout=10):
219226
)
220227

221228
def write_one_sample_port_byte(self, data, timeout=10):
222-
"""Writes a single 8-bit unsigned integer sample to one or more digital output channels in a task.
229+
"""Writes a single 8-bit unsigned integer sample to one or more digital output channels.
223230
224231
Use this method for devices with up to 8 lines per port.
225232
@@ -242,7 +249,7 @@ def write_one_sample_port_byte(self, data, timeout=10):
242249
once to write the submitted samples. If the method could
243250
not write all the submitted samples, it returns an error
244251
and the number of samples successfully written.
245-
""" # noqa: W505 - doc line too long (106 > 100 characters) (auto-generated noqa)
252+
"""
246253
self._verify_array(data, True, False)
247254

248255
auto_start = self._auto_start if self._auto_start is not AUTO_START_UNSET else True
@@ -252,7 +259,7 @@ def write_one_sample_port_byte(self, data, timeout=10):
252259
)
253260

254261
def write_one_sample_port_uint16(self, data, timeout=10):
255-
"""Writes a single 16-bit unsigned integer sample to one or more digital output channels in a task.
262+
"""Writes a single 16-bit unsigned integer sample to one or more digital output channels.
256263
257264
Use this method for devices with up to 16 lines per port.
258265
@@ -275,7 +282,7 @@ def write_one_sample_port_uint16(self, data, timeout=10):
275282
once to write the submitted samples. If the method could
276283
not write all the submitted samples, it returns an error
277284
and the number of samples successfully written.
278-
""" # noqa: W505 - doc line too long (107 > 100 characters) (auto-generated noqa)
285+
"""
279286
self._verify_array(data, True, False)
280287

281288
auto_start = self._auto_start if self._auto_start is not AUTO_START_UNSET else True
@@ -285,7 +292,7 @@ def write_one_sample_port_uint16(self, data, timeout=10):
285292
)
286293

287294
def write_one_sample_port_uint32(self, data, timeout=10):
288-
"""Writes a single 32-bit unsigned integer sample to one or more digital output channels in a task.
295+
"""Writes a single 32-bit unsigned integer sample to one or more digital output channels.
289296
290297
Use this method for devices with up to 32 lines per port.
291298
@@ -308,11 +315,51 @@ def write_one_sample_port_uint32(self, data, timeout=10):
308315
once to write the submitted samples. If the method could
309316
not write all the submitted samples, it returns an error
310317
and the number of samples successfully written.
311-
""" # noqa: W505 - doc line too long (107 > 100 characters) (auto-generated noqa)
318+
"""
312319
self._verify_array(data, True, False)
313320

314321
auto_start = self._auto_start if self._auto_start is not AUTO_START_UNSET else True
315322

316323
return self._interpreter.write_digital_u32(
317324
self._handle, 1, auto_start, timeout, FillMode.GROUP_BY_CHANNEL.value, data
318325
)
326+
327+
@requires_feature(WAVEFORM_SUPPORT)
328+
def write_waveforms(
329+
self, waveforms: Sequence[DigitalWaveform[Any]], timeout: float = 10.0
330+
) -> int:
331+
"""Writes waveforms to one or more digital output channels in a task.
332+
333+
If the task uses on-demand timing, this method returns only
334+
after the device generates all samples. On-demand is the default
335+
timing type if you do not use the timing property on the task to
336+
configure a sample timing type. If the task uses any timing type
337+
other than on-demand, this method returns immediately and does
338+
not wait for the device to generate all samples. Your
339+
application must determine if the task is done to ensure that
340+
the device generated all samples.
341+
342+
Args:
343+
waveforms (Sequence[DigitalWaveform[Any]]): Specifies the
344+
waveforms to write to the task.
345+
timeout (Optional[float]): Specifies the amount of time in
346+
seconds to wait for the method to write all samples.
347+
NI-DAQmx performs a timeout check only if the method
348+
must wait before it writes data. This method returns an
349+
error if the time elapses. The default timeout is 10
350+
seconds. If you set timeout to
351+
nidaqmx.constants.WAIT_INFINITELY, the method waits
352+
indefinitely. If you set timeout to 0, the method tries
353+
once to write the submitted samples. If the method could
354+
not write all the submitted samples, it returns an error
355+
and the number of samples successfully written.
356+
357+
Returns:
358+
int: Specifies the actual number of samples per channel this method
359+
successfully wrote.
360+
"""
361+
auto_start = self._auto_start if self._auto_start is not AUTO_START_UNSET else False
362+
363+
return self._interpreter.write_digital_waveforms(
364+
self._handle, waveforms, auto_start, timeout
365+
)

generated/nidaqmx/task/_task.py

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1392,9 +1392,7 @@ def write(self, data, auto_start=AUTO_START_UNSET, timeout=10.0):
13921392
Specifies the actual number of samples this method
13931393
successfully wrote.
13941394
"""
1395-
if isinstance(data, (AnalogWaveform, DigitalWaveform)) or (
1396-
isinstance(data, list) and data and all(isinstance(wf, AnalogWaveform) for wf in data)
1397-
):
1395+
if self._is_waveform_data(data):
13981396
return self.write_waveform(data, auto_start, timeout)
13991397

14001398
channels_to_write = self.channels
@@ -1578,10 +1576,27 @@ def write(self, data, auto_start=AUTO_START_UNSET, timeout=10.0):
15781576
else:
15791577
self._raise_no_output_channels_error()
15801578

1579+
def _is_waveform_data(self, data):
1580+
"""Check if data is waveform data (single waveform or list of waveforms)."""
1581+
if isinstance(data, (AnalogWaveform, DigitalWaveform)):
1582+
return True
1583+
1584+
if not isinstance(data, list) or not data:
1585+
return False
1586+
1587+
return all(isinstance(wf, AnalogWaveform) for wf in data) or all(
1588+
isinstance(wf, DigitalWaveform) for wf in data
1589+
)
1590+
15811591
@requires_feature(WAVEFORM_SUPPORT)
15821592
def write_waveform(
15831593
self,
1584-
waveforms: AnalogWaveform[Any] | DigitalWaveform[Any] | Sequence[AnalogWaveform[Any]],
1594+
waveforms: (
1595+
AnalogWaveform[Any]
1596+
| DigitalWaveform[Any]
1597+
| Sequence[AnalogWaveform[Any]]
1598+
| Sequence[DigitalWaveform[Any]]
1599+
),
15851600
auto_start=AUTO_START_UNSET,
15861601
timeout: float = 10.0,
15871602
) -> int:
@@ -1671,6 +1686,12 @@ def write_waveform(
16711686
return self._interpreter.write_digital_waveform(
16721687
self._handle, waveforms, auto_start, timeout
16731688
)
1689+
elif isinstance(waveforms, list) and isinstance(waveforms[0], DigitalWaveform):
1690+
if number_of_channels != len(waveforms):
1691+
self._raise_invalid_write_num_chans_error(number_of_channels, len(waveforms))
1692+
return self._interpreter.write_digital_waveforms(
1693+
self._handle, waveforms, auto_start, timeout
1694+
)
16741695
else:
16751696
self._raise_unsupported_output_type_error(type(waveforms))
16761697

src/codegen/templates/_base_interpreter.py.mako

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,4 +147,13 @@ class BaseInterpreter(abc.ABC):
147147
auto_start: bool,
148148
timeout: float,
149149
) -> int:
150-
raise NotImplementedError
150+
raise NotImplementedError
151+
152+
def write_digital_waveforms(
153+
self,
154+
task_handle: object,
155+
waveform: Sequence[DigitalWaveform[Any]],
156+
auto_start: bool,
157+
timeout: float,
158+
) -> int:
159+
raise NotImplementedError

src/codegen/templates/_grpc_interpreter.py.mako

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -327,6 +327,15 @@ class GrpcStubInterpreter(BaseInterpreter):
327327
) -> int:
328328
raise NotImplementedError
329329

330+
def write_digital_waveforms(
331+
self,
332+
task_handle: object,
333+
waveform: Sequence[DigitalWaveform[Any]],
334+
auto_start: bool,
335+
timeout: float,
336+
) -> int:
337+
raise NotImplementedError
338+
330339
def _assign_numpy_array(numpy_array, grpc_array):
331340
"""
332341
Assigns grpc array to numpy array maintaining the original shape.

src/codegen/templates/_library_interpreter.py.mako

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -936,6 +936,50 @@ class LibraryInterpreter(BaseInterpreter):
936936
return data
937937
return data.copy(order="C")
938938

939+
def write_digital_waveforms(
940+
self,
941+
task_handle: object,
942+
waveforms: Sequence[DigitalWaveform[Any]],
943+
auto_start: bool,
944+
timeout: float,
945+
) -> int:
946+
"""Write digital waveforms."""
947+
channel_count = len(waveforms)
948+
assert channel_count > 0
949+
sample_count = waveforms[0].sample_count
950+
951+
for waveform in waveforms:
952+
if waveform.sample_count != sample_count:
953+
raise DaqError(
954+
"The waveforms must all have the same sample count.",
955+
DAQmxErrors.UNKNOWN
956+
)
957+
958+
bytes_per_chan_array = numpy.array([wf.signal_count for wf in waveforms], dtype=numpy.uint32)
959+
960+
# build a temporary contiguous array to write the data from multiple channels into.
961+
# write_array must be in the format (numChans x numSampsPerChan x maxDataWidth)
962+
write_array = numpy.zeros(
963+
(channel_count, sample_count, max(bytes_per_chan_array)),
964+
dtype=numpy.uint8,
965+
)
966+
for i, waveform in enumerate(waveforms):
967+
signal_count = waveform.signal_count
968+
write_array[i, :, :signal_count] = waveform.data
969+
970+
error_code, samples_written = self._internal_write_digital_waveform(
971+
task_handle,
972+
sample_count,
973+
auto_start,
974+
timeout,
975+
FillMode.GROUP_BY_CHANNEL.value,
976+
write_array,
977+
bytes_per_chan_array,
978+
)
979+
980+
self.check_for_error(error_code, samps_per_chan_written=samples_written)
981+
return samples_written
982+
939983
def _internal_write_digital_waveform(
940984
self,
941985
task_handle: object,

0 commit comments

Comments
 (0)