Task specifications¶
TaskSpec is the typed declaration of one subsystem configured for one
data-flow mode; TaskBuilder translates it into the ordered SDK setter
sequence. See the Task specs guide for the narrative.
Spec and timing models¶
dtollib.tasks.spec ¶
TaskSpec + supporting dataclasses — the configuration entry-point.
A single spec describes single-value, continuous, and finite tasks;
Timing / :class:BufferPlan / :class:RawLogging carry the
continuous-mode configuration consumed by :func:dtollib.streaming.record.
Design reference: docs/design.md §8.1 (TaskSpec), §8.7 (Timing), §8.7a (BufferPlan).
BufferPlan
dataclass
¶
BufferPlan(
*,
buffers=4,
samples_per_buffer=1000,
sample_width_bytes=None,
wrap_mode=WrapMode.MULTIPLE,
queue_strategy=QueueStrategy.REQUEUE,
)
SDK Ready / Inprocess / Done buffer plan for continuous tasks.
Consumed by the buffer pool behind :func:dtollib.streaming.record.
Required when TaskSpec.data_flow in {CONTINUOUS, FINITE,
*_PRETRIGGER, *_ABOUT_TRIGGER}; forbidden for SINGLE_VALUE.
Attributes:
| Name | Type | Description |
|---|---|---|
buffers |
int
|
Number of HBUFs in the Ready/Inprocess/Done cycle. Minimum 3; default 4 (matches QuickDAQ default). |
samples_per_buffer |
int
|
Samples per channel per HBUF. |
sample_width_bytes |
int | None
|
|
wrap_mode |
WrapMode
|
|
queue_strategy |
QueueStrategy
|
How completed HBUFs return to the Ready queue. |
__post_init__ ¶
Enforce the hard minimum-3 floor.
Source code in src/dtollib/tasks/spec.py
RawLogging
dataclass
¶
Driver-side raw-counts logging configuration.
Wired into :class:~dtollib.sinks.RawCountsSink.
Attributes:
| Name | Type | Description |
|---|---|---|
path |
Path
|
Output |
include_metadata |
bool
|
Embed task metadata in the file header. |
compression |
None
|
Compression algorithm (currently always |
RetriggerSpec
dataclass
¶
Triggered-scan retrigger specification.
Wired by the :class:~dtollib.tasks.TaskBuilder into
olDaSetTriggeredScanUsage + olDaSetMultiscanCount +
olDaSetRetriggerMode (+ olDaSetRetriggerFrequency for INTERNAL,
olDaSetRetrigger for EXTRA).
Attributes:
| Name | Type | Description |
|---|---|---|
mode |
RetriggerMode
|
Retrigger mode. Defaults to |
multiscan_count |
int
|
Channel-list scans collected per trigger (>= 1). |
frequency_hz |
float | None
|
Internal retrigger rate; required for
|
source |
TriggerSpec | None
|
Retrigger source trigger; required for
|
__post_init__ ¶
Validate mode-specific field requirements.
Source code in src/dtollib/tasks/spec.py
TaskSpec
dataclass
¶
TaskSpec(
*,
name,
channels,
board=None,
subsystem_type=None,
element=0,
data_flow=DataFlow.SINGLE_VALUE,
timing=None,
trigger=SoftwareStart(),
buffers=None,
logging=None,
stop_on_error=True,
metadata=_empty_metadata(),
)
One DT-Open Layers subsystem configured for one data-flow mode.
A TaskSpec is a typed declaration; the
:class:~dtollib.tasks.TaskBuilder translates it into the actual
SDK call sequence.
Attributes:
| Name | Type | Description |
|---|---|---|
name |
str
|
Human-readable task identifier. Propagated to error contexts, log lines, and sink rows. |
board |
str | None
|
DT-Open Layers board name (e.g. |
subsystem_type |
SubsystemType | None
|
Explicit override for the subsystem to bind.
Usually inferred from the channel kinds — see
:meth: |
element |
int
|
Subsystem element index on the board. |
channels |
Sequence[ChannelSpec]
|
Ordered channel specs. Empty is rejected. All channels must share a subsystem type — mixing voltage and DO in one task is a validation error. |
data_flow |
DataFlow
|
One of :class: |
timing |
Timing | None
|
Required for non- |
trigger |
TriggerSpec
|
Defaults to :class: |
buffers |
BufferPlan | None
|
Required for non- |
logging |
RawLogging | None
|
Optional driver-side raw-counts logging. |
stop_on_error |
bool
|
SDK-level |
metadata |
Mapping[str, str | int | float | bool]
|
Free-form task-level metadata. |
__post_init__ ¶
Validate the spec against the §8.1 / §4.2 matrix.
Source code in src/dtollib/tasks/spec.py
infer_subsystem_type ¶
Derive the subsystem kind from the channel kinds.
Returns:
| Type | Description |
|---|---|
SubsystemType
|
The implied :class: |
Raises:
| Type | Description |
|---|---|
DtolValidationError
|
Channels span multiple subsystem kinds or include an unrecognised kind. One HDASS is one subsystem of one type — mixing is illegal. |
Source code in src/dtollib/tasks/spec.py
Timing
dataclass
¶
Timing(
*,
rate_hz,
clock_source=ClockSource.INTERNAL,
external_divider=None,
retrigger=None,
samples_per_channel=None,
)
Sample-clock + retrigger configuration for a continuous task.
Required when TaskSpec.data_flow != SINGLE_VALUE; forbidden
otherwise (single-value reads ignore the timing engine).
Attributes:
| Name | Type | Description |
|---|---|---|
rate_hz |
float
|
Configured sample rate, in hertz. Driven via
|
clock_source |
ClockSource
|
Internal vs external clock selection. |
external_divider |
int | None
|
Required when |
retrigger |
RetriggerSpec | None
|
Optional triggered-scan retrigger specification.
|
samples_per_channel
class-attribute
instance-attribute
¶
Optional sample ceiling. Required for DataFlow.FINITE; the recorder
stops when the cumulative emitted sample count reaches this value.
Forbidden (None) for CONTINUOUS — that mode runs until stop().
__post_init__ ¶
Validate the external-divider / clock-source / samples-ceiling combos.
Source code in src/dtollib/tasks/spec.py
dtollib.tasks.models ¶
Public enums + payload dataclasses for task / channel / lifecycle modeling.
:class:DaqBlock is a full frozen dataclass, and :class:DaqSample +
:class:SdkEventKind back the continuous-bridge path.
Design reference: docs/design.md §8 (enums), §8.9 (DaqReading), §8.10 (DaqBlock), §8.11 (DaqSample), §12.3.2 (SdkEventKind).
BufferState ¶
Bases: StrEnum
Per-HBUF lifecycle — tracked on the internal RawBuffer.
Borrowed from OIBuffer.State. Promotes use-after-free into an
explicit error and lets the pool refuse free_all() while any buffer
is INPROCESS (the §12.3.2 shutdown invariant).
FILLED is the output-pool addition: an HBUF that has been written
with a waveform chunk (olDmCopyToBuffer) but not yet queued. It
encodes the Fill-before-Queue invariant — an output buffer must be
filled before put_buffer (see :class:~dtollib.backend._buffer_pool.BufferPool).
ClockSource ¶
Bases: StrEnum
Timing.clock_source discriminator.
CounterMode ¶
Bases: StrEnum
Counter/timer operation mode — maps to the SDK OL_CTMODE_* family.
Carried as a ClassVar on each counter channel spec so the
:class:~dtollib.tasks.TaskBuilder dispatches olDaSetCTMode without
branching on the concrete spec class.
DaqBlock
dataclass
¶
DaqBlock(
*,
device,
channels,
data,
task=None,
raw_codes=None,
cjc_data=None,
block_index,
first_sample_index,
samples_per_channel,
sample_rate_hz=None,
block_period_ns=None,
task_started_at,
t0,
t_mono_ns,
t_utc,
t_midpoint_mono_ns=None,
read_started_at,
read_finished_at,
elapsed_s,
units=_empty_block_units(),
is_linearised=False,
sensor_status=_empty_block_sensor_status(),
error=None,
)
One hardware-clocked acquisition buffer copied off the SDK Done queue.
Constructed by the §12.3.2 callback-bridge drainer thread after
olDaGetBuffer + olDmGetBufferPtr. The data array is a
drainer-owned copy (the SDK HBUF is recycled before the block leaves
the drainer); the array is marked read-only so downstream sinks cannot
mutate it in place.
Sample-time reconstruction: derive each sample's t_mono_ns from
block.t_mono_ns + k * block.block_period_ns and t_utc analogously.
Use first_sample_index + k as the absolute sample index across the
whole run. Do not interpolate off t0 — it carries scheduler jitter.
Attributes:
| Name | Type | Description |
|---|---|---|
device |
str
|
|
task |
str | None
|
Underlying |
channels |
tuple[str, ...]
|
Channel display names in array-row order; |
data |
NDArray[float64]
|
Converted samples, shape |
raw_codes |
NDArray[signedinteger[Any]] | None
|
Original SDK codes, shape matches |
cjc_data |
NDArray[float64] | None
|
CJC stream, shape matches |
block_index |
int
|
0-based monotonic per task. |
first_sample_index |
int
|
Cumulative offset since |
samples_per_channel |
int
|
|
sample_rate_hz |
float | None
|
Actual clock rate read back via
|
block_period_ns |
int | None
|
|
task_started_at |
datetime
|
Wall-clock anchor for sample-time reconstruction. |
t0 |
datetime
|
Wall clock at the first sample of THIS block. |
t_mono_ns |
int
|
Monotonic ns at callback receipt (drainer thread). |
t_utc |
datetime
|
Wall clock at the block midpoint — the timestamp consumers should plot against. |
t_midpoint_mono_ns |
int | None
|
Block-midpoint in monotonic ns. |
read_started_at |
datetime
|
Drainer-thread wall clock at |
read_finished_at |
datetime
|
Drainer-thread wall clock after copy + requeue. |
elapsed_s |
float
|
|
units |
Mapping[str, str | None]
|
Channel name → engineering unit ( |
is_linearised |
bool
|
|
sensor_status |
Mapping[str, NDArray[int8]]
|
Channel name → |
error |
DtolError | None
|
|
__getstate__ ¶
__post_init__ ¶
Validate array shapes and freeze mapping + ndarray mutability.
Source code in src/dtollib/tasks/models.py
__setstate__ ¶
DaqReading
dataclass
¶
DaqReading(
*,
device,
task=None,
values=_empty_values(),
units=_empty_units(),
requested_at,
received_at,
t_utc,
t_mono_ns,
t_midpoint_mono_ns=None,
latency_s,
sensor_status=_empty_sensor_status(),
metadata=_empty_metadata(),
error=None,
)
One scalar reading across the channels of a single-value task.
Field shape matches :class:nidaqlib.DaqReading /
:class:alicatlib.Sample / :class:sartoriuslib.Sample for
cross-instrument joinability: the canonical key is
(device, t_mono_ns).
Attributes:
| Name | Type | Description |
|---|---|---|
device |
str
|
|
task |
str | None
|
Underlying |
values |
Mapping[str, float | int | bool]
|
Channel name → scalar value mapping. TC sentinels
appear as |
units |
Mapping[str, str | None]
|
Channel name → engineering unit string ( |
requested_at |
datetime
|
Wall clock at the start of the poll call. |
received_at |
datetime
|
Wall clock at the end of the poll call. |
t_utc |
datetime
|
Wall-clock midpoint of the integration window. This is the timestamp downstream consumers should plot against. |
t_mono_ns |
int
|
Monotonic nanoseconds at the start of the poll call. Canonical join key with sibling-library samples. |
t_midpoint_mono_ns |
int | None
|
Optional monotonic-ns midpoint when the
backend can report it. This is |
latency_s |
float
|
|
sensor_status |
Mapping[str, SensorStatus]
|
Channel name → :class: |
metadata |
Mapping[str, str | int | float | bool]
|
Free-form key/value metadata propagated by the session and the channel specs. |
error |
DtolError | None
|
|
__getstate__ ¶
__post_init__ ¶
Freeze the mapping fields so callers cannot mutate them post-hoc.
Source code in src/dtollib/tasks/models.py
__setstate__ ¶
Restore slotted fields and re-wrap mappings via :meth:__post_init__.
to_dict ¶
JSON-friendly mapping suitable for the row-oriented sinks.
Source code in src/dtollib/tasks/models.py
DaqSample
dataclass
¶
DaqSample(
*,
device,
channel,
value,
sample_index,
block_index,
t_mono_ns,
t_utc,
task=None,
unit=None,
sensor_status=SensorStatus.OK,
is_linearised=False,
metadata=_empty_metadata(),
)
One (channel, sample) pair scalarised from a :class:DaqBlock.
Produced explicitly via :func:block_to_long_rows. Useful for row-oriented
sinks (CSV, JSONL, Postgres) that prefer one row per measurement over a
blob column. Carries the same join contract as :class:DaqReading so
long-form DAQ rows can be merged with sibling-library samples on
(device, t_mono_ns).
is_linearised is inherited from the source :class:DaqBlock: it is
True when value is in engineering units (volts / °C) and
False when it is a raw ADC code cast to float. Row-oriented sinks
persist it so the on-disk row is self-describing.
__getstate__ ¶
__post_init__ ¶
Freeze the metadata mapping.
__setstate__ ¶
to_dict ¶
JSON-friendly mapping suitable for row-oriented sinks.
Source code in src/dtollib/tasks/models.py
DataFlow ¶
Bases: StrEnum
SDK data-flow modes for a configured subsystem.
FINITE is implemented on top of CONTINUOUS + WrapMode.NONE
plus a sample ceiling — the SDK has no dedicated finite mode, but the
recorder stops when the cumulative sample count is reached.
The two *_PRETRIGGER / *_ABOUT_TRIGGER modes are flagged
"Legacy Devices" in the SDK manual and deferred past v0.1.
Edge ¶
Bases: StrEnum
Digital / threshold trigger slope.
GateType ¶
Bases: StrEnum
Counter gate-enable logic — maps to the SDK OL_GATE_* family.
IOType ¶
Bases: StrEnum
Channel measurement-kind discriminator from SupportedChannelInfo.IOType.
Carried on CapabilitySet.channel_caps[ch]["IOType"] so the wrapper
can reject "configure channel 3 as RTD" when that channel reports
VOLTAGE_IN only.
MULTI_SENSOR is the DT9805 case: one physical channel that the SDK
re-types at configure time based on what's wired to it.
PulseType ¶
Bases: StrEnum
Pulse-output polarity — maps to the SDK OL_PULSETYPE_* family.
QuadratureDecodeMode ¶
Bases: StrEnum
Quadrature decoder count multiplier (counts per encoder line).
QueueStrategy ¶
Bases: StrEnum
How completed HBUFs are returned to the Ready queue.
RetriggerMode ¶
Bases: StrEnum
RetriggerSpec.mode — triggered-scan acquisition mode.
SdkEventKind ¶
Bases: StrEnum
SDK notification-procedure message kinds delivered to the callback bridge.
Eleven distinct event types arrive on the same olDaSetNotificationProcedure
callback; the drainer dispatches on the kind. BUFFER_DONE is the happy
path; OVERRUN_ERROR / UNDERRUN_ERROR / TRIGGER_ERROR are wrapped
into typed exceptions (or routed per ErrorPolicy); BUFFER_REUSED
means data was overwritten in WrapMode.MULTIPLE and is logged at WARNING;
QUEUE_DONE / QUEUE_STOPPED / IO_COMPLETE signal end-of-run;
PRETRIGGER_BUFFER_DONE / EVENT_DONE / MEASURE_DONE are
subsystem-specific and routed to dedicated handlers.
SensorStatus ¶
Bases: StrEnum
Per-channel sentinel status preserved through scalarisation.
TC channels can produce sentinel float values that must NOT be coerced
into plausible measurements. The recorder writes the sentinel to a
sensor_status overlay on the reading / block and replaces the data
cell with NaN, so downstream sinks never silently log "23.4 °C" for
an open thermocouple.
SubsystemState ¶
Bases: StrEnum
Canonical subsystem state — exposed via DtolSession.state.
Borrowed from the .NET API's SubsystemBase.State. The SDK already
tracks this; surfacing it instead of synthesising it from
is_running() plus implicit flags lets tests assert exact
transitions and lets error messages name the precise lifecycle phase.
SubsystemType ¶
Bases: StrEnum
SDK subsystem types — one HDASS is one subsystem of one type.
WrapMode ¶
Bases: StrEnum
BufferPlan wrap mode.
NONE = finite acquisition (stop after one fill of the queued
buffers). SINGLE = DAC waveform (loop a single buffer). MULTIPLE
= standard continuous (re-queue completed buffers).
block_to_long_rows ¶
Yield one :class:DaqSample per (channel, sample) pair in block.
Reconstructs each sample's monotonic timestamp from
block.t_mono_ns + k * block.block_period_ns (constant if
block_period_ns is None — only the block-level timestamp is
used). Sensor-status masks are decoded back into :class:SensorStatus
values per sample.
Yields n_channels * samples_per_channel samples in (channel-major,
sample-minor) order.
Source code in src/dtollib/tasks/models.py
Builder¶
dtollib.tasks.builder ¶
TaskBuilder — translates a :class:TaskSpec into ordered backend calls.
The builder is the single place that knows the legal ordering of SDK configuration calls. It provides the single-value sequence and the continuous-mode sequence with channel-list + timing + trigger + buffer-pool setup.
Critical invariant (docs/design.md §8.5a): on IOType.MULTI_SENSOR
channels, set_multi_sensor_type MUST be called BEFORE any
per-type setter on that channel. The builder enforces this
unconditionally; the fake backend rejects out-of-order calls.
Design reference: docs/implementation-plan.md §4.3.
TaskBuilder ¶
Translate a :class:TaskSpec into ordered backend calls.
The builder is stateless other than the references it captures.
It does not own the HDASS; callers (typically
:class:~dtollib.tasks.DtolSession) keep the handle and pass it
in.
Source code in src/dtollib/tasks/builder.py
configure_continuous ¶
Run the continuous-mode pre-commit configuration sequence.
Stops short of commit() — the §12.3.2 ordering requires
notification registration and buffer queueing BEFORE
olDaConfig. The callback bridge / record() drives the
commit step after wiring its bridge.
Sequence (docs/implementation-plan.md §5.7):
set_data_flow(OL_DF_CONTINUOUS)(orOL_DF_CONTINUOUS_*).- For each channel: MULTI_SENSOR retype if needed, then
add_channel(...). set_channel_list([phys, ...])— drives the continuous-mode channel list separately from the gain-list entries.set_clock(...).set_trigger(...).set_wrap_mode(...).set_stop_on_error(...).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
hdass
|
int
|
Subsystem handle. |
required |
spec
|
TaskSpec
|
Task spec with |
required |
capabilities
|
CapabilitySet
|
Subsystem capability snapshot — drives the MULTI_SENSOR dispatch. |
required |
Raises:
| Type | Description |
|---|---|
DtolTaskStateError
|
If |
Source code in src/dtollib/tasks/builder.py
156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 | |
configure_counter ¶
Configure a counter/timer, quadrature, or tachometer subsystem.
Counter subsystems are read on demand after :meth:start; there is
no channel/gain list or sample clock to set up. The critical
ordering invariant is C/T mode first — olDaSetCTMode re-types
the counter and must precede gate / pulse / edge setters. The fake
backend rejects out-of-order calls.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
hdass
|
int
|
Subsystem handle. |
required |
spec
|
TaskSpec
|
Task spec whose channels are counter/quadrature/tachometer. |
required |
capabilities
|
CapabilitySet
|
Subsystem capability snapshot (unused today; kept for signature parity with the AI/continuous paths). |
required |
Source code in src/dtollib/tasks/builder.py
configure_single_value ¶
Run the single-value configuration sequence.
Sequence (docs/implementation-plan.md §4.3):
set_data_flow(OL_DF_SINGLEVALUE).- For each channel:
a. If the channel is MULTI_SENSOR per the capability set,
set_multi_sensor_type(...)FIRST. b.add_channel(...)— drivesolDaSetChannelType+olDaSetChannelRange+olDaSetGainListEntry(+olDaSetThermocoupleType). set_stop_on_error(spec.stop_on_error).commit()—olDaConfig.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
hdass
|
int
|
Subsystem handle from
:meth: |
required |
spec
|
TaskSpec
|
Task specification. Must have
|
required |
capabilities
|
CapabilitySet
|
Live capability snapshot for the subsystem. Drives the MULTI_SENSOR dispatch. |
required |