Skip to content

Commit c167637

Browse files
mikeprosserniMike Prosser
andauthored
Add WriteDigitalWaveforms to NiDAQmxService (#1216)
* 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. * fix lint * fix validate_examples * ReadAnalogWaveforms implementation (untested, but compiles) * add tests * Add support for creating two AI voltage channels in tests * cleanup * cleanup CMakeLists.txt * attempt fix for ubuntu build * more verbose build output * Try some different build settings * add quotes to fix cmake flags * add sampsPerChanRead to ReadAnalogWaveforms response * revert changes in build_cmake.yml and do -fno-var-tracking-assignments instead * use GCC push_options * use -fno-var-tracking-assignments globally for gcc * fix timing conversion * add ReadDigitalWaveforms RPC with stub implementation * implementation and tests for single channel * test multi channel and multi line * add copilot instructions for how to build with ninja, and do some cleanup * fix data layout * brad's feedback * copy protobuf types protos to heirarchical location * fix merge and templatize the SetWfmAttrCallback * copilot-build.ps1 * handle epoch difference (note - SecondsFrom0001EpochTo1904Epoch value is off by 5ish months) * convert_dot_net_daqmx_ticks_to_btf_precision_timestamp * fixes and cleanup * cleanup and more test cases * PrecisionTimestampConverterLiteralTests * fix merge * remove copilot instructions (will be added in separate PR) * use correct logic for NI-BTF * test cleanup * fillMode: 1 = GROUP_BY_SCAN_NUMBER (interleaved) * cleanup merge * cleanup * use waveform->mutable_y_data(); * test cleanup * Brad's feedback * cleanup * implement WriteDigitalWaveforms * memcpy optimization * fix merge * test cleanup * Brad's feedback * trailing metadata * added tests to verify the trailing metadata in error cases * fix asserts * fix crash in ReadAnalogWaveforms * EXPECT_THROW_DRIVER_ERROR * guard against negative-overflow array_size in ReadDigitalWaveforms --------- Co-authored-by: Mike Prosser <Mike.Prosser@emerson.com>
1 parent 1280723 commit c167637

7 files changed

Lines changed: 468 additions & 21 deletions

File tree

generated/nidaqmx/nidaqmx.proto

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -469,6 +469,7 @@ service NiDAQmx {
469469
rpc ReadAnalogWaveforms(ReadAnalogWaveformsRequest) returns (ReadAnalogWaveformsResponse);
470470
rpc ReadDigitalWaveforms(ReadDigitalWaveformsRequest) returns (ReadDigitalWaveformsResponse);
471471
rpc WriteAnalogWaveforms(WriteAnalogWaveformsRequest) returns (WriteAnalogWaveformsResponse);
472+
rpc WriteDigitalWaveforms(WriteDigitalWaveformsRequest) returns (WriteDigitalWaveformsResponse);
472473
}
473474

474475
enum BufferUInt32Attribute {
@@ -11565,3 +11566,15 @@ message WriteAnalogWaveformsResponse {
1156511566
int32 samps_per_chan_written = 2;
1156611567
}
1156711568

11569+
message WriteDigitalWaveformsRequest {
11570+
nidevice_grpc.Session task = 1;
11571+
bool auto_start = 2;
11572+
double timeout = 3;
11573+
repeated ni.protobuf.types.DigitalWaveform waveforms = 4;
11574+
}
11575+
11576+
message WriteDigitalWaveformsResponse {
11577+
int32 status = 1;
11578+
int32 samps_per_chan_written = 2;
11579+
}
11580+

generated/nidaqmx/nidaqmx_client.cpp

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12488,5 +12488,25 @@ write_analog_waveforms(const StubPtr& stub, const nidevice_grpc::Session& task,
1248812488
return response;
1248912489
}
1249012490

12491+
WriteDigitalWaveformsResponse
12492+
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)
12493+
{
12494+
::grpc::ClientContext context;
12495+
12496+
auto request = WriteDigitalWaveformsRequest{};
12497+
request.mutable_task()->CopyFrom(task);
12498+
request.set_auto_start(auto_start);
12499+
request.set_timeout(timeout);
12500+
copy_array(waveforms, request.mutable_waveforms());
12501+
12502+
auto response = WriteDigitalWaveformsResponse{};
12503+
12504+
raise_if_error(
12505+
stub->WriteDigitalWaveforms(&context, request, &response),
12506+
context);
12507+
12508+
return response;
12509+
}
12510+
1249112511

1249212512
} // namespace nidaqmx_grpc::experimental::client

generated/nidaqmx/nidaqmx_client.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -471,6 +471,7 @@ WriteToTEDSFromFileResponse write_to_teds_from_file(const StubPtr& stub, const s
471471
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);
472472
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);
473473
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);
474+
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);
474475

475476
} // namespace nidaqmx_grpc::experimental::client
476477

generated/nidaqmx/nidaqmx_service.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -541,6 +541,7 @@ class NiDAQmxService final : public NiDAQmx::WithCallbackMethod_RegisterSignalEv
541541
::grpc::Status ReadAnalogWaveforms(::grpc::ServerContext* context, const ReadAnalogWaveformsRequest* request, ReadAnalogWaveformsResponse* response) override;
542542
::grpc::Status ReadDigitalWaveforms(::grpc::ServerContext* context, const ReadDigitalWaveformsRequest* request, ReadDigitalWaveformsResponse* response) override;
543543
::grpc::Status WriteAnalogWaveforms(::grpc::ServerContext* context, const WriteAnalogWaveformsRequest* request, WriteAnalogWaveformsResponse* response) override;
544+
::grpc::Status WriteDigitalWaveforms(::grpc::ServerContext* context, const WriteDigitalWaveformsRequest* request, WriteDigitalWaveformsResponse* response) override;
544545
private:
545546
LibrarySharedPtr library_;
546547
ResourceRepositorySharedPtr session_repository_;

source/codegen/metadata/nidaqmx/functions_addon.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,65 @@
197197
'type': 'int32'
198198
}
199199
]
200+
},
201+
'WriteDigitalWaveforms': {
202+
'returns': 'int32',
203+
'codegen_method': 'CustomCodeNoLibrary',
204+
'parameters': [
205+
{
206+
'ctypes_data_type': 'ctypes.TaskHandle',
207+
'direction': 'in',
208+
'is_optional_in_python': False,
209+
'name': 'task',
210+
'python_data_type': 'TaskHandle',
211+
'python_description': '',
212+
'python_type_annotation': 'TaskHandle',
213+
'type': 'TaskHandle'
214+
},
215+
{
216+
'ctypes_data_type': 'ctypes.c_bool',
217+
'direction': 'in',
218+
'is_optional_in_python': True,
219+
'name': 'autoStart',
220+
'python_data_type': 'bool',
221+
'python_default_value': 'False',
222+
'python_description': 'Specifies whether to start the task automatically.',
223+
'python_type_annotation': 'Optional[bool]',
224+
'type': 'bool32'
225+
},
226+
{
227+
'ctypes_data_type': 'ctypes.c_double',
228+
'direction': 'in',
229+
'is_optional_in_python': True,
230+
'name': 'timeout',
231+
'python_data_type': 'float',
232+
'python_default_value': '10.0',
233+
'python_description': 'Specifies the time in seconds to wait for the device to respond before timing out.',
234+
'python_type_annotation': 'Optional[float]',
235+
'type': 'float64'
236+
},
237+
{
238+
'direction': 'in',
239+
'is_optional_in_python': False,
240+
'name': 'waveforms',
241+
'python_data_type': 'object',
242+
'python_description': 'The waveforms to write to the specified channels.',
243+
'python_type_annotation': 'List[object]',
244+
'type': 'repeated ni.protobuf.types.DigitalWaveform'
245+
},
246+
{
247+
'ctypes_data_type': 'ctypes.c_int',
248+
'direction': 'out',
249+
'is_optional_in_python': False,
250+
'is_streaming_type': True,
251+
'name': 'sampsPerChanWritten',
252+
'python_data_type': 'int',
253+
'python_description': '',
254+
'python_type_annotation': 'int',
255+
'return_on_error_key': 'ni-samps-per-chan-written',
256+
'type': 'int32'
257+
}
258+
]
200259
}
201260
}
202261

@@ -245,6 +304,12 @@
245304
# size is determined by the Write.NumChans property
246305
'waveforms': ['ARRAY_PARAMETER_NEEDS_SIZE']
247306
}
307+
},
308+
'WriteDigitalWaveforms': {
309+
'parameters': {
310+
# size is determined by the Write.NumChans property
311+
'waveforms': ['ARRAY_PARAMETER_NEEDS_SIZE']
312+
}
248313
}
249314
}
250315

source/custom/nidaqmx_service.custom.cpp

Lines changed: 95 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
#include <vector>
33
#include <memory>
44
#include <cstdint>
5+
#include <cstring>
56
#include <server/converters.h>
67
#include "NIDAQmxInternalWaveform.h"
78

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

184+
const uInt32 size = (number_of_samples_per_channel > 0) ? number_of_samples_per_channel : 1;
183185
for (uInt32 i = 0; i < num_channels; ++i) {
184-
read_arrays[i].resize(number_of_samples_per_channel);
186+
read_arrays[i].resize(size);
185187
read_array_ptrs[i] = read_arrays[i].data();
186188
}
187189

@@ -218,6 +220,7 @@ ::grpc::Status NiDAQmxService::ReadAnalogWaveforms(::grpc::ServerContext* contex
218220
nullptr
219221
);
220222

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

278281
// Calculate total array size needed (samples * channels * max_bytes_per_chan)
279-
const uInt32 array_size = number_of_samples_per_channel * num_channels * max_bytes_per_chan;
282+
const uInt32 num_samples = (number_of_samples_per_channel > 0) ? number_of_samples_per_channel : 1;
283+
const uInt32 array_size = num_samples * num_channels * max_bytes_per_chan;
280284
std::vector<uInt8> read_array(array_size);
281285

282286
response->mutable_waveforms()->Reserve(num_channels);
@@ -318,6 +322,7 @@ ::grpc::Status NiDAQmxService::ReadDigitalWaveforms(::grpc::ServerContext* conte
318322
nullptr
319323
);
320324

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

412+
context->AddTrailingMetadata("ni-samps-per-chan-written", std::to_string(samples_per_chan_written));
413+
if (!status_ok(status)) {
414+
return ConvertApiErrorStatusForTaskHandle(context, status, task);
415+
}
416+
417+
response->set_samps_per_chan_written(samples_per_chan_written);
418+
response->set_status(status);
419+
return ::grpc::Status::OK;
420+
}
421+
catch (nidevice_grpc::NonDriverException& ex) {
422+
return ex.GetStatus();
423+
}
424+
}
425+
426+
::grpc::Status NiDAQmxService::WriteDigitalWaveforms(::grpc::ServerContext* context, const WriteDigitalWaveformsRequest* request, WriteDigitalWaveformsResponse* response)
427+
{
428+
if (context->IsCancelled()) {
429+
return ::grpc::Status::CANCELLED;
430+
}
431+
try {
432+
auto task_grpc_session = request->task();
433+
TaskHandle task = session_repository_->access_session(task_grpc_session.name());
434+
435+
const auto auto_start = request->auto_start();
436+
const auto timeout = request->timeout();
437+
const auto& waveforms = request->waveforms();
438+
439+
if (waveforms.empty()) {
440+
return ::grpc::Status(::grpc::INVALID_ARGUMENT, "No waveforms provided");
441+
}
442+
443+
const uInt32 num_channels = static_cast<uInt32>(waveforms.size());
444+
const int32 number_of_samples_per_channel = static_cast<int32>(waveforms[0].y_data().size() / waveforms[0].signal_count());
445+
446+
uInt32 max_bytes_per_chan = 0;
447+
for (const auto& waveform : waveforms) {
448+
const auto signal_count = waveform.signal_count();
449+
max_bytes_per_chan = std::max(max_bytes_per_chan, static_cast<uInt32>(signal_count));
450+
}
451+
452+
std::vector<uInt32> bytes_per_chan_array(num_channels);
453+
for (uInt32 channel = 0; channel < num_channels; ++channel) {
454+
const auto& waveform = waveforms[channel];
455+
const auto signal_count = waveform.signal_count();
456+
const auto& y_data = waveform.y_data();
457+
458+
if (y_data.size() != number_of_samples_per_channel * signal_count) {
459+
return ::grpc::Status(::grpc::INVALID_ARGUMENT, "The waveforms must all have the same sample count.");
460+
}
461+
462+
bytes_per_chan_array[channel] = static_cast<uInt32>(signal_count);
463+
}
464+
465+
const uInt32 array_size = number_of_samples_per_channel * num_channels * max_bytes_per_chan;
466+
std::vector<uInt8> write_array(array_size, 0);
467+
468+
for (uInt32 channel = 0; channel < num_channels; ++channel) {
469+
const auto& waveform = waveforms[channel];
470+
const auto signal_count = waveform.signal_count();
471+
const auto& y_data = waveform.y_data();
472+
473+
// Data layout: grouped by scan number (interleaved)
474+
// Sample 0: Channel 0 signals, Channel 1 signals, Channel 2 signals, ...
475+
// Sample 1: Channel 0 signals, Channel 1 signals, Channel 2 signals, ...
476+
// Within each channel's data in a sample, signals are sequential: Signal0, Signal1, Signal2, ...
477+
for (int32 sample = 0; sample < number_of_samples_per_channel; ++sample) {
478+
const uInt32 dest_index = sample * num_channels * max_bytes_per_chan + channel * max_bytes_per_chan;
479+
const uInt32 src_index = sample * signal_count;
480+
std::memcpy(&write_array[dest_index], &y_data[src_index], signal_count);
481+
}
482+
}
483+
484+
int32 samples_per_chan_written = 0;
485+
auto status = library_->InternalWriteDigitalWaveform(
486+
task,
487+
number_of_samples_per_channel,
488+
auto_start,
489+
timeout,
490+
GROUP_BY_GROUP_BY_SCAN_NUMBER,
491+
write_array.data(),
492+
bytes_per_chan_array.data(),
493+
num_channels,
494+
&samples_per_chan_written,
495+
nullptr
496+
);
497+
498+
context->AddTrailingMetadata("ni-samps-per-chan-written", std::to_string(samples_per_chan_written));
407499
if (!status_ok(status)) {
408500
return ConvertApiErrorStatusForTaskHandle(context, status, task);
409501
}
@@ -417,4 +509,4 @@ ::grpc::Status NiDAQmxService::WriteAnalogWaveforms(::grpc::ServerContext* conte
417509
}
418510
}
419511

420-
} // namespace nidaqmx_grpc
512+
} // namespace nidaqmx_grpc

0 commit comments

Comments
 (0)