Hardware Integration: NI DAQmx
NI DAQmx Overview
The autocore-ni module integrates National Instruments DAQmx data acquisition hardware with AutoCore. It runs as an external module process, connects to autocore-server via IPC, and provides:
- Live streaming via shared memory (one segment per task, updated every callback)
- Triggered capture via shared memory (one segment per DAQ config, written on trigger)
- Scalar statistics via Global Memory variables (value, min, max, rate per channel)
- Console commands for building and managing configurations interactively
The module supports any mix of NI DAQmx channel types: analog voltage, strain gage, accelerometer, force sensor, linear/angular encoder, frequency, and edge counting.
Initial Setup
Before using the NI module, register its executable in config.ini:
[modules]
ni = /opt/autocore/bin/modules/autocore-ni
Then start the module from the console (no project.json entry needed yet):
ni> system.load_module --name ni
The module is now running and addressable. You can configure it interactively, then save to project.json so it starts automatically on future server restarts.
Registering Hardware on Linux
On Windows, NI MAX automatically discovers and registers network cDAQ chassis. On Linux there is no NI MAX, so devices must be registered manually by importing a .ini configuration file via nidaqmxconfig. The NI module can generate this file for you.
Step 1: Discover chassis on the network
The chassis must be powered on and network-reachable. Run:
ni> discover_devices
This calls nilsdev --verbose and returns a JSON array with each chassis’s name, product type, serial number, IP address, MAC address, and hostname. Make note of the device name — you will need it in the next step.
Step 2: Add the device to your configuration
Use add_device with the chassis name from step 1 and a modules array describing which NI modules are installed in each slot:
ni> add_device --name cdaq-3814 --modules '[{"product_type":"NI 9218","slot_num":1},{"product_type":"NI 9401","slot_num":2}]'
Each module entry requires product_type (the NI model, e.g. “NI 9218”) and slot_num (1-based physical slot). You can also provide an optional name field — if omitted, modules are named <device_name>-<slot_num> (e.g. cdaq-3814-1).
By default, reserve is set to true, which gives autocore-ni exclusive access to the chassis. Set --reserve false if you need to share the device.
Step 3: Generate the .ini and import it
ni> generate_device_config --import true
This combines the device configuration from step 2 with the runtime information from nilsdev to produce a .ini file, then imports it via nidaqmxconfig --import --replace. The generated file is saved alongside your project.json as ni_devices.ini.
To generate without importing (for review):
ni> generate_device_config
Step 4: Save and verify
ni> save_config
Verify the device is registered:
nilsdev --verbose
You should see your chassis and modules listed. The device is now ready for DAQmx tasks.
Complete example: register a cDAQ-9181 with two modules
# Start the module
ni> system.load_module --name ni
# See what's on the network
ni> discover_devices
# Register the chassis with a force sensor module (slot 1) and digital I/O (slot 2)
ni> add_device --name cdaq-3814 --modules '[{"product_type":"NI 9218","slot_num":1},{"product_type":"NI 9401","slot_num":2,"name":"digital-io"}]'
# Generate and import
ni> generate_device_config --import true
# Now configure tasks and channels as usual
ni> new_project --task_name AnalogInput
ni> add_channel --task AnalogInput --name load --physical_channel cdaq-3814Mod1/ai0 --type voltage --min_val -5 --max_val 5
ni> save_config
ni> start
The devices array is persisted in project.json alongside tasks and DAQ configs:
{
"modules": {
"ni": {
"config": {
"devices": [
{
"name": "cdaq-3814",
"reserve": true,
"modules": [
{ "product_type": "NI 9218", "slot_num": 1 },
{ "product_type": "NI 9401", "slot_num": 2, "name": "digital-io" }
]
}
],
"tasks": [ ... ],
"daq": [ ... ]
}
}
}
}
Configuring a Project from the Console
The fastest way to set up a new NI configuration is through console commands. This example creates a project with one analog voltage channel:
ni> new_project --task_name AnalogInput
ni> add_channel --task AnalogInput --name load --physical_channel Dev1/ai0 --type voltage --min_val -5 --max_val 5
ni> save_config
ni> start
Step by step:
new_projectcreates a minimal config with one empty task (1 kHz, 1000 sample buffer)add_channeladds a channel using thevoltagepreset. The--typeflag selects sensible defaults;--min_valand--max_valoverride specific parameters.save_configwrites the config into the currently loadedproject.json. On future server restarts, the module loads this config automatically.startbegins hardware acquisition.
To modify the task parameters (sample rate, buffer size, etc.), use add_task with explicit values:
ni> new_project
ni> remove_task --name Task
ni> add_task --name AnalogInput --sample_rate 100000 --samples_per_channel 10000 --samples_per_event 1000
ni> add_channel --task AnalogInput --name load --physical_channel cDAQ1Mod1/ai0 --type force_bridge --min_val -2224 --max_val 2224 --voltage_excit_val 3.3
ni> save_config
ni> restart
Channel Type Presets
The --type flag on add_channel maps to a DAQmx channel creation function with sensible defaults. Use ni.list_channel_types to see all available presets.
| Type | DAQmx Function | Description |
|---|---|---|
voltage | CreateAIVoltageChan | Analog voltage input |
strain_gage | CreateAIStrainGageChan | Strain gage (quarter/half/full bridge) |
accelerometer | CreateAIAccelChan | IEPE accelerometer |
force_bridge | CreateAIForceBridgeTwoPointLinChan | Force sensor via bridge two-point linear calibration |
force_iepe | CreateAIForceIEPEChan | IEPE force sensor (e.g. PCB impact hammer) |
linear_encoder | CreateCILinEncoderChan | Quadrature linear encoder (one per task) |
angular_encoder | CreateCIAngEncoderChan | Quadrature angular/rotary encoder (one per task) |
count_edges | CreateCICountEdgesChan | Counter edge counting |
frequency | CreateCIFreqChan | Frequency measurement |
Any additional --key value flags are passed through as parameter overrides on top of the preset defaults. For example, --type voltage --min_val -10 --max_val 10 uses the voltage preset but changes the input range.
For advanced use, you can bypass presets entirely with --create_function and --create_args:
ni> add_channel --task AI --name ch0 --physical_channel Dev1/ai0 --create_function CreateAIVoltageChan --create_args '{"terminal_config":10106,"min_val":-5,"max_val":5}'
Multi-Task Configurations (Encoders)
DAQmx counter input channels (encoders, frequency, edge counting) each require their own task. To synchronize them with an analog input task, set clock_type to external and parent_task to the master task name:
ni> new_project --task_name AnalogInput
ni> remove_task --name AnalogInput
ni> add_task --name AnalogInput --sample_rate 100000 --samples_per_channel 10000 --samples_per_event 1000
ni> add_channel --task AnalogInput --name load --physical_channel cDAQ1Mod1/ai0 --type force_bridge --min_val -2224 --max_val 2224
ni> add_task --name Encoder1 --sample_rate 100000 --samples_per_channel 10000 --samples_per_event 1000 --clock_type external --parent_task AnalogInput
ni> add_channel --task Encoder1 --name pos_x --physical_channel cDAQ1Mod2/ctr0 --type linear_encoder --dist_per_pulse 0.000004
ni> add_task --name Encoder2 --sample_rate 100000 --samples_per_channel 10000 --samples_per_event 1000 --clock_type external --parent_task AnalogInput
ni> add_channel --task Encoder2 --name pos_y --physical_channel cDAQ1Mod2/ctr1 --type linear_encoder --dist_per_pulse 0.000004
ni> add_task --name Encoder3 --sample_rate 100000 --samples_per_channel 10000 --samples_per_event 1000 --clock_type external --parent_task AnalogInput
ni> add_channel --task Encoder3 --name angle --physical_channel cDAQ1Mod2/ctr2 --type angular_encoder --pulses_per_rev 2048
ni> save_config
ni> restart
When parent_task is set, the module automatically synchronizes the clocks: it copies the master’s timebase, sets the arm start trigger, and starts slave tasks before the master. All channels across all synchronized tasks are sample-aligned.
Adding Triggered Capture (DAQ)
A DAQ configuration captures a fixed window of data from one or more channels when a trigger condition is met. Channels can span multiple tasks (as long as they share a clock via parent_task).
ni> add_daq --name impact --capture_length 10000 --pre_trigger_samples 100 --channels '["load","pos_x"]' --trigger '{"type":"rising_edge","source_channels":["load"],"level":50.0,"hysteresis":2.0}'
ni> save_config
ni> restart
The source_channels field accepts an array of one or more channel names. When multiple channels are specified, their sample values are summed before the trigger condition is evaluated. This is useful for force plates and multi-sensor setups where the trigger should fire on the combined signal regardless of which sensor sees the event:
ni> add_daq --name impact --capture_length 10000 --pre_trigger_samples 100 \
--channels '["s1","s2","s3","s4","pos_x"]' \
--trigger '{"type":"rising_edge","source_channels":["s1","s2","s3","s4"],"level":50.0,"hysteresis":2.0}'
All source channels must be in the same task.
To arm the trigger at runtime:
ni> impact.arm
When the trigger fires, the module writes the capture data to shared memory and sets the impact_data_ready GM variable.
Available trigger types: rising_edge, falling_edge, rising_window, falling_window.
Full Example: Load Cell + Three Encoders
This example configures a complete impact test system with a force sensor and three position encoders, all synchronized at 100 kHz:
ni> new_project
ni> remove_task --name Task
# Master task: analog input with load cell
ni> add_task --name AnalogInput --sample_rate 100000 --samples_per_channel 10000 --samples_per_event 1000
ni> add_channel --task AnalogInput --name load --physical_channel cDAQ1Mod1/ai0 --type force_bridge --min_val -2224 --max_val 2224 --voltage_excit_val 3.3
# Three encoder tasks, each synced to AnalogInput
ni> add_task --name EncX --sample_rate 100000 --samples_per_channel 10000 --samples_per_event 1000 --clock_type external --parent_task AnalogInput
ni> add_channel --task EncX --name pos_x --physical_channel cDAQ1Mod2/ctr0 --type linear_encoder --dist_per_pulse 0.000004
ni> add_task --name EncY --sample_rate 100000 --samples_per_channel 10000 --samples_per_event 1000 --clock_type external --parent_task AnalogInput
ni> add_channel --task EncY --name pos_y --physical_channel cDAQ1Mod2/ctr1 --type linear_encoder --dist_per_pulse 0.000004
ni> add_task --name EncZ --sample_rate 100000 --samples_per_channel 10000 --samples_per_event 1000 --clock_type external --parent_task AnalogInput
ni> add_channel --task EncZ --name pos_z --physical_channel cDAQ1Mod2/ctr2 --type linear_encoder --dist_per_pulse 0.000004
# Triggered capture across tasks
ni> add_daq --name impact --capture_length 10000 --pre_trigger_samples 100 --channels '["load","pos_x","pos_y","pos_z"]' --trigger '{"type":"rising_edge","source_channels":["load"],"level":50.0,"hysteresis":2.0}'
# Save and start
ni> save_config
ni> restart
# Check status
ni> status
# Arm the trigger
ni> impact.arm
After save_config, the full configuration is persisted in project.json. On future server restarts, the module starts automatically and begins acquisition (set auto_start: true in the config for production).
Modifying Configuration In-Place
You don’t need to remove and re-add a task or channel to change a single field. Use set_task and set_channel to update fields directly, and reset_task_field / reset_channel_field to revert them to defaults.
Updating task fields
Any task field except name can be updated:
# Make Encoder1 a slave of AnalogInput
ni> set_task --name Encoder1 --parent_task AnalogInput --clock_type external
# Change the sample rate on the master
ni> set_task --name AnalogInput --sample_rate 50000 --samples_per_event 500
# Apply changes
ni> restart
Updating channel fields
Top-level channel fields (physical_channel, create_function, value_aggregation, compute_rate, create_args) are set by name. Any unrecognized key is treated as a create_args sub-field, so you can set hardware parameters directly:
# Change the input range on a voltage channel
ni> set_channel --task AnalogInput --name load --min_val -500 --max_val 500
# Change the encoder resolution
ni> set_channel --task Encoder1 --name pos_x --dist_per_pulse 0.000002
# Switch aggregation mode
ni> set_channel --task AnalogInput --name load --value_aggregation rms
Resetting fields to defaults
Use reset_task_field and reset_channel_field to revert a field. For tasks, resettable fields are timeout_ms, phase_offset_samples, clock_type, clock_source, and parent_task. For channels, resettable fields are value_aggregation, compute_rate, and create_args (or any create_args sub-field by name):
# Remove clock sync — make this an independent task again
ni> reset_task_field --name Encoder1 --field parent_task
ni> reset_task_field --name Encoder1 --field clock_type
# Remove a create_args override so the preset default applies
ni> reset_channel_field --task Encoder1 --name pos_x --field z_index_enable
# Reset aggregation back to auto-inferred default
ni> reset_channel_field --task AnalogInput --name load --field value_aggregation
Inspecting configuration
# Full config as JSON
ni> get_config
# List all tasks with channels and actual timing
ni> list_tasks
# Detailed view of one task (includes create_args for each channel)
ni> task --name AnalogInput
When the worker is running, list_tasks and task include actual_sample_rate and time_increment fields read back from the hardware. These values are also written to shared memory as <task>_actual_sample_rate (f64) and <task>_time_increment (f64) so control programs can read them.
NI Module Command Reference
All commands use the ni. topic prefix. Use ni.help for a summary or ni.help --command <name> for detailed argument information.
Acquisition Control
| Command | Arguments | Description |
|---|---|---|
start | – | Start DAQmx acquisition |
stop | – | Stop DAQmx acquisition |
restart | – | Stop and restart (applies config changes) |
status | – | Show module status, channel values, and errors |
Configuration Inspection
| Command | Arguments | Description |
|---|---|---|
get_config | – | Show current in-memory configuration as JSON |
show_config | – | Alias for get_config |
describe | – | Human-readable summary of tasks, channels, DAQ, and timing |
list_tasks | – | List all tasks with channels and actual timing info (JSON) |
task | --name (required) | Show full configuration for a specific task (JSON) |
Project Lifecycle
| Command | Arguments | Description |
|---|---|---|
new_project | --task_name (optional, default: “Task”) | Create a minimal starter configuration |
save_config | --path (optional), --generate_variables (optional, bool) | Save config to project.json. --generate_variables true also writes variable declarations. |
generate_variables | --path (optional) | Generate project.json variable declarations for all NI channels |
Task Management
| Command | Arguments | Description |
|---|---|---|
add_task | --name, --sample_rate, --samples_per_channel, --samples_per_event (all required); --timeout_ms (default: 2500), --clock_type (default: “internal”), --clock_source, --parent_task | Add a new DAQmx task |
set_task | --name (required), plus any field --key value pairs | Update fields on an existing task |
remove_task | --name (required) | Remove a task by name |
reset_task_field | --name (required), --field (required) | Reset a task field to its default value |
duplicate_task | --name (required), --new_name (required) | Clone a task with a new name (channels not copied) |
rename_task | --name (required), --new_name (required) | Rename a task (updates parent_task references) |
calc_timing | --name (required), --callback_hz (default: 100), --buffer_seconds (default: 1.0), --apply (default: true) | Calculate and set samples_per_event and samples_per_channel from target callback rate |
Channel Management
| Command | Arguments | Description |
|---|---|---|
add_channel | --task, --name, --physical_channel (all required); --type or --create_function (one required). Extra --key value flags override preset defaults. | Add a channel to a task |
set_channel | --task, --name (required), plus any --key value pairs. Unrecognized keys become create_args sub-fields. | Update fields on an existing channel |
remove_channel | --task (required), --name (required) | Remove a channel from a task |
reset_channel_field | --task, --name, --field (all required) | Reset a channel field to default, or remove a create_args sub-field |
rename_channel | --task, --name, --new_name (all required) | Rename a channel (updates DAQ channel lists and trigger references) |
list_channel_types | – | List available channel type presets with default parameters |
DAQ (Triggered Capture) Management
| Command | Arguments | Description |
|---|---|---|
add_daq | --name, --capture_length, --channels (JSON array), --trigger (JSON object) (all required); --pre_trigger_samples (default: 0) | Add a triggered capture configuration |
remove_daq | --name (required) | Remove a DAQ configuration |
Device Management (Linux)
| Command | Arguments | Description |
|---|---|---|
discover_devices | – | Run nilsdev --verbose and return discovered chassis info |
add_device | --name (required), --modules (JSON array), --reserve (default: true) | Add a cDAQ chassis device configuration |
remove_device | --name (required) | Remove a device configuration |
set_device | --name (required), plus --key value pairs | Update device fields (reserve, modules) |
generate_device_config | --path (optional), --import (default: false) | Generate .ini from config + nilsdev and optionally import |
Runtime Monitoring
| Command | Arguments | Description |
|---|---|---|
reset_minmax | --channel (optional, omit for all) | Reset min/max tracking |
list_devices | – | List connected NI DAQmx devices |
clear_errors | – | Clear the error log |
<channel> | – | Read all fields for a channel (value, min, max, rate, data_received) |
<channel>.value | – | Read only the current value (scalar f64) |
<channel>.min | – | Read only the running minimum |
<channel>.max | – | Read only the running maximum |
<channel>.rate | – | Read only the rate |
<daq>.arm | – | Arm a DAQ trigger |
<daq>.disarm | – | Disarm a DAQ trigger |
Individual channel sub-fields (ni.ai0.value, ni.ai0.rate, etc.) return scalar values and can be used as link targets in project.json variable declarations.
Help and Discovery
| Command | Arguments | Description |
|---|---|---|
help | --command (optional) | Show command list, or detailed help for one command |
get_catalog | --detailed (optional, bool) | List all available endpoints |