Instrumental
Instrumental copied to clipboard
Support for NIDAQBase?
Hey Nate,
So I just got GPIB and NI-DAQmx-Base working on a CentOS 7 install in the Digonnet lab. I'm super excited to be able to run my experiment on a linux machine, and exercise all of the power of the Linux.
The problem is that NI doesn't play nice with driver support for Linux, and so they only support our NI 9215 USB BNC DAQ via NI-DAQmx-Base, not NI-DAQmx. This means that I can't control the device via the nidaqmx Python package, but I can control it using the PyDAQmx package.
If I wanted to create some sort of support for this instance in instrumental, should I make another class, perhaps NIDAQbase that wraps around NIDAQmx-Base-supported devices.
I believe I know someone who was using daqs.ni
with NI-DAQmx Base on a Mac a few years ago, so that suggests we may be able to get away without creating a separate class. What are the main issues you're seeing when using Base?
>>> from instrumental.drivers.daq.ni import DAQ
Traceback (most recent call last):
File "/home/daq/anaconda3/lib/python3.7/site-packages/nicelib/__init__.py", line 67, in load_lib
lib_module = import_module(lib_name, pkg)
File "/home/daq/anaconda3/lib/python3.7/importlib/__init__.py", line 127, in import_module
return _bootstrap._gcd_import(name[level:], package, level)
File "<frozen importlib._bootstrap>", line 1006, in _gcd_import
File "<frozen importlib._bootstrap>", line 983, in _find_and_load
File "<frozen importlib._bootstrap>", line 965, in _find_and_load_unlocked
ModuleNotFoundError: No module named 'instrumental.drivers.daq._nilib'
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/home/daq/src/Instrumental/instrumental/drivers/daq/ni.py", line 104, in <module>
class NiceNI(NiceLib):
File "/home/daq/src/Instrumental/instrumental/drivers/daq/ni.py", line 105, in NiceNI
_info_ = load_lib('ni', __package__)
File "/home/daq/anaconda3/lib/python3.7/site-packages/nicelib/__init__.py", line 73, in load_lib
build_module.build(**kwargs)
File "/home/daq/src/Instrumental/instrumental/drivers/daq/_build_ni.py", line 27, in build
build_lib(header_info, lib_names, '_nilib', __file__)
File "/home/daq/anaconda3/lib/python3.7/site-packages/nicelib/build.py", line 113, in build_lib
lib_path = handle_lib_name(lib_name, filedir)
File "/home/daq/anaconda3/lib/python3.7/site-packages/nicelib/util.py", line 156, in handle_lib_name
raise ValueError("Cannot find library '{}'".format(lib_names))
ValueError: Cannot find library '('nidaqmx',)'
Try changing the lib name for linux*
in _build_ni.py
to the name of the Base lib on your machine, perhaps nidaqmxbase
.
Is NI-DAQmx available on Linux at all, or just Base? If it's only base, then _build_ni.py
should probably be fixed for everyone.
You may also have to update header_info
if that's pointing to the wrong thing.
;tldr
NI-DAQmx is available on Linux, but it only supports a small subset of NI DAQs. (see http://www.ni.com/product-documentation/6913/en/, http://www.ni.com/download/ni-linux-device-drivers-2018/7664/en/).
The problem is that a lot of the kernel specs for linux are GPL, and NI doesn't develop drivers for these GPL kernel specs, leaving the Linux community relying on older, more limited versions of NI drivers software. The kernel specs weren't always GPL, but as GPL was added more more things, NI supported fewer of them.
So this is what I've got:
$ ls /usr/local/natinst/nidaqmxbase/include/
NIDAQmxBase.h
And I changed the _build_ni.py
to
$ cat _build_ni.py
# -*- coding: utf-8 -*-
# Copyright 2016 Nate Bogdanowicz
from nicelib import build_lib
header_info = {
'win*': {
'path': (
r"{PROGRAMFILES(X86)}\National Instruments\NI-DAQ\DAQmx ANSI C Dev\include",
r"{PROGRAMFILES}\National Instruments\NI-DAQ\DAQmx ANSI C Dev\include",
),
'header': 'NIDAQmx.h'
},
'linux*': {
'path': '/usr/local/natinst/nidaqmxbase/include',
'header': 'NIDAQmxBase.h'
},
'darwin*': {
'path': '/Applications/National Instruments/NI-DAQmx Base/includes',
'header': 'NIDAQmxBase.h'
},
}
lib_names = {'win*': 'nicaiu', 'linux*': 'nidaqmxbase', 'darwin*': 'nidaqmxbase'}
def build():
build_lib(header_info, lib_names, '_nilib', __file__)
if __name__ == '__main__':
build()
After your changes:
>>> from instrumental.drivers.daq.ni import DAQ
/home/daq/anaconda3/lib/python3.7/site-packages/nicelib/process.py:1451: UserWarning: Unsupported c_ast type <class 'pycparser.c_ast.EmptyStatement'>
warnings.warn(str(e))
/home/daq/anaconda3/lib/python3.7/site-packages/nicelib/process.py:1454: UserWarning: Un-pythonable macro NICatHelper
warnings.warn("Un-pythonable macro {}".format(macro.name))
/home/daq/anaconda3/lib/python3.7/site-packages/nicelib/process.py:1455: UserWarning: <generator>:2:34: before: #
warnings.warn(str(e))
/home/daq/anaconda3/lib/python3.7/site-packages/nicelib/process.py:1451: UserWarning: Unsupported constant type 'double'
warnings.warn(str(e))
Traceback (most recent call last):
File "/home/daq/anaconda3/lib/python3.7/site-packages/nicelib/nicelib.py", line 1076, in __getattr__
return getattr(cls._ffilib, name)
AttributeError: cffi library 'libnidaqmxbase.so.15' has no function, constant or global variable named 'CreateTask'
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/home/daq/src/Instrumental/instrumental/drivers/daq/ni.py", line 104, in <module>
class NiceNI(NiceLib):
File "/home/daq/anaconda3/lib/python3.7/site-packages/nicelib/nicelib.py", line 942, in __init__
cls._create_niceobject_classes()
File "/home/daq/anaconda3/lib/python3.7/site-packages/nicelib/nicelib.py", line 1043, in _create_niceobject_classes
niceclass._patch(cls)
File "/home/daq/anaconda3/lib/python3.7/site-packages/nicelib/nicelib.py", line 596, in _patch
init = getattr(parent_lib, init)
File "/home/daq/anaconda3/lib/python3.7/site-packages/nicelib/nicelib.py", line 1078, in __getattr__
raise AttributeError("{} has no attribute named '{}'".format(cls.__name__, name))
AttributeError: NiceNI has no attribute named 'CreateTask'
You might enable logging with log_to_screen()
to get more detailed info about what's going on. My first guess is that you might have to add a prefix to the list, though DAQmxBase_
is already included.
This is a pretty tricky situation. The simplest thing might be to switch wholesale to Base for linux. A more involved solution might include creating separate low-level libs for DAQmx and DAQmx Base, and switching which one gets used. There's still the problem that the "ideal" behavior would use the DAQmx library if supported, and fall back to Base. This is an issue because the NiceNI class can only point to one library. So, there may need to be separate NiceNI classes if the support needs to be this good. That may also require some changes to nicelib, to add better support for inheriting from a common base, if we're to avoid duplicating all of the Sig definitions.
I set up log_to_screen
>>> import logging
>>> from instrumental.log import log_to_screen
>>> log_to_screen(level=logging.DEBUG)
<RootLogger root (DEBUG)>
And here's the log dump (because it's long, I put it in a gist)
https://gist.github.com/jondoesntgit/8a9cb4584893471b96123cf19ce39395
Here is a working script that dumps an array 1000 samples onto the screen from the 9215 USB (BNC):
#!/usr/bin/env python
from PyDAQmx import *
import numpy
from ctypes import byref
# Declaration of variable passed by reference
taskHandle = TaskHandle()
read = int32()
data = numpy.zeros((1000,), dtype=numpy.float64)
try:
# DAQmx Configure Code
DAQmxCreateTask("",byref(taskHandle))
DAQmxCreateAIVoltageChan(taskHandle,b"Dev1/ai0","",DAQmx_Val_Cfg_Default,-10.0,10.0,DAQmx_Val_Volts,None)
DAQmxCfgSampClkTiming(taskHandle,"",10000.0,DAQmx_Val_Rising,DAQmx_Val_FiniteSamps,1000)
# DAQmx Start Code
DAQmxStartTask(taskHandle)
# DAQmx Read Code
DAQmxReadAnalogF64(taskHandle,1000,10.0,DAQmx_Val_GroupByChannel,data,1000, read,None)
print("Acquired %d points"%read.value)
except DAQError as err:
print("DAQmx Error: %s"%err)
finally:
if taskHandle:
# DAQmx Stop Code
DAQmxStopTask(taskHandle)
DAQmxClearTask(taskHandle)
print(data)
If it's helpful, I can give you SSH access to the computer in the lab, and we can debug together.
Here is where NIDAQmxBase.h provides CreateTask:
https://gist.github.com/jondoesntgit/154d6ffbc71f0987f31b06ca3aad8005#file-nidaqmxbase-h-L1177-L1189
I made some progress. I added this to instrumental/drivers/daq/ni.py
:
$ git diff
diff --git a/instrumental/drivers/daq/ni.py b/instrumental/drivers/daq/ni.py
index a708ebb..66ab1e4 100644
--- a/instrumental/drivers/daq/ni.py
+++ b/instrumental/drivers/daq/ni.py
@@ -103,7 +103,7 @@ def ret_errcheck(code):
class NiceNI(NiceLib):
_info_ = load_lib('ni', __package__)
- _prefix_ = ('DAQmxBase_', 'DAQmx_', 'DAQmx')
+ _prefix_ = ('DAQmxBase_', 'DAQmxBase', 'DAQmx_', 'DAQmx')
_buflen_ = 1024
_use_numpy_ = True
_ret_ = ret_errcheck
Now, I'm getting this error
>>> from instrumental.drivers.daq.ni import DAQ
/home/daq/anaconda3/lib/python3.7/site-packages/nicelib/nicelib.py:1059: UserWarning: Conflicting name assert_line___LINE__, ignoring
warnings.warn("Conflicting name {}, ignoring".format(shortname))
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/home/daq/src/Instrumental/instrumental/drivers/daq/ni.py", line 372, in <module>
ProductCategory.MSeriesDAQ:
File "/home/daq/anaconda3/lib/python3.7/enum.py", line 348, in __getattr__
raise AttributeError(name) from None
AttributeError: MSeriesDAQ
The problem is that this block always goes to the except statement:
https://github.com/mabuchilab/Instrumental/blob/82a23b841f15491d3e9b3746d4baeb176b78d40a/instrumental/drivers/daq/ni.py#L289-L293
Which ends up removing all of the devices from ProductCategory
I found that replacing the try block with pass
suppressed the error, but I'm not sure if that is what's causing my new problems:
$ lsdaq
--------------------------------
Detecting National Instruments DAQ Devices
Found the following DAQ Devices:
NI USB-9215A with BNC:"Dev1" (USB0::0x3923::0x71DB::00E52E4C::RAW)
--------------------------------
>>> from instrumental import list_instruments
>>> list_instruments()
[]
>>> from instrumental.drivers.daq.ni import NIDAQ
>>> NIDAQ(name='Dev1')
Traceback (most recent call last):
File "/home/daq/anaconda3/lib/python3.7/site-packages/nicelib/nicelib.py", line 1076, in __getattr__
return getattr(cls._ffilib, name)
AttributeError: cffi library 'libnidaqmxbase.so.15' has no function, constant or global variable named 'GetSysDevNames'
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/home/daq/src/Instrumental/instrumental/drivers/__init__.py", line 524, in __new__
return instrument(inst, **kwds)
File "/home/daq/src/Instrumental/instrumental/drivers/__init__.py", line 1275, in instrument
inst = find_nonvisa_instrument(params)
File "/home/daq/src/Instrumental/instrumental/drivers/__init__.py", line 1166, in find_nonvisa_instrument
full_params = find_full_params(normalized_params, driver_module)
File "/home/daq/src/Instrumental/instrumental/drivers/__init__.py", line 1245, in find_full_params
for inst_params in driver_module.list_instruments():
File "/home/daq/src/Instrumental/instrumental/drivers/daq/ni.py", line 71, in list_instruments
dev_names = split_list(NiceNI.GetSysDevNames())
File "/home/daq/anaconda3/lib/python3.7/site-packages/nicelib/nicelib.py", line 1078, in __getattr__
raise AttributeError("{} has no attribute named '{}'".format(cls.__name__, name))
AttributeError: NiceNI has no attribute named 'GetSysDevNames'
>>> NIDAQ(model='USB-9215A')
Traceback (most recent call last):
File "/home/daq/anaconda3/lib/python3.7/site-packages/nicelib/nicelib.py", line 1076, in __getattr__
return getattr(cls._ffilib, name)
AttributeError: cffi library 'libnidaqmxbase.so.15' has no function, constant or global variable named 'GetSysDevNames'
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/home/daq/src/Instrumental/instrumental/drivers/__init__.py", line 524, in __new__
return instrument(inst, **kwds)
File "/home/daq/src/Instrumental/instrumental/drivers/__init__.py", line 1275, in instrument
inst = find_nonvisa_instrument(params)
File "/home/daq/src/Instrumental/instrumental/drivers/__init__.py", line 1166, in find_nonvisa_instrument
full_params = find_full_params(normalized_params, driver_module)
File "/home/daq/src/Instrumental/instrumental/drivers/__init__.py", line 1245, in find_full_params
for inst_params in driver_module.list_instruments():
File "/home/daq/src/Instrumental/instrumental/drivers/daq/ni.py", line 71, in list_instruments
dev_names = split_list(NiceNI.GetSysDevNames())
File "/home/daq/anaconda3/lib/python3.7/site-packages/nicelib/nicelib.py", line 1078, in __getattr__
raise AttributeError("{} has no attribute named '{}'".format(cls.__name__, name))
AttributeError: NiceNI has no attribute named 'GetSysDevNames'
>>>
Yeah, it appears that nidaqmxbase.h
does not have the ProductCategory constants. We can patch over that by using strings instead of enums as the key to the _internal_channels
dict, so that's not a serious issue.
It would be helpful to see an up-to-date log output that shows this most recent error.
Besides what I already posted, what would be the most helpful?
I can get past the current error caused by GetSysDevNames not being defined by hardcoding dev_names = ['Dev1']
, and then commenting out mode=NiceNI.Device.GetDevProductType(dev_name)
... but then there are a lot of library calls that go on when the instrument is initialized, and it doesn't seem the the Device object supports any of them. I could probably hard-code a bunch of patches in to make it work, but I think for now, my time will be best spent by using PyDAQmx.
Nate, it appears that NI doesn't fully support all of NIDAQBase on Linux machines. This renders nidaq
almost useless on the machine that I'm using.
I've been using the workaround of a custom script that utilizes PyDAQmx
in the mean time. Is there any value to writing an alternate class that duck-types the same as the DAQ classes that you've written in this repo? The class would be loaded as a fallback if nidaq causes errors.
If that Linux subset is quite restricted, I do think it could be valuable to make a Linux variant of the class as you suggest.
However, anything that's implementable with PyDAQmx
should be implementable with NiceLib
, since they're just wrapping the same lib (either NI-DAQmx or NI-DAQmx Base). Given that, I don't see the value in mixing the two approaches. Is there something in particular you can get working with PyDAQmx that you can't with NiceLib
? Or is it just that Instrumental's NiceNI
class as-implemented has too many unsupported functions in it?
In order to answer that question, I'd have to understand the NiceNI class a little better. I know that I ran into a lot of difficulties trying to run the Instrumental
implementation out of the box on my linux distro. I may try a ground-up approach where I try to get some low-level functionality working, and build up from there.
I had a chance to spend a few hours wading through NiceNI, nicelib, and comparing NIDAQmxBase to NIDAQmx. Here's what I found:
NIDAQmxBase supports a very limited subset of NIDAQmx. Specifically, none of the NiceNI
Device subclass attributes are supported (starting with GetDevIsSimulated
and onwards). A smattering of other attributes need to be removed as well (see the commented-out lines)
class NiceNI(NiceLib):
_info_ = load_lib('ni', __package__)
_prefix_ = ('DAQmxBase_', 'DAQmxBase', 'DAQmx_', 'DAQmx')
_buflen_ = 1024
_use_numpy_ = True
_ret_ = ret_errcheck
# GetErrorString = Sig('in', 'buf', 'len')
# GetSysDevNames = Sig('buf', 'len')
GetExtendedErrorInfo = Sig('buf', 'len=2048')
CreateTask = Sig('in', 'out')
class Task(NiceObject):
"""A Nice-wrapped NI Task"""
_init_ = 'CreateTask'
StartTask = Sig('in')
StopTask = Sig('in')
ClearTask = Sig('in')
# WaitUntilTaskDone = Sig('in', 'in')
IsTaskDone = Sig('in', 'out')
# TaskControl = Sig('in', 'in')
CreateAIVoltageChan = Sig('in', 'in', 'in', 'in', 'in', 'in', 'in', 'in')
CreateAOVoltageChan = Sig('in', 'in', 'in', 'in', 'in', 'in', 'in')
CreateDIChan = Sig('in', 'in', 'in', 'in')
CreateDOChan = Sig('in', 'in', 'in', 'in')
ReadAnalogF64 = Sig('in', 'in', 'in', 'in', 'arr', 'len=in', 'out', 'ignore')
# ReadAnalogScalarF64 = Sig('in', 'in', 'out', 'ignore')
ReadDigitalScalarU32 = Sig('in', 'in', 'out', 'ignore')
ReadDigitalU32 = Sig('in', 'in', 'in', 'in', 'arr', 'len=in', 'out', 'ignore')
# ReadDigitalLines = Sig('in', 'in', 'in', 'in', 'arr', 'len=in', 'out', 'out', 'ignore')
WriteAnalogF64 = Sig('in', 'in', 'in', 'in', 'in', 'in', 'out', 'ignore')
# WriteAnalogScalarF64 = Sig('in', 'in', 'in', 'in', 'ignore')
WriteDigitalU32 = Sig('in', 'in', 'in', 'in', 'in', 'in', 'out', 'ignore')
WriteDigitalScalarU32 = Sig('in', 'in', 'in', 'in', 'ignore')
CfgSampClkTiming = Sig('in', 'in', 'in', 'in', 'in', 'in')
CfgImplicitTiming = Sig('in', 'in', 'in')
# CfgOutputBuffer = Sig('in', 'in')
CfgAnlgEdgeStartTrig = Sig('in', 'in', 'in', 'in')
CfgDigEdgeStartTrig = Sig('in', 'in', 'in')
CfgDigEdgeRefTrig = Sig('in', 'in', 'in', 'in')
# GetAOUseOnlyOnBrdMem = Sig('in', 'in', 'out')
# SetAOUseOnlyOnBrdMem = Sig('in', 'in', 'in')
# GetBufInputOnbrdBufSize = Sig('in', 'out')
_sigs_ = sig_pattern((
('Get{}', Sig('in', 'out')),
('Set{}', Sig('in', 'in')),
),(
# 'SampTimingType',
# 'SampQuantSampMode',
# 'ReadOffset',
# 'ReadRelativeTo',
# 'ReadOverWrite',
# 'SampQuantSampPerChan',
# 'BufInputBufSize',
# 'BufOutputBufSize',
# 'BufOutputOnbrdBufSize',
))
None of these commented-out commands are accessible using PyDAQmx. The program will run if they're not commented out, but will generate many warnings in the logger.
I can now import DAQ
from ni
, but without any surviving class attributes for a Device, Device merely becomes a relatively trivial class that only holds onto the device name. NIDAQmxBase also does not have the ability to list attached devices. On the other hand, I have access to a binary executable at /etc/natinst/nidaqmxbase/bin/daqmxbase_listdevices
that outputs
NI USB-9215A with BNC:"Dev1" (USB0::0x3923::0x71DB::00E52E4C::RAW)
I may be able to use this in place of the list-devices command.
One thing that concerns me is that I get a segmentation fault whenever I close the program. As I said earlier, I can now import DAQ
from ni
, but even if I don't do anything further, I get a segmentation fault upon merely from importing DAQ. Do you know of anything in nicelib that would cause this behavior? I don't get segmentation faults when I load PyDAQmx.
Regarding the Seg Fault, the issue seems to be with the cffi
package. Whenever the cffi
package tries to open libnidaqmxbase.so.15
, when the program eventually finishes, there's a Segmentation Fault. In PyDAQmx
, they load libnidaqmxbase.so.15
with ctypes
, not with cffi
, which does not produce an error.
I've documented the behavior in the cffi
bug tracker: https://foss.heptapod.net/pypy/cffi/issues/448
Interesting re the segfault, thanks for investigating. This isn't something I recall coming across before.
NIDAQmxBase doesn't support ReadAnalogScalarF64 (which reads exactly one value from a channel). I've tried to duck-type it by setting up a task to read two values very quickly using ReadAnalogF64 (which reads multiple values, provided that the clock is set up). I've been trying to mirror the configuration and read steps that work when I use PyDAQmx, but no matter what happens, it says that I read zero samples and returns an array of zeros.
There don't seem to be any functions in the NIDAQmxBase instruction set that tell me that I'm getting any heartbeat from the device (unless you're actually getting full data from it). The only one that gets somewhat close is IsTaskClose.
I'm wondering if whatever is causing the segfault is messing up ReadAnalogF64.
In my case, IsTaskDone() always returns 0. Here is a function that I'm using to try to duck_type the Minitask
's read_AI_scalar
function...
n_samples = 20
buf_size = n_samples
fsamp = '1000.0 Hz'
timeout_s = 2
self.config_timing(fsamp, n_samples)
print('DEBUG: IS TASK DONE:', self._mx_task.IsTaskDone())
print('DEBUG:Started task')
self.start()
print('DEBUG: IS TASK DONE:', self._mx_task.IsTaskDone())
data, n_samples_read = self._mx_task.ReadAnalogF64(n_samples, timeout_s, Val.GroupByChannel, b uf_size)
print(data, n_samples_read)
print('DEBUG: IS TASK DONE:', self._mx_task.IsTaskDone())
self.stop()
print('DEBUG: IS TASK DONE:', self._mx_task.IsTaskDone())
value = np.mean(data)
I always read 0 samples, get an array of zeros, and all of the IsTaskDone() queries return zero.
I wonder if there is some problem opening up the library.
I tried different versions of cffi
, but that didn't fix the problem. I installed yum install libffi-devel
but still got segfaults.
I can confirm that cffi works on my system for a simple program (using this tutorial)
//cmult.c
float cmult(int int_param, float float_param) {
float return_value = int_param * float_param;
return return_value;
}
//cmult.h
float cmult(int int_param, float float_param);
# task.py
import cffi
import pathlib
print('building CFFI module')
ffi = cffi.FFI()
this_dir = pathlib.Path().absolute()
h_file_name = this_dir / 'cmult.h'
with open(h_file_name) as h_file:
ffi.cdef(h_file.read())
ffi.set_source('cffi_example',
'#include "cmult.h"',
libraries=['cmult'],library_dirs=[this_dir.as_posix()],
extra_link_args=['-Wl,-rpath,.'],
)
ffi.compile()
# cffi_test.py
import cffi_example
if __name__ == '__main__':
x, y = 6, 2.3
answer = cffi_example.lib.cmult(x, y)
print('%.1f' % answer)
$ gcc -shared -o libcmult.so -fPIC cmult.c
$ python task.py
$ python cffi_test.py
13.8
Another quirk to deal with:
In AnalogReadF64, NIDAQmxBase always errors out if the timeout is 0 or -1. If the number of requested samples is set to -1 (or any other negative number), it always clears the device buffer, and writes two samples to the array by reference, and says that two samples were read.
I'm doing some benchmarking, and a Python call on my lab computer indicates that about 96% of the time, AnalogReadF64 executes in 5 ms or less when enough samples are in the buffer. The execution time seems to be independent of the number of samples read (whether its 1 sample, 10 samples, or 100 samples).
Execution time (ms) | % timeout |
---|---|
0 | 100.00 |
1 | 37.96 |
2 | 21.20 |
3 | 7.56 |
4 | 1.69 |
5 | 0.06 |
6 | 0.02 |
7 | 0.02 |
8 | 0.01 |
9 | 0.01 |
My idea for a workaround is to have the minitask to guess how many samples are available, and try to read it in chunks, with extremely short timeouts for smaller chunks.
For example, if the minitask thinks that there are 200 samples available,
- Read 180 samples with a 100 ms timeout
- Read 18 samples with 10 ms timeouts until the DAQ returns a timeout error
- Read 2 samples with a 10 ms timeout until DAQ returns a timeout error
- Read 1 sample with 1 ms timeout until DAQ returns a timeout error
This todo-list would change based on the sample rate, the timeout that the user passes to the algorithm, and the number of expected data points to read.
That's my best idea to replace the behavior of trying to duck-type the functions to read all available samples in the buffer.