From 5e53c3684bd364d71512164d6595c848786ef59d Mon Sep 17 00:00:00 2001 From: Mike Prosser Date: Thu, 16 Oct 2025 13:32:52 -0500 Subject: [PATCH 01/25] Enhance NIDAQmx API with waveform support and additional metadata - Added ReadAnalogWaveforms method stub to NiDAQmxService for reading analog waveforms. - Updated metadata validation to include CustomCodeNoLibrary. - Introduced new waveform attributes and functions in metadata. - Enhanced CMake configuration for new protobuf files. - Improved CONTRIBUTING.md with Ninja build instructions. --- CMakeLists.txt | 46 ++- CONTRIBUTING.md | 12 + generated/nidaqmx/nidaqmx.proto | 23 ++ generated/nidaqmx/nidaqmx_client.cpp | 27 ++ generated/nidaqmx/nidaqmx_client.h | 1 + generated/nidaqmx/nidaqmx_library.cpp | 45 +++ generated/nidaqmx/nidaqmx_library.h | 15 + generated/nidaqmx/nidaqmx_library_interface.h | 6 + generated/nidaqmx/nidaqmx_mock_library.h | 5 + generated/nidaqmx/nidaqmx_service.h | 1 + imports/include/NIDAQmxInternalWaveform.h | 57 +++ source/codegen/common_helpers.py | 4 +- source/codegen/metadata/nidaqmx/__init__.py | 6 +- source/codegen/metadata/nidaqmx/config.py | 3 +- .../codegen/metadata/nidaqmx/enums_addon.py | 28 +- source/codegen/metadata/nidaqmx/functions.py | 347 ++++++++++++++++++ .../metadata/nidaqmx/functions_addon.py | 70 ++++ source/codegen/metadata_validation.py | 1 + source/codegen/service_helpers.py | 2 +- source/custom/nidaqmx_service.custom.cpp | 39 ++ 20 files changed, 728 insertions(+), 10 deletions(-) create mode 100644 imports/include/NIDAQmxInternalWaveform.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 62a9d1bf3..150655532 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -289,6 +289,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) #---------------------------------------------------------------------- @@ -302,9 +304,7 @@ function(GenerateGrpcSources) set(output_files "${GENERATE_ARGS_OUTPUT}") 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 + 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/ -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) @@ -372,6 +372,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 +445,32 @@ GenerateGrpcSources( "${data_moniker_grpc_hdrs}" ) +# Custom generation for precision_timestamp to use correct include paths +add_custom_command( + OUTPUT "${precision_timestamp_proto_srcs}" "${precision_timestamp_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/ + ni/protobuf/types/precision_timestamp.proto + DEPENDS "${precision_timestamp_proto}" + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/third_party/ni-apis/ + VERBATIM +) + +# Custom generation for waveform to use correct include paths +add_custom_command( + OUTPUT "${waveform_proto_srcs}" "${waveform_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/ + ni/protobuf/types/waveform.proto + DEPENDS "${waveform_proto}" "${precision_timestamp_proto_hdrs}" + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/third_party/ni-apis/ + VERBATIM +) + set(nidriver_service_library_hdrs ${nidriver_service_library_hdrs} "${session_proto_hdrs}" @@ -454,6 +484,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 +546,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 +689,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 +778,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 +917,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..60aff522e 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,18 @@ 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; +} + 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..a44ca19bf 100644 --- a/source/codegen/common_helpers.py +++ b/source/codegen/common_helpers.py @@ -478,7 +478,7 @@ 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 +488,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..537819af5 100644 --- a/source/codegen/metadata/nidaqmx/functions_addon.py +++ b/source/codegen/metadata/nidaqmx/functions_addon.py @@ -1,4 +1,62 @@ 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' + } + ] + } } functions_validation_suppressions = { @@ -22,6 +80,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..4dbc843b6 100644 --- a/source/codegen/service_helpers.py +++ b/source/codegen/service_helpers.py @@ -322,7 +322,7 @@ 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/custom/nidaqmx_service.custom.cpp b/source/custom/nidaqmx_service.custom.cpp index b20b55ab6..19c750b7e 100644 --- a/source/custom/nidaqmx_service.custom.cpp +++ b/source/custom/nidaqmx_service.custom.cpp @@ -10,4 +10,43 @@ ::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()); + + auto number_of_samples_per_channel = request->number_of_samples_per_channel(); + auto timeout = request->timeout(); + auto waveform_attribute_mode = request->waveform_attribute_mode(); + + // TODO: Implement the actual waveform reading logic, similar to read_analog_waveforms() in nidaqmx-python\generated\nidaqmx\_library_interpreter.py + + // library_->InternalReadAnalogWaveformPerChan( + // task, + // number_of_samples_per_channel, + // timeout, + // nullptr, // t0Array + // nullptr, // dtArray + // 0, // timingArraySize + // nullptr, // setWfmAttrCallback + // nullptr, // setWfmAttrCallbackData + // nullptr, // readArrayPtrs + // 0, // readArrayCount + // 0, // arraySizeInSampsPerChan + // nullptr, // sampsPerChanRead + // nullptr // reserved + // ); + + // For now, just return UNIMPLEMENTED status. + return ::grpc::Status(::grpc::StatusCode::UNIMPLEMENTED, "ReadAnalogWaveforms implementation pending"); + } + catch (nidevice_grpc::NonDriverException& ex) { + return ex.GetStatus(); + } +} + } // namespace nidaqmx_grpc From fff7769d7aaa52b674506e4847b81eb3655e3ff7 Mon Sep 17 00:00:00 2001 From: Mike Prosser Date: Thu, 16 Oct 2025 13:46:56 -0500 Subject: [PATCH 02/25] fix lint --- source/codegen/common_helpers.py | 7 ++++++- source/codegen/service_helpers.py | 4 +++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/source/codegen/common_helpers.py b/source/codegen/common_helpers.py index a44ca19bf..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", "CustomCodeNoLibrary"} + functions_for_proto = { + "public", + "CustomCode", + "CustomCodeCustomProtoMessage", + "CustomCodeNoLibrary", + } return [ name for name, function in functions.items() diff --git a/source/codegen/service_helpers.py b/source/codegen/service_helpers.py index 4dbc843b6..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", "") in ["no", "CustomCodeNoLibrary"] 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 From 01ea5e969630a009649c4c8ba241eea191f76a87 Mon Sep 17 00:00:00 2001 From: Mike Prosser Date: Thu, 16 Oct 2025 14:02:04 -0500 Subject: [PATCH 03/25] fix validate_examples --- source/codegen/validate_examples.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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()): From 1270fd7c40135e116bf2c95dae40f21103f45d47 Mon Sep 17 00:00:00 2001 From: Mike Prosser Date: Thu, 16 Oct 2025 15:14:37 -0500 Subject: [PATCH 04/25] ReadAnalogWaveforms implementation (untested, but compiles) --- source/custom/nidaqmx_service.custom.cpp | 244 +++++++++++++++++++++-- 1 file changed, 222 insertions(+), 22 deletions(-) diff --git a/source/custom/nidaqmx_service.custom.cpp b/source/custom/nidaqmx_service.custom.cpp index 19c750b7e..366bdf470 100644 --- a/source/custom/nidaqmx_service.custom.cpp +++ b/source/custom/nidaqmx_service.custom.cpp @@ -1,7 +1,99 @@ #include +#include +#include namespace nidaqmx_grpc { +// 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( + uInt32 channel_index, + const char attribute_name[], + int32 attribute_type, + const void* value, + uInt32 value_size_in_bytes, + void* callback_data) +{ + try { + auto* data = static_cast(callback_data); + if (!data || channel_index >= data->waveforms.size()) { + return -1; // Invalid data or index + } + + auto* waveform = data->waveforms[channel_index]; + if (!waveform) { + return -1; // Invalid waveform pointer + } + + // Get the attributes map for this waveform + auto* attributes = waveform->mutable_attributes(); + + // Create the attribute value based on type + ::ni::protobuf::types::WaveformAttributeValue attr_value; + + switch (attribute_type) { + case 1: // DAQmx_Val_WfmAttrType_Bool32 + if (value_size_in_bytes == 4) { + bool bool_val = (*static_cast(value)) != 0; + attr_value.set_bool_value(bool_val); + } else { + return -1; // Invalid size + } + break; + + case 2: // DAQmx_Val_WfmAttrType_Float64 + if (value_size_in_bytes == 8) { + double double_val = *static_cast(value); + attr_value.set_double_value(double_val); + } else { + return -1; // Invalid size + } + break; + + case 3: // DAQmx_Val_WfmAttrType_Int32 + if (value_size_in_bytes == 4) { + int32 int_val = *static_cast(value); + attr_value.set_integer_value(int_val); + } else { + return -1; // Invalid size + } + break; + + case 4: // DAQmx_Val_WfmAttrType_String + if (value_size_in_bytes > 0) { + // Ensure null-terminated string + const char* str_val = static_cast(value); + std::string string_val(str_val, value_size_in_bytes - 1); // Exclude null terminator + attr_value.set_string_value(string_val); + } else { + return -1; // Invalid size + } + break; + + default: + return -1; // Unsupported attribute type + } + + // Set the attribute in the waveform + (*attributes)[std::string(attribute_name)] = attr_value; + + return 0; // Success + } + catch (...) { + return -1; // Error occurred + } +} + ::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. @@ -21,28 +113,136 @@ ::grpc::Status NiDAQmxService::ReadAnalogWaveforms(::grpc::ServerContext* contex auto number_of_samples_per_channel = request->number_of_samples_per_channel(); auto timeout = request->timeout(); - auto waveform_attribute_mode = request->waveform_attribute_mode(); - - // TODO: Implement the actual waveform reading logic, similar to read_analog_waveforms() in nidaqmx-python\generated\nidaqmx\_library_interpreter.py - - // library_->InternalReadAnalogWaveformPerChan( - // task, - // number_of_samples_per_channel, - // timeout, - // nullptr, // t0Array - // nullptr, // dtArray - // 0, // timingArraySize - // nullptr, // setWfmAttrCallback - // nullptr, // setWfmAttrCallbackData - // nullptr, // readArrayPtrs - // 0, // readArrayCount - // 0, // arraySizeInSampsPerChan - // nullptr, // sampsPerChanRead - // nullptr // reserved - // ); - - // For now, just return UNIMPLEMENTED status. - return ::grpc::Status(::grpc::StatusCode::UNIMPLEMENTED, "ReadAnalogWaveforms implementation pending"); + + // Get waveform attribute mode + 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; + } + } + + // Get the number of channels + uInt32 num_channels = 0; + auto status = library_->GetTaskAttributeUInt32(task, 8571 /* 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"); + } + + // Prepare arrays for reading data + std::vector> read_arrays(num_channels); + std::vector read_array_ptrs(num_channels); + + // Allocate memory for each channel + 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(); + } + + // Prepare timing arrays if timing is requested + std::vector t0_array, dt_array; + int64* t0_ptr = nullptr; + int64* dt_ptr = nullptr; + uInt32 timing_array_size = 0; + + if (waveform_attribute_mode & 1 /* 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; + } + + // Prepare callback data for extended properties if requested + std::unique_ptr callback_data; + DAQmxSetWfmAttrCallbackPtr callback_ptr = nullptr; + + if (waveform_attribute_mode & 2 /* WAVEFORM_ATTRIBUTE_MODE_EXTENDED_PROPERTIES */) { + callback_data = std::make_unique(); + callback_data->waveforms.resize(num_channels); + callback_ptr = SetWfmAttrCallback; + + // Pre-populate the waveforms in the response so we can pass pointers to the callback + for (uInt32 i = 0; i < num_channels; ++i) { + callback_data->waveforms[i] = response->add_waveforms(); + } + } + + // Read the waveforms + 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 // reserved + ); + + if (!status_ok(status)) { + return ConvertApiErrorStatusForTaskHandle(context, status, task); + } + + // Populate the response waveforms + for (uInt32 i = 0; i < num_channels; ++i) { + ::ni::protobuf::types::DoubleAnalogWaveform* waveform; + + if (waveform_attribute_mode & 2 /* WAVEFORM_ATTRIBUTE_MODE_EXTENDED_PROPERTIES */) { + // Waveforms were already created for the callback + waveform = callback_data->waveforms[i]; + } else { + // Create new waveforms + waveform = response->add_waveforms(); + } + + // Set the actual samples read + 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]); + } + + // Set timing information if requested + if (waveform_attribute_mode & 1 /* WAVEFORM_ATTRIBUTE_MODE_TIMING */) { + auto* t0 = waveform->mutable_t0(); + // Convert from 100ns ticks (DAQmx format) to PrecisionTimestamp + // t0_array[i] contains 100ns ticks since Jan 1, 0001 + // PrecisionTimestamp expects seconds and fractional seconds + const int64 ticks_per_second = 10000000; // 100ns ticks per second + int64 seconds = t0_array[i] / ticks_per_second; + int64 fractional_ticks = t0_array[i] % ticks_per_second; + double fractional_seconds = static_cast(fractional_ticks) / ticks_per_second; + + t0->set_seconds(seconds); + t0->set_fractional_seconds(fractional_seconds); + + // Set sample interval (dt) + waveform->set_dt(static_cast(dt_array[i]) * 100e-9); // Convert 100ns ticks to seconds + } + } + + response->set_status(status); + return ::grpc::Status::OK; } catch (nidevice_grpc::NonDriverException& ex) { return ex.GetStatus(); From 836c408e49afc6d085d4fa64df0ddede8139032a Mon Sep 17 00:00:00 2001 From: Mike Prosser Date: Thu, 16 Oct 2025 16:35:59 -0500 Subject: [PATCH 05/25] add tests --- source/custom/nidaqmx_service.custom.cpp | 13 +- .../tests/system/nidaqmx_driver_api_tests.cpp | 200 ++++++++++++++++++ 2 files changed, 210 insertions(+), 3 deletions(-) diff --git a/source/custom/nidaqmx_service.custom.cpp b/source/custom/nidaqmx_service.custom.cpp index 366bdf470..11ca1225d 100644 --- a/source/custom/nidaqmx_service.custom.cpp +++ b/source/custom/nidaqmx_service.custom.cpp @@ -133,7 +133,7 @@ ::grpc::Status NiDAQmxService::ReadAnalogWaveforms(::grpc::ServerContext* contex // Get the number of channels uInt32 num_channels = 0; - auto status = library_->GetTaskAttributeUInt32(task, 8571 /* READ_ATTRIBUTE_NUM_CHANS */, &num_channels); + auto status = library_->GetReadAttributeUInt32(task, 8571 /* READ_ATTRIBUTE_NUM_CHANS */, &num_channels); if (!status_ok(status)) { return ConvertApiErrorStatusForTaskHandle(context, status, task); } @@ -224,6 +224,13 @@ ::grpc::Status NiDAQmxService::ReadAnalogWaveforms(::grpc::ServerContext* contex // Set timing information if requested if (waveform_attribute_mode & 1 /* WAVEFORM_ATTRIBUTE_MODE_TIMING */) { + // Check if DAQmx provided timing information + if (dt_array[i] == 0) { + // No timing information available - task likely not configured with sample clock timing + 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 @@ -236,8 +243,8 @@ ::grpc::Status NiDAQmxService::ReadAnalogWaveforms(::grpc::ServerContext* contex t0->set_seconds(seconds); t0->set_fractional_seconds(fractional_seconds); - // Set sample interval (dt) - waveform->set_dt(static_cast(dt_array[i]) * 100e-9); // Convert 100ns ticks to seconds + // Set sample interval (dt) - convert 100ns ticks to seconds + waveform->set_dt(static_cast(dt_array[i]) * 1e-7); // 100ns = 1e-7 seconds } } diff --git a/source/tests/system/nidaqmx_driver_api_tests.cpp b/source/tests/system/nidaqmx_driver_api_tests.cpp index fd08ff950..2dce7ed04 100644 --- a/source/tests/system/nidaqmx_driver_api_tests.cpp +++ b/source/tests/system/nidaqmx_driver_api_tests.cpp @@ -422,6 +422,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 +1496,189 @@ 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_ai_voltage_chan(-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(), 1); // One channel + + const auto& waveform = read_response.waveforms(0); + EXPECT_EQ(waveform.y_data_size(), NUM_SAMPLES); + + // Verify data is in expected range (simulated device should provide sine wave between -1 and 1) + 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_ai_voltage_chan(-5.0, 5.0, create_channel_response); + EXPECT_SUCCESS(create_channel_status, create_channel_response); + + // Configure sample clock timing to enable timing information + 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(), 1); // One channel + + const auto& waveform = read_response.waveforms(0); + EXPECT_EQ(waveform.y_data_size(), NUM_SAMPLES); + + // Verify timing information is present + EXPECT_TRUE(waveform.has_t0()); + EXPECT_GT(waveform.dt(), 0.0); // Sample interval should be positive + + // Verify data is in expected range + 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_ai_voltage_chan(-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(), 1); // One channel + + const auto& waveform = read_response.waveforms(0); + EXPECT_EQ(waveform.y_data_size(), NUM_SAMPLES); + + // Verify extended properties (attributes) are present + EXPECT_GT(waveform.attributes_size(), 0); // Should have some attributes + + // Check for expected waveform attributes that should be present + // Based on debug output: NI_ChannelName and NI_UnitDescription + bool has_channel_name = false; + bool has_units = false; + 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() == "ai0") { // 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); // Should have channel name attribute + EXPECT_TRUE(has_units); // Should have units attribute set to "Volts" + + // Verify data is in expected range + 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_ai_voltage_chan(-3.0, 3.0, create_channel_response); + EXPECT_SUCCESS(create_channel_status, create_channel_response); + + // Configure sample clock timing to enable timing information + 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; + // Use bitwise OR to combine TIMING and EXTENDED_PROPERTIES modes + 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(), 1); // One channel + + const auto& waveform = read_response.waveforms(0); + EXPECT_EQ(waveform.y_data_size(), NUM_SAMPLES); + + // Verify timing information is present (from TIMING mode) + EXPECT_TRUE(waveform.has_t0()); + EXPECT_GT(waveform.dt(), 0.0); // Sample interval should be positive + + // Verify extended properties (attributes) are present (from EXTENDED_PROPERTIES mode) + EXPECT_GT(waveform.attributes_size(), 0); // Should have some attributes + + // Check for expected waveform attributes that should be present + // Based on debug output: NI_ChannelName and NI_UnitDescription + bool has_channel_name = false; + bool has_units = false; + 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() == "ai0") { // 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); // Should have channel name attribute + EXPECT_TRUE(has_units); // Should have units attribute set to "Volts" + + // Verify data is in expected range + 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; From b9cf67a59b615eba5f423634f2b4f9a8d80562c9 Mon Sep 17 00:00:00 2001 From: Mike Prosser Date: Mon, 20 Oct 2025 10:54:20 -0500 Subject: [PATCH 06/25] Add support for creating two AI voltage channels in tests --- .../tests/system/nidaqmx_driver_api_tests.cpp | 185 +++++++++--------- 1 file changed, 96 insertions(+), 89 deletions(-) diff --git a/source/tests/system/nidaqmx_driver_api_tests.cpp b/source/tests/system/nidaqmx_driver_api_tests.cpp index 2dce7ed04..e37eb84b3 100644 --- a/source/tests/system/nidaqmx_driver_api_tests.cpp +++ b/source/tests/system/nidaqmx_driver_api_tests.cpp @@ -65,6 +65,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 +179,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; @@ -1501,7 +1515,7 @@ TEST_F(NiDAQmxDriverApiTests, ReadAnalogWaveforms_WithNoAttributeMode_ReturnsWav const auto NUM_SAMPLES = 1000; const auto TIMEOUT = 10.0; CreateAIVoltageChanResponse create_channel_response; - auto create_channel_status = create_ai_voltage_chan(-1.0, 1.0, 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(); @@ -1511,15 +1525,16 @@ TEST_F(NiDAQmxDriverApiTests, ReadAnalogWaveforms_WithNoAttributeMode_ReturnsWav EXPECT_SUCCESS(read_status, read_response); EXPECT_EQ(read_response.status(), DAQMX_SUCCESS); - EXPECT_EQ(read_response.waveforms_size(), 1); // One channel - - const auto& waveform = read_response.waveforms(0); - EXPECT_EQ(waveform.y_data_size(), NUM_SAMPLES); + EXPECT_EQ(read_response.waveforms_size(), 2); - // Verify data is in expected range (simulated device should provide sine wave between -1 and 1) - for (const auto& sample : waveform.y_data()) { - EXPECT_GE(sample, -1.0); - EXPECT_LE(sample, 1.0); + 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); + } } } @@ -1528,10 +1543,9 @@ TEST_F(NiDAQmxDriverApiTests, ReadAnalogWaveforms_WithTimingMode_ReturnsWaveform const auto NUM_SAMPLES = 100; const auto TIMEOUT = 10.0; CreateAIVoltageChanResponse create_channel_response; - auto create_channel_status = create_ai_voltage_chan(-5.0, 5.0, 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); - // Configure sample clock timing to enable timing information 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); @@ -1544,19 +1558,18 @@ TEST_F(NiDAQmxDriverApiTests, ReadAnalogWaveforms_WithTimingMode_ReturnsWaveform EXPECT_SUCCESS(read_status, read_response); EXPECT_EQ(read_response.status(), DAQMX_SUCCESS); - EXPECT_EQ(read_response.waveforms_size(), 1); // One channel + EXPECT_EQ(read_response.waveforms_size(), 2); - const auto& waveform = read_response.waveforms(0); - EXPECT_EQ(waveform.y_data_size(), NUM_SAMPLES); - - // Verify timing information is present - EXPECT_TRUE(waveform.has_t0()); - EXPECT_GT(waveform.dt(), 0.0); // Sample interval should be positive - - // Verify data is in expected range - for (const auto& sample : waveform.y_data()) { - EXPECT_GE(sample, -5.0); - EXPECT_LE(sample, 5.0); + 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); + + for (const auto& sample : waveform.y_data()) { + EXPECT_GE(sample, -5.0); + EXPECT_LE(sample, 5.0); + } } } @@ -1565,7 +1578,7 @@ TEST_F(NiDAQmxDriverApiTests, ReadAnalogWaveforms_WithExtendedPropertiesMode_Ret const auto NUM_SAMPLES = 50; const auto TIMEOUT = 10.0; CreateAIVoltageChanResponse create_channel_response; - auto create_channel_status = create_ai_voltage_chan(-2.0, 2.0, 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(); @@ -1575,40 +1588,39 @@ TEST_F(NiDAQmxDriverApiTests, ReadAnalogWaveforms_WithExtendedPropertiesMode_Ret EXPECT_SUCCESS(read_status, read_response); EXPECT_EQ(read_response.status(), DAQMX_SUCCESS); - EXPECT_EQ(read_response.waveforms_size(), 1); // One channel + EXPECT_EQ(read_response.waveforms_size(), 2); - const auto& waveform = read_response.waveforms(0); - EXPECT_EQ(waveform.y_data_size(), NUM_SAMPLES); - - // Verify extended properties (attributes) are present - EXPECT_GT(waveform.attributes_size(), 0); // Should have some attributes - - // Check for expected waveform attributes that should be present - // Based on debug output: NI_ChannelName and NI_UnitDescription - bool has_channel_name = false; - bool has_units = false; - for (const auto& attr_pair : waveform.attributes()) { - const auto& attr_name = attr_pair.first; - const auto& attr_value = attr_pair.second; + 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"; - if (attr_name == "NI_ChannelName" && attr_value.has_string_value()) { - if (attr_value.string_value() == "ai0") { // Expected channel name - has_channel_name = true; + 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; + if (attr_name == "NI_UnitDescription" && attr_value.has_string_value()) { + if (attr_value.string_value() == "Volts") { + has_units = true; + } } } - } - EXPECT_TRUE(has_channel_name); // Should have channel name attribute - EXPECT_TRUE(has_units); // Should have units attribute set to "Volts" - - // Verify data is in expected range - for (const auto& sample : waveform.y_data()) { - EXPECT_GE(sample, -2.0); - EXPECT_LE(sample, 2.0); + 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); + } } } @@ -1617,10 +1629,9 @@ TEST_F(NiDAQmxDriverApiTests, ReadAnalogWaveforms_WithTimingAndExtendedPropertie const auto NUM_SAMPLES = 75; const auto TIMEOUT = 10.0; CreateAIVoltageChanResponse create_channel_response; - auto create_channel_status = create_ai_voltage_chan(-3.0, 3.0, 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); - // Configure sample clock timing to enable timing information 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); @@ -1628,7 +1639,6 @@ TEST_F(NiDAQmxDriverApiTests, ReadAnalogWaveforms_WithTimingAndExtendedPropertie start_task(); ReadAnalogWaveformsResponse read_response; - // Use bitwise OR to combine TIMING and EXTENDED_PROPERTIES modes const auto combined_mode = static_cast( static_cast(WaveformAttributeMode::WAVEFORM_ATTRIBUTE_MODE_TIMING) | static_cast(WaveformAttributeMode::WAVEFORM_ATTRIBUTE_MODE_EXTENDED_PROPERTIES) @@ -1638,44 +1648,41 @@ TEST_F(NiDAQmxDriverApiTests, ReadAnalogWaveforms_WithTimingAndExtendedPropertie EXPECT_SUCCESS(read_status, read_response); EXPECT_EQ(read_response.status(), DAQMX_SUCCESS); - EXPECT_EQ(read_response.waveforms_size(), 1); // One channel - - const auto& waveform = read_response.waveforms(0); - EXPECT_EQ(waveform.y_data_size(), NUM_SAMPLES); + EXPECT_EQ(read_response.waveforms_size(), 2); - // Verify timing information is present (from TIMING mode) - EXPECT_TRUE(waveform.has_t0()); - EXPECT_GT(waveform.dt(), 0.0); // Sample interval should be positive - - // Verify extended properties (attributes) are present (from EXTENDED_PROPERTIES mode) - EXPECT_GT(waveform.attributes_size(), 0); // Should have some attributes - - // Check for expected waveform attributes that should be present - // Based on debug output: NI_ChannelName and NI_UnitDescription - bool has_channel_name = false; - bool has_units = false; - for (const auto& attr_pair : waveform.attributes()) { - const auto& attr_name = attr_pair.first; - const auto& attr_value = attr_pair.second; + 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); - if (attr_name == "NI_ChannelName" && attr_value.has_string_value()) { - if (attr_value.string_value() == "ai0") { // Expected channel name - has_channel_name = true; + 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; + if (attr_name == "NI_UnitDescription" && attr_value.has_string_value()) { + if (attr_value.string_value() == "Volts") { + has_units = true; + } } } - } - EXPECT_TRUE(has_channel_name); // Should have channel name attribute - EXPECT_TRUE(has_units); // Should have units attribute set to "Volts" - - // Verify data is in expected range - for (const auto& sample : waveform.y_data()) { - EXPECT_GE(sample, -3.0); - EXPECT_LE(sample, 3.0); + 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); + } } } From 12ca5e53f137b1df8f2c6f025024889eb54ef338 Mon Sep 17 00:00:00 2001 From: Mike Prosser Date: Mon, 20 Oct 2025 11:41:45 -0500 Subject: [PATCH 07/25] cleanup --- source/custom/nidaqmx_service.custom.cpp | 112 ++++++++++------------- 1 file changed, 46 insertions(+), 66 deletions(-) diff --git a/source/custom/nidaqmx_service.custom.cpp b/source/custom/nidaqmx_service.custom.cpp index 11ca1225d..28c3864e8 100644 --- a/source/custom/nidaqmx_service.custom.cpp +++ b/source/custom/nidaqmx_service.custom.cpp @@ -1,9 +1,13 @@ #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) { @@ -17,80 +21,74 @@ struct WaveformCallbackData { // Callback function for setting waveform attributes int32 CVICALLBACK SetWfmAttrCallback( - uInt32 channel_index, + const uInt32 channel_index, const char attribute_name[], - int32 attribute_type, + const int32 attribute_type, const void* value, - uInt32 value_size_in_bytes, + const uInt32 value_size_in_bytes, void* callback_data) { try { - auto* data = static_cast(callback_data); + const auto* data = static_cast(callback_data); if (!data || channel_index >= data->waveforms.size()) { - return -1; // Invalid data or index + return -1; } auto* waveform = data->waveforms[channel_index]; if (!waveform) { - return -1; // Invalid waveform pointer + return -1; } - // Get the attributes map for this waveform auto* attributes = waveform->mutable_attributes(); - - // Create the attribute value based on type ::ni::protobuf::types::WaveformAttributeValue attr_value; switch (attribute_type) { - case 1: // DAQmx_Val_WfmAttrType_Bool32 - if (value_size_in_bytes == 4) { - bool bool_val = (*static_cast(value)) != 0; + 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; // Invalid size + return -1; } break; - case 2: // DAQmx_Val_WfmAttrType_Float64 - if (value_size_in_bytes == 8) { - double double_val = *static_cast(value); + 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; // Invalid size + return -1; } break; - case 3: // DAQmx_Val_WfmAttrType_Int32 - if (value_size_in_bytes == 4) { - int32 int_val = *static_cast(value); + 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; // Invalid size + return -1; } break; - case 4: // DAQmx_Val_WfmAttrType_String + case DAQmx_Val_WfmAttrType_String: if (value_size_in_bytes > 0) { - // Ensure null-terminated string const char* str_val = static_cast(value); - std::string string_val(str_val, value_size_in_bytes - 1); // Exclude null terminator + const std::string string_val(str_val, value_size_in_bytes - 1); attr_value.set_string_value(string_val); } else { - return -1; // Invalid size + return -1; } break; default: - return -1; // Unsupported attribute type + return -1; } - // Set the attribute in the waveform - (*attributes)[std::string(attribute_name)] = attr_value; - - return 0; // Success + (*attributes)[attribute_name] = attr_value; + return 0; } catch (...) { - return -1; // Error occurred + return -1; } } @@ -111,10 +109,9 @@ ::grpc::Status NiDAQmxService::ReadAnalogWaveforms(::grpc::ServerContext* contex auto task_grpc_session = request->task(); TaskHandle task = session_repository_->access_session(task_grpc_session.name()); - auto number_of_samples_per_channel = request->number_of_samples_per_channel(); - auto timeout = request->timeout(); + const auto number_of_samples_per_channel = request->number_of_samples_per_channel(); + const auto timeout = request->timeout(); - // Get waveform attribute mode int32 waveform_attribute_mode; switch (request->waveform_attribute_mode_enum_case()) { case nidaqmx_grpc::ReadAnalogWaveformsRequest::WaveformAttributeModeEnumCase::kWaveformAttributeMode: { @@ -131,9 +128,8 @@ ::grpc::Status NiDAQmxService::ReadAnalogWaveforms(::grpc::ServerContext* contex } } - // Get the number of channels uInt32 num_channels = 0; - auto status = library_->GetReadAttributeUInt32(task, 8571 /* READ_ATTRIBUTE_NUM_CHANS */, &num_channels); + auto status = library_->GetReadAttributeUInt32(task, ReadUInt32Attribute::READ_ATTRIBUTE_NUM_CHANS, &num_channels); if (!status_ok(status)) { return ConvertApiErrorStatusForTaskHandle(context, status, task); } @@ -142,23 +138,20 @@ ::grpc::Status NiDAQmxService::ReadAnalogWaveforms(::grpc::ServerContext* contex return ::grpc::Status(::grpc::INVALID_ARGUMENT, "No channels found in task"); } - // Prepare arrays for reading data std::vector> read_arrays(num_channels); std::vector read_array_ptrs(num_channels); - // Allocate memory for each channel 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(); } - // Prepare timing arrays if timing is requested std::vector t0_array, dt_array; int64* t0_ptr = nullptr; int64* dt_ptr = nullptr; uInt32 timing_array_size = 0; - if (waveform_attribute_mode & 1 /* WAVEFORM_ATTRIBUTE_MODE_TIMING */) { + 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(); @@ -166,22 +159,23 @@ ::grpc::Status NiDAQmxService::ReadAnalogWaveforms(::grpc::ServerContext* contex timing_array_size = num_channels; } - // Prepare callback data for extended properties if requested std::unique_ptr callback_data; DAQmxSetWfmAttrCallbackPtr callback_ptr = nullptr; - if (waveform_attribute_mode & 2 /* WAVEFORM_ATTRIBUTE_MODE_EXTENDED_PROPERTIES */) { + 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; - // Pre-populate the waveforms in the response so we can pass pointers to the callback for (uInt32 i = 0; i < num_channels; ++i) { - callback_data->waveforms[i] = response->add_waveforms(); + callback_data->waveforms[i] = response->mutable_waveforms(i); } } - // Read the waveforms int32 samples_per_chan_read = 0; status = library_->InternalReadAnalogWaveformPerChan( task, @@ -196,37 +190,24 @@ ::grpc::Status NiDAQmxService::ReadAnalogWaveforms(::grpc::ServerContext* contex num_channels, number_of_samples_per_channel, &samples_per_chan_read, - nullptr // reserved + nullptr ); if (!status_ok(status)) { return ConvertApiErrorStatusForTaskHandle(context, status, task); } - // Populate the response waveforms for (uInt32 i = 0; i < num_channels; ++i) { - ::ni::protobuf::types::DoubleAnalogWaveform* waveform; - - if (waveform_attribute_mode & 2 /* WAVEFORM_ATTRIBUTE_MODE_EXTENDED_PROPERTIES */) { - // Waveforms were already created for the callback - waveform = callback_data->waveforms[i]; - } else { - // Create new waveforms - waveform = response->add_waveforms(); - } + auto* waveform = response->mutable_waveforms(i); - // Set the actual samples read 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]); } - // Set timing information if requested - if (waveform_attribute_mode & 1 /* WAVEFORM_ATTRIBUTE_MODE_TIMING */) { - // Check if DAQmx provided timing information + if (waveform_attribute_mode & WaveformAttributeMode::WAVEFORM_ATTRIBUTE_MODE_TIMING) { if (dt_array[i] == 0) { - // No timing information available - task likely not configured with sample clock timing 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."); } @@ -235,16 +216,15 @@ ::grpc::Status NiDAQmxService::ReadAnalogWaveforms(::grpc::ServerContext* contex // Convert from 100ns ticks (DAQmx format) to PrecisionTimestamp // t0_array[i] contains 100ns ticks since Jan 1, 0001 // PrecisionTimestamp expects seconds and fractional seconds - const int64 ticks_per_second = 10000000; // 100ns ticks per second - int64 seconds = t0_array[i] / ticks_per_second; - int64 fractional_ticks = t0_array[i] % ticks_per_second; - double fractional_seconds = static_cast(fractional_ticks) / ticks_per_second; + const int64 seconds = t0_array[i] / TICKS_PER_SECOND; + const int64 fractional_ticks = t0_array[i] % TICKS_PER_SECOND; + const double fractional_seconds = static_cast(fractional_ticks) / TICKS_PER_SECOND; t0->set_seconds(seconds); t0->set_fractional_seconds(fractional_seconds); // Set sample interval (dt) - convert 100ns ticks to seconds - waveform->set_dt(static_cast(dt_array[i]) * 1e-7); // 100ns = 1e-7 seconds + waveform->set_dt(static_cast(dt_array[i]) * TICKS_TO_SECONDS); } } From 8e5eb7739efe3776b85fabf15e50dbe638badab5 Mon Sep 17 00:00:00 2001 From: Mike Prosser Date: Mon, 20 Oct 2025 11:56:32 -0500 Subject: [PATCH 08/25] cleanup CMakeLists.txt --- CMakeLists.txt | 64 ++++++++++++++++++++++++++++++++------------------ 1 file changed, 41 insertions(+), 23 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 150655532..4c3e1dfa5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -304,7 +304,10 @@ function(GenerateGrpcSources) set(output_files "${GENERATE_ARGS_OUTPUT}") 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/ -I ${CMAKE_SOURCE_DIR}/third_party/ni-apis/) + 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/ni-apis/) endif() get_filename_component(proto_name "${proto_file}" NAME) get_filename_component(proto_path "${proto_file}" PATH) @@ -346,6 +349,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") @@ -445,30 +476,17 @@ GenerateGrpcSources( "${data_moniker_grpc_hdrs}" ) -# Custom generation for precision_timestamp to use correct include paths -add_custom_command( - OUTPUT "${precision_timestamp_proto_srcs}" "${precision_timestamp_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/ - ni/protobuf/types/precision_timestamp.proto - DEPENDS "${precision_timestamp_proto}" - WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/third_party/ni-apis/ - VERBATIM +GenerateNiApisProtoSources( + PROTO_PATH "ni/protobuf/types/precision_timestamp" + OUTPUT_SRCS precision_timestamp_proto_srcs + OUTPUT_HDRS precision_timestamp_proto_hdrs ) -# Custom generation for waveform to use correct include paths -add_custom_command( - OUTPUT "${waveform_proto_srcs}" "${waveform_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/ - ni/protobuf/types/waveform.proto - DEPENDS "${waveform_proto}" "${precision_timestamp_proto_hdrs}" - WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/third_party/ni-apis/ - VERBATIM +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 From 7b50a84bcb1f878e17de0c5cc293ae6a27428378 Mon Sep 17 00:00:00 2001 From: Mike Prosser Date: Mon, 20 Oct 2025 14:12:54 -0500 Subject: [PATCH 09/25] attempt fix for ubuntu build --- imports/include/NIDAQmxInternalWaveform.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/imports/include/NIDAQmxInternalWaveform.h b/imports/include/NIDAQmxInternalWaveform.h index 0ebf9fbe5..0e8f64aa3 100644 --- a/imports/include/NIDAQmxInternalWaveform.h +++ b/imports/include/NIDAQmxInternalWaveform.h @@ -19,7 +19,7 @@ extern "C" // - 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); + typedef int32(__CFUNC *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); From 3d29cbc794453ea9edd69099b957d617057da60a Mon Sep 17 00:00:00 2001 From: Mike Prosser Date: Mon, 20 Oct 2025 15:06:14 -0500 Subject: [PATCH 10/25] more verbose build output --- .github/workflows/build_cmake.yml | 1 + imports/include/NIDAQmxInternalWaveform.h | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build_cmake.yml b/.github/workflows/build_cmake.yml index b27d62a97..0d5d24a94 100644 --- a/.github/workflows/build_cmake.yml +++ b/.github/workflows/build_cmake.yml @@ -93,6 +93,7 @@ jobs: --clean-first --config ${{ matrix.config.build_type }} -j ${{ steps.cpu-cores.outputs.count }} + --verbose # The generated source in git must match the output of workflow builds. # If this step fails, something is changing during build. Either: diff --git a/imports/include/NIDAQmxInternalWaveform.h b/imports/include/NIDAQmxInternalWaveform.h index 0e8f64aa3..0ebf9fbe5 100644 --- a/imports/include/NIDAQmxInternalWaveform.h +++ b/imports/include/NIDAQmxInternalWaveform.h @@ -19,7 +19,7 @@ extern "C" // - 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(__CFUNC *DAQmxSetWfmAttrCallbackPtr)(uInt32 channelIndex, const char attributeName[], int32 attributeType, const void *value, uInt32 valueSizeInBytes, void *callbackData); + 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); From 9b266703286028519373810d740c7a137ea08df1 Mon Sep 17 00:00:00 2001 From: Mike Prosser Date: Mon, 20 Oct 2025 15:46:26 -0500 Subject: [PATCH 11/25] Try some different build settings --- .github/workflows/build_cmake.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build_cmake.yml b/.github/workflows/build_cmake.yml index 0d5d24a94..e9fdd4033 100644 --- a/.github/workflows/build_cmake.yml +++ b/.github/workflows/build_cmake.yml @@ -25,7 +25,8 @@ jobs: cc: "gcc-9", cxx: "g++-9", glibc_version: "2_31", cmake_generator: '-G "Ninja"', - build_type: RelWithDebInfo + build_type: RelWithDebInfo, + cmake_flags: "-DCMAKE_CXX_FLAGS_RELWITHDEBINFO=-O1 -g -DNDEBUG" } steps: @@ -82,6 +83,7 @@ jobs: -B build -D CMAKE_BUILD_TYPE=${{ matrix.config.build_type }} ${{ matrix.config.cmake_generator }} + ${{ matrix.config.cmake_flags }} # It is preferable to do a clean build to ensure all object files are # deleted and then rebuilt from scratch, which can help resolve @@ -92,8 +94,7 @@ jobs: --build build --clean-first --config ${{ matrix.config.build_type }} - -j ${{ steps.cpu-cores.outputs.count }} - --verbose + -j 2 # The generated source in git must match the output of workflow builds. # If this step fails, something is changing during build. Either: From d117ec2f195f16cd74562a462dc54d83f5eb1e99 Mon Sep 17 00:00:00 2001 From: Mike Prosser Date: Mon, 20 Oct 2025 16:02:34 -0500 Subject: [PATCH 12/25] add quotes to fix cmake flags --- .github/workflows/build_cmake.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build_cmake.yml b/.github/workflows/build_cmake.yml index e9fdd4033..5f5e19338 100644 --- a/.github/workflows/build_cmake.yml +++ b/.github/workflows/build_cmake.yml @@ -26,7 +26,7 @@ jobs: glibc_version: "2_31", cmake_generator: '-G "Ninja"', build_type: RelWithDebInfo, - cmake_flags: "-DCMAKE_CXX_FLAGS_RELWITHDEBINFO=-O1 -g -DNDEBUG" + cmake_flags: '"-DCMAKE_CXX_FLAGS_RELWITHDEBINFO=-O1 -g -DNDEBUG"' } steps: From 23a6e987d677bca585b0c8367a67a1229f908299 Mon Sep 17 00:00:00 2001 From: Mike Prosser Date: Wed, 22 Oct 2025 10:39:26 -0500 Subject: [PATCH 13/25] add sampsPerChanRead to ReadAnalogWaveforms response --- generated/nidaqmx/nidaqmx.proto | 1 + source/codegen/metadata/nidaqmx/functions_addon.py | 12 ++++++++++++ source/custom/nidaqmx_service.custom.cpp | 1 + source/tests/system/nidaqmx_driver_api_tests.cpp | 4 ++++ 4 files changed, 18 insertions(+) diff --git a/generated/nidaqmx/nidaqmx.proto b/generated/nidaqmx/nidaqmx.proto index 60aff522e..f7423a579 100644 --- a/generated/nidaqmx/nidaqmx.proto +++ b/generated/nidaqmx/nidaqmx.proto @@ -11532,5 +11532,6 @@ message ReadAnalogWaveformsRequest { message ReadAnalogWaveformsResponse { int32 status = 1; repeated ni.protobuf.types.DoubleAnalogWaveform waveforms = 2; + int32 samps_per_chan_read = 3; } diff --git a/source/codegen/metadata/nidaqmx/functions_addon.py b/source/codegen/metadata/nidaqmx/functions_addon.py index 537819af5..1a345c111 100644 --- a/source/codegen/metadata/nidaqmx/functions_addon.py +++ b/source/codegen/metadata/nidaqmx/functions_addon.py @@ -54,6 +54,18 @@ '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' } ] } diff --git a/source/custom/nidaqmx_service.custom.cpp b/source/custom/nidaqmx_service.custom.cpp index 28c3864e8..283d5730b 100644 --- a/source/custom/nidaqmx_service.custom.cpp +++ b/source/custom/nidaqmx_service.custom.cpp @@ -228,6 +228,7 @@ ::grpc::Status NiDAQmxService::ReadAnalogWaveforms(::grpc::ServerContext* contex } } + response->set_samps_per_chan_read(samples_per_chan_read); response->set_status(status); return ::grpc::Status::OK; } diff --git a/source/tests/system/nidaqmx_driver_api_tests.cpp b/source/tests/system/nidaqmx_driver_api_tests.cpp index e37eb84b3..eba446583 100644 --- a/source/tests/system/nidaqmx_driver_api_tests.cpp +++ b/source/tests/system/nidaqmx_driver_api_tests.cpp @@ -1526,6 +1526,7 @@ TEST_F(NiDAQmxDriverApiTests, ReadAnalogWaveforms_WithNoAttributeMode_ReturnsWav 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); @@ -1559,6 +1560,7 @@ TEST_F(NiDAQmxDriverApiTests, ReadAnalogWaveforms_WithTimingMode_ReturnsWaveform 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); @@ -1589,6 +1591,7 @@ TEST_F(NiDAQmxDriverApiTests, ReadAnalogWaveforms_WithExtendedPropertiesMode_Ret 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); @@ -1649,6 +1652,7 @@ TEST_F(NiDAQmxDriverApiTests, ReadAnalogWaveforms_WithTimingAndExtendedPropertie 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); From 157711652d3e22dc2a5de4779eeffb9d4438a137 Mon Sep 17 00:00:00 2001 From: Mike Prosser Date: Wed, 22 Oct 2025 12:40:53 -0500 Subject: [PATCH 14/25] revert changes in build_cmake.yml and do -fno-var-tracking-assignments instead --- .github/workflows/build_cmake.yml | 6 ++---- generated/nidaqmx/nidaqmx_service.cpp | 3 +++ source/codegen/templates/service.cpp.mako | 3 +++ 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build_cmake.yml b/.github/workflows/build_cmake.yml index 5f5e19338..b27d62a97 100644 --- a/.github/workflows/build_cmake.yml +++ b/.github/workflows/build_cmake.yml @@ -25,8 +25,7 @@ jobs: cc: "gcc-9", cxx: "g++-9", glibc_version: "2_31", cmake_generator: '-G "Ninja"', - build_type: RelWithDebInfo, - cmake_flags: '"-DCMAKE_CXX_FLAGS_RELWITHDEBINFO=-O1 -g -DNDEBUG"' + build_type: RelWithDebInfo } steps: @@ -83,7 +82,6 @@ jobs: -B build -D CMAKE_BUILD_TYPE=${{ matrix.config.build_type }} ${{ matrix.config.cmake_generator }} - ${{ matrix.config.cmake_flags }} # It is preferable to do a clean build to ensure all object files are # deleted and then rebuilt from scratch, which can help resolve @@ -94,7 +92,7 @@ jobs: --build build --clean-first --config ${{ matrix.config.build_type }} - -j 2 + -j ${{ steps.cpu-cores.outputs.count }} # The generated source in git must match the output of workflow builds. # If this step fails, something is changing during build. Either: diff --git a/generated/nidaqmx/nidaqmx_service.cpp b/generated/nidaqmx/nidaqmx_service.cpp index 21cd67787..fe2de11fa 100644 --- a/generated/nidaqmx/nidaqmx_service.cpp +++ b/generated/nidaqmx/nidaqmx_service.cpp @@ -15,6 +15,9 @@ #include #include #include "nidaqmx_library.h" +#ifdef __GNUC__ +#pragma GCC optimize("-fno-var-tracking-assignments") +#endif #include namespace nidaqmx_grpc { diff --git a/source/codegen/templates/service.cpp.mako b/source/codegen/templates/service.cpp.mako index aa55edbd5..78a1c7181 100644 --- a/source/codegen/templates/service.cpp.mako +++ b/source/codegen/templates/service.cpp.mako @@ -51,6 +51,9 @@ resource_repository_deps = service_helpers.get_driver_shared_resource_repository % endif % if any_non_mockable_functions: #include "${module_name}_library.h" +#ifdef __GNUC__ +#pragma GCC optimize("-fno-var-tracking-assignments") +#endif % endif % if streaming_functions_to_generate: #include From c03a50d6b1751e96a1a326e7a4a7fa9ed1123fb1 Mon Sep 17 00:00:00 2001 From: Mike Prosser Date: Wed, 22 Oct 2025 13:19:49 -0500 Subject: [PATCH 15/25] use GCC push_options --- source/codegen/templates/service.cpp.mako | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/source/codegen/templates/service.cpp.mako b/source/codegen/templates/service.cpp.mako index 78a1c7181..231adf4c2 100644 --- a/source/codegen/templates/service.cpp.mako +++ b/source/codegen/templates/service.cpp.mako @@ -51,14 +51,18 @@ resource_repository_deps = service_helpers.get_driver_shared_resource_repository % endif % if any_non_mockable_functions: #include "${module_name}_library.h" -#ifdef __GNUC__ -#pragma GCC optimize("-fno-var-tracking-assignments") -#endif % endif % if streaming_functions_to_generate: #include % endif +% if any_non_mockable_functions: +#ifdef __GNUC__ +#pragma GCC push_options +#pragma GCC optimize("-fno-var-tracking-assignments") +#endif +% endif + namespace ${config["namespace_component"]}_grpc { using nidevice_grpc::converters::allocate_output_storage; @@ -198,6 +202,12 @@ ${mako_helper.define_simple_method_body(function_name=function_name, function_da } } // namespace ${config["namespace_component"]}_grpc +% if any_non_mockable_functions: +#ifdef __GNUC__ +#pragma GCC pop_options +#endif +% endif + % if any(input_custom_types) or any(output_custom_types): namespace nidevice_grpc { namespace converters { From 4c36ac8d533ff08f00ca94f91b3465d74792662f Mon Sep 17 00:00:00 2001 From: Mike Prosser Date: Wed, 22 Oct 2025 14:12:50 -0500 Subject: [PATCH 16/25] use -fno-var-tracking-assignments globally for gcc --- CMakeLists.txt | 6 ++++++ generated/nidaqmx/nidaqmx_service.cpp | 3 --- source/codegen/templates/service.cpp.mako | 13 ------------- 3 files changed, 6 insertions(+), 16 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 4c3e1dfa5..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) #---------------------------------------------------------------------- diff --git a/generated/nidaqmx/nidaqmx_service.cpp b/generated/nidaqmx/nidaqmx_service.cpp index fe2de11fa..21cd67787 100644 --- a/generated/nidaqmx/nidaqmx_service.cpp +++ b/generated/nidaqmx/nidaqmx_service.cpp @@ -15,9 +15,6 @@ #include #include #include "nidaqmx_library.h" -#ifdef __GNUC__ -#pragma GCC optimize("-fno-var-tracking-assignments") -#endif #include namespace nidaqmx_grpc { diff --git a/source/codegen/templates/service.cpp.mako b/source/codegen/templates/service.cpp.mako index 231adf4c2..aa55edbd5 100644 --- a/source/codegen/templates/service.cpp.mako +++ b/source/codegen/templates/service.cpp.mako @@ -56,13 +56,6 @@ resource_repository_deps = service_helpers.get_driver_shared_resource_repository #include % endif -% if any_non_mockable_functions: -#ifdef __GNUC__ -#pragma GCC push_options -#pragma GCC optimize("-fno-var-tracking-assignments") -#endif -% endif - namespace ${config["namespace_component"]}_grpc { using nidevice_grpc::converters::allocate_output_storage; @@ -202,12 +195,6 @@ ${mako_helper.define_simple_method_body(function_name=function_name, function_da } } // namespace ${config["namespace_component"]}_grpc -% if any_non_mockable_functions: -#ifdef __GNUC__ -#pragma GCC pop_options -#endif -% endif - % if any(input_custom_types) or any(output_custom_types): namespace nidevice_grpc { namespace converters { From e7055d13e6dd12e9d4ff1300ccb3a779db11defd Mon Sep 17 00:00:00 2001 From: Mike Prosser Date: Wed, 22 Oct 2025 17:01:16 -0500 Subject: [PATCH 17/25] fix timing conversion --- source/custom/nidaqmx_service.custom.cpp | 11 +++++------ source/tests/system/nidaqmx_driver_api_tests.cpp | 13 +++++++++++++ 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/source/custom/nidaqmx_service.custom.cpp b/source/custom/nidaqmx_service.custom.cpp index 283d5730b..437a746df 100644 --- a/source/custom/nidaqmx_service.custom.cpp +++ b/source/custom/nidaqmx_service.custom.cpp @@ -1,6 +1,7 @@ #include #include #include +#include #include "NIDAQmxInternalWaveform.h" namespace nidaqmx_grpc { @@ -214,14 +215,12 @@ ::grpc::Status NiDAQmxService::ReadAnalogWaveforms(::grpc::ServerContext* contex auto* t0 = waveform->mutable_t0(); // Convert from 100ns ticks (DAQmx format) to PrecisionTimestamp - // t0_array[i] contains 100ns ticks since Jan 1, 0001 - // PrecisionTimestamp expects seconds and fractional seconds - const int64 seconds = t0_array[i] / TICKS_PER_SECOND; - const int64 fractional_ticks = t0_array[i] % TICKS_PER_SECOND; - const double fractional_seconds = static_cast(fractional_ticks) / TICKS_PER_SECOND; + // 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_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); diff --git a/source/tests/system/nidaqmx_driver_api_tests.cpp b/source/tests/system/nidaqmx_driver_api_tests.cpp index eba446583..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 @@ -1566,6 +1567,18 @@ TEST_F(NiDAQmxDriverApiTests, ReadAnalogWaveforms_WithTimingMode_ReturnsWaveform 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()) { From ae53b1c29460a38cb277e2de3077ae99ffbaca3b Mon Sep 17 00:00:00 2001 From: Mike Prosser Date: Fri, 24 Oct 2025 14:49:33 -0500 Subject: [PATCH 18/25] brad's feedback --- CMakeLists.txt | 1 + generated/nidaqmx/nidaqmx.proto | 2 +- generated/nidaqmx/nidaqmx_client.cpp | 4 +- generated/nidaqmx/nidaqmx_client.h | 2 +- .../metadata/nidaqmx/functions_addon.py | 2 +- source/codegen/stage_client_files.py | 7 ++ source/custom/nidaqmx_service.custom.cpp | 63 +++++-------- source/server/converters.h | 14 +++ .../tests/system/nidaqmx_driver_api_tests.cpp | 66 ++++++------- .../precision_timestamp_converter_tests.cpp | 92 +++++++++++++++++++ 10 files changed, 175 insertions(+), 78 deletions(-) create mode 100644 source/tests/unit/precision_timestamp_converter_tests.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index d7fbfad89..95b1ff781 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -771,6 +771,7 @@ add_executable(UnitTestsRunner "source/tests/unit/ni_fake_non_ivi_service_tests.cpp" "source/tests/unit/ni_fake_service_tests.cpp" "source/tests/unit/nimxlc_terminal_adaptor_converters_tests.cpp" + "source/tests/unit/precision_timestamp_converter_tests.cpp" "source/tests/unit/shared_library_tests.cpp" "source/tests/unit/syscfg_library_tests.cpp" "source/tests/unit/syscfg_resource_accessor_tests.cpp" diff --git a/generated/nidaqmx/nidaqmx.proto b/generated/nidaqmx/nidaqmx.proto index f7423a579..3b7bd303c 100644 --- a/generated/nidaqmx/nidaqmx.proto +++ b/generated/nidaqmx/nidaqmx.proto @@ -11521,7 +11521,7 @@ message WriteToTEDSFromFileResponse { message ReadAnalogWaveformsRequest { nidevice_grpc.Session task = 1; - int32 number_of_samples_per_channel = 2; + int32 num_samps_per_chan = 2; double timeout = 3; oneof waveform_attribute_mode_enum { WaveformAttributeMode waveform_attribute_mode = 4; diff --git a/generated/nidaqmx/nidaqmx_client.cpp b/generated/nidaqmx/nidaqmx_client.cpp index 0d5a345fc..d0f665cae 100644 --- a/generated/nidaqmx/nidaqmx_client.cpp +++ b/generated/nidaqmx/nidaqmx_client.cpp @@ -12415,13 +12415,13 @@ write_to_teds_from_file(const StubPtr& stub, const std::string& physical_channel } 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) +read_analog_waveforms(const StubPtr& stub, const nidevice_grpc::Session& task, const pb::int32& num_samps_per_chan, 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_num_samps_per_chan(num_samps_per_chan); 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(); diff --git a/generated/nidaqmx/nidaqmx_client.h b/generated/nidaqmx/nidaqmx_client.h index 7e28dc8ed..3912ec212 100644 --- a/generated/nidaqmx/nidaqmx_client.h +++ b/generated/nidaqmx/nidaqmx_client.h @@ -468,7 +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); +ReadAnalogWaveformsResponse read_analog_waveforms(const StubPtr& stub, const nidevice_grpc::Session& task, const pb::int32& num_samps_per_chan, const double& timeout, const simple_variant& waveform_attribute_mode); } // namespace nidaqmx_grpc::experimental::client diff --git a/source/codegen/metadata/nidaqmx/functions_addon.py b/source/codegen/metadata/nidaqmx/functions_addon.py index 1a345c111..d6bd68ccf 100644 --- a/source/codegen/metadata/nidaqmx/functions_addon.py +++ b/source/codegen/metadata/nidaqmx/functions_addon.py @@ -17,7 +17,7 @@ 'ctypes_data_type': 'ctypes.c_int', 'direction': 'in', 'is_optional_in_python': False, - 'name': 'numberOfSamplesPerChannel', + 'name': 'numSampsPerChan', 'python_data_type': 'int', 'python_description': '', 'python_type_annotation': 'int', diff --git a/source/codegen/stage_client_files.py b/source/codegen/stage_client_files.py index 307c9ec7d..c692507d0 100644 --- a/source/codegen/stage_client_files.py +++ b/source/codegen/stage_client_files.py @@ -92,6 +92,10 @@ def restricted_protos(self) -> Path: def grpcdevice_protos(self) -> Path: return self.repo_root / "third_party" / "ni-apis" / "ni" / "grpcdevice" / "v1" + @property + def protobuftypes_protos(self) -> Path: + return self.repo_root / "third_party" / "ni-apis" / "ni" / "protobuf" / "types" + @property def metadata_dir(self) -> Path: return self.repo_root / "source" / "codegen" / "metadata" @@ -128,6 +132,9 @@ def stage_client_files(output_path: Path, ignore_release_readiness: bool): for file in artifact_locations.grpcdevice_protos.glob("*.proto"): copy2(file, proto_path) + for file in artifact_locations.protobuftypes_protos.glob("*.proto"): + copy2(file, proto_path) + for file in _get_release_proto_files(artifact_locations, readiness): copy2(file, proto_path) diff --git a/source/custom/nidaqmx_service.custom.cpp b/source/custom/nidaqmx_service.custom.cpp index 437a746df..17dbe6cce 100644 --- a/source/custom/nidaqmx_service.custom.cpp +++ b/source/custom/nidaqmx_service.custom.cpp @@ -2,12 +2,16 @@ #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 +using nidevice_grpc::converters::convert_to_grpc; +using nidevice_grpc::converters::convert_ticks_to_precision_timestamp; +using nidevice_grpc::converters::SecondsPerTick; +using google::protobuf::RepeatedPtrField; +using ::ni::protobuf::types::DoubleAnalogWaveform; // Returns true if it's safe to use outputs of a method with the given status. inline bool status_ok(int32 status) @@ -15,11 +19,6 @@ 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, @@ -30,12 +29,12 @@ int32 CVICALLBACK SetWfmAttrCallback( void* callback_data) { try { - const auto* data = static_cast(callback_data); - if (!data || channel_index >= data->waveforms.size()) { + auto* waveforms = static_cast*>(callback_data); + if (!waveforms || channel_index >= static_cast(waveforms->size())) { return -1; } - auto* waveform = data->waveforms[channel_index]; + auto* waveform = waveforms->Mutable(channel_index); if (!waveform) { return -1; } @@ -74,7 +73,8 @@ int32 CVICALLBACK SetWfmAttrCallback( 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); + std::string string_val; + convert_to_grpc(std::string(str_val), &string_val); attr_value.set_string_value(string_val); } else { return -1; @@ -88,7 +88,7 @@ int32 CVICALLBACK SetWfmAttrCallback( (*attributes)[attribute_name] = attr_value; return 0; } - catch (...) { + catch (const std::exception&) { return -1; } } @@ -110,7 +110,7 @@ ::grpc::Status NiDAQmxService::ReadAnalogWaveforms(::grpc::ServerContext* contex 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 number_of_samples_per_channel = request->num_samps_per_chan(); const auto timeout = request->timeout(); int32 waveform_attribute_mode; @@ -136,7 +136,7 @@ ::grpc::Status NiDAQmxService::ReadAnalogWaveforms(::grpc::ServerContext* contex } if (num_channels == 0) { - return ::grpc::Status(::grpc::INVALID_ARGUMENT, "No channels found in task"); + return ::grpc::Status(::grpc::INVALID_ARGUMENT, "No channels to read"); } std::vector> read_arrays(num_channels); @@ -160,21 +160,15 @@ ::grpc::Status NiDAQmxService::ReadAnalogWaveforms(::grpc::ServerContext* contex timing_array_size = num_channels; } - std::unique_ptr callback_data; DAQmxSetWfmAttrCallbackPtr callback_ptr = nullptr; - + + response->mutable_waveforms()->Reserve(num_channels); 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; @@ -186,7 +180,7 @@ ::grpc::Status NiDAQmxService::ReadAnalogWaveforms(::grpc::ServerContext* contex dt_ptr, timing_array_size, callback_ptr, - callback_data.get(), + response->mutable_waveforms(), read_array_ptrs.data(), num_channels, number_of_samples_per_channel, @@ -203,27 +197,12 @@ ::grpc::Status NiDAQmxService::ReadAnalogWaveforms(::grpc::ServerContext* contex 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]); - } + y_data->Add(read_arrays[i].data(), read_arrays[i].data() + samples_per_chan_read); 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); + auto* waveform_t0 = waveform->mutable_t0(); + convert_ticks_to_precision_timestamp(t0_array[i], waveform_t0); + waveform->set_dt(static_cast(dt_array[i]) * SecondsPerTick); } } diff --git a/source/server/converters.h b/source/server/converters.h index 827cb7ec8..0138733d6 100644 --- a/source/server/converters.h +++ b/source/server/converters.h @@ -5,6 +5,7 @@ #include #include #include // For common grpc types. +#include #include // For common C types. #include #include @@ -342,6 +343,8 @@ inline void convert_to_grpc(const SmtSpectrumInfoType& input, nidevice_grpc::Smt const int64 SecondsFromCVI1904EpochTo1970Epoch = 2082844800LL; const double TwoToSixtyFour = (double)(1 << 31) * (double)(1 << 31) * (double)(1 << 2); const double NanosecondsPerSecond = 1000000000.0; +const int64 TicksPerSecond = 1e7; // each tick is 100ns +const double SecondsPerTick = 1e-7; template <> inline void convert_to_grpc(const CVIAbsoluteTime& value, google::protobuf::Timestamp* timestamp) @@ -368,6 +371,17 @@ inline CVIAbsoluteTime convert_from_grpc(const google::protobuf::Timestamp& valu return cviTime; } +// Convert ticks (100ns since Jan 1, 0001) to PrecisionTimestamp +inline void convert_ticks_to_precision_timestamp(int64 ticks, ::ni::protobuf::types::PrecisionTimestamp* timestamp) +{ + const double seconds = static_cast(ticks) * SecondsPerTick; + const int64 seconds_int = static_cast(std::floor(seconds)); + timestamp->set_seconds(seconds_int); + const double fractional_seconds = std::abs(seconds - static_cast(seconds_int)); + const uint64_t fractional_seconds_uint = static_cast(fractional_seconds * UINT64_MAX); + timestamp->set_fractional_seconds(fractional_seconds_uint); +} + // Or together input_array and input_raw to implement the "bitfield_as_enum_array" feature for inputs. // Note: TEnum is unused because protobuf C++ represents repeated enums as a int32 arrays. template diff --git a/source/tests/system/nidaqmx_driver_api_tests.cpp b/source/tests/system/nidaqmx_driver_api_tests.cpp index 75f2c671c..19f97450e 100644 --- a/source/tests/system/nidaqmx_driver_api_tests.cpp +++ b/source/tests/system/nidaqmx_driver_api_tests.cpp @@ -147,12 +147,12 @@ class NiDAQmxDriverApiTests : public Test { return clear_task(task()); } - CreateAIVoltageChanRequest create_ai_voltage_request(double min_val, double max_val, const std::string& custom_scale_name = "") + CreateAIVoltageChanRequest create_ai_voltage_request(double min_val, double max_val, const std::string& custom_scale_name = "", const std::string& physical_channel = "gRPCSystemTestDAQ/ai0", const std::string& channel_name = "ai0") { CreateAIVoltageChanRequest request; set_request_session_name(request); - request.set_physical_channel("gRPCSystemTestDAQ/ai0"); - request.set_name_to_assign_to_channel("ai0"); + request.set_physical_channel(physical_channel); + request.set_name_to_assign_to_channel(channel_name); request.set_terminal_config(InputTermCfgWithDefault::INPUT_TERM_CFG_WITH_DEFAULT_CFG_DEFAULT); request.set_min_val(min_val); request.set_max_val(max_val); @@ -180,16 +180,9 @@ 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()) + ::grpc::Status create_ai_voltage_chan(const std::string& physical_channel, const std::string& channel_name, 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); + auto request = create_ai_voltage_request(min_val, max_val, "", physical_channel, channel_name); return create_ai_voltage_chan(request, response); } @@ -438,7 +431,7 @@ class NiDAQmxDriverApiTests : public Test { } ::grpc::Status read_analog_waveforms( - int32 number_of_samples_per_channel, + int32 num_samps_per_chan, double timeout = 10.0, WaveformAttributeMode waveform_attribute_mode = WaveformAttributeMode::WAVEFORM_ATTRIBUTE_MODE_NONE, ReadAnalogWaveformsResponse& response = ThrowawayResponse::response()) @@ -446,7 +439,7 @@ class NiDAQmxDriverApiTests : public Test { ::grpc::ClientContext context; ReadAnalogWaveformsRequest request; set_request_session_name(request); - request.set_number_of_samples_per_channel(number_of_samples_per_channel); + request.set_num_samps_per_chan(num_samps_per_chan); request.set_timeout(timeout); request.set_waveform_attribute_mode(waveform_attribute_mode); auto status = stub()->ReadAnalogWaveforms(&context, request, &response); @@ -1196,6 +1189,16 @@ class NiDAQmxDriverApiTests : public Test { EXPECT_THAT(data, Each(Not(Gt(max_val)))); } + // Get number of seconds since year 1 AD (Jan 1, 0001) - .NET DateTime epoch + // From year 1 AD to 1970-01-01 is 719162 days * 24 * 3600 = 62135596800 seconds + int64_t get_seconds_since_year1() const + { + const auto epoch_offset_year1_to_1970 = 62135596800LL; + auto now = std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch()).count(); + return now + epoch_offset_year1_to_1970; + } + DeviceServerInterface* device_server_; std::unique_ptr<::nidevice_grpc::Session> driver_session_; std::unique_ptr nidaqmx_stub_; @@ -1516,7 +1519,9 @@ TEST_F(NiDAQmxDriverApiTests, ReadAnalogWaveforms_WithNoAttributeMode_ReturnsWav 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); + auto create_channel_status = create_ai_voltage_chan("gRPCSystemTestDAQ/ai0", "ai0", -1.0, 1.0, create_channel_response); + EXPECT_SUCCESS(create_channel_status, create_channel_response); + create_channel_status = create_ai_voltage_chan("gRPCSystemTestDAQ/ai1", "ai1", -1.0, 1.0, create_channel_response); EXPECT_SUCCESS(create_channel_status, create_channel_response); start_task(); @@ -1531,7 +1536,10 @@ TEST_F(NiDAQmxDriverApiTests, ReadAnalogWaveforms_WithNoAttributeMode_ReturnsWav 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_EQ(waveform.y_data_size(), NUM_SAMPLES); + EXPECT_FALSE(waveform.has_t0()); + EXPECT_EQ(waveform.dt(), 0.0); + EXPECT_EQ(waveform.attributes_size(), 0); for (const auto& sample : waveform.y_data()) { EXPECT_GE(sample, -1.0); @@ -1545,7 +1553,9 @@ TEST_F(NiDAQmxDriverApiTests, ReadAnalogWaveforms_WithTimingMode_ReturnsWaveform 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); + auto create_channel_status = create_ai_voltage_chan("gRPCSystemTestDAQ/ai0", "ai0", -5.0, 5.0, create_channel_response); + EXPECT_SUCCESS(create_channel_status, create_channel_response); + create_channel_status = create_ai_voltage_chan("gRPCSystemTestDAQ/ai1", "ai1", -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); @@ -1567,18 +1577,7 @@ TEST_F(NiDAQmxDriverApiTests, ReadAnalogWaveforms_WithTimingMode_ReturnsWaveform 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_NEAR(waveform.t0().seconds(), get_seconds_since_year1(), 1); EXPECT_GT(waveform.dt(), 0.0); for (const auto& sample : waveform.y_data()) { @@ -1593,7 +1592,9 @@ TEST_F(NiDAQmxDriverApiTests, ReadAnalogWaveforms_WithExtendedPropertiesMode_Ret 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); + auto create_channel_status = create_ai_voltage_chan("gRPCSystemTestDAQ/ai0", "ai0", -2.0, 2.0, create_channel_response); + EXPECT_SUCCESS(create_channel_status, create_channel_response); + create_channel_status = create_ai_voltage_chan("gRPCSystemTestDAQ/ai1", "ai1", -2.0, 2.0, create_channel_response); EXPECT_SUCCESS(create_channel_status, create_channel_response); start_task(); @@ -1645,7 +1646,9 @@ TEST_F(NiDAQmxDriverApiTests, ReadAnalogWaveforms_WithTimingAndExtendedPropertie 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); + auto create_channel_status = create_ai_voltage_chan("gRPCSystemTestDAQ/ai0", "ai0", -3.0, 3.0, create_channel_response); + EXPECT_SUCCESS(create_channel_status, create_channel_response); + create_channel_status = create_ai_voltage_chan("gRPCSystemTestDAQ/ai1", "ai1", -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); @@ -1671,6 +1674,7 @@ TEST_F(NiDAQmxDriverApiTests, ReadAnalogWaveforms_WithTimingAndExtendedPropertie const auto& waveform = read_response.waveforms(i); EXPECT_EQ(waveform.y_data_size(), NUM_SAMPLES); EXPECT_TRUE(waveform.has_t0()); + EXPECT_NEAR(waveform.t0().seconds(), get_seconds_since_year1(), 1); EXPECT_GT(waveform.dt(), 0.0); EXPECT_GT(waveform.attributes_size(), 0); diff --git a/source/tests/unit/precision_timestamp_converter_tests.cpp b/source/tests/unit/precision_timestamp_converter_tests.cpp new file mode 100644 index 000000000..de0a9ff18 --- /dev/null +++ b/source/tests/unit/precision_timestamp_converter_tests.cpp @@ -0,0 +1,92 @@ +#include +#include +#include +#include + +namespace ni { +namespace tests { +namespace unit { +namespace { + +using nidevice_grpc::converters::TicksPerSecond; +using nidevice_grpc::converters::convert_ticks_to_precision_timestamp; +using ::ni::protobuf::types::PrecisionTimestamp; + +TEST(PrecisionTimestampConverterTests, ConvertZeroTicks_ReturnsZeroTimestamp) +{ + PrecisionTimestamp timestamp; + convert_ticks_to_precision_timestamp(0, ×tamp); + + EXPECT_EQ(timestamp.seconds(), 0); + EXPECT_EQ(timestamp.fractional_seconds(), 0); +} + +TEST(PrecisionTimestampConverterTests, ConvertOneSecond_ReturnsOneSecondZeroFractional) +{ + PrecisionTimestamp timestamp; + convert_ticks_to_precision_timestamp(TicksPerSecond, ×tamp); + + EXPECT_EQ(timestamp.seconds(), 1); + EXPECT_EQ(timestamp.fractional_seconds(), 0); +} + +TEST(PrecisionTimestampConverterTests, ConvertHalfSecond_ReturnsCorrectFractionalSeconds) +{ + PrecisionTimestamp timestamp; + convert_ticks_to_precision_timestamp(0.5 * TicksPerSecond, ×tamp); + + EXPECT_EQ(timestamp.seconds(), 0); + EXPECT_NEAR(timestamp.fractional_seconds(), 0.5 * UINT64_MAX, 1); +} + +TEST(PrecisionTimestampConverterTests, ConvertQuarterSecond_ReturnsCorrectFractionalSeconds) +{ + PrecisionTimestamp timestamp; + convert_ticks_to_precision_timestamp(0.25 * TicksPerSecond, ×tamp); + + EXPECT_EQ(timestamp.seconds(), 0); + EXPECT_NEAR(timestamp.fractional_seconds(), 0.25 * UINT64_MAX, 1); +} + +TEST(PrecisionTimestampConverterTests, ConvertThreeQuartersSecond_ReturnsCorrectFractionalSeconds) +{ + PrecisionTimestamp timestamp; + convert_ticks_to_precision_timestamp(0.75 * TicksPerSecond, ×tamp); + + EXPECT_EQ(timestamp.seconds(), 0); + EXPECT_NEAR(timestamp.fractional_seconds(), 0.75 * UINT64_MAX, 1); +} + +TEST(PrecisionTimestampConverterTests, ConvertOneAndHalfSeconds_ReturnsCorrectSecondsAndFractional) +{ + PrecisionTimestamp timestamp; + convert_ticks_to_precision_timestamp(1.5 * TicksPerSecond, ×tamp); + + EXPECT_EQ(timestamp.seconds(), 1); + EXPECT_NEAR(timestamp.fractional_seconds(), 0.5 * UINT64_MAX, 1); +} + +TEST(PrecisionTimestampConverterTests, ConvertSingleTick_ReturnsCorrectFractionalSeconds) +{ + PrecisionTimestamp timestamp; + convert_ticks_to_precision_timestamp(1, ×tamp); + + EXPECT_EQ(timestamp.seconds(), 0); + // Single tick (1e-7 seconds) should equal this specific fractional value + EXPECT_NEAR(timestamp.fractional_seconds(), 1844674407371, 1); +} + +TEST(PrecisionTimestampConverterTests, ConvertLargeTimestamp_HandlesCorrectly) +{ + PrecisionTimestamp timestamp; + // Year 2024 approximately: many seconds since year 1 AD plus some fractional part + convert_ticks_to_precision_timestamp(63861753600.25 * TicksPerSecond, ×tamp); + + EXPECT_EQ(timestamp.seconds(), 63861753600LL); + EXPECT_NEAR(timestamp.fractional_seconds(), 0.25 * UINT64_MAX, 1e-5 * UINT64_MAX); +} + +} // namespace +} // namespace unit +} // namespace tests +} // namespace ni \ No newline at end of file From 9c0c3330c1c0723755066ad75e0280f6d984e915 Mon Sep 17 00:00:00 2001 From: Mike Prosser Date: Fri, 24 Oct 2025 15:15:04 -0500 Subject: [PATCH 19/25] copy protobuf types protos to heirarchical location --- source/codegen/stage_client_files.py | 4 +++- source/codegen/validate_examples.py | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/source/codegen/stage_client_files.py b/source/codegen/stage_client_files.py index c692507d0..94d6257d5 100644 --- a/source/codegen/stage_client_files.py +++ b/source/codegen/stage_client_files.py @@ -132,8 +132,10 @@ def stage_client_files(output_path: Path, ignore_release_readiness: bool): for file in artifact_locations.grpcdevice_protos.glob("*.proto"): copy2(file, proto_path) + protobuf_types_path = proto_path / "ni" / "protobuf" / "types" + protobuf_types_path.mkdir(parents=True, exist_ok=True) for file in artifact_locations.protobuftypes_protos.glob("*.proto"): - copy2(file, proto_path) + copy2(file, protobuf_types_path) for file in _get_release_proto_files(artifact_locations, readiness): copy2(file, proto_path) diff --git a/source/codegen/validate_examples.py b/source/codegen/validate_examples.py index e9c0b796d..ec218b966 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/ -I{ni_apis_root}/ --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/ --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()): From 7b90ab4d4fe278a5e79d8cbd09b953c85dc120ca Mon Sep 17 00:00:00 2001 From: Mike Prosser Date: Mon, 27 Oct 2025 12:24:42 -0500 Subject: [PATCH 20/25] convert_dot_net_daqmx_ticks_to_btf_precision_timestamp --- source/custom/nidaqmx_service.custom.cpp | 8 +- source/server/converters.h | 19 ++- .../precision_timestamp_converter_tests.cpp | 126 +++++++++++++----- 3 files changed, 106 insertions(+), 47 deletions(-) diff --git a/source/custom/nidaqmx_service.custom.cpp b/source/custom/nidaqmx_service.custom.cpp index 17dbe6cce..67d4aa358 100644 --- a/source/custom/nidaqmx_service.custom.cpp +++ b/source/custom/nidaqmx_service.custom.cpp @@ -8,8 +8,8 @@ namespace nidaqmx_grpc { using nidevice_grpc::converters::convert_to_grpc; -using nidevice_grpc::converters::convert_ticks_to_precision_timestamp; -using nidevice_grpc::converters::SecondsPerTick; +using nidevice_grpc::converters::convert_dot_net_daqmx_ticks_to_btf_precision_timestamp; +using nidevice_grpc::converters::SecondsPerDotNetTick; using google::protobuf::RepeatedPtrField; using ::ni::protobuf::types::DoubleAnalogWaveform; @@ -201,8 +201,8 @@ ::grpc::Status NiDAQmxService::ReadAnalogWaveforms(::grpc::ServerContext* contex if (waveform_attribute_mode & WaveformAttributeMode::WAVEFORM_ATTRIBUTE_MODE_TIMING) { auto* waveform_t0 = waveform->mutable_t0(); - convert_ticks_to_precision_timestamp(t0_array[i], waveform_t0); - waveform->set_dt(static_cast(dt_array[i]) * SecondsPerTick); + convert_dot_net_daqmx_ticks_to_btf_precision_timestamp(t0_array[i], waveform_t0); + waveform->set_dt(static_cast(dt_array[i]) * SecondsPerDotNetTick); } } diff --git a/source/server/converters.h b/source/server/converters.h index 0138733d6..31cc28e78 100644 --- a/source/server/converters.h +++ b/source/server/converters.h @@ -341,10 +341,11 @@ inline void convert_to_grpc(const SmtSpectrumInfoType& input, nidevice_grpc::Smt } const int64 SecondsFromCVI1904EpochTo1970Epoch = 2082844800LL; +const int64 SecondsFromDAQmx0001EpochToCVI1904Epoch = -((static_cast(0xfffffff2) << 32) | 0x0493b980); // extracted from NITYPES_ABSOLUTETIME_EPOCH_BIAS_FROM_0001 in ni-central/src/platform_services/abstractions/niatomicd/nitypes/source/nitypes/time/AbsoluteTime.h const double TwoToSixtyFour = (double)(1 << 31) * (double)(1 << 31) * (double)(1 << 2); const double NanosecondsPerSecond = 1000000000.0; -const int64 TicksPerSecond = 1e7; // each tick is 100ns -const double SecondsPerTick = 1e-7; +const int64 DotNetTicksPerSecond = 1e7; // each tick is 100ns +const double SecondsPerDotNetTick = 1e-7; template <> inline void convert_to_grpc(const CVIAbsoluteTime& value, google::protobuf::Timestamp* timestamp) @@ -371,15 +372,13 @@ inline CVIAbsoluteTime convert_from_grpc(const google::protobuf::Timestamp& valu return cviTime; } -// Convert ticks (100ns since Jan 1, 0001) to PrecisionTimestamp -inline void convert_ticks_to_precision_timestamp(int64 ticks, ::ni::protobuf::types::PrecisionTimestamp* timestamp) +// Convert .NET/DAQmx ticks (100ns since Jan 1, 0001) to NI-BTF PrecisionTimestamp +inline void convert_dot_net_daqmx_ticks_to_btf_precision_timestamp(int64 dot_net_ticks, ::ni::protobuf::types::PrecisionTimestamp* timestamp) { - const double seconds = static_cast(ticks) * SecondsPerTick; - const int64 seconds_int = static_cast(std::floor(seconds)); - timestamp->set_seconds(seconds_int); - const double fractional_seconds = std::abs(seconds - static_cast(seconds_int)); - const uint64_t fractional_seconds_uint = static_cast(fractional_seconds * UINT64_MAX); - timestamp->set_fractional_seconds(fractional_seconds_uint); + const int64 dot_net_ticks_since_1904 = dot_net_ticks - SecondsFromDAQmx0001EpochToCVI1904Epoch * DotNetTicksPerSecond; + timestamp->set_seconds(dot_net_ticks_since_1904 / DotNetTicksPerSecond); + const int64 remaining_ticks = dot_net_ticks_since_1904 % DotNetTicksPerSecond; + timestamp->set_fractional_seconds(static_cast((static_cast(remaining_ticks) / DotNetTicksPerSecond) * TwoToSixtyFour)); } // Or together input_array and input_raw to implement the "bitfield_as_enum_array" feature for inputs. diff --git a/source/tests/unit/precision_timestamp_converter_tests.cpp b/source/tests/unit/precision_timestamp_converter_tests.cpp index de0a9ff18..397610d48 100644 --- a/source/tests/unit/precision_timestamp_converter_tests.cpp +++ b/source/tests/unit/precision_timestamp_converter_tests.cpp @@ -8,82 +8,142 @@ namespace tests { namespace unit { namespace { -using nidevice_grpc::converters::TicksPerSecond; -using nidevice_grpc::converters::convert_ticks_to_precision_timestamp; +using nidevice_grpc::converters::DotNetTicksPerSecond; +using nidevice_grpc::converters::TwoToSixtyFour; +using nidevice_grpc::converters::convert_dot_net_daqmx_ticks_to_btf_precision_timestamp; +using nidevice_grpc::converters::SecondsFromDAQmx0001EpochToCVI1904Epoch; using ::ni::protobuf::types::PrecisionTimestamp; -TEST(PrecisionTimestampConverterTests, ConvertZeroTicks_ReturnsZeroTimestamp) +// // Use the same constant as the CVIAbsoluteTime conversion +// // const double TwoToSixtyFour = (double)(1 << 31) * (double)(1 << 31) * (double)(1 << 2); + +// // The epoch bias from year 1 (0001) to 1904 - extracted from NITYPES_ABSOLUTETIME_EPOCH_BIAS_FROM_0001 +// // The actual computed value from our conversion function +// // const int64_t kEpochBiasSeconds = 60052752000LL; +// const int64 NiBtfEpochBiasSeconds = (static_cast(0xfffffff2) << 32) | 0x0493b980; + +// // Convert ticks (100ns since Jan 1, 0001) to PrecisionTimestamp +// inline void convert_dot_net_ticks_to_btf_precision_timestamp(int64 dot_net_ticks, ::ni::protobuf::types::PrecisionTimestamp* timestamp) +// { +// // Convert to ticks since 1904 epoch by adding the epoch bias +// // NITYPES_ABSOLUTETIME_EPOCH_BIAS_FROM_0001 represents the offset from year 1 to 1904 +// // The FixedPointSigned64 format: high 32 bits (0xfffffff2) and low 32 bits (0x0493b980) +// // This is a negative offset representing seconds from 0001 to 1904 +// // const int64 epoch_bias_seconds = (static_cast(0xfffffff2) << 32) | 0x0493b980; +// // const int64 epoch_bias_ticks = NiBtfEpochBiasSeconds * DotNetTicksPerSecond; +// const int64 ticks_since_1904 = NiBtfEpochBiasSeconds * DotNetTicksPerSecond + dot_net_ticks; + +// // Convert ticks to seconds and fractional seconds +// const int64 seconds_int = ticks_since_1904 / DotNetTicksPerSecond; +// const int64 remaining_ticks = ticks_since_1904 % DotNetTicksPerSecond; + +// timestamp->set_seconds(seconds_int); +// // Convert remaining ticks to fractional seconds at 2^-64 resolution using floating point +// // Similar to CVIAbsoluteTime conversion: fractional_seconds = (remaining_ticks / DotNetTicksPerSecond) * 2^64 +// const uint64_t fractional_seconds_uint = static_cast((static_cast(remaining_ticks) / DotNetTicksPerSecond) * TwoToSixtyFour); + +// timestamp->set_fractional_seconds(fractional_seconds_uint); +// } + +//const int64 NiBtfEpochPosativeBiasSeconds = -NiBtfEpochBiasSeconds; // NiBtfEpochBiasSeconds is negative, but we need the positive version to calculate test values +const int64 EpochTicks = SecondsFromDAQmx0001EpochToCVI1904Epoch * DotNetTicksPerSecond; + +// January 1, 2025 constants +// From January 1, 1904 to January 1, 2025 = 121 years = 44,165 days (including leap years) +// 121 years contains: 29 leap years (1904, 1908, ..., 2020, 2024) and 92 regular years +// Days: 29 * 366 + 92 * 365 = 10,614 + 33,580 = 44,194 days +// Wait, let me be more precise: 2025 - 1904 = 121 years +// Leap years in this period: every 4 years except century years not divisible by 400 +// 1904, 1908, 1912, ..., 2020, 2024 = 31 leap years (1904 to 2024 inclusive, every 4 years) +// Regular years: 121 - 31 = 90 years +// Total days: 31 * 366 + 90 * 365 = 11,346 + 32,850 = 44,196 days + +TEST(PrecisionTimestampConverterTests, ConvertZeroTicks_ReturnsNegativeEpochTimestamp) +{ + PrecisionTimestamp timestamp; + convert_dot_net_daqmx_ticks_to_btf_precision_timestamp(0, ×tamp); + + EXPECT_EQ(timestamp.seconds(), -SecondsFromDAQmx0001EpochToCVI1904Epoch); + EXPECT_EQ(timestamp.fractional_seconds(), 0); +} + +TEST(PrecisionTimestampConverterTests, ConvertEpochTicks_ReturnsZeroTimestamp) { PrecisionTimestamp timestamp; - convert_ticks_to_precision_timestamp(0, ×tamp); + convert_dot_net_daqmx_ticks_to_btf_precision_timestamp(EpochTicks, ×tamp); EXPECT_EQ(timestamp.seconds(), 0); EXPECT_EQ(timestamp.fractional_seconds(), 0); } -TEST(PrecisionTimestampConverterTests, ConvertOneSecond_ReturnsOneSecondZeroFractional) +TEST(PrecisionTimestampConverterTests, ConvertOneSecondOfTicks_ReturnsOneSecond) { PrecisionTimestamp timestamp; - convert_ticks_to_precision_timestamp(TicksPerSecond, ×tamp); + const int64_t one_second_ticks = EpochTicks + 1 * DotNetTicksPerSecond; + convert_dot_net_daqmx_ticks_to_btf_precision_timestamp(one_second_ticks, ×tamp); EXPECT_EQ(timestamp.seconds(), 1); EXPECT_EQ(timestamp.fractional_seconds(), 0); } -TEST(PrecisionTimestampConverterTests, ConvertHalfSecond_ReturnsCorrectFractionalSeconds) +TEST(PrecisionTimestampConverterTests, ConvertHalfSecondOfTicks_ReturnsCorrectFractionalSeconds) { - PrecisionTimestamp timestamp; - convert_ticks_to_precision_timestamp(0.5 * TicksPerSecond, ×tamp); - + PrecisionTimestamp timestamp; + convert_dot_net_daqmx_ticks_to_btf_precision_timestamp(EpochTicks + DotNetTicksPerSecond / 2, ×tamp); + EXPECT_EQ(timestamp.seconds(), 0); - EXPECT_NEAR(timestamp.fractional_seconds(), 0.5 * UINT64_MAX, 1); + EXPECT_EQ(timestamp.fractional_seconds(), static_cast(0.5 * TwoToSixtyFour)); } -TEST(PrecisionTimestampConverterTests, ConvertQuarterSecond_ReturnsCorrectFractionalSeconds) +TEST(PrecisionTimestampConverterTests, ConvertQuarterSecondOfTicks_ReturnsCorrectFractionalSeconds) { - PrecisionTimestamp timestamp; - convert_ticks_to_precision_timestamp(0.25 * TicksPerSecond, ×tamp); - + PrecisionTimestamp timestamp; + convert_dot_net_daqmx_ticks_to_btf_precision_timestamp(EpochTicks + DotNetTicksPerSecond / 4, ×tamp); + EXPECT_EQ(timestamp.seconds(), 0); - EXPECT_NEAR(timestamp.fractional_seconds(), 0.25 * UINT64_MAX, 1); + EXPECT_EQ(timestamp.fractional_seconds(), static_cast(0.25 * TwoToSixtyFour)); } -TEST(PrecisionTimestampConverterTests, ConvertThreeQuartersSecond_ReturnsCorrectFractionalSeconds) +TEST(PrecisionTimestampConverterTests, ConvertThreeQuartersSecondOfTicks_ReturnsCorrectFractionalSeconds) { - PrecisionTimestamp timestamp; - convert_ticks_to_precision_timestamp(0.75 * TicksPerSecond, ×tamp); + PrecisionTimestamp timestamp; + convert_dot_net_daqmx_ticks_to_btf_precision_timestamp(EpochTicks + DotNetTicksPerSecond / 2 + DotNetTicksPerSecond / 4, ×tamp); EXPECT_EQ(timestamp.seconds(), 0); - EXPECT_NEAR(timestamp.fractional_seconds(), 0.75 * UINT64_MAX, 1); + EXPECT_EQ(timestamp.fractional_seconds(), static_cast(0.75 * TwoToSixtyFour)); } -TEST(PrecisionTimestampConverterTests, ConvertOneAndHalfSeconds_ReturnsCorrectSecondsAndFractional) +TEST(PrecisionTimestampConverterTests, ConvertOneAndHalfSecondsOfTicks_ReturnsCorrectSecondsAndFractional) { - PrecisionTimestamp timestamp; - convert_ticks_to_precision_timestamp(1.5 * TicksPerSecond, ×tamp); + PrecisionTimestamp timestamp; + convert_dot_net_daqmx_ticks_to_btf_precision_timestamp(EpochTicks + 1 * DotNetTicksPerSecond + DotNetTicksPerSecond / 2, ×tamp); EXPECT_EQ(timestamp.seconds(), 1); - EXPECT_NEAR(timestamp.fractional_seconds(), 0.5 * UINT64_MAX, 1); + EXPECT_EQ(timestamp.fractional_seconds(), static_cast(0.5 * TwoToSixtyFour)); } -TEST(PrecisionTimestampConverterTests, ConvertSingleTick_ReturnsCorrectFractionalSeconds) +TEST(PrecisionTimestampConverterTests, ConvertExact2500000Ticks_ReturnsExactFractionalSeconds) { - PrecisionTimestamp timestamp; - convert_ticks_to_precision_timestamp(1, ×tamp); + PrecisionTimestamp timestamp; + convert_dot_net_daqmx_ticks_to_btf_precision_timestamp(EpochTicks + 2500000, ×tamp); EXPECT_EQ(timestamp.seconds(), 0); - // Single tick (1e-7 seconds) should equal this specific fractional value - EXPECT_NEAR(timestamp.fractional_seconds(), 1844674407371, 1); + EXPECT_EQ(timestamp.fractional_seconds(), 0x4000000000000000ULL); } TEST(PrecisionTimestampConverterTests, ConvertLargeTimestamp_HandlesCorrectly) { PrecisionTimestamp timestamp; - // Year 2024 approximately: many seconds since year 1 AD plus some fractional part - convert_ticks_to_precision_timestamp(63861753600.25 * TicksPerSecond, ×tamp); + const int64 DaysFromNiBtfEpochToJan1_2025 = 44196; + const int64 SecondsFromNiBtfEpochToJan1_2025 = DaysFromNiBtfEpochToJan1_2025 * 24 * 60 * 60; + const int64 Jan1_2025_Seconds = SecondsFromNiBtfEpochToJan1_2025; // Seconds since 1904 epoch + const int64 Jan1_2025_Ticks = EpochTicks + SecondsFromNiBtfEpochToJan1_2025 * DotNetTicksPerSecond; + const int64_t large_timestamp_ticks = Jan1_2025_Ticks + DotNetTicksPerSecond / 4; + + convert_dot_net_daqmx_ticks_to_btf_precision_timestamp(large_timestamp_ticks, ×tamp); - EXPECT_EQ(timestamp.seconds(), 63861753600LL); - EXPECT_NEAR(timestamp.fractional_seconds(), 0.25 * UINT64_MAX, 1e-5 * UINT64_MAX); + EXPECT_EQ(timestamp.seconds(), Jan1_2025_Seconds); + EXPECT_EQ(timestamp.fractional_seconds(), static_cast(0.25 * TwoToSixtyFour)); } } // namespace From 9f7723256e7e5893759d40a1a8b082a04ec1b75c Mon Sep 17 00:00:00 2001 From: Mike Prosser Date: Mon, 27 Oct 2025 13:46:01 -0500 Subject: [PATCH 21/25] fixes and cleanup --- source/custom/nidaqmx_service.custom.cpp | 4 +- source/server/converters.h | 16 +- .../tests/system/nidaqmx_driver_api_tests.cpp | 18 +- .../precision_timestamp_converter_tests.cpp | 160 +++++++++--------- 4 files changed, 99 insertions(+), 99 deletions(-) diff --git a/source/custom/nidaqmx_service.custom.cpp b/source/custom/nidaqmx_service.custom.cpp index 67d4aa358..1f7e952e7 100644 --- a/source/custom/nidaqmx_service.custom.cpp +++ b/source/custom/nidaqmx_service.custom.cpp @@ -9,7 +9,7 @@ namespace nidaqmx_grpc { using nidevice_grpc::converters::convert_to_grpc; using nidevice_grpc::converters::convert_dot_net_daqmx_ticks_to_btf_precision_timestamp; -using nidevice_grpc::converters::SecondsPerDotNetTick; +using nidevice_grpc::converters::DotNetTicksPerSecond; using google::protobuf::RepeatedPtrField; using ::ni::protobuf::types::DoubleAnalogWaveform; @@ -202,7 +202,7 @@ ::grpc::Status NiDAQmxService::ReadAnalogWaveforms(::grpc::ServerContext* contex if (waveform_attribute_mode & WaveformAttributeMode::WAVEFORM_ATTRIBUTE_MODE_TIMING) { auto* waveform_t0 = waveform->mutable_t0(); convert_dot_net_daqmx_ticks_to_btf_precision_timestamp(t0_array[i], waveform_t0); - waveform->set_dt(static_cast(dt_array[i]) * SecondsPerDotNetTick); + waveform->set_dt(static_cast(dt_array[i]) / DotNetTicksPerSecond); } } diff --git a/source/server/converters.h b/source/server/converters.h index 31cc28e78..2a7a2d7e7 100644 --- a/source/server/converters.h +++ b/source/server/converters.h @@ -11,6 +11,7 @@ #include #include +#include #include #include #include @@ -345,7 +346,6 @@ const int64 SecondsFromDAQmx0001EpochToCVI1904Epoch = -((static_cast(0xff const double TwoToSixtyFour = (double)(1 << 31) * (double)(1 << 31) * (double)(1 << 2); const double NanosecondsPerSecond = 1000000000.0; const int64 DotNetTicksPerSecond = 1e7; // each tick is 100ns -const double SecondsPerDotNetTick = 1e-7; template <> inline void convert_to_grpc(const CVIAbsoluteTime& value, google::protobuf::Timestamp* timestamp) @@ -376,9 +376,17 @@ inline CVIAbsoluteTime convert_from_grpc(const google::protobuf::Timestamp& valu inline void convert_dot_net_daqmx_ticks_to_btf_precision_timestamp(int64 dot_net_ticks, ::ni::protobuf::types::PrecisionTimestamp* timestamp) { const int64 dot_net_ticks_since_1904 = dot_net_ticks - SecondsFromDAQmx0001EpochToCVI1904Epoch * DotNetTicksPerSecond; - timestamp->set_seconds(dot_net_ticks_since_1904 / DotNetTicksPerSecond); - const int64 remaining_ticks = dot_net_ticks_since_1904 % DotNetTicksPerSecond; - timestamp->set_fractional_seconds(static_cast((static_cast(remaining_ticks) / DotNetTicksPerSecond) * TwoToSixtyFour)); + const double total_seconds = static_cast(dot_net_ticks_since_1904) / DotNetTicksPerSecond; + double integer_part; + double fractional_part = std::modf(total_seconds, &integer_part); + + if (fractional_part < 0) { + integer_part -= 1; + fractional_part += 1; + } + + timestamp->set_seconds(static_cast(integer_part)); + timestamp->set_fractional_seconds(static_cast(fractional_part * TwoToSixtyFour)); } // Or together input_array and input_raw to implement the "bitfield_as_enum_array" feature for inputs. diff --git a/source/tests/system/nidaqmx_driver_api_tests.cpp b/source/tests/system/nidaqmx_driver_api_tests.cpp index 19f97450e..7a1c5dd5f 100644 --- a/source/tests/system/nidaqmx_driver_api_tests.cpp +++ b/source/tests/system/nidaqmx_driver_api_tests.cpp @@ -12,6 +12,7 @@ #include "device_server.h" #include "enumerate_devices.h" #include "nidaqmx/nidaqmx_client.h" +#include "server/converters.h" #include "tests/utilities/async_helpers.h" #include "tests/utilities/scope_exit.h" #include "tests/utilities/test_helpers.h" @@ -19,6 +20,7 @@ using namespace ::testing; using namespace nidaqmx_grpc; using google::protobuf::uint32; +using nidevice_grpc::converters::SecondsFromCVI1904EpochTo1970Epoch; namespace client = nidaqmx_grpc::experimental::client; namespace ni { @@ -1189,14 +1191,12 @@ class NiDAQmxDriverApiTests : public Test { EXPECT_THAT(data, Each(Not(Gt(max_val)))); } - // Get number of seconds since year 1 AD (Jan 1, 0001) - .NET DateTime epoch - // From year 1 AD to 1970-01-01 is 719162 days * 24 * 3600 = 62135596800 seconds - int64_t get_seconds_since_year1() const - { - const auto epoch_offset_year1_to_1970 = 62135596800LL; + // Get number of seconds since 1904 epoch (CVI/BTF epoch) for comparison with waveform timestamps + int64_t get_seconds_since_1904() const + { auto now = std::chrono::duration_cast( - std::chrono::system_clock::now().time_since_epoch()).count(); - return now + epoch_offset_year1_to_1970; + std::chrono::system_clock::now().time_since_epoch()).count(); + return now + SecondsFromCVI1904EpochTo1970Epoch; } DeviceServerInterface* device_server_; @@ -1577,7 +1577,7 @@ TEST_F(NiDAQmxDriverApiTests, ReadAnalogWaveforms_WithTimingMode_ReturnsWaveform const auto& waveform = read_response.waveforms(i); EXPECT_EQ(waveform.y_data_size(), NUM_SAMPLES); EXPECT_TRUE(waveform.has_t0()); - EXPECT_NEAR(waveform.t0().seconds(), get_seconds_since_year1(), 1); + EXPECT_NEAR(waveform.t0().seconds(), get_seconds_since_1904(), 1); EXPECT_GT(waveform.dt(), 0.0); for (const auto& sample : waveform.y_data()) { @@ -1674,7 +1674,7 @@ TEST_F(NiDAQmxDriverApiTests, ReadAnalogWaveforms_WithTimingAndExtendedPropertie const auto& waveform = read_response.waveforms(i); EXPECT_EQ(waveform.y_data_size(), NUM_SAMPLES); EXPECT_TRUE(waveform.has_t0()); - EXPECT_NEAR(waveform.t0().seconds(), get_seconds_since_year1(), 1); + EXPECT_NEAR(waveform.t0().seconds(), get_seconds_since_1904(), 1); EXPECT_GT(waveform.dt(), 0.0); EXPECT_GT(waveform.attributes_size(), 0); diff --git a/source/tests/unit/precision_timestamp_converter_tests.cpp b/source/tests/unit/precision_timestamp_converter_tests.cpp index 397610d48..b6d2a9f21 100644 --- a/source/tests/unit/precision_timestamp_converter_tests.cpp +++ b/source/tests/unit/precision_timestamp_converter_tests.cpp @@ -8,56 +8,14 @@ namespace tests { namespace unit { namespace { +using nidevice_grpc::converters::convert_dot_net_daqmx_ticks_to_btf_precision_timestamp; using nidevice_grpc::converters::DotNetTicksPerSecond; using nidevice_grpc::converters::TwoToSixtyFour; -using nidevice_grpc::converters::convert_dot_net_daqmx_ticks_to_btf_precision_timestamp; using nidevice_grpc::converters::SecondsFromDAQmx0001EpochToCVI1904Epoch; using ::ni::protobuf::types::PrecisionTimestamp; -// // Use the same constant as the CVIAbsoluteTime conversion -// // const double TwoToSixtyFour = (double)(1 << 31) * (double)(1 << 31) * (double)(1 << 2); - -// // The epoch bias from year 1 (0001) to 1904 - extracted from NITYPES_ABSOLUTETIME_EPOCH_BIAS_FROM_0001 -// // The actual computed value from our conversion function -// // const int64_t kEpochBiasSeconds = 60052752000LL; -// const int64 NiBtfEpochBiasSeconds = (static_cast(0xfffffff2) << 32) | 0x0493b980; - -// // Convert ticks (100ns since Jan 1, 0001) to PrecisionTimestamp -// inline void convert_dot_net_ticks_to_btf_precision_timestamp(int64 dot_net_ticks, ::ni::protobuf::types::PrecisionTimestamp* timestamp) -// { -// // Convert to ticks since 1904 epoch by adding the epoch bias -// // NITYPES_ABSOLUTETIME_EPOCH_BIAS_FROM_0001 represents the offset from year 1 to 1904 -// // The FixedPointSigned64 format: high 32 bits (0xfffffff2) and low 32 bits (0x0493b980) -// // This is a negative offset representing seconds from 0001 to 1904 -// // const int64 epoch_bias_seconds = (static_cast(0xfffffff2) << 32) | 0x0493b980; -// // const int64 epoch_bias_ticks = NiBtfEpochBiasSeconds * DotNetTicksPerSecond; -// const int64 ticks_since_1904 = NiBtfEpochBiasSeconds * DotNetTicksPerSecond + dot_net_ticks; - -// // Convert ticks to seconds and fractional seconds -// const int64 seconds_int = ticks_since_1904 / DotNetTicksPerSecond; -// const int64 remaining_ticks = ticks_since_1904 % DotNetTicksPerSecond; - -// timestamp->set_seconds(seconds_int); -// // Convert remaining ticks to fractional seconds at 2^-64 resolution using floating point -// // Similar to CVIAbsoluteTime conversion: fractional_seconds = (remaining_ticks / DotNetTicksPerSecond) * 2^64 -// const uint64_t fractional_seconds_uint = static_cast((static_cast(remaining_ticks) / DotNetTicksPerSecond) * TwoToSixtyFour); - -// timestamp->set_fractional_seconds(fractional_seconds_uint); -// } - -//const int64 NiBtfEpochPosativeBiasSeconds = -NiBtfEpochBiasSeconds; // NiBtfEpochBiasSeconds is negative, but we need the positive version to calculate test values const int64 EpochTicks = SecondsFromDAQmx0001EpochToCVI1904Epoch * DotNetTicksPerSecond; -// January 1, 2025 constants -// From January 1, 1904 to January 1, 2025 = 121 years = 44,165 days (including leap years) -// 121 years contains: 29 leap years (1904, 1908, ..., 2020, 2024) and 92 regular years -// Days: 29 * 366 + 92 * 365 = 10,614 + 33,580 = 44,194 days -// Wait, let me be more precise: 2025 - 1904 = 121 years -// Leap years in this period: every 4 years except century years not divisible by 400 -// 1904, 1908, 1912, ..., 2020, 2024 = 31 leap years (1904 to 2024 inclusive, every 4 years) -// Regular years: 121 - 31 = 90 years -// Total days: 31 * 366 + 90 * 365 = 11,346 + 32,850 = 44,196 days - TEST(PrecisionTimestampConverterTests, ConvertZeroTicks_ReturnsNegativeEpochTimestamp) { PrecisionTimestamp timestamp; @@ -86,66 +44,100 @@ TEST(PrecisionTimestampConverterTests, ConvertOneSecondOfTicks_ReturnsOneSecond) EXPECT_EQ(timestamp.fractional_seconds(), 0); } -TEST(PrecisionTimestampConverterTests, ConvertHalfSecondOfTicks_ReturnsCorrectFractionalSeconds) -{ - PrecisionTimestamp timestamp; - convert_dot_net_daqmx_ticks_to_btf_precision_timestamp(EpochTicks + DotNetTicksPerSecond / 2, ×tamp); +// Parameterized test for fractional seconds +struct FractionalSecondsTestParam { + double fraction; + int64_t ticks_offset; + uint64_t expected_fractional_seconds; + std::string description; +}; - EXPECT_EQ(timestamp.seconds(), 0); - EXPECT_EQ(timestamp.fractional_seconds(), static_cast(0.5 * TwoToSixtyFour)); -} +class PrecisionTimestampConverterFractionalSecondsTests : public ::testing::TestWithParam {}; -TEST(PrecisionTimestampConverterTests, ConvertQuarterSecondOfTicks_ReturnsCorrectFractionalSeconds) +TEST_P(PrecisionTimestampConverterFractionalSecondsTests, ConvertFractionalSeconds_ReturnsCorrectFractionalSeconds) { + const auto& param = GetParam(); PrecisionTimestamp timestamp; - convert_dot_net_daqmx_ticks_to_btf_precision_timestamp(EpochTicks + DotNetTicksPerSecond / 4, ×tamp); + convert_dot_net_daqmx_ticks_to_btf_precision_timestamp(EpochTicks + param.ticks_offset, ×tamp); - EXPECT_EQ(timestamp.seconds(), 0); - EXPECT_EQ(timestamp.fractional_seconds(), static_cast(0.25 * TwoToSixtyFour)); + EXPECT_EQ(timestamp.seconds(), 0) << "Failed for " << param.description; + EXPECT_EQ(timestamp.fractional_seconds(), param.expected_fractional_seconds) << "Failed for " << param.description; } -TEST(PrecisionTimestampConverterTests, ConvertThreeQuartersSecondOfTicks_ReturnsCorrectFractionalSeconds) +INSTANTIATE_TEST_SUITE_P( + FractionalSecondsTests, + PrecisionTimestampConverterFractionalSecondsTests, + ::testing::Values( + FractionalSecondsTestParam{0.25, DotNetTicksPerSecond / 4, static_cast(0.25 * TwoToSixtyFour), "quarter second"}, + FractionalSecondsTestParam{0.5, DotNetTicksPerSecond / 2, static_cast(0.5 * TwoToSixtyFour), "half second"}, + FractionalSecondsTestParam{0.75, DotNetTicksPerSecond / 2 + DotNetTicksPerSecond / 4, static_cast(0.75 * TwoToSixtyFour), "three quarters second"}, + FractionalSecondsTestParam{0.25, 2500000, 0x4000000000000000ULL, "quarter second (literal)"}, + FractionalSecondsTestParam{0.5, 5000000, 0x8000000000000000ULL, "half second (literal)"}, + FractionalSecondsTestParam{0.75, 7500000, 0xC000000000000000ULL, "three quarters second (literal)"} + ) +); + +// Parameterized test for seconds and fractional combinations +struct SecondsAndFractionalTestParam { + int64_t seconds; + double fraction; + int64_t ticks_offset; + std::string description; +}; + +class PrecisionTimestampConverterSecondsAndFractionalTests : public ::testing::TestWithParam {}; + +TEST_P(PrecisionTimestampConverterSecondsAndFractionalTests, ConvertSecondsAndFractional_ReturnsCorrectValues) { + const auto& param = GetParam(); PrecisionTimestamp timestamp; - convert_dot_net_daqmx_ticks_to_btf_precision_timestamp(EpochTicks + DotNetTicksPerSecond / 2 + DotNetTicksPerSecond / 4, ×tamp); - - EXPECT_EQ(timestamp.seconds(), 0); - EXPECT_EQ(timestamp.fractional_seconds(), static_cast(0.75 * TwoToSixtyFour)); -} + convert_dot_net_daqmx_ticks_to_btf_precision_timestamp(EpochTicks + param.ticks_offset, ×tamp); -TEST(PrecisionTimestampConverterTests, ConvertOneAndHalfSecondsOfTicks_ReturnsCorrectSecondsAndFractional) -{ - PrecisionTimestamp timestamp; - convert_dot_net_daqmx_ticks_to_btf_precision_timestamp(EpochTicks + 1 * DotNetTicksPerSecond + DotNetTicksPerSecond / 2, ×tamp); - - EXPECT_EQ(timestamp.seconds(), 1); - EXPECT_EQ(timestamp.fractional_seconds(), static_cast(0.5 * TwoToSixtyFour)); + EXPECT_EQ(timestamp.seconds(), param.seconds) << "Failed for " << param.description; + EXPECT_EQ(timestamp.fractional_seconds(), static_cast(param.fraction * TwoToSixtyFour)) << "Failed for " << param.description; } -TEST(PrecisionTimestampConverterTests, ConvertExact2500000Ticks_ReturnsExactFractionalSeconds) -{ - PrecisionTimestamp timestamp; - convert_dot_net_daqmx_ticks_to_btf_precision_timestamp(EpochTicks + 2500000, ×tamp); - - EXPECT_EQ(timestamp.seconds(), 0); - EXPECT_EQ(timestamp.fractional_seconds(), 0x4000000000000000ULL); -} +INSTANTIATE_TEST_SUITE_P( + SecondsAndFractionalTests, + PrecisionTimestampConverterSecondsAndFractionalTests, + ::testing::Values( + SecondsAndFractionalTestParam{1, 0.5, 1 * DotNetTicksPerSecond + DotNetTicksPerSecond / 2, "one and half seconds"} + ) +); -TEST(PrecisionTimestampConverterTests, ConvertLargeTimestamp_HandlesCorrectly) +// Parameterized test for large timestamps across different years +struct LargeTimestampTestParam { + int year; + int64_t days_from_1904_epoch; + std::string description; +}; + +class PrecisionTimestampConverterLargeTimestampTests : public ::testing::TestWithParam {}; + +TEST_P(PrecisionTimestampConverterLargeTimestampTests, ConvertLargeTimestamp_HandlesCorrectly) { + const auto& param = GetParam(); PrecisionTimestamp timestamp; - const int64 DaysFromNiBtfEpochToJan1_2025 = 44196; - const int64 SecondsFromNiBtfEpochToJan1_2025 = DaysFromNiBtfEpochToJan1_2025 * 24 * 60 * 60; - const int64 Jan1_2025_Seconds = SecondsFromNiBtfEpochToJan1_2025; // Seconds since 1904 epoch - const int64 Jan1_2025_Ticks = EpochTicks + SecondsFromNiBtfEpochToJan1_2025 * DotNetTicksPerSecond; - const int64_t large_timestamp_ticks = Jan1_2025_Ticks + DotNetTicksPerSecond / 4; + const int64 SecondsFromNiBtfEpochToYear = param.days_from_1904_epoch * 24 * 60 * 60; + const int64 YearTicks = EpochTicks + SecondsFromNiBtfEpochToYear * DotNetTicksPerSecond; - convert_dot_net_daqmx_ticks_to_btf_precision_timestamp(large_timestamp_ticks, ×tamp); + convert_dot_net_daqmx_ticks_to_btf_precision_timestamp(YearTicks + DotNetTicksPerSecond / 4, ×tamp); - EXPECT_EQ(timestamp.seconds(), Jan1_2025_Seconds); - EXPECT_EQ(timestamp.fractional_seconds(), static_cast(0.25 * TwoToSixtyFour)); + EXPECT_EQ(timestamp.seconds(), SecondsFromNiBtfEpochToYear) << "Failed for " << param.description; + EXPECT_EQ(timestamp.fractional_seconds(), static_cast(0.25 * TwoToSixtyFour)) << "Failed for " << param.description; } +INSTANTIATE_TEST_SUITE_P( + LargeTimestampTests, + PrecisionTimestampConverterLargeTimestampTests, + ::testing::Values( + LargeTimestampTestParam{2025, 44196, "January 1, 2025"}, // 121 years from 1904 = 44196 days (accounting for leap years) + LargeTimestampTestParam{2002, 35794, "January 1, 2002"}, // 98 years from 1904 = 35794 days (accounting for leap years) + LargeTimestampTestParam{1950, 16801, "January 1, 1950"}, // 46 years from 1904 = 16801 days (accounting for leap years) + LargeTimestampTestParam{1850, -19723, "January 1, 1850"} // 54 years before 1904 = -19723 days (accounting for leap years) + ) +); + } // namespace } // namespace unit } // namespace tests From 4f7cf86062cacacb89bbfcc860190979b9053a2d Mon Sep 17 00:00:00 2001 From: Mike Prosser Date: Mon, 27 Oct 2025 14:01:45 -0500 Subject: [PATCH 22/25] cleanup and more test cases --- source/custom/nidaqmx_service.custom.cpp | 1 - .../precision_timestamp_converter_tests.cpp | 43 ++++--------------- 2 files changed, 9 insertions(+), 35 deletions(-) diff --git a/source/custom/nidaqmx_service.custom.cpp b/source/custom/nidaqmx_service.custom.cpp index 1f7e952e7..5ecb105ce 100644 --- a/source/custom/nidaqmx_service.custom.cpp +++ b/source/custom/nidaqmx_service.custom.cpp @@ -196,7 +196,6 @@ ::grpc::Status NiDAQmxService::ReadAnalogWaveforms(::grpc::ServerContext* contex auto* waveform = response->mutable_waveforms(i); auto* y_data = waveform->mutable_y_data(); - y_data->Reserve(samples_per_chan_read); y_data->Add(read_arrays[i].data(), read_arrays[i].data() + samples_per_chan_read); if (waveform_attribute_mode & WaveformAttributeMode::WAVEFORM_ATTRIBUTE_MODE_TIMING) { diff --git a/source/tests/unit/precision_timestamp_converter_tests.cpp b/source/tests/unit/precision_timestamp_converter_tests.cpp index b6d2a9f21..f813b1264 100644 --- a/source/tests/unit/precision_timestamp_converter_tests.cpp +++ b/source/tests/unit/precision_timestamp_converter_tests.cpp @@ -44,39 +44,6 @@ TEST(PrecisionTimestampConverterTests, ConvertOneSecondOfTicks_ReturnsOneSecond) EXPECT_EQ(timestamp.fractional_seconds(), 0); } -// Parameterized test for fractional seconds -struct FractionalSecondsTestParam { - double fraction; - int64_t ticks_offset; - uint64_t expected_fractional_seconds; - std::string description; -}; - -class PrecisionTimestampConverterFractionalSecondsTests : public ::testing::TestWithParam {}; - -TEST_P(PrecisionTimestampConverterFractionalSecondsTests, ConvertFractionalSeconds_ReturnsCorrectFractionalSeconds) -{ - const auto& param = GetParam(); - PrecisionTimestamp timestamp; - convert_dot_net_daqmx_ticks_to_btf_precision_timestamp(EpochTicks + param.ticks_offset, ×tamp); - - EXPECT_EQ(timestamp.seconds(), 0) << "Failed for " << param.description; - EXPECT_EQ(timestamp.fractional_seconds(), param.expected_fractional_seconds) << "Failed for " << param.description; -} - -INSTANTIATE_TEST_SUITE_P( - FractionalSecondsTests, - PrecisionTimestampConverterFractionalSecondsTests, - ::testing::Values( - FractionalSecondsTestParam{0.25, DotNetTicksPerSecond / 4, static_cast(0.25 * TwoToSixtyFour), "quarter second"}, - FractionalSecondsTestParam{0.5, DotNetTicksPerSecond / 2, static_cast(0.5 * TwoToSixtyFour), "half second"}, - FractionalSecondsTestParam{0.75, DotNetTicksPerSecond / 2 + DotNetTicksPerSecond / 4, static_cast(0.75 * TwoToSixtyFour), "three quarters second"}, - FractionalSecondsTestParam{0.25, 2500000, 0x4000000000000000ULL, "quarter second (literal)"}, - FractionalSecondsTestParam{0.5, 5000000, 0x8000000000000000ULL, "half second (literal)"}, - FractionalSecondsTestParam{0.75, 7500000, 0xC000000000000000ULL, "three quarters second (literal)"} - ) -); - // Parameterized test for seconds and fractional combinations struct SecondsAndFractionalTestParam { int64_t seconds; @@ -101,7 +68,15 @@ INSTANTIATE_TEST_SUITE_P( SecondsAndFractionalTests, PrecisionTimestampConverterSecondsAndFractionalTests, ::testing::Values( - SecondsAndFractionalTestParam{1, 0.5, 1 * DotNetTicksPerSecond + DotNetTicksPerSecond / 2, "one and half seconds"} + SecondsAndFractionalTestParam{0, 0.25, DotNetTicksPerSecond / 4, "quarter second"}, + SecondsAndFractionalTestParam{0, 0.5, DotNetTicksPerSecond / 2, "half second"}, + SecondsAndFractionalTestParam{0, 0.75, DotNetTicksPerSecond / 2 + DotNetTicksPerSecond / 4, "three quarters second"}, + SecondsAndFractionalTestParam{0, 0.25, 2500000, "quarter second (literal)"}, + SecondsAndFractionalTestParam{0, 0.5, 5000000, "half second (literal)"}, + SecondsAndFractionalTestParam{0, 0.75, 7500000, "three quarters second (literal)"}, + SecondsAndFractionalTestParam{1, 0.5, 1 * DotNetTicksPerSecond + DotNetTicksPerSecond / 2, "one and half seconds"}, + SecondsAndFractionalTestParam{2, 0.25, 2 * DotNetTicksPerSecond + DotNetTicksPerSecond / 4, "two and quarter seconds"}, + SecondsAndFractionalTestParam{3, 0.75, 3 * DotNetTicksPerSecond + DotNetTicksPerSecond / 2 + DotNetTicksPerSecond / 4, "three and three quarters seconds"} ) ); From 2962d82749bef796ed238667e82905012665631d Mon Sep 17 00:00:00 2001 From: Mike Prosser Date: Mon, 27 Oct 2025 14:26:31 -0500 Subject: [PATCH 23/25] PrecisionTimestampConverterLiteralTests --- .../precision_timestamp_converter_tests.cpp | 34 +++++++++++++++++-- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/source/tests/unit/precision_timestamp_converter_tests.cpp b/source/tests/unit/precision_timestamp_converter_tests.cpp index f813b1264..94a6239c4 100644 --- a/source/tests/unit/precision_timestamp_converter_tests.cpp +++ b/source/tests/unit/precision_timestamp_converter_tests.cpp @@ -71,15 +71,43 @@ INSTANTIATE_TEST_SUITE_P( SecondsAndFractionalTestParam{0, 0.25, DotNetTicksPerSecond / 4, "quarter second"}, SecondsAndFractionalTestParam{0, 0.5, DotNetTicksPerSecond / 2, "half second"}, SecondsAndFractionalTestParam{0, 0.75, DotNetTicksPerSecond / 2 + DotNetTicksPerSecond / 4, "three quarters second"}, - SecondsAndFractionalTestParam{0, 0.25, 2500000, "quarter second (literal)"}, - SecondsAndFractionalTestParam{0, 0.5, 5000000, "half second (literal)"}, - SecondsAndFractionalTestParam{0, 0.75, 7500000, "three quarters second (literal)"}, SecondsAndFractionalTestParam{1, 0.5, 1 * DotNetTicksPerSecond + DotNetTicksPerSecond / 2, "one and half seconds"}, SecondsAndFractionalTestParam{2, 0.25, 2 * DotNetTicksPerSecond + DotNetTicksPerSecond / 4, "two and quarter seconds"}, SecondsAndFractionalTestParam{3, 0.75, 3 * DotNetTicksPerSecond + DotNetTicksPerSecond / 2 + DotNetTicksPerSecond / 4, "three and three quarters seconds"} ) ); +// Parameterized test for literal tick values +struct LiteralTestParam { + int64_t dot_net_ticks; + uint64_t btf_fractional_ticks; + std::string description; +}; + +class PrecisionTimestampConverterLiteralTests : public ::testing::TestWithParam {}; + +TEST_P(PrecisionTimestampConverterLiteralTests, ConvertLiteralTicks_ReturnsCorrectValues) +{ + const auto& param = GetParam(); + PrecisionTimestamp timestamp; + convert_dot_net_daqmx_ticks_to_btf_precision_timestamp(EpochTicks + param.dot_net_ticks, ×tamp); + + EXPECT_EQ(timestamp.seconds(), 0) << "Failed for " << param.description; + EXPECT_EQ(timestamp.fractional_seconds(), param.btf_fractional_ticks) << "Failed for " << param.description; +} + +INSTANTIATE_TEST_SUITE_P( + LiteralTests, + PrecisionTimestampConverterLiteralTests, + ::testing::Values( + LiteralTestParam{0000000, 0x0000000000000000ULL, "zero fractional seconds"}, + LiteralTestParam{2500000, 0x4000000000000000ULL, "quarter second"}, + LiteralTestParam{5000000, 0x8000000000000000ULL, "half second"}, + LiteralTestParam{7500000, 0xC000000000000000ULL, "three quarters second"}, + LiteralTestParam{9999999, 0xFFFFFE5280D65800ULL, "99.99999% of a second"} + ) +); + // Parameterized test for large timestamps across different years struct LargeTimestampTestParam { int year; From ba592571bdd32405333739a978487167a02c30a1 Mon Sep 17 00:00:00 2001 From: Mike Prosser Date: Tue, 28 Oct 2025 10:50:32 -0500 Subject: [PATCH 24/25] use correct logic for NI-BTF --- source/custom/nidaqmx_service.custom.cpp | 4 +- source/server/converters.h | 17 ++-- .../precision_timestamp_converter_tests.cpp | 90 ++++++++----------- 3 files changed, 45 insertions(+), 66 deletions(-) diff --git a/source/custom/nidaqmx_service.custom.cpp b/source/custom/nidaqmx_service.custom.cpp index 5ecb105ce..0f6dae5eb 100644 --- a/source/custom/nidaqmx_service.custom.cpp +++ b/source/custom/nidaqmx_service.custom.cpp @@ -8,7 +8,7 @@ namespace nidaqmx_grpc { using nidevice_grpc::converters::convert_to_grpc; -using nidevice_grpc::converters::convert_dot_net_daqmx_ticks_to_btf_precision_timestamp; +using nidevice_grpc::converters::convert_dot_net_ticks_to_precision_timestamp; using nidevice_grpc::converters::DotNetTicksPerSecond; using google::protobuf::RepeatedPtrField; using ::ni::protobuf::types::DoubleAnalogWaveform; @@ -200,7 +200,7 @@ ::grpc::Status NiDAQmxService::ReadAnalogWaveforms(::grpc::ServerContext* contex if (waveform_attribute_mode & WaveformAttributeMode::WAVEFORM_ATTRIBUTE_MODE_TIMING) { auto* waveform_t0 = waveform->mutable_t0(); - convert_dot_net_daqmx_ticks_to_btf_precision_timestamp(t0_array[i], waveform_t0); + convert_dot_net_ticks_to_precision_timestamp(t0_array[i], waveform_t0); waveform->set_dt(static_cast(dt_array[i]) / DotNetTicksPerSecond); } } diff --git a/source/server/converters.h b/source/server/converters.h index 2a7a2d7e7..429bb480b 100644 --- a/source/server/converters.h +++ b/source/server/converters.h @@ -342,10 +342,10 @@ inline void convert_to_grpc(const SmtSpectrumInfoType& input, nidevice_grpc::Smt } const int64 SecondsFromCVI1904EpochTo1970Epoch = 2082844800LL; -const int64 SecondsFromDAQmx0001EpochToCVI1904Epoch = -((static_cast(0xfffffff2) << 32) | 0x0493b980); // extracted from NITYPES_ABSOLUTETIME_EPOCH_BIAS_FROM_0001 in ni-central/src/platform_services/abstractions/niatomicd/nitypes/source/nitypes/time/AbsoluteTime.h +const int64 SecondsFromDotNet0001EpochToCVI1904Epoch = -((static_cast(0xfffffff2) << 32) | 0x0493b980); // extracted from NITYPES_ABSOLUTETIME_EPOCH_BIAS_FROM_0001 in ni-central/src/platform_services/abstractions/niatomicd/nitypes/source/nitypes/time/AbsoluteTime.h const double TwoToSixtyFour = (double)(1 << 31) * (double)(1 << 31) * (double)(1 << 2); const double NanosecondsPerSecond = 1000000000.0; -const int64 DotNetTicksPerSecond = 1e7; // each tick is 100ns +const int64 DotNetTicksPerSecond = 10000000; // each tick is 100ns template <> inline void convert_to_grpc(const CVIAbsoluteTime& value, google::protobuf::Timestamp* timestamp) @@ -372,21 +372,16 @@ inline CVIAbsoluteTime convert_from_grpc(const google::protobuf::Timestamp& valu return cviTime; } -// Convert .NET/DAQmx ticks (100ns since Jan 1, 0001) to NI-BTF PrecisionTimestamp -inline void convert_dot_net_daqmx_ticks_to_btf_precision_timestamp(int64 dot_net_ticks, ::ni::protobuf::types::PrecisionTimestamp* timestamp) +// Convert .NET ticks (100ns since Jan 1, 0001) to PrecisionTimestamp (NI-BTF) +inline void convert_dot_net_ticks_to_precision_timestamp(int64 dot_net_ticks, ::ni::protobuf::types::PrecisionTimestamp* timestamp) { - const int64 dot_net_ticks_since_1904 = dot_net_ticks - SecondsFromDAQmx0001EpochToCVI1904Epoch * DotNetTicksPerSecond; + const int64 dot_net_ticks_since_1904 = dot_net_ticks - SecondsFromDotNet0001EpochToCVI1904Epoch * DotNetTicksPerSecond; const double total_seconds = static_cast(dot_net_ticks_since_1904) / DotNetTicksPerSecond; double integer_part; double fractional_part = std::modf(total_seconds, &integer_part); - if (fractional_part < 0) { - integer_part -= 1; - fractional_part += 1; - } - timestamp->set_seconds(static_cast(integer_part)); - timestamp->set_fractional_seconds(static_cast(fractional_part * TwoToSixtyFour)); + timestamp->set_fractional_seconds(static_cast(std::abs(fractional_part) * TwoToSixtyFour)); } // Or together input_array and input_raw to implement the "bitfield_as_enum_array" feature for inputs. diff --git a/source/tests/unit/precision_timestamp_converter_tests.cpp b/source/tests/unit/precision_timestamp_converter_tests.cpp index 94a6239c4..6ae23b961 100644 --- a/source/tests/unit/precision_timestamp_converter_tests.cpp +++ b/source/tests/unit/precision_timestamp_converter_tests.cpp @@ -8,27 +8,27 @@ namespace tests { namespace unit { namespace { -using nidevice_grpc::converters::convert_dot_net_daqmx_ticks_to_btf_precision_timestamp; +using nidevice_grpc::converters::convert_dot_net_ticks_to_precision_timestamp; using nidevice_grpc::converters::DotNetTicksPerSecond; using nidevice_grpc::converters::TwoToSixtyFour; -using nidevice_grpc::converters::SecondsFromDAQmx0001EpochToCVI1904Epoch; +using nidevice_grpc::converters::SecondsFromDotNet0001EpochToCVI1904Epoch; using ::ni::protobuf::types::PrecisionTimestamp; -const int64 EpochTicks = SecondsFromDAQmx0001EpochToCVI1904Epoch * DotNetTicksPerSecond; +const int64 EpochTicks = SecondsFromDotNet0001EpochToCVI1904Epoch * DotNetTicksPerSecond; TEST(PrecisionTimestampConverterTests, ConvertZeroTicks_ReturnsNegativeEpochTimestamp) { PrecisionTimestamp timestamp; - convert_dot_net_daqmx_ticks_to_btf_precision_timestamp(0, ×tamp); + convert_dot_net_ticks_to_precision_timestamp(0, ×tamp); - EXPECT_EQ(timestamp.seconds(), -SecondsFromDAQmx0001EpochToCVI1904Epoch); + EXPECT_EQ(timestamp.seconds(), -SecondsFromDotNet0001EpochToCVI1904Epoch); EXPECT_EQ(timestamp.fractional_seconds(), 0); } TEST(PrecisionTimestampConverterTests, ConvertEpochTicks_ReturnsZeroTimestamp) { PrecisionTimestamp timestamp; - convert_dot_net_daqmx_ticks_to_btf_precision_timestamp(EpochTicks, ×tamp); + convert_dot_net_ticks_to_precision_timestamp(EpochTicks, ×tamp); EXPECT_EQ(timestamp.seconds(), 0); EXPECT_EQ(timestamp.fractional_seconds(), 0); @@ -38,7 +38,7 @@ TEST(PrecisionTimestampConverterTests, ConvertOneSecondOfTicks_ReturnsOneSecond) { PrecisionTimestamp timestamp; const int64_t one_second_ticks = EpochTicks + 1 * DotNetTicksPerSecond; - convert_dot_net_daqmx_ticks_to_btf_precision_timestamp(one_second_ticks, ×tamp); + convert_dot_net_ticks_to_precision_timestamp(one_second_ticks, ×tamp); EXPECT_EQ(timestamp.seconds(), 1); EXPECT_EQ(timestamp.fractional_seconds(), 0); @@ -46,10 +46,10 @@ TEST(PrecisionTimestampConverterTests, ConvertOneSecondOfTicks_ReturnsOneSecond) // Parameterized test for seconds and fractional combinations struct SecondsAndFractionalTestParam { + int64_t ticks_offset; int64_t seconds; double fraction; - int64_t ticks_offset; - std::string description; + uint64_t btf_fractional_ticks; }; class PrecisionTimestampConverterSecondsAndFractionalTests : public ::testing::TestWithParam {}; @@ -58,53 +58,31 @@ TEST_P(PrecisionTimestampConverterSecondsAndFractionalTests, ConvertSecondsAndFr { const auto& param = GetParam(); PrecisionTimestamp timestamp; - convert_dot_net_daqmx_ticks_to_btf_precision_timestamp(EpochTicks + param.ticks_offset, ×tamp); + convert_dot_net_ticks_to_precision_timestamp(EpochTicks + param.ticks_offset, ×tamp); - EXPECT_EQ(timestamp.seconds(), param.seconds) << "Failed for " << param.description; - EXPECT_EQ(timestamp.fractional_seconds(), static_cast(param.fraction * TwoToSixtyFour)) << "Failed for " << param.description; + EXPECT_EQ(timestamp.seconds(), param.seconds) << "Failed for ticks_offset: " << param.ticks_offset; + EXPECT_EQ(timestamp.fractional_seconds(), param.btf_fractional_ticks) << "Failed for ticks_offset: " << param.ticks_offset; } INSTANTIATE_TEST_SUITE_P( SecondsAndFractionalTests, PrecisionTimestampConverterSecondsAndFractionalTests, ::testing::Values( - SecondsAndFractionalTestParam{0, 0.25, DotNetTicksPerSecond / 4, "quarter second"}, - SecondsAndFractionalTestParam{0, 0.5, DotNetTicksPerSecond / 2, "half second"}, - SecondsAndFractionalTestParam{0, 0.75, DotNetTicksPerSecond / 2 + DotNetTicksPerSecond / 4, "three quarters second"}, - SecondsAndFractionalTestParam{1, 0.5, 1 * DotNetTicksPerSecond + DotNetTicksPerSecond / 2, "one and half seconds"}, - SecondsAndFractionalTestParam{2, 0.25, 2 * DotNetTicksPerSecond + DotNetTicksPerSecond / 4, "two and quarter seconds"}, - SecondsAndFractionalTestParam{3, 0.75, 3 * DotNetTicksPerSecond + DotNetTicksPerSecond / 2 + DotNetTicksPerSecond / 4, "three and three quarters seconds"} - ) -); - -// Parameterized test for literal tick values -struct LiteralTestParam { - int64_t dot_net_ticks; - uint64_t btf_fractional_ticks; - std::string description; -}; - -class PrecisionTimestampConverterLiteralTests : public ::testing::TestWithParam {}; - -TEST_P(PrecisionTimestampConverterLiteralTests, ConvertLiteralTicks_ReturnsCorrectValues) -{ - const auto& param = GetParam(); - PrecisionTimestamp timestamp; - convert_dot_net_daqmx_ticks_to_btf_precision_timestamp(EpochTicks + param.dot_net_ticks, ×tamp); - - EXPECT_EQ(timestamp.seconds(), 0) << "Failed for " << param.description; - EXPECT_EQ(timestamp.fractional_seconds(), param.btf_fractional_ticks) << "Failed for " << param.description; -} - -INSTANTIATE_TEST_SUITE_P( - LiteralTests, - PrecisionTimestampConverterLiteralTests, - ::testing::Values( - LiteralTestParam{0000000, 0x0000000000000000ULL, "zero fractional seconds"}, - LiteralTestParam{2500000, 0x4000000000000000ULL, "quarter second"}, - LiteralTestParam{5000000, 0x8000000000000000ULL, "half second"}, - LiteralTestParam{7500000, 0xC000000000000000ULL, "three quarters second"}, - LiteralTestParam{9999999, 0xFFFFFE5280D65800ULL, "99.99999% of a second"} + SecondsAndFractionalTestParam{ 0000000, 0, 0.00, 0x0000000000000000ULL}, + SecondsAndFractionalTestParam{ 2500000, 0, 0.25, 0x4000000000000000ULL}, + SecondsAndFractionalTestParam{ 5000000, 0, 0.50, 0x8000000000000000ULL}, + SecondsAndFractionalTestParam{ 7500000, 0, 0.75, 0xC000000000000000ULL}, + SecondsAndFractionalTestParam{ 9999999, 0, 0.9999999, 0xFFFFFE5280D65800ULL}, + SecondsAndFractionalTestParam{ 12500000, 1, 0.25, 0x4000000000000000ULL}, + SecondsAndFractionalTestParam{ 25000000, 2, 0.50, 0x8000000000000000ULL}, + SecondsAndFractionalTestParam{ 37500000, 3, 0.75, 0xC000000000000000ULL}, + SecondsAndFractionalTestParam{ -2500000, -0, 0.25, 0x4000000000000000ULL}, + SecondsAndFractionalTestParam{ -5000000, -0, 0.50, 0x8000000000000000ULL}, + SecondsAndFractionalTestParam{ -7500000, -0, 0.75, 0xC000000000000000ULL}, + SecondsAndFractionalTestParam{ -9999999, -0, 0.9999999, 0xFFFFFE5280D65800ULL}, + SecondsAndFractionalTestParam{-12500000, -1, 0.25, 0x4000000000000000ULL}, + SecondsAndFractionalTestParam{-25000000, -2, 0.25, 0x8000000000000000ULL}, + SecondsAndFractionalTestParam{-37500000, -3, 0.75, 0xC000000000000000ULL} ) ); @@ -124,10 +102,15 @@ TEST_P(PrecisionTimestampConverterLargeTimestampTests, ConvertLargeTimestamp_Han const int64 SecondsFromNiBtfEpochToYear = param.days_from_1904_epoch * 24 * 60 * 60; const int64 YearTicks = EpochTicks + SecondsFromNiBtfEpochToYear * DotNetTicksPerSecond; - convert_dot_net_daqmx_ticks_to_btf_precision_timestamp(YearTicks + DotNetTicksPerSecond / 4, ×tamp); + convert_dot_net_ticks_to_precision_timestamp(YearTicks + DotNetTicksPerSecond / 4, ×tamp); - EXPECT_EQ(timestamp.seconds(), SecondsFromNiBtfEpochToYear) << "Failed for " << param.description; - EXPECT_EQ(timestamp.fractional_seconds(), static_cast(0.25 * TwoToSixtyFour)) << "Failed for " << param.description; + if (SecondsFromNiBtfEpochToYear < 0) { + EXPECT_EQ(timestamp.seconds(), SecondsFromNiBtfEpochToYear + 1) << "Failed for " << param.description; + EXPECT_EQ(timestamp.fractional_seconds(), static_cast(0.75 * TwoToSixtyFour)) << "Failed for " << param.description; + } else { + EXPECT_EQ(timestamp.seconds(), SecondsFromNiBtfEpochToYear) << "Failed for " << param.description; + EXPECT_EQ(timestamp.fractional_seconds(), static_cast(0.25 * TwoToSixtyFour)) << "Failed for " << param.description; + } } INSTANTIATE_TEST_SUITE_P( @@ -137,7 +120,8 @@ INSTANTIATE_TEST_SUITE_P( LargeTimestampTestParam{2025, 44196, "January 1, 2025"}, // 121 years from 1904 = 44196 days (accounting for leap years) LargeTimestampTestParam{2002, 35794, "January 1, 2002"}, // 98 years from 1904 = 35794 days (accounting for leap years) LargeTimestampTestParam{1950, 16801, "January 1, 1950"}, // 46 years from 1904 = 16801 days (accounting for leap years) - LargeTimestampTestParam{1850, -19723, "January 1, 1850"} // 54 years before 1904 = -19723 days (accounting for leap years) + LargeTimestampTestParam{1850, -19723, "January 1, 1850"}, // 54 years before 1904 = -19723 days (accounting for leap years) + LargeTimestampTestParam{1066, -306022, "January 1, 1066"} // 838 years before 1904 = -306022 days (accounting for leap years) ) ); From cf85e3735b553504beb94a03a9634714ec85b7dc Mon Sep 17 00:00:00 2001 From: Mike Prosser Date: Tue, 28 Oct 2025 12:04:44 -0500 Subject: [PATCH 25/25] test cleanup --- .../precision_timestamp_converter_tests.cpp | 31 +++++++++---------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/source/tests/unit/precision_timestamp_converter_tests.cpp b/source/tests/unit/precision_timestamp_converter_tests.cpp index 6ae23b961..168c8edf0 100644 --- a/source/tests/unit/precision_timestamp_converter_tests.cpp +++ b/source/tests/unit/precision_timestamp_converter_tests.cpp @@ -48,7 +48,6 @@ TEST(PrecisionTimestampConverterTests, ConvertOneSecondOfTicks_ReturnsOneSecond) struct SecondsAndFractionalTestParam { int64_t ticks_offset; int64_t seconds; - double fraction; uint64_t btf_fractional_ticks; }; @@ -68,21 +67,21 @@ INSTANTIATE_TEST_SUITE_P( SecondsAndFractionalTests, PrecisionTimestampConverterSecondsAndFractionalTests, ::testing::Values( - SecondsAndFractionalTestParam{ 0000000, 0, 0.00, 0x0000000000000000ULL}, - SecondsAndFractionalTestParam{ 2500000, 0, 0.25, 0x4000000000000000ULL}, - SecondsAndFractionalTestParam{ 5000000, 0, 0.50, 0x8000000000000000ULL}, - SecondsAndFractionalTestParam{ 7500000, 0, 0.75, 0xC000000000000000ULL}, - SecondsAndFractionalTestParam{ 9999999, 0, 0.9999999, 0xFFFFFE5280D65800ULL}, - SecondsAndFractionalTestParam{ 12500000, 1, 0.25, 0x4000000000000000ULL}, - SecondsAndFractionalTestParam{ 25000000, 2, 0.50, 0x8000000000000000ULL}, - SecondsAndFractionalTestParam{ 37500000, 3, 0.75, 0xC000000000000000ULL}, - SecondsAndFractionalTestParam{ -2500000, -0, 0.25, 0x4000000000000000ULL}, - SecondsAndFractionalTestParam{ -5000000, -0, 0.50, 0x8000000000000000ULL}, - SecondsAndFractionalTestParam{ -7500000, -0, 0.75, 0xC000000000000000ULL}, - SecondsAndFractionalTestParam{ -9999999, -0, 0.9999999, 0xFFFFFE5280D65800ULL}, - SecondsAndFractionalTestParam{-12500000, -1, 0.25, 0x4000000000000000ULL}, - SecondsAndFractionalTestParam{-25000000, -2, 0.25, 0x8000000000000000ULL}, - SecondsAndFractionalTestParam{-37500000, -3, 0.75, 0xC000000000000000ULL} + SecondsAndFractionalTestParam{ 0000000, 0, 0x0000000000000000ULL}, + SecondsAndFractionalTestParam{ 2500000, 0, 0x4000000000000000ULL}, + SecondsAndFractionalTestParam{ 5000000, 0, 0x8000000000000000ULL}, + SecondsAndFractionalTestParam{ 7500000, 0, 0xC000000000000000ULL}, + SecondsAndFractionalTestParam{ 9999999, 0, 0xFFFFFE5280D65800ULL}, + SecondsAndFractionalTestParam{ 12500000, 1, 0x4000000000000000ULL}, + SecondsAndFractionalTestParam{ 25000000, 2, 0x8000000000000000ULL}, + SecondsAndFractionalTestParam{ 37500000, 3, 0xC000000000000000ULL}, + SecondsAndFractionalTestParam{ -2500000, -0, 0x4000000000000000ULL}, + SecondsAndFractionalTestParam{ -5000000, -0, 0x8000000000000000ULL}, + SecondsAndFractionalTestParam{ -7500000, -0, 0xC000000000000000ULL}, + SecondsAndFractionalTestParam{ -9999999, -0, 0xFFFFFE5280D65800ULL}, + SecondsAndFractionalTestParam{-12500000, -1, 0x4000000000000000ULL}, + SecondsAndFractionalTestParam{-25000000, -2, 0x8000000000000000ULL}, + SecondsAndFractionalTestParam{-37500000, -3, 0xC000000000000000ULL} ) );