InstroDAQ
InstroDAQ is a hardware abstraction layer (HAL) that provides a unified interface for data acquisition devices from multiple vendors. The key benefit is vendor-independent code: swap the driver instance you hand toInstroDAQ and the same configuration/acquisition code works across different hardware.
Supported Vendors
- National Instruments (NI DAQmx) - Using NI-DAQmx driver
- LabJack T-Series - T4, T7, T8 models using LJM driver
- Measurement Computing (MCC) - USB and Ethernet DAQ devices using the MCC Universal Library (
mcculw) - Keysight 34980A - Multifunction switch/measure unit via SCPI/VISA
Key Concepts
Lifecycle Pattern
The typical InstroDAQ workflow follows this pattern:InstroDAQ(name, driver, ...)- Instantiate the DAQ with a vendor driveropen()- Establish connection to the hardware- Configure - Set up , timing, and other settings
start()- Begin acquisition (if using hardware timing or the software-timed daemon)- Acquire data - Read/fetch measurements
stop()- End acquisition (if started)close()- Disconnect from hardware
Channels
InstroDAQ supports both analog and digital I/O:- Analog Input/Output: Measurements and generation with configurable ranges
- Digital Input/Output: Line-based or port-based digital I/O
Aliases
Map vendor-specific physical channel names (e.g., “AIN0”) to logical names (e.g., “temperature_sensor”) Channel aliases are used as channel names when data is published to Nominal Core or Connect.Timing Modes
InstroDAQ supports three acquisition modes: Software-Timed (Manual Polling)- You call
read_analog()orread_digital()when you want a sample - No need to call
start()orstop() - Use case: Low-frequency monitoring, event-driven sampling
- Configure the polling rate with
configure_sw_sample_rate() - Call
start()to launch the background daemon, which reads every configured analog and digital input channel once per period and publishes the results - Use case: Low-frequency continuous monitoring without a hardware sample clock
- Configure sample rate with
configure_hw_sample_rate() - Call
start()to begin background acquisition - Data automatically published via the background daemon, OR manually fetch with
read_analog()/read_digital() - Use case: Mid to high frequency continuous monitoring with deterministic sample timing.
TimingConfigException. To run both against one device, build a separate InstroDAQ per mode with non-overlapping channels: see the NI hardware- and software-timed example.
Creating an InstroDAQ Instance
Construct a vendor driver and pass it toInstroDAQ:
Parameters
name: A name for this DAQ instance. Used as a prefix for channel names when publishing to Nominal Core.driver: A concreteDAQDriverBasesubclass instance. Each driver owns its own transport (vendor SDK or SCPI/VISA) and accepts vendor-specific connection parameters at construction.publishers: List of publishers to attachdataset_rid: Shorthand to add aNominalCorePublisher
Vendor-Specific Driver Examples
Configuring Channels
Analog Input Channels
Configure analog input channels with measurement range and logical names:The
physical_channel naming convention depends on your DAQ vendor:- NI DAQmx: fully qualified
device/channel, for example “Dev1/ai0”, “Dev1/ai1”. - LabJack: “AIN0”, “AIN1”, etc.
- MCC: Integer channel index as a string, for example
"0","1". - Keysight: Depends on module slot and channel configuration
Scalers
It’s common for the data being read by an analog input channel to need scaling to represent a real-world physical phenomenon. For example, a 0-5 volt sensor measuring pressure.InstroDAQ supports adding a Scaler object when you configure your analog channel. The Measurement published and returned by read_analog will contain these scaled values.
Example of a 0-5V pressure sensor that measures 0-3000 psia.
ScalerPipeline scaler.
For example, a thermocouple that’s fed into an amplifier and then the DAQ will require two stages of scaling.
- Scaling out the amplifier to get to the actual voltage seen across the thermocouple terminals.
- Scaling the voltage seen across the thermocouple terminals to temperature.
Scaler and implementing scale and units methods.
Analog Output Channels
Configure analog output channels:The
physical_channel naming convention depends on your DAQ vendor:- NI DAQmx: fully qualified
device/channel, for example “Dev1/ao0”, “Dev1/ao1”. - LabJack: “DAC0”, “DAC1”, etc.
- MCC: Integer channel index as a string, for example
"0","1". - Keysight: Depends on module slot and channel configuration
Digital Channels
Before configuring a digital channel, you need to specify the following parameters to match your application and DAQ hardware:InstroDAQ exposes two methods: configure_digital_line and configure_digital_port.
- direction: Use
Direction.INPUTfor digital input orDirection.OUTPUTfor output. - physical_channel: The physical line or port on your DAQ device (e.g.,
"5101","5101/2"for Keysight, or"FIRSTPORTA","FIRSTPORTA/0"for MCC). This name is vendor-specific. Refer to your device documentation. - logic: Sets whether the channel treats a HIGH or LOW physical level as “True”. This is required for correct logic interpretation.
- logic_level (optional): Specifies the voltage threshold (in volts) used to distinguish HIGH from LOW, if your device supports changing this.
- alias (optional): A logical name for the channel, helpful for clarity and for use with publishers.
- port_width (port only): Port width in bits (8/16/32/64), required by
configure_digital_port.
Typed Channel Configuration
Alongside the genericconfigure_analog_channel / configure_digital_line, InstroDAQ exposes measurement-specific configuration methods. Each builds a typed channel (AnalogVoltageChannel, AnalogCurrentChannel, AnalogThermocoupleChannel, or a DigitalLineChannel) and delegates to a dedicated driver method:
Driver support. NI-DAQmx implements every typed method above. LabJack T-Series implements the typed voltage, thermocouple, and digital methods; current input/output is not supported by LabJack hardware, so the driver points to the measuring current app note.
LabJack thermocouple channels
The LabJack driver registers a thermocouple’sAIN# in the scan list as raw volts and converts each sample to temperature on read with LJM’s TCVoltsToTemp and the devices internal CJC. More info about using thermocouples with a LabJack device can be found here.
cjc_source:
INTERNAL(default): the driver reads the device’s cold-junction sensor alongside the thermocouple. The T8 streams the per-channelTEMPERATURE#sensor, the T7 streams the internal sensor’s raw volts (AIN14), and the T4 snapshotsTEMPERATURE_DEVICE_Kbefore each software-timed read (theTEMPERATURE_DEVICE_Kregister cannot be streamed).CONSTANT: pass the reference junction temperature viacjc_temp, expressed in the channel’sunit.CHANNEL: not yet supported by this driver.
The T4 snapshots CJC once when the stream starts, so its thermocouple readings can drift over a long hardware-timed acquisition.
tc_input_scaler when configuring the channel. The T4’s 12-bit ADC cannot resolve a bare thermocouple, so LabJack recommends using an LJTick-InAmp: the T4 defaults tc_input_scaler to the InAmp’s x51 gain and 1.25 V offset jumpers.
unit is required and is a TC_UNIT member. It governs cjc_temp, range_min/range_max, and the returned readings. A channel scaler applies to the converted temperatures.
Hardware-Timed Sample Rate
For continuous hardware-timed acquisition, configure the sample rate withconfigure_hw_sample_rate(). It applies to every configured input channel.
samples_per_channel parameter determines how many samples, per channel, are returned on every call to read_analog().
- The lower the
samples_per_channel, the more responsive and lower latency your app will be, but may not be able to keep up with the sample rate. - The ratio of
sample_ratetosamples_per_channeldetermines how often data will be fetched from the DAQ buffer.- Example, if
sample_rateis 1000 andsamples_per_channelis 500, you’ll see 500 sample batches of 1000Hz data twice a second, for every channel.
- Example, if
- The default for
samples_per_channel, if left unset, is dynamically set to enable fetching batches 10 times per second. This is a reasonable balance between reliably keeping up with the data stream and app responsiveness.
A single
InstroDAQ instance carries one analog input sample rate. To run multiple NI DAQmx tasks at different hardware sample rates, create one InstroDAQ instance per task: see the NI multi-rate acquisition example.Software-Timed Sample Rate
For continuous acquisition without a hardware sample clock, configure a software-timed polling rate instead. The background daemon then pacesread_analog() / read_digital() calls at 1 / sample_rate.
Analog Input
Software-Timed Acquisition (Manual Polling)
For manual, on-demand sampling:read_analog() method returns a Measurement object (or list of Measurement objects for multiple channels) containing:
channel_data: Dictionary mapping channel aliases to lists of valuestimestamps: List of timestamps (nanoseconds since epoch)values: Property returning all values as a list (convenience for single-channel reads)latest: Property returning the most recent value (convenience for single-channel reads)
Software-Timed Acquisition with the Background Daemon
For continuous software-timed acquisition, configure a polling rate andstart() the background daemon. The daemon calls read_analog() once per period and publishes the results.
read() and read_batch() serve analog and digital channels from the daemon’s channel buffer, so the same read code works whether or not the daemon is running. read_analog() and read_digital() raise instead: the daemon owns the hardware reads. Call get_channel() directly for more control over buffered reads (sample count, blocking for new samples, timeouts). See the software-timed example.
Hardware-Timed Acquisition with Background Fetching
See Two ways to get data for more information regarding background fetching of measurements. For continuous high-speed acquisition.start()begins hardware-timed acquisition in the background daemonstop()ends the background acquisition
Important Note about PublishersData is published as a direct result of an instrument method being called.For example, when you call
read_analog(), this not only returns the DAQ data but also causes all attached Publishers to publish the measurements automatically.Therefore the background daemon, which is calling instrument methods, is publishing the data!Hardware-Timed Acquisition with Manual Fetching
For hardware-timed acquisition where you control when to fetch buffered data, start without the background daemon and manually callread_analog().
start(background=False), calling read_analog() during hardware-timed acquisition fetches from the hardware buffer rather than triggering a new conversion.
Analog Output
Software-Timed Generation
For manual, on demand updates of set points:Digital I/O
Reading Digital Lines
Software Timed Generation
For manual, on-demand setting of analog output channels:Writing Digital Lines
Reading and Writing Digital Ports
For devices that expose digital I/O as parallel ports (multiple lines read or written together), useread_digital_port() and write_digital_port(). Configure the channel with a port_width that matches the hardware port, then read or write the full port as a single integer value.
Port-based I/O is implemented for MCC, NI DAQmx, and Keysight devices. On LabJack,
write_digital_port() and read_digital_port() raise NotImplementedError; use write_digital_line() / read_digital_line() to address individual lines instead. Keysight groups a single port into one channel of at most 32 bits, so DigitalPortWidth.WIDTH_64 is rejected — configure a 64-bit span as two channels. On NI DAQmx, port_width is checked against the port’s physical line count and a mismatch raises ValueError — pass the DigitalPortWidth that matches the hardware port.Unified Read and Write
Reading by Alias
read() accepts a single alias and returns that channel’s Measurement. read_batch() accepts a list of aliases, or None for every configured input channel, and returns a dict keyed by alias, with each value the channel’s Measurement. Analog aliases are served from one batched analog read; digital aliases are read per line or port. While the background daemon is running, both analog and digital aliases are served from the daemon’s channel buffer (latest sample) instead of triggering a hardware read; call get_channel() directly when you need more control (sample count, blocking for new samples, timeouts).
Writing by Alias
write() writes a single value to a single alias and returns the resulting Command. write_batch() writes values[i] to channels[i] and returns a list of Command objects. Both route each alias to analog or digital output by channel type.
By default, the first write that fails at the device logs a warning and raises RuntimeError naming the failed channel; later channels in the batch are not written. Pass continue_on_failed_write=True to log the failure and continue with the remaining channels instead; the returned list then contains only the successful commands. Every write emits a per-channel log line: <alias> -> succeeded at debug level, or <alias> -> failed: <reason> at warning level.
float, int, or bool, and every channel and value is validated before anything is written: each alias must be configured as an output channel (unknown aliases raise KeyError), aliases may appear only once per batch, analog outputs require a finite number within the channel’s configured range_min/range_max, digital lines require 0 or 1 (in any of the three forms), and digital ports require an integer. An invalid value, a duplicate alias, or a channels/values length mismatch raises ValueError with no channels written. Digital values are coerced to int. Relays are not routed through write(); use open_relay() and close_relay().
Batched Digital Input
read_digital() mirrors read_analog(): it reads every configured digital input channel in one driver call, dispatching a software-timed sample or a hardware-timed buffer fetch based on the configured timing mode. Lines and ports are both covered — a line channel carries 0/1, a port channel carries its whole N-bit integer:
read_digital() returns one Measurement per timebase cluster. There is no batched digital write; use write_digital_line() and write_digital_port().
No packaged driver implements the
read_digital / fetch_digital driver methods yet, and read_batch() still reads digital aliases per line or port.Complete Examples
Example 1: Multi-Channel Software-Timed Acquisition
Example 2: Hardware-Timed Continuous Acquisition
Published channels
Every measurement/command call produces a channel keyed under{name}.{descriptor}, where {name} is the constructor argument and {descriptor} is the row below. For analog, digital, and relay operations, {alias} is the alias you assigned with configure_*_channel(alias=...) (or the physical channel name if no alias was provided). If you depended on the pre-v1.0 form for digital channels (bare {alias} with no {name}. prefix and no .cmd suffix), pass legacy_naming=True.
The base
Instrument background daemon additionally publishes per-iteration diagnostic telemetry on loop_time and daemon_work_time. Those descriptors are shared across all instruments and are not affected by legacy_naming.
Method Reference
Vendor Independence
The power of InstroDAQ is that the same code works across different vendors. To switch vendors, just swap the driver instance:Driver Development
This section is for developers implementing InstroDAQ support for DAQ devices / vendors that are not supported out of the box.Overview
Driver developers implement theDAQDriverBase abstract interface to add support for new DAQ vendors. The driver is responsible for translating InstroDAQ’s vendor-independent API calls into vendor-specific hardware operations, ensuring users get consistent behavior regardless of the underlying hardware (within its capabilities).
Driver Responsibilities
A DAQ driver shall:- Hardware connection lifecycle: Implement
open()andclose()for establishing and terminating hardware connections. - Channel and state tracking: The driver is the single source of truth for what’s configured.
DAQDriverBase.__init__initializes the private dicts (_ai_channels,_ao_channels,_di_channels,_do_channels,_relay_channels) and the private timing-config slots (_ai_hw_timing_config, plus AO/DI/DO slots); concrete drivers callsuper().__init__(), then populate those privates inside theirconfigure_*methods and read from them inread_analog,fetch_analog,start,write_analog_value, etc.InstroDAQexposes the same state to end users via read-only@propertyaccessors that hand back frozen snapshots (daq.ai_channels,daq.ai_hw_timing_config, …) — no duplication. See Driver-owned state below. - Channel configuration: Translate generic channel configurations into vendor-specific setup.
- Timing abstraction: Implement both software-timed reads and hardware-timed buffered acquisition.
InstroDAQmanages the background daemon for both timing modes; software-timed pacing never reaches the driver. - Data format conversion: Convert vendor-specific data formats to
Measurementobjects. - Constraint validation: Handle vendor-specific hardware constraints and raise clear exceptions when the vendor library cannot.
DAQDriverBase Interface
All drivers must implement theDAQDriverBase abstract base class. Key methods:
Connection Management
open(): Establish connection to the hardware deviceclose(): Disconnect from the hardware device
Channel Configuration
Each of the methods below must (a) program the device and (b) record the resulting channel on the driver’s own private dict (self._ai_channels, self._di_channels, etc.) so later calls (read_analog, start, etc.) can find it. See Driver-owned state for the full rationale.
configure_ai_channel(channel): Configure an analog input channel on the hardware; record onself._ai_channels[channel.alias].configure_ao_channel(channel): Configure an analog output channel; record onself._ao_channels[channel.alias]. Default raisesNotImplementedErrorfor drivers that don’t support AO.configure_di_line_channel(physical_channel, logic, ...): Parse, program, and register a digital input line. Record onself._di_channels[channel.alias].configure_do_line_channel(physical_channel, logic, ...): Parse, program, and register a digital output line. Record onself._do_channels[channel.alias].configure_di_port_channel(physical_channel, logic, port_width, ...): Parse, program, and register a digital input port. Record onself._di_channels[channel.alias]. Default raisesNotImplementedErrorfor line-only devices.configure_do_port_channel(physical_channel, logic, port_width, ...): Parse, program, and register a digital output port. Record onself._do_channels[channel.alias]. Default raisesNotImplementedErrorfor line-only devices.
InstroDAQ’s typed channel configuration. Of the packaged drivers, only NI-DAQmx and LabJack T-Series implement them.
configure_ai_voltage_channel(channel)/configure_ao_voltage_channel(channel): Configure a typed voltage input/output channel from anAnalogVoltageChannel; record onself._ai_channels/self._ao_channels.configure_ai_current_channel(channel)/configure_ao_current_channel(channel): Configure a typed current input/output channel from anAnalogCurrentChannel; record onself._ai_channels/self._ao_channels.configure_ai_thermocouple_channel(channel): Configure a thermocouple input channel from anAnalogThermocoupleChannel(thermocouple type plus cold-junction config); record onself._ai_channels.
Timing Configuration
configure_ai_hw_timing(hw_timing_config): Program hardware-timed sampling on the device, then record onself._ai_hw_timing_configsostart/fetch_analogcan read it back.
Acquisition Control
-
start(): Begin hardware-timed data acquisition- Start hardware sampling and buffer filling
-
stop(): End hardware-timed acquisition- Stop sampling and flush buffers
Data Operations
-
read_analog() -> Any: Perform software-timed analog read- Trigger immediate conversion and return data
-
fetch_analog() -> Any: Fetch samples from hardware-timed acquisition buffer- Used during hardware-timed acquisition to retrieve buffered samples; reads timing from
self.ai_hw_timing_config
- Used during hardware-timed acquisition to retrieve buffered samples; reads timing from
-
read_digital()/fetch_digital(): Sample every configured DI channel in one call, lines and ports alike -
read_digital_line(channel) -> int: Read a digital input line -
write_digital_line(channel, data): Write to a digital output line -
read_digital_port(channel) -> int: Read a digital input port -
write_digital_port(channel, data): Write to a digital output port- Port read/write are required of every driver; LabJack T-Series raises
NotImplementedError
- Port read/write are required of every driver; LabJack T-Series raises
Data Conversion
_read_to_measurements(response, channel_list, daq_name, default_tags, **kwargs) -> list[Measurement]: Convert vendor data format to InstroDAQMeasurementobjects- Maps vendor data to channel names
- Creates
Measurementobjects for publishing
Properties
points_in_buffer(int): Number of samples currently in the hardware buffer
Implementation Considerations
Resource Handling
Each concrete driver owns its own transport and accepts the connection parameters it needs at construction:-
Vendor SDK drivers (NI-DAQmx, LabJack LJM, MCC): Accept a
device_idstring (device name, serial number, or IP address) and use the vendor library directly. -
SCPI/VISA drivers (e.g., Keysight 34980A): Accept a VISA resource string or a
VisaConfigand compose aVisaDriverinternally for all SCPI command/query operations.
Driver-owned state
The driver is the single source of truth for every channel and every timing config that has been configured. It holds that state in private dicts/slots that only theconfigure_* path mutates. InstroDAQ does not hold its own copies — daq.ai_channels, daq.ai_hw_timing_config, and friends are read-only @property accessors that delegate straight to the driver’s read-only accessors, which hand back frozen snapshots captured at call time. There is no back-channel into InstroDAQ, and the driver does not import InstroDAQ.
To keep every driver consistent, DAQDriverBase.__init__ initializes the private dicts and slots; concrete drivers call super().__init__() once and then populate them inside their configure_* methods.
State on every driver
DAQDriverBase.__init__ initializes the private storage below — populate it inside the matching configure_* method, and read from it (via the private attribute) anywhere the driver needs to know what’s configured. Each private dict has a matching read-only @property (ai_channels, ao_channels, …) that returns a frozen snapshot for external consumers; drivers use the private form internally.
Add vendor-specific state alongside these as needed (NI keeps an
nidaqmx.Task per ChannelType; MCC caches a ULRange per channel; LabJack holds its LJM handle — all live on the driver instance, declared in the driver’s own __init__ after super().__init__()).
Pattern
self._<dict>[channel.alias] = channel pattern applies to configure_ao_channel and the four digital configure methods (configure_di_line_channel, configure_do_line_channel, configure_di_port_channel, configure_do_port_channel). DAQDriverBase.define_relay_channel’s default builds a RelayChannel and records it on self._relay_channels already — only override if your hardware needs different parsing, and ensure your override records too.
How InstroDAQ exposes this
daq.ai_channels as a mapping, but it is a frozen snapshot: the returned MappingProxyType rejects writes, the channels inside are frozen dataclasses, and the snapshot does not change when later configure_* calls run — read the property again to see new state. The only sanctioned way to change configuration is the configure_* / define_* path, which programs the device and then records the channel. Reaching the driver via daq.driver exposes the same read-only accessors, not the private dicts.
Example: Vendor differences in timing
Different vendors configure timing differently, but InstroDAQ users must always configure a timing mode beforestart().
-
NI-DAQmx: Sample rate is configured on the device prior to starting an acquisition. The driver implements this in
configure_ai_hw_timing()by calling DAQmx’stask.timing.cfg_samp_clk_timing()and then stores the config onself._ai_hw_timing_config. -
LabJack LJM: Sample rate is configured when
ljm.eStreamStart()is called. The driver records theHWTimingConfiginconfigure_ai_hw_timing()and reads it back fromself._ai_hw_timing_configinsidestart().
InstroDAQ level.
Summary
Driver development requires careful abstraction of vendor-specific behaviors to provide the unified InstroDAQ interface. Focus on:- Implementing all
DAQDriverBaseabstract methods - Abstracting timing configuration differences between vendors
- Validating hardware constraints and providing clear error messages
- Converting vendor data formats to
Measurementobjects correctly - Managing state in a thread-safe manner for background acquisition