Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
5e53c36
Enhance NIDAQmx API with waveform support and additional metadata
Oct 16, 2025
fff7769
fix lint
Oct 16, 2025
01ea5e9
fix validate_examples
Oct 16, 2025
1270fd7
ReadAnalogWaveforms implementation (untested, but compiles)
Oct 16, 2025
836c408
add tests
Oct 16, 2025
b9cf67a
Add support for creating two AI voltage channels in tests
Oct 20, 2025
12ca5e5
cleanup
Oct 20, 2025
8e5eb77
cleanup CMakeLists.txt
Oct 20, 2025
7b50a84
attempt fix for ubuntu build
Oct 20, 2025
3d29cbc
more verbose build output
Oct 20, 2025
9b26670
Try some different build settings
Oct 20, 2025
d117ec2
add quotes to fix cmake flags
Oct 20, 2025
23a6e98
add sampsPerChanRead to ReadAnalogWaveforms response
Oct 22, 2025
1577116
revert changes in build_cmake.yml and do -fno-var-tracking-assignment…
Oct 22, 2025
c03a50d
use GCC push_options
Oct 22, 2025
4c36ac8
use -fno-var-tracking-assignments globally for gcc
Oct 22, 2025
e7055d1
fix timing conversion
Oct 22, 2025
5aa982d
add ReadDigitalWaveforms RPC with stub implementation
Oct 23, 2025
a765c94
implementation and tests for single channel
Oct 23, 2025
26c1608
test multi channel and multi line
Oct 23, 2025
0b45f4b
add copilot instructions for how to build with ninja, and do some cle…
Oct 23, 2025
ec3e076
fix data layout
Oct 23, 2025
ae53b1c
brad's feedback
Oct 24, 2025
9c0c333
copy protobuf types protos to heirarchical location
Oct 24, 2025
85d899b
Merge branch 'users/mprosser/task-3424627-read-analog-waveforms' into…
Oct 24, 2025
c56bd62
fix merge and templatize the SetWfmAttrCallback
Oct 24, 2025
95f15c2
copilot-build.ps1
Oct 24, 2025
e3109e7
handle epoch difference (note - SecondsFrom0001EpochTo1904Epoch value…
Oct 24, 2025
7b90ab4
convert_dot_net_daqmx_ticks_to_btf_precision_timestamp
Oct 27, 2025
9f77232
fixes and cleanup
Oct 27, 2025
4f7cf86
cleanup and more test cases
Oct 27, 2025
2962d82
PrecisionTimestampConverterLiteralTests
Oct 27, 2025
50767bb
Merge branch 'users/mprosser/task-3424627-read-analog-waveforms' into…
Oct 27, 2025
4a87aa8
fix merge
Oct 27, 2025
897e6f3
remove copilot instructions (will be added in separate PR)
Oct 27, 2025
ba59257
use correct logic for NI-BTF
Oct 28, 2025
cf85e37
test cleanup
Oct 28, 2025
5df95b9
Merge branch 'users/mprosser/task-3424627-read-analog-waveforms' into…
Oct 28, 2025
3b6681f
fillMode: 1 = GROUP_BY_SCAN_NUMBER (interleaved)
Oct 28, 2025
664f03a
Merge remote-tracking branch 'origin/main' into users/mprosser/task-3…
Oct 28, 2025
1579f84
cleanup merge
Oct 28, 2025
c33446b
cleanup
Oct 28, 2025
30355e1
use waveform->mutable_y_data();
Oct 28, 2025
ca6e216
test cleanup
Oct 28, 2025
cfef232
Merge remote-tracking branch 'origin/main' into users/mprosser/task-3…
Oct 29, 2025
b40de09
Brad's feedback
Oct 30, 2025
0321106
cleanup
Oct 30, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions source/codegen/metadata/nidaqmx/functions_addon.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,13 +171,13 @@
},
'ReadAnalogWaveforms': {
'parameters': {
# size is determined by the number of channels in the task
# size is determined by the Read.ChannelsToRead property
'waveforms': ['ARRAY_PARAMETER_NEEDS_SIZE']
}
},
'ReadDigitalWaveforms': {
'parameters': {
# size is determined by the number of channels in the task
# size is determined by the Read.ChannelsToRead property
'waveforms': ['ARRAY_PARAMETER_NEEDS_SIZE']
}
}
Expand Down
159 changes: 64 additions & 95 deletions source/custom/nidaqmx_service.custom.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,37 @@ 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;
using ::ni::protobuf::types::DigitalWaveform;

// 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;
}

// Encapsulates timing arrays and pointers for waveform operations
struct TimingData {
std::vector<int64> t0_array;
std::vector<int64> dt_array;
int64* t0_ptr = nullptr;
int64* dt_ptr = nullptr;
uInt32 timing_array_size = 0;

TimingData(uInt32 num_channels) {
if (num_channels == 0) {
return;
}
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;
}
};

// Universal callback function for setting waveform attributes (both analog and digital)
template<typename WaveformCollectionType>
int32 CVICALLBACK SetWfmAttrCallbackTemplated(
static int32 CVICALLBACK SetWfmAttrCallbackTemplated(
const uInt32 channel_index,
const char attribute_name[],
const int32 attribute_type,
Expand Down Expand Up @@ -94,32 +115,8 @@ int32 CVICALLBACK SetWfmAttrCallbackTemplated(
}
}

int32 CVICALLBACK SetAnalogWfmAttrCallback(
const uInt32 channel_index,
const char attribute_name[],
const int32 attribute_type,
const void* value,
const uInt32 value_size_in_bytes,
void* callback_data)
{
return SetWfmAttrCallbackTemplated<RepeatedPtrField<DoubleAnalogWaveform>>(
channel_index, attribute_name, attribute_type, value, value_size_in_bytes, callback_data);
}

int32 CVICALLBACK SetDigitalWfmAttrCallback(
const uInt32 channel_index,
const char attribute_name[],
const int32 attribute_type,
const void* value,
const uInt32 value_size_in_bytes,
void* callback_data)
{
return SetWfmAttrCallbackTemplated<RepeatedPtrField<::ni::protobuf::types::DigitalWaveform>>(
channel_index, attribute_name, attribute_type, value, value_size_in_bytes, callback_data);
}

template<typename RequestType>
int32 ParseWaveformAttributeMode(const RequestType* request)
static int32 GetWaveformAttributeMode(const RequestType* request)
{
switch (request->waveform_attribute_mode_enum_case()) {
case RequestType::WaveformAttributeModeEnumCase::kWaveformAttributeMode: {
Expand All @@ -135,36 +132,15 @@ int32 ParseWaveformAttributeMode(const RequestType* request)
return 0;
}

void SetupTimingArrays(
int32 waveform_attribute_mode,
uInt32 num_channels,
std::vector<int64>& t0_array,
std::vector<int64>& dt_array,
int64*& t0_ptr,
int64*& dt_ptr,
uInt32& timing_array_size)
{
if (waveform_attribute_mode & WaveformAttributeMode::WAVEFORM_ATTRIBUTE_MODE_TIMING) {
t0_array.resize(num_channels, 0);
dt_array.resize(num_channels, 0);
t0_ptr = t0_array.data();
dt_ptr = dt_array.data();
timing_array_size = num_channels;
}
}

template<typename WaveformType>
void ProcessWaveformTiming(
int32 waveform_attribute_mode,
static void SetWaveformTiming(
uInt32 channel_index,
const std::vector<int64>& t0_array, const std::vector<int64>& dt_array,
const TimingData& timing_data,
WaveformType* waveform)
{
if (waveform_attribute_mode & WaveformAttributeMode::WAVEFORM_ATTRIBUTE_MODE_TIMING) {
auto* waveform_t0 = waveform->mutable_t0();
convert_dot_net_ticks_to_precision_timestamp(t0_array[channel_index], waveform_t0);
waveform->set_dt(static_cast<double>(dt_array[channel_index]) / DotNetTicksPerSecond);
}
auto* waveform_t0 = waveform->mutable_t0();
convert_dot_net_ticks_to_precision_timestamp(timing_data.t0_array[channel_index], waveform_t0);
waveform->set_dt(static_cast<double>(timing_data.dt_array[channel_index]) / DotNetTicksPerSecond);
}

::grpc::Status NiDAQmxService::ConvertApiErrorStatusForTaskHandle(::grpc::ServerContextBase* context, int32_t status, TaskHandle task)
Expand All @@ -187,8 +163,6 @@ ::grpc::Status NiDAQmxService::ReadAnalogWaveforms(::grpc::ServerContext* contex
const auto number_of_samples_per_channel = request->num_samps_per_chan();
const auto timeout = request->timeout();

int32 waveform_attribute_mode = ParseWaveformAttributeMode(request);

uInt32 num_channels = 0;
auto status = library_->GetReadAttributeUInt32(task, ReadUInt32Attribute::READ_ATTRIBUTE_NUM_CHANS, &num_channels);
if (!status_ok(status)) {
Expand All @@ -207,32 +181,30 @@ ::grpc::Status NiDAQmxService::ReadAnalogWaveforms(::grpc::ServerContext* contex
read_array_ptrs[i] = read_arrays[i].data();
}

std::vector<int64> t0_array, dt_array;
int64* t0_ptr = nullptr;
int64* dt_ptr = nullptr;
uInt32 timing_array_size = 0;

SetupTimingArrays(waveform_attribute_mode, num_channels, t0_array, dt_array, t0_ptr, dt_ptr, timing_array_size);

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_ptr = SetAnalogWfmAttrCallback;
int32 waveform_attribute_mode = GetWaveformAttributeMode(request);
const bool timing_enabled = (waveform_attribute_mode & WaveformAttributeMode::WAVEFORM_ATTRIBUTE_MODE_TIMING) != 0;
const bool extended_properties_enabled = (waveform_attribute_mode & WaveformAttributeMode::WAVEFORM_ATTRIBUTE_MODE_EXTENDED_PROPERTIES) != 0;

TimingData timing_data(timing_enabled ? num_channels : 0);

DAQmxSetWfmAttrCallbackPtr callback_ptr = nullptr;
if (extended_properties_enabled) {
callback_ptr = &SetWfmAttrCallbackTemplated<RepeatedPtrField<DoubleAnalogWaveform>>;
}

int32 samples_per_chan_read = 0;
status = library_->InternalReadAnalogWaveformPerChan(
task,
number_of_samples_per_channel,
timeout,
t0_ptr,
dt_ptr,
timing_array_size,
timing_data.t0_array.data(),
timing_data.dt_array.data(),
timing_data.timing_array_size,
callback_ptr,
response->mutable_waveforms(),
read_array_ptrs.data(),
Expand All @@ -246,13 +218,15 @@ ::grpc::Status NiDAQmxService::ReadAnalogWaveforms(::grpc::ServerContext* contex
return ConvertApiErrorStatusForTaskHandle(context, status, task);
}

for (uInt32 i = 0; i < num_channels; ++i) {
auto* waveform = response->mutable_waveforms(i);
for (uInt32 channel = 0; channel < num_channels; ++channel) {
auto* waveform = response->mutable_waveforms(channel);

auto* y_data = waveform->mutable_y_data();
y_data->Add(read_arrays[i].data(), read_arrays[i].data() + samples_per_chan_read);
y_data->Add(read_arrays[channel].data(), read_arrays[channel].data() + samples_per_chan_read);

ProcessWaveformTiming(waveform_attribute_mode, i, t0_array, dt_array, waveform);
if (timing_enabled) {
SetWaveformTiming(channel, timing_data, waveform);
}
}

response->set_samps_per_chan_read(samples_per_chan_read);
Expand All @@ -276,8 +250,6 @@ ::grpc::Status NiDAQmxService::ReadDigitalWaveforms(::grpc::ServerContext* conte
const auto number_of_samples_per_channel = request->num_samps_per_chan();
const auto timeout = request->timeout();

int32 waveform_attribute_mode = ParseWaveformAttributeMode(request);

uInt32 num_channels = 0;
auto status = library_->GetReadAttributeUInt32(task, ReadUInt32Attribute::READ_ATTRIBUTE_NUM_CHANS, &num_channels);
if (!status_ok(status)) {
Expand All @@ -296,29 +268,27 @@ ::grpc::Status NiDAQmxService::ReadDigitalWaveforms(::grpc::ServerContext* conte
}

if (max_bytes_per_chan == 0) {
return ::grpc::Status(::grpc::INVALID_ARGUMENT, "Digital lines bytes per channel is 0");
return ::grpc::Status(::grpc::UNKNOWN, "Digital lines bytes per channel is 0");
}

// Calculate total array size needed (samples * channels * max_bytes_per_chan)
const uInt32 array_size = number_of_samples_per_channel * num_channels * max_bytes_per_chan;
std::vector<uInt8> read_array(array_size);

std::vector<int64> t0_array, dt_array;
int64* t0_ptr = nullptr;
int64* dt_ptr = nullptr;
uInt32 timing_array_size = 0;

SetupTimingArrays(waveform_attribute_mode, num_channels, t0_array, dt_array, t0_ptr, dt_ptr, timing_array_size);

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_ptr = SetDigitalWfmAttrCallback;
int32 waveform_attribute_mode = GetWaveformAttributeMode(request);
const bool timing_enabled = (waveform_attribute_mode & WaveformAttributeMode::WAVEFORM_ATTRIBUTE_MODE_TIMING) != 0;
const bool extended_properties_enabled = (waveform_attribute_mode & WaveformAttributeMode::WAVEFORM_ATTRIBUTE_MODE_EXTENDED_PROPERTIES) != 0;

TimingData timing_data(timing_enabled ? num_channels : 0);

DAQmxSetWfmAttrCallbackPtr callback_ptr = nullptr;
if (extended_properties_enabled) {
callback_ptr = &SetWfmAttrCallbackTemplated<RepeatedPtrField<DigitalWaveform>>;
}

std::vector<uInt32> bytes_per_chan_array(num_channels);
Expand All @@ -330,9 +300,9 @@ ::grpc::Status NiDAQmxService::ReadDigitalWaveforms(::grpc::ServerContext* conte
number_of_samples_per_channel,
timeout,
GROUP_BY_GROUP_BY_SCAN_NUMBER,
t0_ptr,
dt_ptr,
timing_array_size,
timing_data.t0_array.data(),
timing_data.dt_array.data(),
timing_data.timing_array_size,
callback_ptr,
response->mutable_waveforms(),
read_array.data(),
Expand Down Expand Up @@ -364,13 +334,12 @@ ::grpc::Status NiDAQmxService::ReadDigitalWaveforms(::grpc::ServerContext* conte
for (int32 sample = 0; sample < samples_per_chan_read; ++sample) {
const uInt32 sample_start = sample * num_channels * max_bytes_per_chan;
const uInt32 channel_offset = sample_start + channel * max_bytes_per_chan;
for (uInt32 signal = 0; signal < bytes_per_chan; ++signal) {
const uInt32 offset = channel_offset + signal;
y_data->append(reinterpret_cast<const char*>(&read_array[offset]), 1);
}
y_data->append(reinterpret_cast<const char*>(&read_array[channel_offset]), bytes_per_chan);
}

ProcessWaveformTiming(waveform_attribute_mode, channel, t0_array, dt_array, waveform);
if (timing_enabled) {
SetWaveformTiming(channel, timing_data, waveform);
}
}

response->set_samps_per_chan_read(samples_per_chan_read);
Expand Down