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 follow 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 hardware-timed acquisition (if using hardware timing)- Acquire data - Read/fetch measurements
stop()- End acquisition (if using hardware timing)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 two acquisition modes: Software-Timed (Manual Polling)- You call
read_analog()when you want a sample - No need to call
start()orstop() - Use case: Low-frequency monitoring, event-driven sampling
- Configure sample rate with
configure_ai_sample_rate() - Call
start()to begin background acquisition - Data automatically published via the background daemon, OR manually fetch with
read_analog() - Use case: Mid to high frequency continuous monitoring with deterministic sample timing.
Creating a 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.
Hardware-Timed Sample Rate
For continuous hardware-timed acquisition, configure the sample rate.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.
Analog Input
Software-Timed Acquisition
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)value: Property returning the first value (convenience for single-value reads)
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. Disable the Background Daemon and manually callread_analog().
background_enable = 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.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.
| Method | Descriptor | Type |
|---|---|---|
read_analog() (per channel) | {alias} | telemetry |
write_analog_value(channel) | {alias}.cmd | command |
read_digital_line(channel) / read_digital_port(channel) | {alias} | telemetry |
write_digital_line(channel, data) / write_digital_port(channel, data) | {alias}.cmd | command |
close_relay(channel) | {alias}.cmd (value "CLOSED") | command |
open_relay(channel) | {alias}.cmd (value "OPEN") | command |
get_points_in_buffer() | buffer | telemetry |
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
| Method | Purpose |
|---|---|
InstroDAQ(name, driver, publishers=None, legacy_naming=False, **kwargs) | Construct an InstroDAQ instance around a vendor driver |
open() | Establish connection to the DAQ device |
close() | Disconnect from device and close publishers |
configure_analog_channel(direction, physical_channel, alias, range_min, range_max) | Configure an analog I/O channel |
configure_digital_line(direction, physical_channel, logic, logic_level=None, alias=None) | Configure a single digital I/O line |
configure_digital_port(direction, physical_channel, logic, port_width, logic_level=None, alias=None) | Configure a digital I/O port (multi-line) |
configure_ai_sample_rate(sample_rate, samples_per_channel=None) | Set hardware timing for analog input |
start() | Begin hardware-timed acquisition (background daemon) |
stop() | End hardware-timed acquisition |
read_analog() | Read analog input (SW-timed) or fetch from buffer (HW-timed) |
read_digital_line(channel) | Read digital input line |
write_digital_line(channel, data) | Write to digital output line |
read_digital_port(channel) | Read all lines on a digital input port as a single integer |
write_digital_port(channel, data) | Write all lines on a digital output port as a single integer |
add_publisher(publisher) | Attach a publisher for data routing |
get_points_in_buffer() | Get number of samples in hardware buffer |
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.
InstroDAQwill manage the background daemon for the hardware-timed acquisition. - 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.
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_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.
| Private attribute | Read-only accessor | Type | Populated in | Read by (typical) |
|---|---|---|---|---|
_ai_channels | ai_channels | dict[str, AnalogChannel] | configure_ai_channel | read_analog, fetch_analog, start, … |
_ao_channels | ao_channels | dict[str, AnalogChannel] | configure_ao_channel | write_analog_value |
_di_channels | di_channels | dict[str, DigitalChannel] | configure_di_line_channel / configure_di_port_channel | digital-input read paths |
_do_channels | do_channels | dict[str, DigitalChannel] | configure_do_line_channel / configure_do_port_channel | digital-output write paths |
_relay_channels | relay_channels | dict[str, RelayChannel] | define_relay_channel (base default already records; overrides must too) | open_relay / close_relay |
_ai_hw_timing_config | ai_hw_timing_config | HWTimingConfig | None | configure_ai_hw_timing | start, fetch_analog |
_ao_hw_timing_config / _di_hw_timing_config / _do_hw_timing_config | ao/di/do_hw_timing_config | HWTimingConfig | None | (forward-compat; unused today) | — |
points_in_buffer | points_in_buffer | int | fetch_analog (per-call) | InstroDAQ.get_points_in_buffer |
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 callconfigure_ai_sample_rate() before start().
-
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