Skip to content

Commit b97ffcd

Browse files
mikeprosserniMike Prosser
andauthored
Implement gRPC Waveform Support (#878)
* remove all "not implemented in GRPC" test markers * implement methods in _grpc_interpreter.mako * update changelog * update nidaqmx.proto and stub_generator.py * fix lint and some mypy issues * generate .pyi for timestamp and waveform * _fix_ni_protobuf_imports() * lint * Sort proto files for deterministic generation order * lint * normalize line endings * revert sorting fix * lint * fix some line endings * fix stub_generator line endings * write_bytes in stub_generator * stub_generator cleanup * force crlf endings * line endings * cleanup * cleanup * fix line endings * cleanup * cleanup * _get_num_samps_per_chan helper * cleanup * improve changelog * _waveform_utils.py * add ni-protobuf-types dependency and revert changes in stub_generator * grpc interpreter cleanup * use waveform_conversion methods * cleanup * target_waveform.timing = temp_waveform.timing * simplify read_analog_waveforms and read_digital_waveforms * simplify write_analog_waveforms and write_digital_waveforms * cleanup * revert _handle_rpc_error * cleanup --------- Co-authored-by: Mike Prosser <[email protected]>
1 parent f62b0ca commit b97ffcd

34 files changed

+3129
-2568
lines changed

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ All notable changes to this project will be documented in this file.
3434

3535
* ### Major Changes
3636
* Removed support for Python 3.9.
37+
* (IN PROGRESS behind "WAVEFORM_SUPPORT" feature toggle) Added support for reading and writing Waveform data through gRPC using [NI gRPC Device Server](https://github.com/ni/grpc-device).
3738

3839
* ### Known Issues
3940
* ...
@@ -47,7 +48,7 @@ All notable changes to this project will be documented in this file.
4748
* [639: first_samp_timestamp_val property does not work because LibraryInterpreter is missing a method](https://github.com/ni/nidaqmx-python/issues/639)
4849

4950
* ### Major Changes
50-
* (IN PROGRESS behind "WAVEFORM_SUPPORT" feature toggle) Added support for reading and writing Waveform data.
51+
* (IN PROGRESS behind "WAVEFORM_SUPPORT" feature toggle) Added support for reading and writing Waveform data.
5152
* Simplify `numpy` and `click` version constraints.
5253
* `nidaqmx` supports a wider range of Python versions than `numpy` and `click`, so testing
5354
`nidaqmx` against its oldest supported Python version (currently 3.9) requires downgrading

generated/nidaqmx/_base_interpreter.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1879,7 +1879,7 @@ def read_analog_waveforms(
18791879
waveform_attribute_mode: WaveformAttributeMode
18801880
) -> int:
18811881
raise NotImplementedError
1882-
1882+
18831883
@abc.abstractmethod
18841884
def read_digital_waveform(
18851885
self,
@@ -1935,7 +1935,7 @@ def write_analog_waveforms(
19351935
timeout: float
19361936
) -> int:
19371937
raise NotImplementedError
1938-
1938+
19391939
@abc.abstractmethod
19401940
def write_digital_waveform(
19411941
self,
@@ -1945,7 +1945,7 @@ def write_digital_waveform(
19451945
timeout: float,
19461946
) -> int:
19471947
raise NotImplementedError
1948-
1948+
19491949
def write_digital_waveforms(
19501950
self,
19511951
task_handle: object,

generated/nidaqmx/_grpc_interpreter.py

Lines changed: 123 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,17 @@
1818
from nidaqmx._base_interpreter import BaseEventHandler, BaseInterpreter
1919
from nidaqmx._stubs import nidaqmx_pb2 as grpc_types
2020
from nidaqmx._stubs import nidaqmx_pb2_grpc as nidaqmx_grpc
21+
from ni.protobuf.types.waveform_conversion import (
22+
digital_waveform_from_protobuf,
23+
digital_waveform_to_protobuf,
24+
float64_analog_waveform_from_protobuf,
25+
float64_analog_waveform_to_protobuf
26+
)
2127
from nidaqmx.constants import WaveformAttributeMode
2228
from nidaqmx.error_codes import DAQmxErrors
2329
from nidaqmx._grpc_time import convert_time_to_timestamp, convert_timestamp_to_time
30+
from nidaqmx._waveform_utils import get_num_samps_per_chan
31+
from session_pb2 import Session
2432

2533
_logger = logging.getLogger(__name__)
2634

@@ -3616,7 +3624,13 @@ def read_analog_waveform(
36163624
waveform: AnalogWaveform[numpy.float64],
36173625
waveform_attribute_mode: WaveformAttributeMode
36183626
) -> int:
3619-
raise NotImplementedError
3627+
return self.read_analog_waveforms(
3628+
task_handle,
3629+
number_of_samples_per_channel,
3630+
timeout,
3631+
[waveform],
3632+
waveform_attribute_mode
3633+
)
36203634

36213635
def read_analog_waveforms(
36223636
self,
@@ -3626,7 +3640,30 @@ def read_analog_waveforms(
36263640
waveforms: Sequence[AnalogWaveform[numpy.float64]],
36273641
waveform_attribute_mode: WaveformAttributeMode
36283642
) -> int:
3629-
raise NotImplementedError
3643+
assert isinstance(task_handle, Session)
3644+
response = self._invoke(
3645+
self._client.ReadAnalogWaveforms,
3646+
grpc_types.ReadAnalogWaveformsRequest(
3647+
task=task_handle,
3648+
num_samps_per_chan=number_of_samples_per_channel,
3649+
timeout=timeout,
3650+
waveform_attribute_mode_raw=waveform_attribute_mode.value
3651+
))
3652+
3653+
if len(response.waveforms) != len(waveforms):
3654+
raise ValueError(f"Expected {len(waveforms)} waveforms but received {len(response.waveforms)} from server")
3655+
3656+
for i, grpc_waveform in enumerate(response.waveforms):
3657+
temp_waveform = float64_analog_waveform_from_protobuf(grpc_waveform)
3658+
waveforms[i].load_data(temp_waveform.scaled_data)
3659+
3660+
waveforms[i].scale_mode = temp_waveform.scale_mode
3661+
waveforms[i].timing = temp_waveform.timing
3662+
waveforms[i].extended_properties.clear()
3663+
waveforms[i].extended_properties.update(temp_waveform.extended_properties)
3664+
3665+
self._check_for_error_from_response(response.status, samps_per_chan_read=response.samps_per_chan_read)
3666+
return response.samps_per_chan_read
36303667

36313668
def read_digital_waveform(
36323669
self,
@@ -3636,7 +3673,15 @@ def read_digital_waveform(
36363673
waveform: DigitalWaveform[Any],
36373674
waveform_attribute_mode: WaveformAttributeMode
36383675
) -> int:
3639-
raise NotImplementedError
3676+
return self.read_digital_waveforms(
3677+
task_handle,
3678+
1, # channel_count
3679+
number_of_samples_per_channel,
3680+
waveform.signal_count, # number_of_signals_per_sample
3681+
timeout,
3682+
[waveform],
3683+
waveform_attribute_mode
3684+
)
36403685

36413686
def read_digital_waveforms(
36423687
self,
@@ -3648,7 +3693,32 @@ def read_digital_waveforms(
36483693
waveforms: Sequence[DigitalWaveform[Any]],
36493694
waveform_attribute_mode: WaveformAttributeMode,
36503695
) -> int:
3651-
raise NotImplementedError
3696+
assert isinstance(task_handle, Session)
3697+
response = self._invoke(
3698+
self._client.ReadDigitalWaveforms,
3699+
grpc_types.ReadDigitalWaveformsRequest(
3700+
task=task_handle,
3701+
num_samps_per_chan=number_of_samples_per_channel,
3702+
timeout=timeout,
3703+
waveform_attribute_mode_raw=waveform_attribute_mode.value
3704+
))
3705+
3706+
if len(response.waveforms) != len(waveforms):
3707+
raise ValueError(f"Expected {len(waveforms)} waveforms but received {len(response.waveforms)} from server")
3708+
3709+
for i, grpc_waveform in enumerate(response.waveforms):
3710+
temp_waveform = digital_waveform_from_protobuf(grpc_waveform)
3711+
data = temp_waveform.data
3712+
if data.dtype != waveforms[i].dtype:
3713+
data = data.view(waveforms[i].dtype)
3714+
waveforms[i].load_data(data)
3715+
3716+
waveforms[i].timing = temp_waveform.timing
3717+
waveforms[i].extended_properties.clear()
3718+
waveforms[i].extended_properties.update(temp_waveform.extended_properties)
3719+
3720+
self._check_for_error_from_response(response.status, samps_per_chan_read=response.samps_per_chan_read)
3721+
return response.samps_per_chan_read
36523722

36533723
def read_new_digital_waveforms(
36543724
self,
@@ -3659,7 +3729,20 @@ def read_new_digital_waveforms(
36593729
timeout: float,
36603730
waveform_attribute_mode: WaveformAttributeMode,
36613731
) -> Sequence[DigitalWaveform[numpy.uint8]]:
3662-
raise NotImplementedError
3732+
assert isinstance(task_handle, Session)
3733+
response = self._invoke(
3734+
self._client.ReadDigitalWaveforms,
3735+
grpc_types.ReadDigitalWaveformsRequest(
3736+
task=task_handle,
3737+
num_samps_per_chan=number_of_samples_per_channel,
3738+
timeout=timeout,
3739+
waveform_attribute_mode_raw=waveform_attribute_mode.value
3740+
))
3741+
3742+
waveforms = [digital_waveform_from_protobuf(grpc_waveform) for grpc_waveform in response.waveforms]
3743+
3744+
self._check_for_error_from_response(response.status, samps_per_chan_read=response.samps_per_chan_read)
3745+
return waveforms
36633746

36643747
def write_analog_waveform(
36653748
self,
@@ -3668,7 +3751,7 @@ def write_analog_waveform(
36683751
auto_start: bool,
36693752
timeout: float
36703753
) -> int:
3671-
raise NotImplementedError
3754+
return self.write_analog_waveforms(task_handle, [waveform], auto_start, timeout)
36723755

36733756
def write_analog_waveforms(
36743757
self,
@@ -3677,7 +3760,22 @@ def write_analog_waveforms(
36773760
auto_start: bool,
36783761
timeout: float
36793762
) -> int:
3680-
raise NotImplementedError
3763+
assert isinstance(task_handle, Session)
3764+
num_samps_per_chan = get_num_samps_per_chan(waveforms)
3765+
3766+
grpc_waveforms = [float64_analog_waveform_to_protobuf(waveform) for waveform in waveforms]
3767+
3768+
response = self._invoke(
3769+
self._client.WriteAnalogWaveforms,
3770+
grpc_types.WriteAnalogWaveformsRequest(
3771+
task=task_handle,
3772+
auto_start=auto_start,
3773+
timeout=timeout,
3774+
waveforms=grpc_waveforms
3775+
))
3776+
3777+
self._check_for_error_from_response(response.status, samps_per_chan_written=response.samps_per_chan_written)
3778+
return response.samps_per_chan_written
36813779

36823780
def write_digital_waveform(
36833781
self,
@@ -3686,16 +3784,31 @@ def write_digital_waveform(
36863784
auto_start: bool,
36873785
timeout: float,
36883786
) -> int:
3689-
raise NotImplementedError
3787+
return self.write_digital_waveforms(task_handle, [waveform], auto_start, timeout)
36903788

36913789
def write_digital_waveforms(
36923790
self,
36933791
task_handle: object,
3694-
waveform: Sequence[DigitalWaveform[Any]],
3792+
waveforms: Sequence[DigitalWaveform[Any]],
36953793
auto_start: bool,
36963794
timeout: float,
36973795
) -> int:
3698-
raise NotImplementedError
3796+
assert isinstance(task_handle, Session)
3797+
num_samps_per_chan = get_num_samps_per_chan(waveforms)
3798+
3799+
grpc_waveforms = [digital_waveform_to_protobuf(waveform) for waveform in waveforms]
3800+
3801+
response = self._invoke(
3802+
self._client.WriteDigitalWaveforms,
3803+
grpc_types.WriteDigitalWaveformsRequest(
3804+
task=task_handle,
3805+
auto_start=auto_start,
3806+
timeout=timeout,
3807+
waveforms=grpc_waveforms
3808+
))
3809+
3810+
self._check_for_error_from_response(response.status, samps_per_chan_written=response.samps_per_chan_written)
3811+
return response.samps_per_chan_written
36993812

37003813
def _assign_numpy_array(numpy_array, grpc_array):
37013814
"""

generated/nidaqmx/_library_interpreter.py

Lines changed: 3 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
from nidaqmx.error_codes import DAQmxErrors, DAQmxWarnings
2121
from nidaqmx.errors import DaqError, DaqFunctionNotSupportedError, DaqReadError, DaqWarning, DaqWriteError
2222
from nidaqmx._lib_time import AbsoluteTime
23+
from nidaqmx._waveform_utils import get_num_samps_per_chan
2324
from nitypes.waveform.typing import ExtendedPropertyValue
2425
from nitypes.waveform import AnalogWaveform, DigitalWaveform, SampleIntervalMode, Timing, ExtendedPropertyDictionary
2526

@@ -7011,15 +7012,7 @@ def write_analog_waveforms(
70117012
timeout: float
70127013
) -> int:
70137014
"""Write analog waveforms."""
7014-
assert len(waveforms) > 0
7015-
num_samps_per_chan = waveforms[0].sample_count
7016-
7017-
for waveform in waveforms:
7018-
if waveform.sample_count != num_samps_per_chan:
7019-
raise DaqError(
7020-
"The waveforms must all have the same sample count.",
7021-
DAQmxErrors.UNKNOWN
7022-
)
7015+
num_samps_per_chan = get_num_samps_per_chan(waveforms)
70237016

70247017
write_arrays = [self._get_analog_write_array(waveform) for waveform in waveforms]
70257018

@@ -7130,15 +7123,7 @@ def write_digital_waveforms(
71307123
) -> int:
71317124
"""Write digital waveforms."""
71327125
channel_count = len(waveforms)
7133-
assert channel_count > 0
7134-
sample_count = waveforms[0].sample_count
7135-
7136-
for waveform in waveforms:
7137-
if waveform.sample_count != sample_count:
7138-
raise DaqError(
7139-
"The waveforms must all have the same sample count.",
7140-
DAQmxErrors.UNKNOWN
7141-
)
7126+
sample_count = get_num_samps_per_chan(waveforms)
71427127

71437128
bytes_per_chan_array = numpy.array([wf.signal_count for wf in waveforms], dtype=numpy.uint32)
71447129

generated/nidaqmx/_stubs/nidaqmx_pb2.py

Lines changed: 2322 additions & 2303 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)