Skip to content

Channels

Every entry in TaskSpec.channels is a typed, frozen channel spec. See the Channels guide for the field tables and construction patterns, and Safety for the output-channel gate model.

dtollib.channels

Channel-spec dataclasses — the typed input shape for TaskSpec.channels.

Provides the analog-input subset (voltage + thermocouple) needed for the DT9805 happy path, the DT9806 output surface (:class:AnalogOutputVoltage, :class:DigitalInputPort, :class:DigitalOutputPort with :class:DigitalLine views), and the multi-sensor kinds (:class:RtdInput, :class:ThermistorInput, :class:ResistanceInput, :class:CurrentInput, :class:IepeInput, :class:StrainInput, :class:BridgeInput) — all share :class:AnalogInputBase and reuse its knobs.

The kind discriminator on each concrete spec drives serialisation: :func:channel_from_dict reverses :meth:ChannelSpec.to_dict via the :data:_CHANNEL_KINDS registry below.

Design reference: docs/design.md §8.2–§8.6, §18.3.

AnalogInputBase dataclass

AnalogInputBase(
    *,
    physical_channel,
    name=None,
    unit=None,
    metadata=_empty_metadata(),
    channel_type=ChannelType.SINGLE_ENDED,
    gain=1.0,
    filter=None,
    encoding=None,
    coupling=None,
)

Bases: ChannelSpec

Common AI knobs shared by every analog-input subclass.

Maps to olDaSetChannelType / olDaSetGainListEntry / olDaSetChannelFilter / olDaSetEncoding / olDaSetCouplingType. The backend issues these after the channel is added to the channel list.

Attributes:

Name Type Description
channel_type ChannelType

Wiring (single-ended / differential).

gain float

Programmable-gain-amplifier setting. 1.0 = unity.

filter FilterType | None

Optional analog filter selection.

encoding Encoding | None

Optional sample-code encoding override.

coupling CouplingType | None

Optional AC/DC coupling. None defers to subsystem default.

AnalogInputVoltage dataclass

AnalogInputVoltage(
    *,
    physical_channel,
    name=None,
    unit=None,
    metadata=_empty_metadata(),
    channel_type=ChannelType.SINGLE_ENDED,
    gain=1.0,
    filter=None,
    encoding=None,
    coupling=None,
    min_val=-10.0,
    max_val=10.0,
)

Bases: AnalogInputBase

Voltage-mode analog input.

Attributes:

Name Type Description
min_val float

Lower input voltage range (olDaSetChannelRange).

max_val float

Upper input voltage range.

__post_init__

__post_init__()

Reject min_val >= max_val before the SDK does.

Source code in src/dtollib/channels/analog_input.py
def __post_init__(self) -> None:
    """Reject ``min_val >= max_val`` before the SDK does."""
    # Explicit two-arg super(): @dataclass(slots=True) recreates the class,
    # which breaks zero-arg super() on Python < 3.14 (CPython gh-90562).
    super(AnalogInputVoltage, self).__post_init__()
    if self.min_val >= self.max_val:
        raise DtolValidationError(
            f"AnalogInputVoltage: min_val={self.min_val} must be "
            f"strictly less than max_val={self.max_val}",
            context=ErrorContext(
                operation="AnalogInputVoltage.__post_init__",
                channel=self.physical_channel,
                channel_name=self.name,
            ),
        )

kind_to_multi_sensor_type

kind_to_multi_sensor_type()

AnalogInputVoltage → :attr:IOType.VOLTAGE_IN.

Source code in src/dtollib/channels/analog_input.py
def kind_to_multi_sensor_type(self) -> IOType:
    """``AnalogInputVoltage`` → :attr:`IOType.VOLTAGE_IN`."""
    return IOType.VOLTAGE_IN

AnalogOutputVoltage dataclass

AnalogOutputVoltage(
    *,
    physical_channel,
    name=None,
    unit=None,
    metadata=_empty_metadata(),
    min_val=-10.0,
    max_val=10.0,
    safe_min=None,
    safe_max=None,
    requires_confirm=True,
    gain=1.0,
)

Bases: ChannelSpec

Voltage-mode analog output (olDaPutSingleValue / waveform D/A).

Two range layers:

  • [min_val, max_val] — the device electrical range. A write outside this is electrically impossible and always raises :class:~dtollib.errors.DtolValidationError; confirm does not override it.
  • [safe_min, safe_max] — an optional operator safe band, a subset of the device range. A write outside the safe band (or any write to a channel with requires_confirm=True) needs confirm=True, else :meth:DtolSession.write raises :class:~dtollib.errors.DtolConfirmationRequiredError (docs/design.md §18.1).

Attributes:

Name Type Description
min_val float

Lower output range, volts.

max_val float

Upper output range, volts.

safe_min float | None

Lower safe-band bound, volts. None = no lower gate.

safe_max float | None

Upper safe-band bound, volts. None = no upper gate.

requires_confirm bool

When true, every write to this channel needs confirm=True regardless of the safe band.

gain float

Output-gain-list entry passed to the SDK write call.

__post_init__

__post_init__()

Reject inconsistent ranges before the SDK ever sees them.

Source code in src/dtollib/channels/analog_output.py
def __post_init__(self) -> None:
    """Reject inconsistent ranges before the SDK ever sees them."""
    # Explicit two-arg super(): @dataclass(slots=True) recreates the class,
    # which breaks zero-arg super() on Python < 3.14 (CPython gh-90562).
    super(AnalogOutputVoltage, self).__post_init__()
    if self.min_val >= self.max_val:
        raise self._reject(
            f"min_val={self.min_val} must be strictly less than max_val={self.max_val}"
        )
    if self.safe_min is not None and self.safe_min < self.min_val:
        raise self._reject(
            f"safe_min={self.safe_min} is below the device range min_val={self.min_val}"
        )
    if self.safe_max is not None and self.safe_max > self.max_val:
        raise self._reject(
            f"safe_max={self.safe_max} is above the device range max_val={self.max_val}"
        )
    if (
        self.safe_min is not None
        and self.safe_max is not None
        and self.safe_min >= self.safe_max
    ):
        raise self._reject(
            f"safe_min={self.safe_min} must be strictly less than safe_max={self.safe_max}"
        )

in_device_range

in_device_range(value)

True if value lies within the device electrical range.

Source code in src/dtollib/channels/analog_output.py
def in_device_range(self, value: float) -> bool:
    """True if ``value`` lies within the device electrical range."""
    return self.min_val <= value <= self.max_val

in_safe_band

in_safe_band(value)

True if value lies within the configured safe band.

An unset bound (None) does not constrain that side. With both bounds unset, any in-range value is considered "in band".

Source code in src/dtollib/channels/analog_output.py
def in_safe_band(self, value: float) -> bool:
    """True if ``value`` lies within the configured safe band.

    An unset bound (``None``) does not constrain that side. With both
    bounds unset, any in-range value is considered "in band".
    """
    if self.safe_min is not None and value < self.safe_min:
        return False
    return not (self.safe_max is not None and value > self.safe_max)

kind_to_multi_sensor_type

kind_to_multi_sensor_type()

AnalogOutputVoltage → :attr:IOType.VOLTAGE_OUT.

Source code in src/dtollib/channels/analog_output.py
def kind_to_multi_sensor_type(self) -> IOType:
    """``AnalogOutputVoltage`` → :attr:`IOType.VOLTAGE_OUT`."""
    return IOType.VOLTAGE_OUT

BridgeConfiguration

Bases: StrEnum

Generic bridge-sensor wiring (olDaSetBridgeConfiguration).

Mirrors BRIDGE_CONFIGURATION in OLDADEFS.H — the three-way full/half/quarter subset used for non-strain bridge transducers (load cells, pressure sensors).

BridgeInput dataclass

BridgeInput(
    *,
    physical_channel,
    name=None,
    unit=None,
    metadata=_empty_metadata(),
    channel_type=ChannelType.SINGLE_ENDED,
    gain=1.0,
    filter=None,
    encoding=None,
    coupling=None,
    configuration=BridgeConfiguration.FULL,
    nominal_resistance_ohms=350.0,
    sensitivity_mv_per_v=2.0,
    excitation_source=StrainExcitationSource.INTERNAL,
    excitation_voltage=0.0,
    lead_resistance_ohms=0.0,
    shunt_enabled=False,
)

Bases: AnalogInputBase

Generic bridge-transducer input (load cell, pressure sensor).

Maps to olDaSetBridgeConfiguration + the strain excitation setters, with the volts→engineering transform applied via olDaVoltsToBridgeBasedSensor.

Attributes:

Name Type Description
configuration BridgeConfiguration

Bridge wiring (full / half / quarter).

nominal_resistance_ohms float

Nominal bridge resistance.

sensitivity_mv_per_v float

Rated sensitivity (mV/V at full scale).

excitation_source StrainExcitationSource

Excitation-voltage source.

excitation_voltage float

Bridge excitation voltage in volts.

lead_resistance_ohms float

Lead-wire resistance for quarter-bridge correction.

shunt_enabled bool

Engage the internal shunt-calibration resistor.

__post_init__

__post_init__()

Reject non-physical resistance.

Source code in src/dtollib/channels/analog_input.py
def __post_init__(self) -> None:
    """Reject non-physical resistance."""
    super(BridgeInput, self).__post_init__()
    if self.nominal_resistance_ohms <= 0.0:
        raise DtolValidationError(
            f"BridgeInput: nominal_resistance_ohms must be positive "
            f"(got {self.nominal_resistance_ohms})",
            context=ErrorContext(
                operation="BridgeInput.__post_init__",
                channel=self.physical_channel,
                channel_name=self.name,
            ),
        )

kind_to_multi_sensor_type

kind_to_multi_sensor_type()

BridgeInput → :attr:IOType.BRIDGE.

Source code in src/dtollib/channels/analog_input.py
def kind_to_multi_sensor_type(self) -> IOType:
    """``BridgeInput`` → :attr:`IOType.BRIDGE`."""
    return IOType.BRIDGE

ChannelSpec dataclass

ChannelSpec(
    *,
    physical_channel,
    name=None,
    unit=None,
    metadata=_empty_metadata(),
)

Base class for every channel specification.

Attributes:

Name Type Description
physical_channel int

Zero-based channel index on the subsystem. The DataAcq SDK uses bare integers (not "Dev1/ai0"-style strings).

name str | None

Display name for logs and sink columns. Falls back to f"ch{physical_channel}" at the boundary if omitted.

unit str | None

Display unit (informational; e.g. "V" or "degC").

metadata Mapping[str, str | int | float | bool]

Free-form per-channel metadata. Propagated to :class:~dtollib.tasks.DaqReading.metadata / sink rows.

display_name property

display_name

Effective display name — uses name if set, else f"ch{n}".

__post_init__

__post_init__()

Wrap mutable metadata mappings to enforce immutability.

Source code in src/dtollib/channels/base.py
def __post_init__(self) -> None:
    """Wrap mutable ``metadata`` mappings to enforce immutability."""
    if not isinstance(self.metadata, MappingProxyType):
        object.__setattr__(self, "metadata", MappingProxyType(dict(self.metadata)))

kind_to_multi_sensor_type

kind_to_multi_sensor_type()

Return the :class:IOType discriminator for a MULTI_SENSOR channel.

Called by the :class:~dtollib.tasks.TaskBuilder immediately before any per-type setter on a MULTI_SENSOR channel (docs/design.md §8.5a). Subclasses override; the base raises.

Raises:

Type Description
NotImplementedError

This subclass does not know how to re-type a multi-sensor channel.

Source code in src/dtollib/channels/base.py
def kind_to_multi_sensor_type(self) -> IOType:
    """Return the :class:`IOType` discriminator for a MULTI_SENSOR channel.

    Called by the :class:`~dtollib.tasks.TaskBuilder` immediately
    before any per-type setter on a ``MULTI_SENSOR`` channel
    (docs/design.md §8.5a). Subclasses override; the base raises.

    Raises:
        NotImplementedError: This subclass does not know how to
            re-type a multi-sensor channel.
    """
    raise NotImplementedError(
        f"{type(self).__name__} cannot configure a MULTI_SENSOR channel; "
        "override kind_to_multi_sensor_type()."
    )

to_dict

to_dict()

JSON-friendly mapping with kind discriminator embedded.

Built field-by-field (not via :func:dataclasses.asdict, which deep-copies and cannot pickle the MappingProxyType metadata). :func:~dtollib.channels.channel_from_dict reverses this.

Source code in src/dtollib/channels/base.py
def to_dict(self) -> dict[str, Any]:
    """JSON-friendly mapping with ``kind`` discriminator embedded.

    Built field-by-field (not via :func:`dataclasses.asdict`, which
    deep-copies and cannot pickle the ``MappingProxyType`` metadata).
    :func:`~dtollib.channels.channel_from_dict` reverses this.
    """
    from dataclasses import fields  # noqa: PLC0415

    data: dict[str, Any] = {f.name: getattr(self, f.name) for f in fields(self)}
    data["kind"] = type(self).kind
    # MappingProxyType is not directly JSON serialisable; cast.
    data["metadata"] = dict(self.metadata)
    return data

ChannelType

Bases: StrEnum

Channel-wiring discriminator (olDaSetChannelType).

CjcSource

Bases: StrEnum

Cold-junction-compensation source (olDaSetCjcSource).

CounterEdgeCount dataclass

CounterEdgeCount(
    *,
    physical_channel,
    name=None,
    unit=None,
    metadata=_empty_metadata(),
    clock_source=ClockSource.INTERNAL,
    gate_type=GateType.SOFTWARE,
    count_edge=Edge.RISING,
    cascade=False,
)

Bases: CounterInputBase

Event counter — counts edges on the counter input (OL_CTMODE_COUNT).

Read back via :meth:~dtollib.tasks.DtolSession.read_events.

Attributes:

Name Type Description
count_edge Edge

Edge that increments the counter.

cascade bool

Cascade with the adjacent counter for a 32-bit count.

CounterEdgeToEdge dataclass

CounterEdgeToEdge(
    *,
    physical_channel,
    name=None,
    unit=None,
    metadata=_empty_metadata(),
    clock_source=ClockSource.INTERNAL,
    gate_type=GateType.SOFTWARE,
    start_edge=Edge.RISING,
    stop_edge=Edge.FALLING,
)

Bases: CounterInputBase

Edge-to-edge interval / pulse-width measurement (OL_CTMODE_MEASURE).

Read back via :meth:~dtollib.tasks.DtolSession.read_events (clock ticks between the start and stop edges).

Attributes:

Name Type Description
start_edge Edge

Edge that starts the interval timer.

stop_edge Edge

Edge that stops it.

CounterFrequency dataclass

CounterFrequency(
    *,
    physical_channel,
    name=None,
    unit=None,
    metadata=_empty_metadata(),
    clock_source=ClockSource.INTERNAL,
    gate_type=GateType.SOFTWARE,
    gate_period_s=None,
)

Bases: CounterInputBase

Frequency measurement over a gated window (OL_CTMODE_MEASURE).

Read back via :meth:~dtollib.tasks.DtolSession.measure_frequency.

Attributes:

Name Type Description
gate_period_s float | None

Measurement window in seconds. None uses the device default window.

__post_init__

__post_init__()

Reject a non-positive measurement window.

Source code in src/dtollib/channels/counter_input.py
def __post_init__(self) -> None:
    """Reject a non-positive measurement window."""
    # Explicit two-arg super(): @dataclass(slots=True) recreates the class,
    # which breaks zero-arg super() on Python < 3.14 (CPython gh-90562).
    super(CounterFrequency, self).__post_init__()
    if self.gate_period_s is not None and self.gate_period_s <= 0.0:
        raise DtolValidationError(
            f"CounterFrequency[{self.display_name}]: gate_period_s must be "
            f"positive (got {self.gate_period_s})",
            context=ErrorContext(
                operation="CounterFrequency.__post_init__",
                channel=self.physical_channel,
                channel_name=self.name,
            ),
        )

CouplingType

Bases: StrEnum

AC vs DC coupling (olDaSetCouplingType).

CurrentInput dataclass

CurrentInput(
    *,
    physical_channel,
    name=None,
    unit=None,
    metadata=_empty_metadata(),
    channel_type=ChannelType.SINGLE_ENDED,
    gain=1.0,
    filter=None,
    encoding=None,
    coupling=None,
    min_val=0.0,
    max_val=0.02,
)

Bases: AnalogInputBase

Process-current input (e.g. 4–20 mA) — engineering units are amps.

Attributes:

Name Type Description
min_val float

Lower current range in amps (olDaSetChannelRange).

max_val float

Upper current range in amps.

__post_init__

__post_init__()

Reject min_val >= max_val before the SDK does.

Source code in src/dtollib/channels/analog_input.py
def __post_init__(self) -> None:
    """Reject ``min_val >= max_val`` before the SDK does."""
    super(CurrentInput, self).__post_init__()
    if self.min_val >= self.max_val:
        raise DtolValidationError(
            f"CurrentInput: min_val={self.min_val} must be strictly less "
            f"than max_val={self.max_val}",
            context=ErrorContext(
                operation="CurrentInput.__post_init__",
                channel=self.physical_channel,
                channel_name=self.name,
            ),
        )

kind_to_multi_sensor_type

kind_to_multi_sensor_type()

CurrentInput → :attr:IOType.CURRENT.

Source code in src/dtollib/channels/analog_input.py
def kind_to_multi_sensor_type(self) -> IOType:
    """``CurrentInput`` → :attr:`IOType.CURRENT`."""
    return IOType.CURRENT

DigitalInputPort dataclass

DigitalInputPort(
    *,
    physical_channel,
    name=None,
    unit=None,
    metadata=_empty_metadata(),
    width=None,
    lines=tuple(),
)

Bases: _DigitalPort

A digital-input port on the DIN subsystem.

Read-only: :meth:DtolSession.poll surfaces the raw port byte under the port name plus one bool per declared :class:DigitalLine.

kind_to_multi_sensor_type

kind_to_multi_sensor_type()

DigitalInputPort → :attr:IOType.DIGITAL_INPUT.

Source code in src/dtollib/channels/digital.py
def kind_to_multi_sensor_type(self) -> IOType:
    """``DigitalInputPort`` → :attr:`IOType.DIGITAL_INPUT`."""
    return IOType.DIGITAL_INPUT

DigitalLine dataclass

DigitalLine(
    *,
    bit,
    name=None,
    safe_value=None,
    requires_confirm=None,
)

A single-bit view into a digital port — sugar, not an SDK channel.

A line is addressed in :meth:DtolSession.write / :class:DaqReading by its key: name when set, else f"{port.display_name}.line{bit}".

Attributes:

Name Type Description
bit int

Zero-based bit index within the owning port.

name str | None

Display key for writes/reads. None → derived from the port name and bit index.

safe_value bool | None

The level this line should hold when not explicitly driven (informational; surfaced to operators and sinks).

requires_confirm bool | None

Per-line override of the port's confirm gate. None inherits :attr:DigitalOutputPort.requires_confirm.

from_dict classmethod

from_dict(data)

Reconstruct a :class:DigitalLine from :meth:to_dict.

Source code in src/dtollib/channels/digital.py
@classmethod
def from_dict(cls, data: Mapping[str, Any]) -> DigitalLine:
    """Reconstruct a :class:`DigitalLine` from :meth:`to_dict`."""
    return cls(
        bit=data["bit"],
        name=data.get("name"),
        safe_value=data.get("safe_value"),
        requires_confirm=data.get("requires_confirm"),
    )

to_dict

to_dict()

JSON-friendly mapping (reversed by :meth:from_dict).

Source code in src/dtollib/channels/digital.py
def to_dict(self) -> dict[str, Any]:
    """JSON-friendly mapping (reversed by :meth:`from_dict`)."""
    return {
        "bit": self.bit,
        "name": self.name,
        "safe_value": self.safe_value,
        "requires_confirm": self.requires_confirm,
    }

DigitalOutputPort dataclass

DigitalOutputPort(
    *,
    physical_channel,
    name=None,
    unit=None,
    metadata=_empty_metadata(),
    width=None,
    lines=tuple(),
    safe_value=None,
    requires_confirm=True,
)

Bases: _DigitalPort

A digital-output port on the DOUT subsystem.

physical_channel is the port index (0 on the DT9805/06). A write targets the whole port byte; partial per-line writes are merged into a per-port shadow register so untouched lines are preserved (docs/design.md §18.1).

Attributes:

Name Type Description
safe_value int | None

Full-port byte to hold when not explicitly driven; it also seeds the shadow register at configure time. None → 0.

requires_confirm bool

Port-level confirm gate. A per-line :attr:DigitalLine.requires_confirm overrides it for that line.

kind_to_multi_sensor_type

kind_to_multi_sensor_type()

DigitalOutputPort → :attr:IOType.DIGITAL_OUTPUT.

Source code in src/dtollib/channels/digital.py
def kind_to_multi_sensor_type(self) -> IOType:
    """``DigitalOutputPort`` → :attr:`IOType.DIGITAL_OUTPUT`."""
    return IOType.DIGITAL_OUTPUT

Encoding

Bases: StrEnum

Sample-code encoding (olDaSetEncoding).

ExcitationSource

Bases: StrEnum

Excitation-current source (olDaSetExcitationCurrentSource).

Mirrors EXCITATION_CURRENT_SRC in OLDADEFS.H. Used by IEPE, resistance, RTD, and thermistor channels that need a driven measurement current.

FilterType

Bases: StrEnum

Per-channel filter selection (olDaSetChannelFilter).

IepeInput dataclass

IepeInput(
    *,
    physical_channel,
    name=None,
    unit=None,
    metadata=_empty_metadata(),
    channel_type=ChannelType.SINGLE_ENDED,
    gain=1.0,
    filter=None,
    encoding=None,
    coupling=CouplingType.AC,
    excitation_source=ExcitationSource.INTERNAL,
    excitation_current_a=0.004,
    sensitivity_v_per_unit=None,
)

Bases: AnalogInputBase

IEPE / ICP accelerometer input (constant-current-driven, AC-coupled).

Maps to olDaSetCouplingType + olDaSetExcitationCurrentSource + olDaSetExcitationCurrentValue (the SDK has no single olDaSetIEPE on this build).

Attributes:

Name Type Description
coupling CouplingType | None

Forced to :attr:CouplingType.AC — IEPE sensors ride a DC bias that must be blocked. DC coupling is rejected.

excitation_source ExcitationSource

Constant-current source. :attr:ExcitationSource.DISABLED is rejected — an IEPE sensor needs drive current.

excitation_current_a float

Drive current in amps (typically 0.002–0.004 A).

sensitivity_v_per_unit float | None

Optional sensor sensitivity (V per engineering unit) carried as metadata for downstream scaling.

__post_init__

__post_init__()

Reject DC coupling and disabled excitation — invalid for IEPE.

Source code in src/dtollib/channels/analog_input.py
def __post_init__(self) -> None:
    """Reject DC coupling and disabled excitation — invalid for IEPE."""
    super(IepeInput, self).__post_init__()
    if self.coupling is CouplingType.DC:
        raise DtolValidationError(
            "IepeInput requires AC coupling (an IEPE sensor rides a DC bias "
            "that must be blocked); got coupling=DC",
            context=self._ctx("IepeInput.__post_init__"),
        )
    if self.excitation_source is ExcitationSource.DISABLED:
        raise DtolValidationError(
            "IepeInput requires a drive current; excitation_source=DISABLED is invalid",
            context=self._ctx("IepeInput.__post_init__"),
        )
    if self.excitation_current_a <= 0.0:
        raise DtolValidationError(
            f"IepeInput: excitation_current_a must be positive "
            f"(got {self.excitation_current_a})",
            context=self._ctx("IepeInput.__post_init__"),
        )

kind_to_multi_sensor_type

kind_to_multi_sensor_type()

IepeInput → :attr:IOType.ACCELEROMETER.

Source code in src/dtollib/channels/analog_input.py
def kind_to_multi_sensor_type(self) -> IOType:
    """``IepeInput`` → :attr:`IOType.ACCELEROMETER`."""
    return IOType.ACCELEROMETER

OneShotOutput dataclass

OneShotOutput(
    *,
    physical_channel,
    name=None,
    unit=None,
    metadata=_empty_metadata(),
    clock_source=ClockSource.INTERNAL,
    pulse_type=PulseType.HIGH_TO_LOW,
    gate_type=GateType.SOFTWARE,
    pulse_width_s,
)

Bases: CounterOutputBase

Single output pulse on trigger (OL_CTMODE_ONESHOT).

Attributes:

Name Type Description
pulse_width_s float

Width of the generated pulse in seconds (> 0).

__post_init__

__post_init__()

Reject a non-positive pulse width.

Source code in src/dtollib/channels/counter_output.py
def __post_init__(self) -> None:
    """Reject a non-positive pulse width."""
    # Explicit two-arg super(): @dataclass(slots=True) recreates the class,
    # which breaks zero-arg super() on Python < 3.14 (CPython gh-90562).
    # RepetitiveOneShotOutput inherits this method; ``self`` is a subtype
    # of the recreated OneShotOutput, so the two-arg form stays correct.
    super(OneShotOutput, self).__post_init__()
    if self.pulse_width_s <= 0.0:
        raise DtolValidationError(
            f"{type(self).__name__}[{self.display_name}]: pulse_width_s must be "
            f"positive (got {self.pulse_width_s})",
            context=ErrorContext(
                operation=f"{type(self).__name__}.__post_init__",
                channel=self.physical_channel,
                channel_name=self.name,
            ),
        )

PulseTrainOutput dataclass

PulseTrainOutput(
    *,
    physical_channel,
    name=None,
    unit=None,
    metadata=_empty_metadata(),
    clock_source=ClockSource.INTERNAL,
    pulse_type=PulseType.HIGH_TO_LOW,
    gate_type=GateType.SOFTWARE,
    frequency_hz,
    duty_cycle=0.5,
)

Bases: CounterOutputBase

Continuous pulse-train (rate) generation (OL_CTMODE_RATE).

Attributes:

Name Type Description
frequency_hz float

Output pulse frequency in hertz (> 0).

duty_cycle float

Fraction of each period the output is in its active level, in (0, 1).

__post_init__

__post_init__()

Reject a non-positive frequency or an out-of-range duty cycle.

Source code in src/dtollib/channels/counter_output.py
def __post_init__(self) -> None:
    """Reject a non-positive frequency or an out-of-range duty cycle."""
    # Explicit two-arg super(): @dataclass(slots=True) recreates the class,
    # which breaks zero-arg super() on Python < 3.14 (CPython gh-90562).
    super(PulseTrainOutput, self).__post_init__()
    if self.frequency_hz <= 0.0:
        raise self._reject(f"frequency_hz must be positive (got {self.frequency_hz})")
    if not (0.0 < self.duty_cycle < 1.0):
        raise self._reject(f"duty_cycle must be in (0, 1) (got {self.duty_cycle})")

QuadratureDecoder dataclass

QuadratureDecoder(
    *,
    physical_channel,
    name=None,
    unit=None,
    metadata=_empty_metadata(),
    decode_mode=QuadratureDecodeMode.X4,
    index_reset=False,
)

Bases: ChannelSpec

Quadrature encoder decoder (OLSS_QUAD).

Read back via :meth:~dtollib.tasks.DtolSession.read_events (accumulated position count).

Attributes:

Name Type Description
decode_mode QuadratureDecodeMode

Counts per encoder line (X1 / X2 / X4).

index_reset bool

Reset the position count on the encoder's index/Z pulse.

RepetitiveOneShotOutput dataclass

RepetitiveOneShotOutput(
    *,
    physical_channel,
    name=None,
    unit=None,
    metadata=_empty_metadata(),
    clock_source=ClockSource.INTERNAL,
    pulse_type=PulseType.HIGH_TO_LOW,
    gate_type=GateType.SOFTWARE,
    pulse_width_s,
)

Bases: OneShotOutput

Repetitive one-shot pulse on each trigger (OL_CTMODE_ONESHOT_RPT).

ResistanceInput dataclass

ResistanceInput(
    *,
    physical_channel,
    name=None,
    unit=None,
    metadata=_empty_metadata(),
    channel_type=ChannelType.SINGLE_ENDED,
    gain=1.0,
    filter=None,
    encoding=None,
    coupling=None,
    excitation_source=ExcitationSource.INTERNAL,
    excitation_current_a=None,
)

Bases: AnalogInputBase

Direct resistance measurement — engineering units are ohms.

Configured via the excitation-current setters; the SDK reports the measured resistance directly.

Attributes:

Name Type Description
excitation_source ExcitationSource

Measurement-current source.

excitation_current_a float | None

Driven current in amps, or None for the subsystem default.

kind_to_multi_sensor_type

kind_to_multi_sensor_type()

ResistanceInput → :attr:IOType.RESISTANCE.

Source code in src/dtollib/channels/analog_input.py
def kind_to_multi_sensor_type(self) -> IOType:
    """``ResistanceInput`` → :attr:`IOType.RESISTANCE`."""
    return IOType.RESISTANCE

RtdInput dataclass

RtdInput(
    *,
    physical_channel,
    name=None,
    unit=None,
    metadata=_empty_metadata(),
    channel_type=ChannelType.SINGLE_ENDED,
    gain=1.0,
    filter=None,
    encoding=None,
    coupling=None,
    rtd_type=RtdType.PT3850,
    r0=100.0,
    a=None,
    b=None,
    c=None,
    excitation_source=ExcitationSource.INTERNAL,
    excitation_current_a=None,
)

Bases: AnalogInputBase

Resistance-temperature-detector input — engineering units are °C.

Maps to olDaSetRtdType plus, for :attr:RtdType.CUSTOM, the Callendar–Van Dusen setters olDaSetRtdR0 / olDaSetRtdA / olDaSetRtdB / olDaSetRtdC.

Attributes:

Name Type Description
rtd_type RtdType

Standard RTD curve, or :attr:RtdType.CUSTOM to supply explicit coefficients.

r0 float

Resistance at 0 °C in ohms (PT100 → 100.0).

a, b

Callendar–Van Dusen coefficients. Required when rtd_type is RtdType.CUSTOM; forbidden otherwise (the standard curve already fixes them).

c float | None

Sub-zero Callendar–Van Dusen coefficient (optional even for custom curves; only affects T < 0 °C).

excitation_source ExcitationSource

Measurement-current source.

excitation_current_a float | None

Driven current in amps, or None for the subsystem default.

__post_init__

__post_init__()

Enforce the custom-vs-standard coefficient contract.

Source code in src/dtollib/channels/analog_input.py
def __post_init__(self) -> None:
    """Enforce the custom-vs-standard coefficient contract."""
    super(RtdInput, self).__post_init__()
    is_custom = self.rtd_type is RtdType.CUSTOM
    has_ab = self.a is not None and self.b is not None
    if is_custom and not has_ab:
        raise DtolValidationError(
            "RtdInput(rtd_type=CUSTOM) requires both 'a' and 'b' "
            "Callendar-Van Dusen coefficients",
            context=self._ctx("RtdInput.__post_init__"),
        )
    if not is_custom and (self.a is not None or self.b is not None or self.c is not None):
        raise DtolValidationError(
            f"RtdInput(rtd_type={self.rtd_type.value}): a/b/c coefficients "
            "are only valid with rtd_type=CUSTOM (the standard curve fixes them)",
            context=self._ctx("RtdInput.__post_init__"),
        )
    if self.r0 <= 0.0:
        raise DtolValidationError(
            f"RtdInput: r0 must be positive (got {self.r0})",
            context=self._ctx("RtdInput.__post_init__"),
        )

kind_to_multi_sensor_type

kind_to_multi_sensor_type()

RtdInput → :attr:IOType.RTD.

Source code in src/dtollib/channels/analog_input.py
def kind_to_multi_sensor_type(self) -> IOType:
    """``RtdInput`` → :attr:`IOType.RTD`."""
    return IOType.RTD

RtdType

Bases: StrEnum

RTD curve selection (olDaSetRtdType).

Mirrors the OL_RTD_TYPE_* family in OLDADEFS.H. The numeric suffix is the platinum temperature coefficient (α × 10⁴): PT3850 is the DIN/IEC 60751 standard. :attr:CUSTOM defers the curve to explicit Callendar–Van Dusen coefficients (r0 / a / b / c on :class:RtdInput).

StrainExcitationSource

Bases: StrEnum

Strain-gage excitation-voltage source (olDaSetStrainExcitationVoltageSource).

Mirrors STRAIN_EXCITATION_VOLTAGE_SRC in OLDADEFS.H. Unlike :class:ExcitationSource there is no DISABLED member — a strain bridge always needs an excitation voltage.

StrainGageConfiguration

Bases: StrEnum

Strain-gage bridge wiring (olDaSetStrainBridgeConfiguration).

Mirrors STRAIN_GAGE_CONFIGURATION in OLDADEFS.H — the full seven-way wiring set the SDK distinguishes for a strain gage.

StrainInput dataclass

StrainInput(
    *,
    physical_channel,
    name=None,
    unit=None,
    metadata=_empty_metadata(),
    channel_type=ChannelType.SINGLE_ENDED,
    gain=1.0,
    filter=None,
    encoding=None,
    coupling=None,
    configuration=StrainGageConfiguration.QUARTER_BRIDGE,
    gage_factor=2.0,
    gage_resistance_ohms=350.0,
    poisson_ratio=0.3,
    lead_resistance_ohms=0.0,
    excitation_source=StrainExcitationSource.INTERNAL,
    excitation_voltage=0.0,
    shunt_enabled=False,
)

Bases: AnalogInputBase

Strain-gage input — engineering units are strain (ε, dimensionless).

Maps to olDaSetStrainBridgeConfiguration + olDaSetStrainExcitationVoltageSource + olDaSetStrainExcitationVoltage + olDaSetStrainShuntResistor, with the volts→strain transform applied via :func:dtollib.utils / olDaVoltsToStrain.

Attributes:

Name Type Description
configuration StrainGageConfiguration

Bridge wiring (quarter / half / full).

gage_factor float

Gage factor (sensitivity); must be positive.

gage_resistance_ohms float

Nominal gage resistance (e.g. 120 / 350 Ω).

poisson_ratio float

Poisson's ratio of the specimen (Poisson bridges).

lead_resistance_ohms float

Lead-wire resistance for quarter-bridge correction.

excitation_source StrainExcitationSource

Excitation-voltage source.

excitation_voltage float

Bridge excitation voltage in volts.

shunt_enabled bool

Engage the internal shunt-calibration resistor.

__post_init__

__post_init__()

Reject non-physical gage factor / resistance.

Source code in src/dtollib/channels/analog_input.py
def __post_init__(self) -> None:
    """Reject non-physical gage factor / resistance."""
    super(StrainInput, self).__post_init__()
    if self.gage_factor <= 0.0:
        raise DtolValidationError(
            f"StrainInput: gage_factor must be positive (got {self.gage_factor})",
            context=self._ctx("StrainInput.__post_init__"),
        )
    if self.gage_resistance_ohms <= 0.0:
        raise DtolValidationError(
            f"StrainInput: gage_resistance_ohms must be positive "
            f"(got {self.gage_resistance_ohms})",
            context=self._ctx("StrainInput.__post_init__"),
        )

kind_to_multi_sensor_type

kind_to_multi_sensor_type()

StrainInput → :attr:IOType.STRAIN_GAGE.

Source code in src/dtollib/channels/analog_input.py
def kind_to_multi_sensor_type(self) -> IOType:
    """``StrainInput`` → :attr:`IOType.STRAIN_GAGE`."""
    return IOType.STRAIN_GAGE

Tachometer dataclass

Tachometer(
    *,
    physical_channel,
    name=None,
    unit=None,
    metadata=_empty_metadata(),
    measure_edge=Edge.RISING,
    stop_edge=Edge.FALLING,
)

Bases: ChannelSpec

Tachometer input (OLSS_TACH) — first-class, distinct from C/T.

Read back via :meth:~dtollib.tasks.DtolSession.measure_frequency.

Attributes:

Name Type Description
measure_edge Edge

Edge used to time successive periods.

stop_edge Edge

Edge that ends a measurement window.

TemperatureUnit

Bases: StrEnum

Temperature unit emitted by the subsystem.

Maps to olDaSetTemperatureFilter / unit-selection setters on SDK builds that support per-channel temperature units.

ThermistorInput dataclass

ThermistorInput(
    *,
    physical_channel,
    name=None,
    unit=None,
    metadata=_empty_metadata(),
    channel_type=ChannelType.SINGLE_ENDED,
    gain=1.0,
    filter=None,
    encoding=None,
    coupling=None,
    a,
    b,
    c,
    excitation_source=ExcitationSource.INTERNAL,
    excitation_current_a=None,
)

Bases: AnalogInputBase

Thermistor input — engineering units are °C.

Maps to olDaSetThermistorA / olDaSetThermistorB / olDaSetThermistorC (the Steinhart–Hart coefficients).

Attributes:

Name Type Description
a, (b, c)

Steinhart–Hart coefficients. All three are required — a thermistor has no standard curve the SDK can assume.

excitation_source ExcitationSource

Measurement-current source.

excitation_current_a float | None

Driven current in amps, or None for the subsystem default.

kind_to_multi_sensor_type

kind_to_multi_sensor_type()

ThermistorInput → :attr:IOType.THERMISTOR.

Source code in src/dtollib/channels/analog_input.py
def kind_to_multi_sensor_type(self) -> IOType:
    """``ThermistorInput`` → :attr:`IOType.THERMISTOR`."""
    return IOType.THERMISTOR

ThermocoupleInput dataclass

ThermocoupleInput(
    *,
    physical_channel,
    name=None,
    unit=None,
    metadata=_empty_metadata(),
    channel_type=ChannelType.DIFFERENTIAL,
    gain=100.0,
    filter=None,
    encoding=None,
    coupling=None,
    thermocouple_type,
    min_val_degc,
    max_val_degc,
    cjc_source=CjcSource.INTERNAL,
    cjc_channel=0,
    units=TemperatureUnit.DEG_C,
)

Bases: AnalogInputBase

Thermocouple analog input — engineering units are degrees C/F/K.

Two read paths, selected by the subsystem's capabilities:

  • Firmware-linearised (OLSSC_RETURNS_FLOATS true): the device emits temperature directly via olDaGetSingleFloat.
  • Application-linearised (DT9805/DT9806 A/D — returns_floats false, supports_thermocouples true): the device returns raw codes. The wrapper reads the differential thermo-emf plus the CJC sensor on :attr:cjc_channel, then applies NIST ITS-90 polynomials (:func:dtollib.utils.convert_volts_to_temperature). This requires differential wiring and a high gain to resolve the µV-level emf — hence the defaults below differ from :class:AnalogInputBase.

Defaults are tuned for the DT9805/DT9806: differential wiring, gain=100 (≈3 µV/LSB on the ±10 V/16-bit A/D), CJC on channel 0.

Attributes:

Name Type Description
thermocouple_type ThermocoupleType

NIST letter designation (J/K/T/E/R/S/B/N).

min_val_degc float

Lower temperature limit. Validated against the type's NIST operating range in __post_init__.

max_val_degc float

Upper temperature limit.

cjc_source CjcSource

Cold-junction-compensation source.

cjc_channel int

Channel carrying the cold-junction sensor on the application-linearised path (channel 0 at 10 mV/°C on the DT9805/DT9806). Ignored on firmware-linearised subsystems.

units TemperatureUnit

Reporting unit (deg C today; multi-sensor builds wire up conversion).

channel_type ChannelType

Overrides the base default to DIFFERENTIAL — mandatory for thermocouple/low-level measurement (UM9800 p.36).

gain float

Overrides the base default to 100.0 for emf resolution.

__post_init__

__post_init__()

Reject ranges outside NIST operating envelope, before SDK does.

Source code in src/dtollib/channels/analog_input.py
def __post_init__(self) -> None:
    """Reject ranges outside NIST operating envelope, before SDK does."""
    super(ThermocoupleInput, self).__post_init__()
    if self.min_val_degc >= self.max_val_degc:
        raise DtolValidationError(
            f"ThermocoupleInput: min_val_degc={self.min_val_degc} must be "
            f"strictly less than max_val_degc={self.max_val_degc}",
            context=ErrorContext(
                operation="ThermocoupleInput.__post_init__",
                channel=self.physical_channel,
                channel_name=self.name,
            ),
        )
    envelope_lo, envelope_hi = get_thermocouple_range(self.thermocouple_type.value)
    if self.min_val_degc < envelope_lo or self.max_val_degc > envelope_hi:
        raise DtolValidationError(
            f"ThermocoupleInput Type {self.thermocouple_type.value}: "
            f"range [{self.min_val_degc}, {self.max_val_degc}] °C "
            f"falls outside the NIST envelope "
            f"[{envelope_lo}, {envelope_hi}] °C",
            context=ErrorContext(
                operation="ThermocoupleInput.__post_init__",
                channel=self.physical_channel,
                channel_name=self.name,
                extra={"tc_type": self.thermocouple_type.value},
            ),
        )

kind_to_multi_sensor_type

kind_to_multi_sensor_type()

ThermocoupleInput → :attr:IOType.THERMOCOUPLE.

Source code in src/dtollib/channels/analog_input.py
def kind_to_multi_sensor_type(self) -> IOType:
    """``ThermocoupleInput`` → :attr:`IOType.THERMOCOUPLE`."""
    return IOType.THERMOCOUPLE

ThermocoupleType

Bases: StrEnum

Thermocouple letter designation (olDaSetThermocoupleType).

Lock string values to single uppercase letters so they round-trip cleanly with NIST coefficient lookups in :mod:dtollib.utils.

channel_from_dict

channel_from_dict(data)

Reconstruct a :class:ChannelSpec from its :meth:to_dict mapping.

Dispatches on the kind discriminator. The input is not mutated.

Parameters:

Name Type Description Default
data dict[str, Any]

Mapping produced by :meth:ChannelSpec.to_dict — must carry a kind key matching a registered concrete spec.

required

Returns:

Type Description
ChannelSpec

The reconstructed concrete channel spec.

Raises:

Type Description
DtolValidationError

kind is missing or unrecognised.

Source code in src/dtollib/channels/__init__.py
def channel_from_dict(data: dict[str, Any]) -> ChannelSpec:
    """Reconstruct a :class:`ChannelSpec` from its :meth:`to_dict` mapping.

    Dispatches on the ``kind`` discriminator. The input is not mutated.

    Args:
        data: Mapping produced by :meth:`ChannelSpec.to_dict` — must carry
            a ``kind`` key matching a registered concrete spec.

    Returns:
        The reconstructed concrete channel spec.

    Raises:
        DtolValidationError: ``kind`` is missing or unrecognised.
    """
    fields = dict(data)
    kind = fields.pop("kind", None)
    if kind is None:
        raise DtolValidationError(
            "channel_from_dict: mapping has no 'kind' discriminator",
            context=ErrorContext(operation="channel_from_dict"),
        )
    cls = _CHANNEL_KINDS.get(kind)
    if cls is None:
        raise DtolValidationError(
            f"channel_from_dict: unknown channel kind {kind!r}; "
            f"known kinds: {sorted(_CHANNEL_KINDS)}",
            context=ErrorContext(operation="channel_from_dict", extra={"kind": kind}),
        )
    return cls(**fields)