Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
60 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
d793778
implement WriteDigitalWaveforms
Oct 30, 2025
6c46871
memcpy optimization
Oct 30, 2025
df32725
Merge remote-tracking branch 'origin/main' into users/mprosser/task-3…
Oct 30, 2025
51a4d24
fix merge
Oct 30, 2025
efb4199
test cleanup
Oct 30, 2025
d51ae87
Brad's feedback
Nov 3, 2025
2693279
Merge remote-tracking branch 'origin/main' into users/mprosser/task-3…
Nov 4, 2025
80e0713
trailing metadata
Nov 4, 2025
ae8ed06
added tests to verify the trailing metadata in error cases
Nov 5, 2025
f798d7f
fix asserts
Nov 5, 2025
0394d7d
fix crash in ReadAnalogWaveforms
Nov 5, 2025
834d226
EXPECT_THROW_DRIVER_ERROR
Nov 5, 2025
24c7a58
guard against negative-overflow array_size in ReadDigitalWaveforms
Nov 5, 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
13 changes: 13 additions & 0 deletions generated/nidaqmx/nidaqmx.proto
Original file line number Diff line number Diff line change
Expand Up @@ -469,6 +469,7 @@ service NiDAQmx {
rpc ReadAnalogWaveforms(ReadAnalogWaveformsRequest) returns (ReadAnalogWaveformsResponse);
rpc ReadDigitalWaveforms(ReadDigitalWaveformsRequest) returns (ReadDigitalWaveformsResponse);
rpc WriteAnalogWaveforms(WriteAnalogWaveformsRequest) returns (WriteAnalogWaveformsResponse);
rpc WriteDigitalWaveforms(WriteDigitalWaveformsRequest) returns (WriteDigitalWaveformsResponse);
}

enum BufferUInt32Attribute {
Expand Down Expand Up @@ -11565,3 +11566,15 @@ message WriteAnalogWaveformsResponse {
int32 samps_per_chan_written = 2;
}

message WriteDigitalWaveformsRequest {
nidevice_grpc.Session task = 1;
bool auto_start = 2;
double timeout = 3;
repeated ni.protobuf.types.DigitalWaveform waveforms = 4;
}

message WriteDigitalWaveformsResponse {
int32 status = 1;
int32 samps_per_chan_written = 2;
}

20 changes: 20 additions & 0 deletions generated/nidaqmx/nidaqmx_client.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -12488,5 +12488,25 @@ write_analog_waveforms(const StubPtr& stub, const nidevice_grpc::Session& task,
return response;
}

WriteDigitalWaveformsResponse
write_digital_waveforms(const StubPtr& stub, const nidevice_grpc::Session& task, const bool& auto_start, const double& timeout, const std::vector<ni::protobuf::types::DigitalWaveform>& waveforms)
{
::grpc::ClientContext context;

auto request = WriteDigitalWaveformsRequest{};
request.mutable_task()->CopyFrom(task);
request.set_auto_start(auto_start);
request.set_timeout(timeout);
copy_array(waveforms, request.mutable_waveforms());

auto response = WriteDigitalWaveformsResponse{};

raise_if_error(
stub->WriteDigitalWaveforms(&context, request, &response),
context);

return response;
}


} // namespace nidaqmx_grpc::experimental::client
1 change: 1 addition & 0 deletions generated/nidaqmx/nidaqmx_client.h
Original file line number Diff line number Diff line change
Expand Up @@ -471,6 +471,7 @@ WriteToTEDSFromFileResponse write_to_teds_from_file(const StubPtr& stub, const s
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<WaveformAttributeMode, pb::int32>& waveform_attribute_mode);
ReadDigitalWaveformsResponse read_digital_waveforms(const StubPtr& stub, const nidevice_grpc::Session& task, const pb::int32& num_samps_per_chan, const double& timeout, const simple_variant<WaveformAttributeMode, pb::int32>& waveform_attribute_mode);
WriteAnalogWaveformsResponse write_analog_waveforms(const StubPtr& stub, const nidevice_grpc::Session& task, const bool& auto_start, const double& timeout, const std::vector<ni::protobuf::types::DoubleAnalogWaveform>& waveforms);
WriteDigitalWaveformsResponse write_digital_waveforms(const StubPtr& stub, const nidevice_grpc::Session& task, const bool& auto_start, const double& timeout, const std::vector<ni::protobuf::types::DigitalWaveform>& waveforms);

} // namespace nidaqmx_grpc::experimental::client

Expand Down
1 change: 1 addition & 0 deletions generated/nidaqmx/nidaqmx_service.h
Original file line number Diff line number Diff line change
Expand Up @@ -541,6 +541,7 @@ class NiDAQmxService final : public NiDAQmx::WithCallbackMethod_RegisterSignalEv
::grpc::Status ReadAnalogWaveforms(::grpc::ServerContext* context, const ReadAnalogWaveformsRequest* request, ReadAnalogWaveformsResponse* response) override;
::grpc::Status ReadDigitalWaveforms(::grpc::ServerContext* context, const ReadDigitalWaveformsRequest* request, ReadDigitalWaveformsResponse* response) override;
::grpc::Status WriteAnalogWaveforms(::grpc::ServerContext* context, const WriteAnalogWaveformsRequest* request, WriteAnalogWaveformsResponse* response) override;
::grpc::Status WriteDigitalWaveforms(::grpc::ServerContext* context, const WriteDigitalWaveformsRequest* request, WriteDigitalWaveformsResponse* response) override;
private:
LibrarySharedPtr library_;
ResourceRepositorySharedPtr session_repository_;
Expand Down
65 changes: 65 additions & 0 deletions source/codegen/metadata/nidaqmx/functions_addon.py
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,65 @@
'type': 'int32'
}
]
},
'WriteDigitalWaveforms': {
'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_bool',
'direction': 'in',
'is_optional_in_python': True,
'name': 'autoStart',
'python_data_type': 'bool',
'python_default_value': 'False',
'python_description': 'Specifies whether to start the task automatically.',
'python_type_annotation': 'Optional[bool]',
'type': 'bool32'
},
{
'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'
},
{
'direction': 'in',
'is_optional_in_python': False,
'name': 'waveforms',
'python_data_type': 'object',
'python_description': 'The waveforms to write to the specified channels.',
'python_type_annotation': 'List[object]',
'type': 'repeated ni.protobuf.types.DigitalWaveform'
},
{
'ctypes_data_type': 'ctypes.c_int',
'direction': 'out',
'is_optional_in_python': False,
'is_streaming_type': True,
'name': 'sampsPerChanWritten',
'python_data_type': 'int',
'python_description': '',
'python_type_annotation': 'int',
'return_on_error_key': 'ni-samps-per-chan-written',
'type': 'int32'
}
]
}
}

Expand Down Expand Up @@ -245,6 +304,12 @@
# size is determined by the Write.NumChans property
'waveforms': ['ARRAY_PARAMETER_NEEDS_SIZE']
}
},
'WriteDigitalWaveforms': {
'parameters': {
# size is determined by the Write.NumChans property
'waveforms': ['ARRAY_PARAMETER_NEEDS_SIZE']
}
}
}

Expand Down
98 changes: 95 additions & 3 deletions source/custom/nidaqmx_service.custom.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
#include <vector>
#include <memory>
#include <cstdint>
#include <cstring>
#include <server/converters.h>
#include "NIDAQmxInternalWaveform.h"

Expand Down Expand Up @@ -180,8 +181,9 @@ ::grpc::Status NiDAQmxService::ReadAnalogWaveforms(::grpc::ServerContext* contex
std::vector<std::vector<float64>> read_arrays(num_channels);
std::vector<float64*> read_array_ptrs(num_channels);

const uInt32 size = (number_of_samples_per_channel > 0) ? number_of_samples_per_channel : 1;
for (uInt32 i = 0; i < num_channels; ++i) {
read_arrays[i].resize(number_of_samples_per_channel);
read_arrays[i].resize(size);
read_array_ptrs[i] = read_arrays[i].data();
}

Expand Down Expand Up @@ -218,6 +220,7 @@ ::grpc::Status NiDAQmxService::ReadAnalogWaveforms(::grpc::ServerContext* contex
nullptr
);

context->AddTrailingMetadata("ni-samps-per-chan-read", std::to_string(samples_per_chan_read));
if (!status_ok(status)) {
return ConvertApiErrorStatusForTaskHandle(context, status, task);
}
Expand Down Expand Up @@ -276,7 +279,8 @@ ::grpc::Status NiDAQmxService::ReadDigitalWaveforms(::grpc::ServerContext* conte
}

// 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;
const uInt32 num_samples = (number_of_samples_per_channel > 0) ? number_of_samples_per_channel : 1;
const uInt32 array_size = num_samples * num_channels * max_bytes_per_chan;
std::vector<uInt8> read_array(array_size);

response->mutable_waveforms()->Reserve(num_channels);
Expand Down Expand Up @@ -318,6 +322,7 @@ ::grpc::Status NiDAQmxService::ReadDigitalWaveforms(::grpc::ServerContext* conte
nullptr
);

context->AddTrailingMetadata("ni-samps-per-chan-read", std::to_string(samples_per_chan_read));
if (!status_ok(status)) {
return ConvertApiErrorStatusForTaskHandle(context, status, task);
}
Expand Down Expand Up @@ -404,6 +409,93 @@ ::grpc::Status NiDAQmxService::WriteAnalogWaveforms(::grpc::ServerContext* conte
nullptr
);

context->AddTrailingMetadata("ni-samps-per-chan-written", std::to_string(samples_per_chan_written));
if (!status_ok(status)) {
return ConvertApiErrorStatusForTaskHandle(context, status, task);
}

response->set_samps_per_chan_written(samples_per_chan_written);
response->set_status(status);
return ::grpc::Status::OK;
}
catch (nidevice_grpc::NonDriverException& ex) {
return ex.GetStatus();
}
}

::grpc::Status NiDAQmxService::WriteDigitalWaveforms(::grpc::ServerContext* context, const WriteDigitalWaveformsRequest* request, WriteDigitalWaveformsResponse* response)
{
if (context->IsCancelled()) {
return ::grpc::Status::CANCELLED;
}
try {
auto task_grpc_session = request->task();
TaskHandle task = session_repository_->access_session(task_grpc_session.name());

const auto auto_start = request->auto_start();
const auto timeout = request->timeout();
const auto& waveforms = request->waveforms();

if (waveforms.empty()) {
return ::grpc::Status(::grpc::INVALID_ARGUMENT, "No waveforms provided");
}

const uInt32 num_channels = static_cast<uInt32>(waveforms.size());
const int32 number_of_samples_per_channel = static_cast<int32>(waveforms[0].y_data().size() / waveforms[0].signal_count());

uInt32 max_bytes_per_chan = 0;
for (const auto& waveform : waveforms) {
const auto signal_count = waveform.signal_count();
max_bytes_per_chan = std::max(max_bytes_per_chan, static_cast<uInt32>(signal_count));
}

std::vector<uInt32> bytes_per_chan_array(num_channels);
for (uInt32 channel = 0; channel < num_channels; ++channel) {
const auto& waveform = waveforms[channel];
const auto signal_count = waveform.signal_count();
const auto& y_data = waveform.y_data();

if (y_data.size() != number_of_samples_per_channel * signal_count) {
return ::grpc::Status(::grpc::INVALID_ARGUMENT, "The waveforms must all have the same sample count.");
}

bytes_per_chan_array[channel] = static_cast<uInt32>(signal_count);
}

const uInt32 array_size = number_of_samples_per_channel * num_channels * max_bytes_per_chan;
std::vector<uInt8> write_array(array_size, 0);

for (uInt32 channel = 0; channel < num_channels; ++channel) {
const auto& waveform = waveforms[channel];
const auto signal_count = waveform.signal_count();
const auto& y_data = waveform.y_data();

// Data layout: grouped by scan number (interleaved)
// Sample 0: Channel 0 signals, Channel 1 signals, Channel 2 signals, ...
// Sample 1: Channel 0 signals, Channel 1 signals, Channel 2 signals, ...
// Within each channel's data in a sample, signals are sequential: Signal0, Signal1, Signal2, ...
for (int32 sample = 0; sample < number_of_samples_per_channel; ++sample) {
const uInt32 dest_index = sample * num_channels * max_bytes_per_chan + channel * max_bytes_per_chan;
const uInt32 src_index = sample * signal_count;
std::memcpy(&write_array[dest_index], &y_data[src_index], signal_count);
}
}

int32 samples_per_chan_written = 0;
auto status = library_->InternalWriteDigitalWaveform(
task,
number_of_samples_per_channel,
auto_start,
timeout,
GROUP_BY_GROUP_BY_SCAN_NUMBER,
write_array.data(),
bytes_per_chan_array.data(),
num_channels,
&samples_per_chan_written,
nullptr
);

context->AddTrailingMetadata("ni-samps-per-chan-written", std::to_string(samples_per_chan_written));
if (!status_ok(status)) {
return ConvertApiErrorStatusForTaskHandle(context, status, task);
}
Expand All @@ -417,4 +509,4 @@ ::grpc::Status NiDAQmxService::WriteAnalogWaveforms(::grpc::ServerContext* conte
}
}

} // namespace nidaqmx_grpc
} // namespace nidaqmx_grpc
Loading