diff --git a/CMakeLists.txt b/CMakeLists.txt index 62a9d1bf3..d7fbfad89 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -18,6 +18,12 @@ if(MSVC) set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} /std:c17") # Needed for boringssl endif() +# GCC-specific flags +if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fno-var-tracking-assignments") + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fno-var-tracking-assignments") +endif() + #---------------------------------------------------------------------- # Use C++17 (needed for shared_mutex support on Linux) #---------------------------------------------------------------------- @@ -289,6 +295,8 @@ get_filename_component(deviceid_restricted_proto "source/protobuf_restricted/dev get_filename_component(debugsessionproperties_restricted_proto "source/protobuf_restricted/debugsessionproperties_restricted.proto" ABSOLUTE) get_filename_component(calibrationoperations_restricted_proto "source/protobuf_restricted/calibrationoperations_restricted.proto" ABSOLUTE) get_filename_component(data_moniker_proto "imports/protobuf/data_moniker.proto" ABSOLUTE) +get_filename_component(precision_timestamp_proto "third_party/ni-apis/ni/protobuf/types/precision_timestamp.proto" ABSOLUTE) +get_filename_component(waveform_proto "third_party/ni-apis/ni/protobuf/types/waveform.proto" ABSOLUTE) get_filename_component(session_proto_path "${session_proto}" PATH) #---------------------------------------------------------------------- @@ -303,8 +311,9 @@ function(GenerateGrpcSources) set(proto_file "${GENERATE_ARGS_PROTO}") if(USE_SUBMODULE_LIBS) set(protobuf_includes_arg - -I ${CMAKE_SOURCE_DIR}/third_party/grpc/third_party/protobuf/src/ - -I ${CMAKE_SOURCE_DIR}/third_party/ni-apis/ni/grpcdevice/v1/) # for session.proto + -I ${CMAKE_SOURCE_DIR}/third_party/grpc/third_party/protobuf/src/ + -I ${CMAKE_SOURCE_DIR}/third_party/ni-apis/ni/grpcdevice/v1/ # for session.proto + -I ${CMAKE_SOURCE_DIR}/third_party/ni-apis/) endif() get_filename_component(proto_name "${proto_file}" NAME) get_filename_component(proto_path "${proto_file}" PATH) @@ -346,6 +355,34 @@ function(GenerateGrpcSources) endif() endfunction() +#---------------------------------------------------------------------- +# Generate sources from ni-apis proto files +# Usage: GenerateNiApisProtoSources(PROTO_PATH OUTPUT_SRCS OUTPUT_HDRS [DEPENDS ...]) +#---------------------------------------------------------------------- +function(GenerateNiApisProtoSources) + set(oneValueArgs PROTO_PATH OUTPUT_SRCS OUTPUT_HDRS) + set(multiValueArgs DEPENDS) + cmake_parse_arguments(GEN_ARGS "" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) + + set(proto_srcs "${proto_srcs_dir}/${GEN_ARGS_PROTO_PATH}.pb.cc") + set(proto_hdrs "${proto_srcs_dir}/${GEN_ARGS_PROTO_PATH}.pb.h") + + add_custom_command( + OUTPUT "${proto_srcs}" "${proto_hdrs}" + COMMAND ${_PROTOBUF_PROTOC} + ARGS --cpp_out ${proto_srcs_dir} + -I ${CMAKE_SOURCE_DIR}/third_party/ni-apis/ + -I ${CMAKE_SOURCE_DIR}/third_party/grpc/third_party/protobuf/src/ + ${GEN_ARGS_PROTO_PATH}.proto + DEPENDS "${CMAKE_SOURCE_DIR}/third_party/ni-apis/${GEN_ARGS_PROTO_PATH}.proto" ${GEN_ARGS_DEPENDS} + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/third_party/ni-apis/ + VERBATIM + ) + + set(${GEN_ARGS_OUTPUT_SRCS} "${proto_srcs}" PARENT_SCOPE) + set(${GEN_ARGS_OUTPUT_HDRS} "${proto_hdrs}" PARENT_SCOPE) +endfunction() + set(session_proto_srcs "${proto_srcs_dir}/session.pb.cc") set(session_proto_hdrs "${proto_srcs_dir}/session.pb.h") set(session_grpc_srcs "${proto_srcs_dir}/session.grpc.pb.cc") @@ -372,6 +409,10 @@ set(data_moniker_proto_srcs "${proto_srcs_dir}/data_moniker.pb.cc") set(data_moniker_proto_hdrs "${proto_srcs_dir}/data_moniker.pb.h") set(data_moniker_grpc_srcs "${proto_srcs_dir}/data_moniker.grpc.pb.cc") set(data_moniker_grpc_hdrs "${proto_srcs_dir}/data_moniker.grpc.pb.h") +set(precision_timestamp_proto_srcs "${proto_srcs_dir}/ni/protobuf/types/precision_timestamp.pb.cc") +set(precision_timestamp_proto_hdrs "${proto_srcs_dir}/ni/protobuf/types/precision_timestamp.pb.h") +set(waveform_proto_srcs "${proto_srcs_dir}/ni/protobuf/types/waveform.pb.cc") +set(waveform_proto_hdrs "${proto_srcs_dir}/ni/protobuf/types/waveform.pb.h") GenerateGrpcSources( PROTO @@ -441,6 +482,19 @@ GenerateGrpcSources( "${data_moniker_grpc_hdrs}" ) +GenerateNiApisProtoSources( + PROTO_PATH "ni/protobuf/types/precision_timestamp" + OUTPUT_SRCS precision_timestamp_proto_srcs + OUTPUT_HDRS precision_timestamp_proto_hdrs +) + +GenerateNiApisProtoSources( + PROTO_PATH "ni/protobuf/types/waveform" + OUTPUT_SRCS waveform_proto_srcs + OUTPUT_HDRS waveform_proto_hdrs + DEPENDS "${precision_timestamp_proto_hdrs}" +) + set(nidriver_service_library_hdrs ${nidriver_service_library_hdrs} "${session_proto_hdrs}" @@ -454,6 +508,8 @@ set(nidriver_service_library_hdrs "${debugsessionproperties_restricted_grpc_hdrs}" "${calibrationoperations_restricted_proto_hdrs}" "${calibrationoperations_restricted_grpc_hdrs}" + "${precision_timestamp_proto_hdrs}" + "${waveform_proto_hdrs}" ) foreach(api ${nidrivers}) @@ -514,6 +570,8 @@ add_executable(ni_grpc_device_server ${calibrationoperations_restricted_grpc_srcs} ${data_moniker_proto_srcs} ${data_moniker_grpc_srcs} + ${precision_timestamp_proto_srcs} + ${waveform_proto_srcs} ${nidriver_service_srcs}) # Enable warnings only on source that we own, not generated code or dependencies @@ -655,6 +713,8 @@ add_executable(IntegrationTestsRunner ${calibrationoperations_restricted_grpc_srcs} ${data_moniker_proto_srcs} ${data_moniker_grpc_srcs} + ${precision_timestamp_proto_srcs} + ${waveform_proto_srcs} ${nidriver_service_srcs} "${proto_srcs_dir}/nifake.pb.cc" "${proto_srcs_dir}/nifake.grpc.pb.cc" @@ -742,6 +802,8 @@ add_executable(UnitTestsRunner ${calibrationoperations_restricted_grpc_srcs} ${data_moniker_proto_srcs} ${data_moniker_grpc_srcs} + ${precision_timestamp_proto_srcs} + ${waveform_proto_srcs} "${proto_srcs_dir}/nifake.pb.cc" "${proto_srcs_dir}/nifake.grpc.pb.cc" "${proto_srcs_dir}/nifake_extension.pb.cc" @@ -879,6 +941,8 @@ set(system_test_runner_sources ${calibrationoperations_restricted_grpc_srcs} ${data_moniker_proto_srcs} ${data_moniker_grpc_srcs} + ${precision_timestamp_proto_srcs} + ${waveform_proto_srcs} ${nidriver_service_srcs} ${nidriver_client_srcs} ) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4edf948a5..bcbe65cce 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -68,6 +68,18 @@ Build a release build for use in a production environment: > cmake --build . --config Release ``` +### Build with Ninja + +Build faster by using Ninja: + +``` +> "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Auxiliary\Build\vcvars64.bat" +> mkdir build +> cd build +> cmake .. -G "Ninja Multi-Config" +> cmake --build . +``` + ## Building on Linux ### Prerequisites diff --git a/generated/nidaqmx/nidaqmx.proto b/generated/nidaqmx/nidaqmx.proto index eb2ba1d25..f7423a579 100644 --- a/generated/nidaqmx/nidaqmx.proto +++ b/generated/nidaqmx/nidaqmx.proto @@ -16,6 +16,7 @@ package nidaqmx_grpc; import "session.proto"; import "data_moniker.proto"; +import "ni/protobuf/types/waveform.proto"; import "google/protobuf/timestamp.proto"; service NiDAQmx { @@ -465,6 +466,7 @@ service NiDAQmx { rpc BeginWriteRaw(BeginWriteRawRequest) returns (BeginWriteRawResponse); rpc WriteToTEDSFromArray(WriteToTEDSFromArrayRequest) returns (WriteToTEDSFromArrayResponse); rpc WriteToTEDSFromFile(WriteToTEDSFromFileRequest) returns (WriteToTEDSFromFileResponse); + rpc ReadAnalogWaveforms(ReadAnalogWaveformsRequest) returns (ReadAnalogWaveformsResponse); } enum BufferUInt32Attribute { @@ -3558,6 +3560,12 @@ enum WriteBasicTEDSOptions { WRITE_BASIC_TEDS_OPTIONS_DO_NOT_WRITE = 12540; } +enum WaveformAttributeMode { + WAVEFORM_ATTRIBUTE_MODE_NONE = 0; + WAVEFORM_ATTRIBUTE_MODE_TIMING = 1; + WAVEFORM_ATTRIBUTE_MODE_EXTENDED_PROPERTIES = 2; +} + enum ChannelInt32AttributeValues { option allow_alias = true; CHANNEL_INT32_UNSPECIFIED = 0; @@ -11511,3 +11519,19 @@ message WriteToTEDSFromFileResponse { int32 status = 1; } +message ReadAnalogWaveformsRequest { + nidevice_grpc.Session task = 1; + int32 number_of_samples_per_channel = 2; + double timeout = 3; + oneof waveform_attribute_mode_enum { + WaveformAttributeMode waveform_attribute_mode = 4; + int32 waveform_attribute_mode_raw = 5; + } +} + +message ReadAnalogWaveformsResponse { + int32 status = 1; + repeated ni.protobuf.types.DoubleAnalogWaveform waveforms = 2; + int32 samps_per_chan_read = 3; +} + diff --git a/generated/nidaqmx/nidaqmx_client.cpp b/generated/nidaqmx/nidaqmx_client.cpp index 7c9ae15a9..0d5a345fc 100644 --- a/generated/nidaqmx/nidaqmx_client.cpp +++ b/generated/nidaqmx/nidaqmx_client.cpp @@ -12414,5 +12414,32 @@ write_to_teds_from_file(const StubPtr& stub, const std::string& physical_channel return response; } +ReadAnalogWaveformsResponse +read_analog_waveforms(const StubPtr& stub, const nidevice_grpc::Session& task, const pb::int32& number_of_samples_per_channel, const double& timeout, const simple_variant& waveform_attribute_mode) +{ + ::grpc::ClientContext context; + + auto request = ReadAnalogWaveformsRequest{}; + request.mutable_task()->CopyFrom(task); + request.set_number_of_samples_per_channel(number_of_samples_per_channel); + request.set_timeout(timeout); + const auto waveform_attribute_mode_ptr = waveform_attribute_mode.get_if(); + const auto waveform_attribute_mode_raw_ptr = waveform_attribute_mode.get_if(); + if (waveform_attribute_mode_ptr) { + request.set_waveform_attribute_mode(*waveform_attribute_mode_ptr); + } + else if (waveform_attribute_mode_raw_ptr) { + request.set_waveform_attribute_mode_raw(*waveform_attribute_mode_raw_ptr); + } + + auto response = ReadAnalogWaveformsResponse{}; + + raise_if_error( + stub->ReadAnalogWaveforms(&context, request, &response), + context); + + return response; +} + } // namespace nidaqmx_grpc::experimental::client diff --git a/generated/nidaqmx/nidaqmx_client.h b/generated/nidaqmx/nidaqmx_client.h index 9952788b6..7e28dc8ed 100644 --- a/generated/nidaqmx/nidaqmx_client.h +++ b/generated/nidaqmx/nidaqmx_client.h @@ -468,6 +468,7 @@ WriteRawResponse write_raw(const StubPtr& stub, const nidevice_grpc::Session& ta BeginWriteRawResponse begin_write_raw(const StubPtr& stub, const nidevice_grpc::Session& task, const pb::int32& num_samps, const bool& auto_start, const double& timeout); WriteToTEDSFromArrayResponse write_to_teds_from_array(const StubPtr& stub, const std::string& physical_channel, const std::string& bit_stream, const simple_variant& basic_teds_options); WriteToTEDSFromFileResponse write_to_teds_from_file(const StubPtr& stub, const std::string& physical_channel, const std::string& file_path, const simple_variant& basic_teds_options); +ReadAnalogWaveformsResponse read_analog_waveforms(const StubPtr& stub, const nidevice_grpc::Session& task, const pb::int32& number_of_samples_per_channel, const double& timeout, const simple_variant& waveform_attribute_mode); } // namespace nidaqmx_grpc::experimental::client diff --git a/generated/nidaqmx/nidaqmx_library.cpp b/generated/nidaqmx/nidaqmx_library.cpp index 6f0af75d8..6e01aae65 100644 --- a/generated/nidaqmx/nidaqmx_library.cpp +++ b/generated/nidaqmx/nidaqmx_library.cpp @@ -268,6 +268,11 @@ NiDAQmxLibrary::NiDAQmxLibrary(std::shared_ptr(shared_library_->get_function_pointer("DAQmxGetWriteAttribute")); function_pointers_.GetWriteAttributeUInt32 = reinterpret_cast(shared_library_->get_function_pointer("DAQmxGetWriteAttribute")); function_pointers_.GetWriteAttributeUInt64 = reinterpret_cast(shared_library_->get_function_pointer("DAQmxGetWriteAttribute")); + function_pointers_.InternalGetLastCreatedChan = reinterpret_cast(shared_library_->get_function_pointer("DAQmxInternalGetLastCreatedChan")); + function_pointers_.InternalReadAnalogWaveformPerChan = reinterpret_cast(shared_library_->get_function_pointer("DAQmxInternalReadAnalogWaveformPerChan")); + function_pointers_.InternalReadDigitalWaveform = reinterpret_cast(shared_library_->get_function_pointer("DAQmxInternalReadDigitalWaveform")); + function_pointers_.InternalWriteAnalogWaveformPerChan = reinterpret_cast(shared_library_->get_function_pointer("DAQmxInternalWriteAnalogWaveformPerChan")); + function_pointers_.InternalWriteDigitalWaveform = reinterpret_cast(shared_library_->get_function_pointer("DAQmxInternalWriteDigitalWaveform")); function_pointers_.IsTaskDone = reinterpret_cast(shared_library_->get_function_pointer("DAQmxIsTaskDone")); function_pointers_.LoadTask = reinterpret_cast(shared_library_->get_function_pointer("DAQmxLoadTask")); function_pointers_.PerformBridgeOffsetNullingCalEx = reinterpret_cast(shared_library_->get_function_pointer("DAQmxPerformBridgeOffsetNullingCalEx")); @@ -2368,6 +2373,46 @@ int32 NiDAQmxLibrary::GetWriteAttributeUInt64(TaskHandle task, int32 attribute, return function_pointers_.GetWriteAttributeUInt64(task, attribute, value); } +int32 NiDAQmxLibrary::InternalGetLastCreatedChan(char value[], uInt32 size) +{ + if (!function_pointers_.InternalGetLastCreatedChan) { + throw nidevice_grpc::LibraryLoadException("Could not find DAQmxInternalGetLastCreatedChan."); + } + return function_pointers_.InternalGetLastCreatedChan(value, size); +} + +int32 NiDAQmxLibrary::InternalReadAnalogWaveformPerChan(TaskHandle task, int32 numSampsPerChan, float64 timeout, int64 t0Array[], int64 dtArray[], uInt32 timingArraySize, DAQmxSetWfmAttrCallbackPtr setWfmAttrCallback, void* setWfmAttrCallbackData, float64 * readArrayPtrs[], uInt32 readArrayCount, uInt32 arraySizeInSampsPerChan, int32* sampsPerChanRead, bool32* reserved) +{ + if (!function_pointers_.InternalReadAnalogWaveformPerChan) { + throw nidevice_grpc::LibraryLoadException("Could not find DAQmxInternalReadAnalogWaveformPerChan."); + } + return function_pointers_.InternalReadAnalogWaveformPerChan(task, numSampsPerChan, timeout, t0Array, dtArray, timingArraySize, setWfmAttrCallback, setWfmAttrCallbackData, readArrayPtrs, readArrayCount, arraySizeInSampsPerChan, sampsPerChanRead, reserved); +} + +int32 NiDAQmxLibrary::InternalReadDigitalWaveform(TaskHandle task, int32 numSampsPerChan, float64 timeout, bool32 fillMode, int64 t0Array[], int64 dtArray[], uInt32 timingArraySize, DAQmxSetWfmAttrCallbackPtr setWfmAttrCallback, void* setWfmAttrCallbackData, uInt8 readArray[], uInt32 arraySizeInBytes, int32* sampsPerChanRead, int32* numBytesPerSamp, uInt32 bytesPerChanArray[], uInt32 bytesPerChanArraySize, bool32* reserved) +{ + if (!function_pointers_.InternalReadDigitalWaveform) { + throw nidevice_grpc::LibraryLoadException("Could not find DAQmxInternalReadDigitalWaveform."); + } + return function_pointers_.InternalReadDigitalWaveform(task, numSampsPerChan, timeout, fillMode, t0Array, dtArray, timingArraySize, setWfmAttrCallback, setWfmAttrCallbackData, readArray, arraySizeInBytes, sampsPerChanRead, numBytesPerSamp, bytesPerChanArray, bytesPerChanArraySize, reserved); +} + +int32 NiDAQmxLibrary::InternalWriteAnalogWaveformPerChan(TaskHandle task, int32 numSampsPerChan, bool32 autoStart, float64 timeout, const float64 * const writeArrayPtrs[], uInt32 writeArrayCount, int32* sampsPerChanWritten, bool32* reserved) +{ + if (!function_pointers_.InternalWriteAnalogWaveformPerChan) { + throw nidevice_grpc::LibraryLoadException("Could not find DAQmxInternalWriteAnalogWaveformPerChan."); + } + return function_pointers_.InternalWriteAnalogWaveformPerChan(task, numSampsPerChan, autoStart, timeout, writeArrayPtrs, writeArrayCount, sampsPerChanWritten, reserved); +} + +int32 NiDAQmxLibrary::InternalWriteDigitalWaveform(TaskHandle task, int32 numSampsPerChan, bool32 autoStart, float64 timeout, bool32 dataLayout, const uInt8 writeArray[], const uInt32 bytesPerChanArray[], uInt32 bytesPerChanArraySize, int32* sampsPerChanWritten, bool32* reserved) +{ + if (!function_pointers_.InternalWriteDigitalWaveform) { + throw nidevice_grpc::LibraryLoadException("Could not find DAQmxInternalWriteDigitalWaveform."); + } + return function_pointers_.InternalWriteDigitalWaveform(task, numSampsPerChan, autoStart, timeout, dataLayout, writeArray, bytesPerChanArray, bytesPerChanArraySize, sampsPerChanWritten, reserved); +} + int32 NiDAQmxLibrary::IsTaskDone(TaskHandle task, bool32* isTaskDone) { if (!function_pointers_.IsTaskDone) { diff --git a/generated/nidaqmx/nidaqmx_library.h b/generated/nidaqmx/nidaqmx_library.h index 6afacf401..e583bf579 100644 --- a/generated/nidaqmx/nidaqmx_library.h +++ b/generated/nidaqmx/nidaqmx_library.h @@ -261,6 +261,11 @@ class NiDAQmxLibrary : public nidaqmx_grpc::NiDAQmxLibraryInterface { int32 GetWriteAttributeString(TaskHandle task, int32 attribute, char value[], uInt32 size) override; int32 GetWriteAttributeUInt32(TaskHandle task, int32 attribute, uInt32* value) override; int32 GetWriteAttributeUInt64(TaskHandle task, int32 attribute, uInt64* value) override; + int32 InternalGetLastCreatedChan(char value[], uInt32 size) override; + int32 InternalReadAnalogWaveformPerChan(TaskHandle task, int32 numSampsPerChan, float64 timeout, int64 t0Array[], int64 dtArray[], uInt32 timingArraySize, DAQmxSetWfmAttrCallbackPtr setWfmAttrCallback, void* setWfmAttrCallbackData, float64 * readArrayPtrs[], uInt32 readArrayCount, uInt32 arraySizeInSampsPerChan, int32* sampsPerChanRead, bool32* reserved) override; + int32 InternalReadDigitalWaveform(TaskHandle task, int32 numSampsPerChan, float64 timeout, bool32 fillMode, int64 t0Array[], int64 dtArray[], uInt32 timingArraySize, DAQmxSetWfmAttrCallbackPtr setWfmAttrCallback, void* setWfmAttrCallbackData, uInt8 readArray[], uInt32 arraySizeInBytes, int32* sampsPerChanRead, int32* numBytesPerSamp, uInt32 bytesPerChanArray[], uInt32 bytesPerChanArraySize, bool32* reserved) override; + int32 InternalWriteAnalogWaveformPerChan(TaskHandle task, int32 numSampsPerChan, bool32 autoStart, float64 timeout, const float64 * const writeArrayPtrs[], uInt32 writeArrayCount, int32* sampsPerChanWritten, bool32* reserved) override; + int32 InternalWriteDigitalWaveform(TaskHandle task, int32 numSampsPerChan, bool32 autoStart, float64 timeout, bool32 dataLayout, const uInt8 writeArray[], const uInt32 bytesPerChanArray[], uInt32 bytesPerChanArraySize, int32* sampsPerChanWritten, bool32* reserved) override; int32 IsTaskDone(TaskHandle task, bool32* isTaskDone) override; int32 LoadTask(const char sessionName[], TaskHandle* task) override; int32 PerformBridgeOffsetNullingCalEx(TaskHandle task, const char channel[], bool32 skipUnsupportedChannels) override; @@ -666,6 +671,11 @@ class NiDAQmxLibrary : public nidaqmx_grpc::NiDAQmxLibraryInterface { using GetWriteAttributeStringPtr = decltype(&DAQmxGetWriteAttribute); using GetWriteAttributeUInt32Ptr = decltype(&DAQmxGetWriteAttribute); using GetWriteAttributeUInt64Ptr = decltype(&DAQmxGetWriteAttribute); + using InternalGetLastCreatedChanPtr = int32 (*)(char value[], uInt32 size); + using InternalReadAnalogWaveformPerChanPtr = int32 (*)(TaskHandle task, int32 numSampsPerChan, float64 timeout, int64 t0Array[], int64 dtArray[], uInt32 timingArraySize, DAQmxSetWfmAttrCallbackPtr setWfmAttrCallback, void* setWfmAttrCallbackData, float64 * readArrayPtrs[], uInt32 readArrayCount, uInt32 arraySizeInSampsPerChan, int32* sampsPerChanRead, bool32* reserved); + using InternalReadDigitalWaveformPtr = int32 (*)(TaskHandle task, int32 numSampsPerChan, float64 timeout, bool32 fillMode, int64 t0Array[], int64 dtArray[], uInt32 timingArraySize, DAQmxSetWfmAttrCallbackPtr setWfmAttrCallback, void* setWfmAttrCallbackData, uInt8 readArray[], uInt32 arraySizeInBytes, int32* sampsPerChanRead, int32* numBytesPerSamp, uInt32 bytesPerChanArray[], uInt32 bytesPerChanArraySize, bool32* reserved); + using InternalWriteAnalogWaveformPerChanPtr = int32 (*)(TaskHandle task, int32 numSampsPerChan, bool32 autoStart, float64 timeout, const float64 * const writeArrayPtrs[], uInt32 writeArrayCount, int32* sampsPerChanWritten, bool32* reserved); + using InternalWriteDigitalWaveformPtr = int32 (*)(TaskHandle task, int32 numSampsPerChan, bool32 autoStart, float64 timeout, bool32 dataLayout, const uInt8 writeArray[], const uInt32 bytesPerChanArray[], uInt32 bytesPerChanArraySize, int32* sampsPerChanWritten, bool32* reserved); using IsTaskDonePtr = decltype(&DAQmxIsTaskDone); using LoadTaskPtr = decltype(&DAQmxLoadTask); using PerformBridgeOffsetNullingCalExPtr = decltype(&DAQmxPerformBridgeOffsetNullingCalEx); @@ -1070,6 +1080,11 @@ class NiDAQmxLibrary : public nidaqmx_grpc::NiDAQmxLibraryInterface { GetWriteAttributeStringPtr GetWriteAttributeString; GetWriteAttributeUInt32Ptr GetWriteAttributeUInt32; GetWriteAttributeUInt64Ptr GetWriteAttributeUInt64; + InternalGetLastCreatedChanPtr InternalGetLastCreatedChan; + InternalReadAnalogWaveformPerChanPtr InternalReadAnalogWaveformPerChan; + InternalReadDigitalWaveformPtr InternalReadDigitalWaveform; + InternalWriteAnalogWaveformPerChanPtr InternalWriteAnalogWaveformPerChan; + InternalWriteDigitalWaveformPtr InternalWriteDigitalWaveform; IsTaskDonePtr IsTaskDone; LoadTaskPtr LoadTask; PerformBridgeOffsetNullingCalExPtr PerformBridgeOffsetNullingCalEx; diff --git a/generated/nidaqmx/nidaqmx_library_interface.h b/generated/nidaqmx/nidaqmx_library_interface.h index 1385dda23..b0b519e19 100644 --- a/generated/nidaqmx/nidaqmx_library_interface.h +++ b/generated/nidaqmx/nidaqmx_library_interface.h @@ -8,6 +8,7 @@ #include #include +#include "NIDAQmxInternalWaveform.h" namespace nidaqmx_grpc { @@ -251,6 +252,11 @@ class NiDAQmxLibraryInterface { virtual int32 GetWriteAttributeString(TaskHandle task, int32 attribute, char value[], uInt32 size) = 0; virtual int32 GetWriteAttributeUInt32(TaskHandle task, int32 attribute, uInt32* value) = 0; virtual int32 GetWriteAttributeUInt64(TaskHandle task, int32 attribute, uInt64* value) = 0; + virtual int32 InternalGetLastCreatedChan(char value[], uInt32 size) = 0; + virtual int32 InternalReadAnalogWaveformPerChan(TaskHandle task, int32 numSampsPerChan, float64 timeout, int64 t0Array[], int64 dtArray[], uInt32 timingArraySize, DAQmxSetWfmAttrCallbackPtr setWfmAttrCallback, void* setWfmAttrCallbackData, float64 * readArrayPtrs[], uInt32 readArrayCount, uInt32 arraySizeInSampsPerChan, int32* sampsPerChanRead, bool32* reserved) = 0; + virtual int32 InternalReadDigitalWaveform(TaskHandle task, int32 numSampsPerChan, float64 timeout, bool32 fillMode, int64 t0Array[], int64 dtArray[], uInt32 timingArraySize, DAQmxSetWfmAttrCallbackPtr setWfmAttrCallback, void* setWfmAttrCallbackData, uInt8 readArray[], uInt32 arraySizeInBytes, int32* sampsPerChanRead, int32* numBytesPerSamp, uInt32 bytesPerChanArray[], uInt32 bytesPerChanArraySize, bool32* reserved) = 0; + virtual int32 InternalWriteAnalogWaveformPerChan(TaskHandle task, int32 numSampsPerChan, bool32 autoStart, float64 timeout, const float64 * const writeArrayPtrs[], uInt32 writeArrayCount, int32* sampsPerChanWritten, bool32* reserved) = 0; + virtual int32 InternalWriteDigitalWaveform(TaskHandle task, int32 numSampsPerChan, bool32 autoStart, float64 timeout, bool32 dataLayout, const uInt8 writeArray[], const uInt32 bytesPerChanArray[], uInt32 bytesPerChanArraySize, int32* sampsPerChanWritten, bool32* reserved) = 0; virtual int32 IsTaskDone(TaskHandle task, bool32* isTaskDone) = 0; virtual int32 LoadTask(const char sessionName[], TaskHandle* task) = 0; virtual int32 PerformBridgeOffsetNullingCalEx(TaskHandle task, const char channel[], bool32 skipUnsupportedChannels) = 0; diff --git a/generated/nidaqmx/nidaqmx_mock_library.h b/generated/nidaqmx/nidaqmx_mock_library.h index 0037d5340..172f82509 100644 --- a/generated/nidaqmx/nidaqmx_mock_library.h +++ b/generated/nidaqmx/nidaqmx_mock_library.h @@ -253,6 +253,11 @@ class NiDAQmxMockLibrary : public nidaqmx_grpc::NiDAQmxLibraryInterface { MOCK_METHOD(int32, GetWriteAttributeString, (TaskHandle task, int32 attribute, char value[], uInt32 size), (override)); MOCK_METHOD(int32, GetWriteAttributeUInt32, (TaskHandle task, int32 attribute, uInt32* value), (override)); MOCK_METHOD(int32, GetWriteAttributeUInt64, (TaskHandle task, int32 attribute, uInt64* value), (override)); + MOCK_METHOD(int32, InternalGetLastCreatedChan, (char value[], uInt32 size), (override)); + MOCK_METHOD(int32, InternalReadAnalogWaveformPerChan, (TaskHandle task, int32 numSampsPerChan, float64 timeout, int64 t0Array[], int64 dtArray[], uInt32 timingArraySize, DAQmxSetWfmAttrCallbackPtr setWfmAttrCallback, void* setWfmAttrCallbackData, float64 * readArrayPtrs[], uInt32 readArrayCount, uInt32 arraySizeInSampsPerChan, int32* sampsPerChanRead, bool32* reserved), (override)); + int32 InternalReadDigitalWaveform(TaskHandle task, int32 numSampsPerChan, float64 timeout, bool32 fillMode, int64 t0Array[], int64 dtArray[], uInt32 timingArraySize, DAQmxSetWfmAttrCallbackPtr setWfmAttrCallback, void* setWfmAttrCallbackData, uInt8 readArray[], uInt32 arraySizeInBytes, int32* sampsPerChanRead, int32* numBytesPerSamp, uInt32 bytesPerChanArray[], uInt32 bytesPerChanArraySize, bool32* reserved) { throw std::runtime_error("Not implemented."); } + MOCK_METHOD(int32, InternalWriteAnalogWaveformPerChan, (TaskHandle task, int32 numSampsPerChan, bool32 autoStart, float64 timeout, const float64 * const writeArrayPtrs[], uInt32 writeArrayCount, int32* sampsPerChanWritten, bool32* reserved), (override)); + MOCK_METHOD(int32, InternalWriteDigitalWaveform, (TaskHandle task, int32 numSampsPerChan, bool32 autoStart, float64 timeout, bool32 dataLayout, const uInt8 writeArray[], const uInt32 bytesPerChanArray[], uInt32 bytesPerChanArraySize, int32* sampsPerChanWritten, bool32* reserved), (override)); MOCK_METHOD(int32, IsTaskDone, (TaskHandle task, bool32* isTaskDone), (override)); MOCK_METHOD(int32, LoadTask, (const char sessionName[], TaskHandle* task), (override)); MOCK_METHOD(int32, PerformBridgeOffsetNullingCalEx, (TaskHandle task, const char channel[], bool32 skipUnsupportedChannels), (override)); diff --git a/generated/nidaqmx/nidaqmx_service.h b/generated/nidaqmx/nidaqmx_service.h index 9f1655df4..493fbc2c8 100644 --- a/generated/nidaqmx/nidaqmx_service.h +++ b/generated/nidaqmx/nidaqmx_service.h @@ -538,6 +538,7 @@ class NiDAQmxService final : public NiDAQmx::WithCallbackMethod_RegisterSignalEv ::grpc::Status BeginWriteRaw(::grpc::ServerContext* context, const BeginWriteRawRequest* request, BeginWriteRawResponse* response) override; ::grpc::Status WriteToTEDSFromArray(::grpc::ServerContext* context, const WriteToTEDSFromArrayRequest* request, WriteToTEDSFromArrayResponse* response) override; ::grpc::Status WriteToTEDSFromFile(::grpc::ServerContext* context, const WriteToTEDSFromFileRequest* request, WriteToTEDSFromFileResponse* response) override; + ::grpc::Status ReadAnalogWaveforms(::grpc::ServerContext* context, const ReadAnalogWaveformsRequest* request, ReadAnalogWaveformsResponse* response) override; private: LibrarySharedPtr library_; ResourceRepositorySharedPtr session_repository_; diff --git a/imports/include/NIDAQmxInternalWaveform.h b/imports/include/NIDAQmxInternalWaveform.h new file mode 100644 index 000000000..0ebf9fbe5 --- /dev/null +++ b/imports/include/NIDAQmxInternalWaveform.h @@ -0,0 +1,57 @@ +#ifndef ___nicai_NIDAQmxInternal_h___ +#define ___nicai_NIDAQmxInternal_h___ +#include "NIDAQmx.h" +#ifdef __cplusplus +extern "C" +{ +#endif +/******************************************************/ +/*** Read Data ***/ +/******************************************************/ +#define DAQmx_Val_WfmAttrType_Bool32 1 +#define DAQmx_Val_WfmAttrType_Float64 2 +#define DAQmx_Val_WfmAttrType_Int32 3 +#define DAQmx_Val_WfmAttrType_String 4 + // To retrieve waveform attributes, provide this optional callback: + // - attributeName is "NI_ChannelName", "NI_UnitDescription", etc. + // - attributeType uses the WfmAttrType enum. + // - Boolean and numeric values are in native byte order. + // - String values are in the encoding used by the DLL (MBCS for nicaiu.dll, UTF-8 for nicai_utf8.dll). + // - callbackData is used to pass an object instance into the callback. + // - The callback returns an error code. + typedef int32(CVICALLBACK *DAQmxSetWfmAttrCallbackPtr)(uInt32 channelIndex, const char attributeName[], int32 attributeType, const void *value, uInt32 valueSizeInBytes, void *callbackData); + // int64 t0 and dt use the same format as .NET System.DateTime and System.TimeSpan: 100 ns ticks + // with an epoch of Jan 1, 0001. The t0 and dt arrays are optional and may be NULL. + int32 __CFUNC DAQmxInternalReadAnalogWaveformPerChan(TaskHandle taskHandle, int32 numSampsPerChan, float64 timeout, int64 t0Array[], int64 dtArray[], uInt32 timingArraySize, DAQmxSetWfmAttrCallbackPtr setWfmAttrCallback, void *setWfmAttrCallbackData, float64 *readArrayPtrs[], uInt32 readArrayCount, uInt32 arraySizeInSampsPerChan, int32 *sampsPerChanRead, bool32 *reserved); + // DAQmxInternalReadDigitalWaveform reverses the order of lines within ports. For example, + // DAQmxReadDigitalLines expands Dev1/port0 into Dev1/port0/line0:31 (little-endian), but + // DAQmxInternalReadDigitalWaveform expands Dev1/port0 into Dev1/port0/line31:0 (big-endian). This + // matches the data layout of the digital waveform datatype in LabVIEW and .NET. + // + // Depending on fillMode, readArray is assumed to be in the format (numChans x numSampsPerChan x + // maxDataWidth) or (numSampsPerChan x numChans x maxDataWidth), where numChans = ReadNumChans and + // maxDataWidth = ReadDigitalLinesBytesPerChan. + // + // If bytesPerChanArray is specified, this function uses it to return DINumLines[i], to enable + // resizing waveform buffers efficiently. This function does not validate expected data widths. + int32 __CFUNC DAQmxInternalReadDigitalWaveform(TaskHandle taskHandle, int32 numSampsPerChan, float64 timeout, bool32 fillMode, int64 t0Array[], int64 dtArray[], uInt32 timingArraySize, DAQmxSetWfmAttrCallbackPtr setWfmAttrCallback, void *setWfmAttrCallbackData, uInt8 readArray[], uInt32 arraySizeInBytes, int32 *sampsPerChanRead, int32 *numBytesPerSamp, uInt32 bytesPerChanArray[], uInt32 bytesPerChanArraySize, bool32 *reserved); + /******************************************************/ + /*** Write Data ***/ + /******************************************************/ + int32 __CFUNC DAQmxInternalWriteAnalogWaveformPerChan(TaskHandle taskHandle, int32 numSampsPerChan, bool32 autoStart, float64 timeout, const float64 *const writeArrayPtrs[], uInt32 writeArrayCount, int32 *sampsPerChanWritten, bool32 *reserved); + // DAQmxInternalWriteDigitalWaveform reverses the order of lines within ports. See comments about + // DAQmxInternalReadDigitalWaveform, above. + // + // Depending on dataLayout, writeArray is assumed to be in the format (numChans x numSampsPerChan x + // maxDataWidth) or (numSampsPerChan x numChans x maxDataWidth), where numChans = WriteNumChans and + // maxDataWidth = WriteDigitalLinesPerChan. + // + // If bytesPerChanArray is specified, this function validates expected number of channels and + // expected data width per channel: bytesPerArraySize == WriteNumChans and bytesPerChanArray[i] == + // DONumLines[i]. Each channel's data must still be padded to maxDataWidth. If not specified, the + // data is assumed to be in the correct format and no validation is performed. + int32 __CFUNC DAQmxInternalWriteDigitalWaveform(TaskHandle taskHandle, int32 numSampsPerChan, bool32 autoStart, float64 timeout, bool32 dataLayout, const uInt8 writeArray[], const uInt32 bytesPerChanArray[], uInt32 bytesPerChanArraySize, int32 *sampsPerChanWritten, bool32 *reserved); +#ifdef __cplusplus +} +#endif +#endif // ___nicai_NIDAQmxInternal_h___ diff --git a/source/codegen/common_helpers.py b/source/codegen/common_helpers.py index 9c6e5ec40..cb75bb3e0 100644 --- a/source/codegen/common_helpers.py +++ b/source/codegen/common_helpers.py @@ -478,7 +478,12 @@ def pascal_to_snake(pascal_string): def filter_proto_rpc_functions(functions): """Return function metadata only for functions to include for generating proto rpc methods.""" - functions_for_proto = {"public", "CustomCode", "CustomCodeCustomProtoMessage"} + functions_for_proto = { + "public", + "CustomCode", + "CustomCodeCustomProtoMessage", + "CustomCodeNoLibrary", + } return [ name for name, function in functions.items() @@ -488,7 +493,7 @@ def filter_proto_rpc_functions(functions): def filter_proto_rpc_functions_for_message(functions): """Return function metadata only for functions to include for generating proto rpc messages.""" - functions_for_proto = {"public", "CustomCode"} + functions_for_proto = {"public", "CustomCode", "CustomCodeNoLibrary"} return [ name for name, function in functions.items() diff --git a/source/codegen/metadata/nidaqmx/__init__.py b/source/codegen/metadata/nidaqmx/__init__.py index e79f8aacd..f256cee15 100644 --- a/source/codegen/metadata/nidaqmx/__init__.py +++ b/source/codegen/metadata/nidaqmx/__init__.py @@ -1,8 +1,8 @@ from .functions import functions -from .functions_addon import functions_validation_suppressions +from .functions_addon import functions_validation_suppressions, functions_override_metadata from .attributes import attributes from .enums import enums -from .enums_addon import enums_validation_suppressions +from .enums_addon import enums_validation_suppressions, enums_override_metadata from .config import config metadata = { @@ -13,3 +13,5 @@ "enums_validation_suppressions": enums_validation_suppressions, "config" : config } +metadata['functions'].update(functions_override_metadata) +metadata['enums'].update(enums_override_metadata) diff --git a/source/codegen/metadata/nidaqmx/config.py b/source/codegen/metadata/nidaqmx/config.py index 3a2e9a624..c9e019420 100644 --- a/source/codegen/metadata/nidaqmx/config.py +++ b/source/codegen/metadata/nidaqmx/config.py @@ -2,6 +2,7 @@ config = { 'api_version': '23.0.0', 'c_header': 'NIDAQmx.h', + 'additional_headers': { 'NIDAQmxInternalWaveform.h': ['library_interface.h'] }, 'c_function_prefix': 'DAQmx', 'service_class_prefix': 'NiDAQmx', 'java_package': 'com.ni.grpc.nidaqmx', @@ -122,7 +123,7 @@ 'CVIAbsoluteTime': 'google.protobuf.Timestamp' }, 'has_moniker_streaming_apis': True, - 'additional_protos': ['data_moniker.proto'], + 'additional_protos': ['data_moniker.proto', 'ni/protobuf/types/waveform.proto'], 'split_attributes_by_type': True, 'supports_raw_attributes': True, 'code_readiness': 'Release', diff --git a/source/codegen/metadata/nidaqmx/enums_addon.py b/source/codegen/metadata/nidaqmx/enums_addon.py index dbb111db9..443350e5f 100644 --- a/source/codegen/metadata/nidaqmx/enums_addon.py +++ b/source/codegen/metadata/nidaqmx/enums_addon.py @@ -1,7 +1,33 @@ # These dictionaries are applied to the generated enums dictionary at build time # Any changes to the API should be made here. enums.py is code generated -enums_override_metadata = {} +enums_override_metadata = { + 'WaveformAttributeMode': { + 'values': [ + { + 'documentation': { + 'description': 'No waveform attributes returned.' + }, + 'name': 'NONE', + 'value': 0 + }, + { + 'documentation': { + 'description': 'Return timing attributes with waveforms.' + }, + 'name': 'TIMING', + 'value': 1 + }, + { + 'documentation': { + 'description': 'Return extended properties with waveforms.' + }, + 'name': 'EXTENDED_PROPERTIES', + 'value': 2 + } + ] + } +} enums_validation_suppressions = { "CouplingTypes": [ diff --git a/source/codegen/metadata/nidaqmx/functions.py b/source/codegen/metadata/nidaqmx/functions.py index da47c3f32..af109a8bc 100644 --- a/source/codegen/metadata/nidaqmx/functions.py +++ b/source/codegen/metadata/nidaqmx/functions.py @@ -17186,6 +17186,353 @@ 'python_codegen_method': 'CustomCode', 'returns': 'int32' }, + 'InternalGetLastCreatedChan': { + 'calling_convention': 'StdCall', + 'codegen_method': 'private', + 'parameters': [ + { + 'ctypes_data_type': 'ctypes.c_char_p', + 'direction': 'out', + 'name': 'value', + 'python_data_type': 'str', + 'size': { + 'mechanism': 'ivi-dance', + 'value': 'size' + }, + 'type': 'char[]' + }, + { + 'ctypes_data_type': 'ctypes.c_uint32', + 'direction': 'in', + 'name': 'size', + 'python_data_type': 'int', + 'type': 'uInt32' + } + ], + 'python_codegen_method': 'CustomCode', + 'returns': 'int32' + }, + 'InternalReadAnalogWaveformPerChan': { + 'calling_convention': 'StdCall', + 'codegen_method': 'private', + 'parameters': [ + { + 'direction': 'in', + 'name': 'task', + 'type': 'TaskHandle' + }, + { + 'direction': 'in', + 'name': 'numSampsPerChan', + 'type': 'int32' + }, + { + 'direction': 'in', + 'name': 'timeout', + 'type': 'float64' + }, + { + 'direction': 'out', + 'name': 't0Array', + 'size': { + 'mechanism': 'passed-in', + 'value': 'timingArraySize' + }, + 'type': 'int64[]' + }, + { + 'direction': 'out', + 'name': 'dtArray', + 'size': { + 'mechanism': 'passed-in', + 'value': 'timingArraySize' + }, + 'type': 'int64[]' + }, + { + 'direction': 'in', + 'name': 'timingArraySize', + 'type': 'uInt32' + }, + { + 'direction': 'in', + 'name': 'setWfmAttrCallback', + 'type': 'DAQmxSetWfmAttrCallbackPtr' + }, + { + 'direction': 'out', + 'name': 'setWfmAttrCallbackData', + 'type': 'void' + }, + { + 'direction': 'out', + 'is_list': True, + 'name': 'readArrayPtrs', + 'size': { + 'mechanism': 'passed-in', + 'value': 'readArrayCount' + }, + 'type': 'float64 *[]' + }, + { + 'direction': 'in', + 'name': 'readArrayCount', + 'type': 'uInt32' + }, + { + 'direction': 'in', + 'name': 'arraySizeInSampsPerChan', + 'type': 'uInt32' + }, + { + 'direction': 'out', + 'name': 'sampsPerChanRead', + 'type': 'int32' + }, + { + 'direction': 'in', + 'hardcoded_value': 'nullptr', + 'include_in_proto': False, + 'name': 'reserved', + 'pointer': True, + 'type': 'bool32' + } + ], + 'python_codegen_method': 'no', + 'returns': 'int32' + }, + 'InternalReadDigitalWaveform': { + 'calling_convention': 'StdCall', + 'codegen_method': 'private', + 'parameters': [ + { + 'direction': 'in', + 'name': 'task', + 'type': 'TaskHandle' + }, + { + 'direction': 'in', + 'name': 'numSampsPerChan', + 'type': 'int32' + }, + { + 'direction': 'in', + 'name': 'timeout', + 'type': 'float64' + }, + { + 'direction': 'in', + 'name': 'fillMode', + 'type': 'bool32' + }, + { + 'direction': 'out', + 'name': 't0Array', + 'size': { + 'mechanism': 'passed-in', + 'value': 'timingArraySize' + }, + 'type': 'int64[]' + }, + { + 'direction': 'out', + 'name': 'dtArray', + 'size': { + 'mechanism': 'passed-in', + 'value': 'timingArraySize' + }, + 'type': 'int64[]' + }, + { + 'direction': 'in', + 'name': 'timingArraySize', + 'type': 'uInt32' + }, + { + 'direction': 'in', + 'name': 'setWfmAttrCallback', + 'type': 'DAQmxSetWfmAttrCallbackPtr' + }, + { + 'direction': 'out', + 'name': 'setWfmAttrCallbackData', + 'type': 'void' + }, + { + 'direction': 'out', + 'is_list': True, + 'name': 'readArray', + 'size': { + 'mechanism': 'passed-in', + 'value': 'arraySizeInBytes' + }, + 'type': 'uInt8[]' + }, + { + 'direction': 'in', + 'name': 'arraySizeInBytes', + 'type': 'uInt32' + }, + { + 'direction': 'out', + 'name': 'sampsPerChanRead', + 'type': 'int32' + }, + { + 'direction': 'out', + 'name': 'numBytesPerSamp', + 'type': 'int32' + }, + { + 'direction': 'out', + 'is_list': True, + 'name': 'bytesPerChanArray', + 'size': { + 'mechanism': 'passed-in', + 'value': 'bytesPerChanArraySize' + }, + 'type': 'uInt32[]' + }, + { + 'direction': 'in', + 'name': 'bytesPerChanArraySize', + 'type': 'uInt32' + }, + { + 'direction': 'in', + 'hardcoded_value': 'nullptr', + 'include_in_proto': False, + 'name': 'reserved', + 'pointer': True, + 'type': 'bool32' + } + ], + 'python_codegen_method': 'no', + 'returns': 'int32' + }, + 'InternalWriteAnalogWaveformPerChan': { + 'calling_convention': 'StdCall', + 'codegen_method': 'private', + 'parameters': [ + { + 'direction': 'in', + 'name': 'task', + 'type': 'TaskHandle' + }, + { + 'direction': 'in', + 'name': 'numSampsPerChan', + 'type': 'int32' + }, + { + 'direction': 'in', + 'name': 'autoStart', + 'type': 'bool32' + }, + { + 'direction': 'in', + 'name': 'timeout', + 'type': 'float64' + }, + { + 'direction': 'in', + 'is_list': True, + 'name': 'writeArrayPtrs', + 'size': { + 'mechanism': 'len', + 'value': 'writeArrayCount' + }, + 'type': 'const float64 * const[]' + }, + { + 'direction': 'in', + 'name': 'writeArrayCount', + 'type': 'uInt32' + }, + { + 'direction': 'out', + 'name': 'sampsPerChanWritten', + 'type': 'int32' + }, + { + 'direction': 'in', + 'hardcoded_value': 'nullptr', + 'include_in_proto': False, + 'name': 'reserved', + 'pointer': True, + 'type': 'bool32' + } + ], + 'python_codegen_method': 'no', + 'returns': 'int32' + }, + 'InternalWriteDigitalWaveform': { + 'calling_convention': 'StdCall', + 'codegen_method': 'private', + 'parameters': [ + { + 'direction': 'in', + 'name': 'task', + 'type': 'TaskHandle' + }, + { + 'direction': 'in', + 'name': 'numSampsPerChan', + 'type': 'int32' + }, + { + 'direction': 'in', + 'name': 'autoStart', + 'type': 'bool32' + }, + { + 'direction': 'in', + 'name': 'timeout', + 'type': 'float64' + }, + { + 'direction': 'in', + 'name': 'dataLayout', + 'type': 'bool32' + }, + { + 'direction': 'in', + 'is_list': True, + 'name': 'writeArray', + 'type': 'const uInt8[]' + }, + { + 'direction': 'in', + 'is_list': True, + 'name': 'bytesPerChanArray', + 'size': { + 'mechanism': 'len', + 'value': 'bytesPerChanArraySize' + }, + 'type': 'const uInt32[]' + }, + { + 'direction': 'in', + 'name': 'bytesPerChanArraySize', + 'type': 'uInt32' + }, + { + 'direction': 'out', + 'name': 'sampsPerChanWritten', + 'type': 'int32' + }, + { + 'direction': 'in', + 'hardcoded_value': 'nullptr', + 'include_in_proto': False, + 'name': 'reserved', + 'pointer': True, + 'type': 'bool32' + } + ], + 'python_codegen_method': 'no', + 'returns': 'int32' + }, 'IsTaskDone': { 'calling_convention': 'StdCall', 'handle_parameter': { diff --git a/source/codegen/metadata/nidaqmx/functions_addon.py b/source/codegen/metadata/nidaqmx/functions_addon.py index fcdfbf1bd..1a345c111 100644 --- a/source/codegen/metadata/nidaqmx/functions_addon.py +++ b/source/codegen/metadata/nidaqmx/functions_addon.py @@ -1,4 +1,74 @@ functions_override_metadata = { + 'ReadAnalogWaveforms': { + 'returns': 'int32', + 'codegen_method': 'CustomCodeNoLibrary', + 'parameters': [ + { + 'ctypes_data_type': 'ctypes.TaskHandle', + 'direction': 'in', + 'is_optional_in_python': False, + 'name': 'task', + 'python_data_type': 'TaskHandle', + 'python_description': '', + 'python_type_annotation': 'TaskHandle', + 'type': 'TaskHandle' + }, + { + 'ctypes_data_type': 'ctypes.c_int', + 'direction': 'in', + 'is_optional_in_python': False, + 'name': 'numberOfSamplesPerChannel', + 'python_data_type': 'int', + 'python_description': '', + 'python_type_annotation': 'int', + 'type': 'int32' + }, + { + 'ctypes_data_type': 'ctypes.c_double', + 'direction': 'in', + 'is_optional_in_python': True, + 'name': 'timeout', + 'python_data_type': 'float', + 'python_default_value': '10.0', + 'python_description': 'Specifies the time in seconds to wait for the device to respond before timing out.', + 'python_type_annotation': 'Optional[float]', + 'type': 'float64' + }, + { + 'ctypes_data_type': 'ctypes.c_int', + 'direction': 'in', + 'enum': 'WaveformAttributeMode', + 'is_optional_in_python': True, + 'name': 'waveformAttributeMode', + 'python_data_type': 'WaveformAttributeMode', + 'python_default_value': 'WaveformAttributeMode.NONE', + 'python_description': 'Specifies which waveform attributes to return with the waveforms.', + 'python_type_annotation': 'Optional[nidaqmx.constants.WaveformAttributeMode]', + 'type': 'int32' + }, + { + 'direction': 'out', + 'is_optional_in_python': False, + 'name': 'waveforms', + 'python_data_type': 'object', + 'python_description': 'The waveforms read from the specified channels.', + 'python_type_annotation': 'List[object]', + 'type': 'repeated ni.protobuf.types.DoubleAnalogWaveform' + }, + { + 'ctypes_data_type': 'ctypes.c_int', + 'direction': 'out', + 'is_optional_in_python': False, + 'is_streaming_type': True, + 'name': 'sampsPerChanRead', + 'python_data_type': 'int', + 'python_description': '', + 'python_type_annotation': 'int', + 'return_on_error_key': 'ni-samps-per-chan-read', + 'type': 'int32' + } + ] + } } functions_validation_suppressions = { @@ -22,6 +92,18 @@ 'highTime': ['ARRAY_PARAMETER_NEEDS_SIZE'], 'lowTime': ['ARRAY_PARAMETER_NEEDS_SIZE'], } + }, + 'InternalWriteDigitalWaveform': { + 'parameters': { + # size is determined by numSampsPerChan and how many channels are in the task + 'writeArray': ['ARRAY_PARAMETER_NEEDS_SIZE'], + } + }, + 'ReadAnalogWaveforms': { + 'parameters': { + # size is determined by the number of channels in the task + 'waveforms': ['ARRAY_PARAMETER_NEEDS_SIZE'] + } } } diff --git a/source/codegen/metadata_validation.py b/source/codegen/metadata_validation.py index d6cf594aa..7fdc0ef90 100644 --- a/source/codegen/metadata_validation.py +++ b/source/codegen/metadata_validation.py @@ -145,6 +145,7 @@ class RULES: "no", "python-only", "CustomCodeCustomProtoMessage", + "CustomCodeNoLibrary", ), ), Optional("python_codegen_method"): And( diff --git a/source/codegen/service_helpers.py b/source/codegen/service_helpers.py index dd69e4211..498946e8c 100644 --- a/source/codegen/service_helpers.py +++ b/source/codegen/service_helpers.py @@ -322,7 +322,9 @@ def filter_api_functions(functions, only_mockable_functions=True): """Filter function metadata to only include those to be generated into the API library.""" def filter_function(function): - if function.get("codegen_method", "") == "no" or function.get("is_streaming_api", False): + if function.get("codegen_method", "") in ["no", "CustomCodeNoLibrary"] or function.get( + "is_streaming_api", False + ): return False if only_mockable_functions and not common_helpers.can_mock_function(function["parameters"]): return False diff --git a/source/codegen/validate_examples.py b/source/codegen/validate_examples.py index ec218b966..e9c0b796d 100644 --- a/source/codegen/validate_examples.py +++ b/source/codegen/validate_examples.py @@ -72,7 +72,7 @@ def _validate_examples( _system("poetry install") _system( - rf"poetry run python -m grpc_tools.protoc -I{proto_dir} -I{ni_apis_root}/ni/grpcdevice/v1/ --python_out=. --grpc_python_out=. --mypy_out=. --mypy_grpc_out=. {proto_files_str}" + rf"poetry run python -m grpc_tools.protoc -I{proto_dir} -I{ni_apis_root}/ni/grpcdevice/v1/ -I{ni_apis_root}/ --python_out=. --grpc_python_out=. --mypy_out=. --mypy_grpc_out=. {proto_files_str}" ) for dir in examples_dir.glob(driver_glob_expression): if exclude and re.search(exclude, dir.as_posix()): diff --git a/source/custom/nidaqmx_service.custom.cpp b/source/custom/nidaqmx_service.custom.cpp index b20b55ab6..437a746df 100644 --- a/source/custom/nidaqmx_service.custom.cpp +++ b/source/custom/nidaqmx_service.custom.cpp @@ -1,7 +1,98 @@ #include +#include +#include +#include +#include "NIDAQmxInternalWaveform.h" namespace nidaqmx_grpc { +constexpr int64 TICKS_PER_SECOND = 10000000; // 100ns ticks per second +constexpr double TICKS_TO_SECONDS = 1e-7; // Convert 100ns ticks to seconds + +// Returns true if it's safe to use outputs of a method with the given status. +inline bool status_ok(int32 status) +{ + return status >= 0; +} + +// Callback data structure to pass waveform pointers to the callback +struct WaveformCallbackData { + std::vector<::ni::protobuf::types::DoubleAnalogWaveform*> waveforms; +}; + +// Callback function for setting waveform attributes +int32 CVICALLBACK SetWfmAttrCallback( + const uInt32 channel_index, + const char attribute_name[], + const int32 attribute_type, + const void* value, + const uInt32 value_size_in_bytes, + void* callback_data) +{ + try { + const auto* data = static_cast(callback_data); + if (!data || channel_index >= data->waveforms.size()) { + return -1; + } + + auto* waveform = data->waveforms[channel_index]; + if (!waveform) { + return -1; + } + + auto* attributes = waveform->mutable_attributes(); + ::ni::protobuf::types::WaveformAttributeValue attr_value; + + switch (attribute_type) { + case DAQmx_Val_WfmAttrType_Bool32: + if (value_size_in_bytes == sizeof(int32)) { + const bool bool_val = (*static_cast(value)) != 0; + attr_value.set_bool_value(bool_val); + } else { + return -1; + } + break; + + case DAQmx_Val_WfmAttrType_Float64: + if (value_size_in_bytes == sizeof(double)) { + const double double_val = *static_cast(value); + attr_value.set_double_value(double_val); + } else { + return -1; + } + break; + + case DAQmx_Val_WfmAttrType_Int32: + if (value_size_in_bytes == sizeof(int32)) { + const int32 int_val = *static_cast(value); + attr_value.set_integer_value(int_val); + } else { + return -1; + } + break; + + case DAQmx_Val_WfmAttrType_String: + if (value_size_in_bytes > 0) { + const char* str_val = static_cast(value); + const std::string string_val(str_val, value_size_in_bytes - 1); + attr_value.set_string_value(string_val); + } else { + return -1; + } + break; + + default: + return -1; + } + + (*attributes)[attribute_name] = attr_value; + return 0; + } + catch (...) { + return -1; + } +} + ::grpc::Status NiDAQmxService::ConvertApiErrorStatusForTaskHandle(::grpc::ServerContextBase* context, int32_t status, TaskHandle task) { // This implementation assumes this method is always called on the same thread where the error occurred. @@ -10,4 +101,139 @@ ::grpc::Status NiDAQmxService::ConvertApiErrorStatusForTaskHandle(::grpc::Server return nidevice_grpc::ApiErrorAndDescriptionToStatus(context, status, description); } +::grpc::Status NiDAQmxService::ReadAnalogWaveforms(::grpc::ServerContext* context, const ReadAnalogWaveformsRequest* request, ReadAnalogWaveformsResponse* response) +{ + if (context->IsCancelled()) { + return ::grpc::Status::CANCELLED; + } + try { + auto task_grpc_session = request->task(); + TaskHandle task = session_repository_->access_session(task_grpc_session.name()); + + const auto number_of_samples_per_channel = request->number_of_samples_per_channel(); + const auto timeout = request->timeout(); + + int32 waveform_attribute_mode; + switch (request->waveform_attribute_mode_enum_case()) { + case nidaqmx_grpc::ReadAnalogWaveformsRequest::WaveformAttributeModeEnumCase::kWaveformAttributeMode: { + waveform_attribute_mode = static_cast(request->waveform_attribute_mode()); + break; + } + case nidaqmx_grpc::ReadAnalogWaveformsRequest::WaveformAttributeModeEnumCase::kWaveformAttributeModeRaw: { + waveform_attribute_mode = static_cast(request->waveform_attribute_mode_raw()); + break; + } + case nidaqmx_grpc::ReadAnalogWaveformsRequest::WaveformAttributeModeEnumCase::WAVEFORM_ATTRIBUTE_MODE_ENUM_NOT_SET: { + return ::grpc::Status(::grpc::INVALID_ARGUMENT, "The value for waveform_attribute_mode was not specified or out of range"); + break; + } + } + + uInt32 num_channels = 0; + auto status = library_->GetReadAttributeUInt32(task, ReadUInt32Attribute::READ_ATTRIBUTE_NUM_CHANS, &num_channels); + if (!status_ok(status)) { + return ConvertApiErrorStatusForTaskHandle(context, status, task); + } + + if (num_channels == 0) { + return ::grpc::Status(::grpc::INVALID_ARGUMENT, "No channels found in task"); + } + + std::vector> read_arrays(num_channels); + std::vector read_array_ptrs(num_channels); + + for (uInt32 i = 0; i < num_channels; ++i) { + read_arrays[i].resize(number_of_samples_per_channel); + read_array_ptrs[i] = read_arrays[i].data(); + } + + std::vector t0_array, dt_array; + int64* t0_ptr = nullptr; + int64* dt_ptr = nullptr; + uInt32 timing_array_size = 0; + + if (waveform_attribute_mode & WaveformAttributeMode::WAVEFORM_ATTRIBUTE_MODE_TIMING) { + t0_array.resize(num_channels, 0); + dt_array.resize(num_channels, 0); + t0_ptr = t0_array.data(); + dt_ptr = dt_array.data(); + timing_array_size = num_channels; + } + + std::unique_ptr callback_data; + DAQmxSetWfmAttrCallbackPtr callback_ptr = nullptr; + + for (uInt32 i = 0; i < num_channels; ++i) { + response->add_waveforms(); + } + + if (waveform_attribute_mode & WaveformAttributeMode::WAVEFORM_ATTRIBUTE_MODE_EXTENDED_PROPERTIES) { + callback_data = std::make_unique(); + callback_data->waveforms.resize(num_channels); + callback_ptr = SetWfmAttrCallback; + + for (uInt32 i = 0; i < num_channels; ++i) { + callback_data->waveforms[i] = response->mutable_waveforms(i); + } + } + + int32 samples_per_chan_read = 0; + status = library_->InternalReadAnalogWaveformPerChan( + task, + number_of_samples_per_channel, + timeout, + t0_ptr, + dt_ptr, + timing_array_size, + callback_ptr, + callback_data.get(), + read_array_ptrs.data(), + num_channels, + number_of_samples_per_channel, + &samples_per_chan_read, + nullptr + ); + + if (!status_ok(status)) { + return ConvertApiErrorStatusForTaskHandle(context, status, task); + } + + for (uInt32 i = 0; i < num_channels; ++i) { + auto* waveform = response->mutable_waveforms(i); + + auto* y_data = waveform->mutable_y_data(); + y_data->Reserve(samples_per_chan_read); + for (int32 j = 0; j < samples_per_chan_read; ++j) { + y_data->Add(read_arrays[i][j]); + } + + if (waveform_attribute_mode & WaveformAttributeMode::WAVEFORM_ATTRIBUTE_MODE_TIMING) { + if (dt_array[i] == 0) { + return ::grpc::Status(::grpc::FAILED_PRECONDITION, + "Timing information requested but not available. Task must be configured with sample clock timing (e.g., CfgSampClkTiming) to provide timing information."); + } + + auto* t0 = waveform->mutable_t0(); + // Convert from 100ns ticks (DAQmx format) to PrecisionTimestamp + // t0_array[i] contains 100ns ticks since Jan 1, 0001 (.NET DateTime epoch) + const int64_t seconds = t0_array[i] / TICKS_PER_SECOND; + const int64_t fractional_ticks = t0_array[i] % TICKS_PER_SECOND; + + t0->set_seconds(seconds); + t0->set_fractional_seconds(fractional_ticks); + + // Set sample interval (dt) - convert 100ns ticks to seconds + waveform->set_dt(static_cast(dt_array[i]) * TICKS_TO_SECONDS); + } + } + + response->set_samps_per_chan_read(samples_per_chan_read); + response->set_status(status); + return ::grpc::Status::OK; + } + catch (nidevice_grpc::NonDriverException& ex) { + return ex.GetStatus(); + } +} + } // namespace nidaqmx_grpc diff --git a/source/tests/system/nidaqmx_driver_api_tests.cpp b/source/tests/system/nidaqmx_driver_api_tests.cpp index fd08ff950..75f2c671c 100644 --- a/source/tests/system/nidaqmx_driver_api_tests.cpp +++ b/source/tests/system/nidaqmx_driver_api_tests.cpp @@ -2,6 +2,7 @@ #include #include +#include #include #include #include @@ -65,6 +66,7 @@ class NiDAQmxDriverApiTests : public Test { const std::string DEVICE_NAME{"gRPCSystemTestDAQ"}; const std::string ANY_DEVICE_MODEL{"[[ANY_DEVICE_MODEL]]"}; const std::string AI_CHANNEL{"gRPCSystemTestDAQ/ai0"}; + const std::string AI_CHANNEL_1{"gRPCSystemTestDAQ/ai1"}; const std::string AO_CHANNEL{"gRPCSystemTestDAQ/ao0"}; NiDAQmxDriverApiTests() @@ -178,6 +180,19 @@ class NiDAQmxDriverApiTests : public Test { return create_ai_voltage_chan(request, response); } + ::grpc::Status create_two_ai_voltage_chans(double min_val, double max_val, CreateAIVoltageChanResponse& response = ThrowawayResponse::response()) + { + CreateAIVoltageChanRequest request; + set_request_session_name(request); + request.set_physical_channel("gRPCSystemTestDAQ/ai0:1"); // This creates channels ai0 and ai1 + request.set_name_to_assign_to_channel("ai0:1"); + request.set_terminal_config(InputTermCfgWithDefault::INPUT_TERM_CFG_WITH_DEFAULT_CFG_DEFAULT); + request.set_min_val(min_val); + request.set_max_val(max_val); + request.set_units(VoltageUnits2::VOLTAGE_UNITS2_VOLTS); + return create_ai_voltage_chan(request, response); + } + CreateAIBridgeChanRequest create_ai_bridge_request(double min_val, double max_val, const std::string& custom_scale_name = "") { CreateAIBridgeChanRequest request; @@ -422,6 +437,23 @@ class NiDAQmxDriverApiTests : public Test { return reader; } + ::grpc::Status read_analog_waveforms( + int32 number_of_samples_per_channel, + double timeout = 10.0, + WaveformAttributeMode waveform_attribute_mode = WaveformAttributeMode::WAVEFORM_ATTRIBUTE_MODE_NONE, + ReadAnalogWaveformsResponse& response = ThrowawayResponse::response()) + { + ::grpc::ClientContext context; + ReadAnalogWaveformsRequest request; + set_request_session_name(request); + request.set_number_of_samples_per_channel(number_of_samples_per_channel); + request.set_timeout(timeout); + request.set_waveform_attribute_mode(waveform_attribute_mode); + auto status = stub()->ReadAnalogWaveforms(&context, request, &response); + client::raise_if_error(status, context); + return status; + } + ::grpc::Status write_analog_f64( const std::vector& data, WriteAnalogF64Response& response) @@ -1479,6 +1511,198 @@ TEST_F(NiDAQmxDriverApiTests, SetPolynomialForwardCoefficients_GetPolynomialForw EXPECT_THAT(actual, ContainerEq(COEFFICIENTS)); } +TEST_F(NiDAQmxDriverApiTests, ReadAnalogWaveforms_WithNoAttributeMode_ReturnsWaveformData) +{ + const auto NUM_SAMPLES = 1000; + const auto TIMEOUT = 10.0; + CreateAIVoltageChanResponse create_channel_response; + auto create_channel_status = create_two_ai_voltage_chans(-1.0, 1.0, create_channel_response); + EXPECT_SUCCESS(create_channel_status, create_channel_response); + + start_task(); + ReadAnalogWaveformsResponse read_response; + auto read_status = read_analog_waveforms(NUM_SAMPLES, TIMEOUT, WaveformAttributeMode::WAVEFORM_ATTRIBUTE_MODE_NONE, read_response); + stop_task(); + + EXPECT_SUCCESS(read_status, read_response); + EXPECT_EQ(read_response.status(), DAQMX_SUCCESS); + EXPECT_EQ(read_response.waveforms_size(), 2); + EXPECT_EQ(read_response.samps_per_chan_read(), NUM_SAMPLES); + + for (int i = 0; i < read_response.waveforms_size(); ++i) { + const auto& waveform = read_response.waveforms(i); + EXPECT_EQ(waveform.y_data_size(), NUM_SAMPLES); + + for (const auto& sample : waveform.y_data()) { + EXPECT_GE(sample, -1.0); + EXPECT_LE(sample, 1.0); + } + } +} + +TEST_F(NiDAQmxDriverApiTests, ReadAnalogWaveforms_WithTimingMode_ReturnsWaveformDataWithTimingInfo) +{ + const auto NUM_SAMPLES = 100; + const auto TIMEOUT = 10.0; + CreateAIVoltageChanResponse create_channel_response; + auto create_channel_status = create_two_ai_voltage_chans(-5.0, 5.0, create_channel_response); + EXPECT_SUCCESS(create_channel_status, create_channel_response); + + auto timing_request = create_cfg_samp_clk_timing_request(1000.0, Edge1::EDGE1_RISING, AcquisitionType::ACQUISITION_TYPE_FINITE_SAMPS, NUM_SAMPLES); + CfgSampClkTimingResponse timing_response; + auto timing_status = cfg_samp_clk_timing(timing_request, timing_response); + EXPECT_SUCCESS(timing_status, timing_response); + + start_task(); + ReadAnalogWaveformsResponse read_response; + auto read_status = read_analog_waveforms(NUM_SAMPLES, TIMEOUT, WaveformAttributeMode::WAVEFORM_ATTRIBUTE_MODE_TIMING, read_response); + stop_task(); + + EXPECT_SUCCESS(read_status, read_response); + EXPECT_EQ(read_response.status(), DAQMX_SUCCESS); + EXPECT_EQ(read_response.waveforms_size(), 2); + EXPECT_EQ(read_response.samps_per_chan_read(), NUM_SAMPLES); + + for (int i = 0; i < read_response.waveforms_size(); ++i) { + const auto& waveform = read_response.waveforms(i); + EXPECT_EQ(waveform.y_data_size(), NUM_SAMPLES); + EXPECT_TRUE(waveform.has_t0()); + + // Get current time in seconds since year 1 AD (Jan 1, 0001) - .NET DateTime epoch + // This matches the format used by DAQmxInternalReadAnalogWaveformPerChan + // From year 1 AD to 1970-01-01 is 719162 days * 24 * 3600 = 62135596800 seconds + const auto epoch_offset_year1_to_1970 = 62135596800LL; + auto now = std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch()).count(); + auto now_since_year1 = now + epoch_offset_year1_to_1970; + const auto& timestamp = waveform.t0(); + EXPECT_NEAR(timestamp.seconds(), now_since_year1, 1); + EXPECT_NE(timestamp.fractional_seconds(), 0); + + EXPECT_GT(waveform.dt(), 0.0); + + for (const auto& sample : waveform.y_data()) { + EXPECT_GE(sample, -5.0); + EXPECT_LE(sample, 5.0); + } + } +} + +TEST_F(NiDAQmxDriverApiTests, ReadAnalogWaveforms_WithExtendedPropertiesMode_ReturnsWaveformDataWithAttributes) +{ + const auto NUM_SAMPLES = 50; + const auto TIMEOUT = 10.0; + CreateAIVoltageChanResponse create_channel_response; + auto create_channel_status = create_two_ai_voltage_chans(-2.0, 2.0, create_channel_response); + EXPECT_SUCCESS(create_channel_status, create_channel_response); + + start_task(); + ReadAnalogWaveformsResponse read_response; + auto read_status = read_analog_waveforms(NUM_SAMPLES, TIMEOUT, WaveformAttributeMode::WAVEFORM_ATTRIBUTE_MODE_EXTENDED_PROPERTIES, read_response); + stop_task(); + + EXPECT_SUCCESS(read_status, read_response); + EXPECT_EQ(read_response.status(), DAQMX_SUCCESS); + EXPECT_EQ(read_response.waveforms_size(), 2); + EXPECT_EQ(read_response.samps_per_chan_read(), NUM_SAMPLES); + + for (int i = 0; i < read_response.waveforms_size(); ++i) { + const auto& waveform = read_response.waveforms(i); + EXPECT_EQ(waveform.y_data_size(), NUM_SAMPLES); + EXPECT_GT(waveform.attributes_size(), 0); + + bool has_channel_name = false; + bool has_units = false; + std::string expected_channel_name = (i == 0) ? "ai0" : "ai1"; + + for (const auto& attr_pair : waveform.attributes()) { + const auto& attr_name = attr_pair.first; + const auto& attr_value = attr_pair.second; + + if (attr_name == "NI_ChannelName" && attr_value.has_string_value()) { + if (attr_value.string_value() == expected_channel_name) { + has_channel_name = true; + } + } + if (attr_name == "NI_UnitDescription" && attr_value.has_string_value()) { + if (attr_value.string_value() == "Volts") { + has_units = true; + } + } + } + EXPECT_TRUE(has_channel_name); + EXPECT_TRUE(has_units); + + for (const auto& sample : waveform.y_data()) { + EXPECT_GE(sample, -2.0); + EXPECT_LE(sample, 2.0); + } + } +} + +TEST_F(NiDAQmxDriverApiTests, ReadAnalogWaveforms_WithTimingAndExtendedPropertiesMode_ReturnsWaveformDataWithTimingAndAttributes) +{ + const auto NUM_SAMPLES = 75; + const auto TIMEOUT = 10.0; + CreateAIVoltageChanResponse create_channel_response; + auto create_channel_status = create_two_ai_voltage_chans(-3.0, 3.0, create_channel_response); + EXPECT_SUCCESS(create_channel_status, create_channel_response); + + auto timing_request = create_cfg_samp_clk_timing_request(2000.0, Edge1::EDGE1_RISING, AcquisitionType::ACQUISITION_TYPE_FINITE_SAMPS, NUM_SAMPLES); + CfgSampClkTimingResponse timing_response; + auto timing_status = cfg_samp_clk_timing(timing_request, timing_response); + EXPECT_SUCCESS(timing_status, timing_response); + + start_task(); + ReadAnalogWaveformsResponse read_response; + const auto combined_mode = static_cast( + static_cast(WaveformAttributeMode::WAVEFORM_ATTRIBUTE_MODE_TIMING) | + static_cast(WaveformAttributeMode::WAVEFORM_ATTRIBUTE_MODE_EXTENDED_PROPERTIES) + ); + auto read_status = read_analog_waveforms(NUM_SAMPLES, TIMEOUT, combined_mode, read_response); + stop_task(); + + EXPECT_SUCCESS(read_status, read_response); + EXPECT_EQ(read_response.status(), DAQMX_SUCCESS); + EXPECT_EQ(read_response.waveforms_size(), 2); + EXPECT_EQ(read_response.samps_per_chan_read(), NUM_SAMPLES); + + for (int i = 0; i < read_response.waveforms_size(); ++i) { + const auto& waveform = read_response.waveforms(i); + EXPECT_EQ(waveform.y_data_size(), NUM_SAMPLES); + EXPECT_TRUE(waveform.has_t0()); + EXPECT_GT(waveform.dt(), 0.0); + EXPECT_GT(waveform.attributes_size(), 0); + + bool has_channel_name = false; + bool has_units = false; + std::string expected_channel_name = (i == 0) ? "ai0" : "ai1"; + + for (const auto& attr_pair : waveform.attributes()) { + const auto& attr_name = attr_pair.first; + const auto& attr_value = attr_pair.second; + + if (attr_name == "NI_ChannelName" && attr_value.has_string_value()) { + if (attr_value.string_value() == expected_channel_name) { + has_channel_name = true; + } + } + if (attr_name == "NI_UnitDescription" && attr_value.has_string_value()) { + if (attr_value.string_value() == "Volts") { + has_units = true; + } + } + } + EXPECT_TRUE(has_channel_name); + EXPECT_TRUE(has_units); + + for (const auto& sample : waveform.y_data()) { + EXPECT_GE(sample, -3.0); + EXPECT_LE(sample, 3.0); + } + } +} + TEST_F(NiDAQmxDriverApiTests, AOVoltageChannel_WriteAOData_Succeeds) { const double AO_MIN = 1.0;