Safety¶
DAQ outputs can drive heaters, valves, igniters, regulators, and other
real-world actuators. nidaqlib treats them with the same seriousness as
Alicat setpoints or Sartorius state-changing operations: writes are
gated, validated, and never silently coerced. See design doc §17.
Tiers¶
nidaqlib recognises four operational tiers. Each sits behind a
different gate and a different test marker:
| Tier | What it covers | How to opt in | Test marker |
|---|---|---|---|
| Read-only | read_block, poll, record, record_polled against AI / DI channels. |
Default. | hardware |
| Stateful | Task-state changes (start/stop, configure new task) without writing. | Default — once you have hardware. | hardware_stateful |
| Output | AO / DO writes and counter-output starts — anything that leaves the device pin energised. | Per-call confirm=True plus safe_min / safe_max clamp where applicable. |
hardware_output |
| Destructive | Calibration, factory ops, anything that can permanently alter the device. | Not implemented in v0.2; reserved. | hardware_destructive |
How the gate works¶
DaqSession.write(values, *, confirm=False) performs three checks in
order, before any I/O:
- Shape check. Keys of
valuesmust exactly match the display names of the task's output channels. Unknown or missing keys raiseNIDaqValidationError. - Safe-range check. For each
AnalogOutputVoltagechannel withsafe_min/safe_maxset, the provided value must lie inside the resolved clamp window (safe_minfalls back tomin_val,safe_maxtomax_val). Out-of-range values raiseNIDaqValidationError. The library never silently clamps. - Confirmation check. If any target channel has
requires_confirm=True, the call raisesNIDaqValidationErrorunlessconfirm=Trueis passed explicitly.
Only after all three checks pass does the call dispatch to the backend.
Counter-output pulse trains (CounterPulseFrequency, CounterPulseTime,
CounterPulseTicks) actuate on task start, not through write(). For
those tasks, pass confirm_start=True to open_device(...) or call
session.start(confirm=True) when restarting an already-open session.
Defaults are conservative¶
| Channel | requires_confirm default |
|---|---|
AnalogOutputVoltage |
True |
DigitalOutput |
True |
CounterPulseFrequency / CounterPulseTime / CounterPulseTicks |
True |
Override per channel only when you have a reason — for example, a non-actuating digital indicator line:
from nidaqlib import DigitalOutput
DigitalOutput(
physical_channel="Dev1/port0/line7",
name="status_led",
requires_confirm=False,
)
Safe-range example¶
from nidaqlib import AnalogOutputVoltage, TaskSpec, open_device
spec = TaskSpec(
name="heater",
channels=[
AnalogOutputVoltage(
physical_channel="Dev1/ao0",
name="heater_command",
min_val=0.0,
max_val=10.0,
safe_min=0.0,
safe_max=5.0, # never command above 5 V
requires_confirm=True,
),
],
)
async with await open_device(spec) as session:
await session.write({"heater_command": 4.5}, confirm=True)
# await session.write({"heater_command": 7.0}, confirm=True)
# ↑ raises NIDaqValidationError — outside [0.0, 5.0]
What the gate does NOT cover¶
- The escape hatch.
session.raw_taskreturns the underlyingnidaqmx.Taskand intentionally bypasses the gate. If you reach for it, you own the safety story end-to-end. Document why. - Setpoints applied outside
nidaqlib. A SCADA system, MAX, another process — these can all drive the same physical lines. The gate is a code-level check on this library's call sites. - Hardware interlocks. Always pair output gating with a
hardware-level safety system (relay coil cut-off, fuse, watchdog).
requires_confirm=Trueis a software flag on a software API.
Recommended pattern¶
Confirm at the session level, once, near the top of a script — not sprinkled across the call graph. That makes the operator-intent audit-trail visible:
async with await open_device(spec) as session:
if not args.dry_run:
await session.write({"heater_command": 4.5}, confirm=True)
else:
log.info("dry-run: would have written %s", values)
See also¶
- design doc §17 — full safety model.
- docs/testing.md — running the
hardware_outputtest tier locally.