Changelog¶
All notable changes to dtollib are documented here. This project
follows Semantic Versioning and
Keep a Changelog conventions.
[Unreleased]¶
Changed (DIO port + bitmask model — breaking)¶
- Digital I/O is now port-shaped, matching the SDK.
DigitalOutputLine/DigitalInputLineare removed and replaced byDigitalOutputPort/DigitalInputPort(whosephysical_channelis the port index) plusDigitalLine(bit=N, name=...)bit-views. The DT9805/06 expose one 8-bit port per direction; the old per-line model addressed each line as its own SDK channel and failed on hardware withECODE 7 (Invalid Channel)for any line ≥1, reaching only relay 0 (docs/bench-dio-ao.md §2D). DtolSession.writepacks lines into one port byte. A write groups keys by port and issues oneolDaPutSingleValueper port. Whole-port writes take an int byte (write({"dout": 0b1010})); per-line writes (write({"relay0": True})) merge into a per-port shadow register so untouched lines hold their last value.poll()surfaces the raw port byte plus one bool per declared line.CapabilitySet.resolution(olDaGetResolution) added — the port width for digital subsystems.builder._validate_digital_portrejects an out-of-range port index or a line bit beyond the port width at configure-time. The fake backend now models the single 8-bit port and raises ECODE 7 past it.
Added (WS-AO — continuous analog output / waveform playback)¶
play(session, source, *, confirm=False, error_policy=...)— the output mirror ofrecord(). A hardware-clocked continuous-AO context manager that fills a buffer ring from a waveformsourceand yields the liveAcquisitionSummary.WrapMode.SINGLElays one period across the ring and loops it;WrapMode.MULTIPLErefills + re-queues each emptied buffer from an async iterator or() -> ndarray | Nonecallable (Noneends finite playback). Exported at the top level alongside thePlaybackSourcealias.- §18 safety gate on every sample.
play()reusesDtolSession.write's confirm gate via a shared validator (tasks/_output_gate.py): a sample outside the device range is aDtolValidationError(pre-seed, always); a sample outside the safe band or arequires_confirmchannel withoutconfirm=Trueis aDtolConfirmationRequiredError. Streamed chunks are validated as they are pulled. - Output buffer-pool fill mode.
BufferPoolgaineddirection=OUTPUTwithfill()/seed_all()and aFILLEDbuffer state, enforcing Fill-before-Queue (an output HBUF must be filled before it is queued or re-queued). The fake mirrors this on the D/A subsystem'sput_buffer. - Output callback bridge. New
backend/_output_callback_bridge.pyrefills and re-queues completed buffers (vs the input bridge's copy-and-emit) and routesOLDA_WM_UNDERRUN_ERROR→DtolBufferUnderrunErrorperErrorPolicy. The shared shutdown discipline (Sentinel,DrainStop, the SDK→asyncio event-kind map) was extracted tobackend/_bridge_common.pyso neither bridge copy-pastes it. Teardown order: mute (when the cap is verified) → stop → unregister → drain. - Docs: new
docs/waveform-output.md. - No current DT9805/06 board supports continuous AO. Bench-confirmed
(2026-05-28) that the DT9806 D/A is single-value only
(
OLSSC_SUP_CONTINUOUS=0);play()raisesDtolCapabilityErroron it (see "Fixed (hardware bring-up)"). Theplay()machinery is verified against the fake and is ready for a future streaming-DAC board.play()readssupports_mutefrom the subsystem capabilities (WS-B) and skips the pre-stop mute when the cap is false.
Added (WS-B — output capability constants)¶
- Six DT9806 output-capability positions transcribed into
capi/constants.pyfromOLDADEFS.H'solssc_tagenum (OLSSC_MAX_DIGITALIOLIST_VALUE=46,OLSSC_SUP_SYNCHRONOUS_DIGITALIO=50,OLSSC_SUP_WRPWAVEFORM=97,OLSSC_CURRENT_OUTPUTS=117,OLSSC_SUP_PUT_SINGLE_VALUES=118,OLSSC_SUP_MUTE=142), each cross-checked against an already-verified neighbour. Wired intoCapabilitySet+query_capabilitiesassupports_mute,supports_wrp_waveform,supports_put_single_values,supports_synchronous_digitalio,current_outputs,max_digitaliolist_value(all default off so the AO path degrades gracefully).play()now passes the realsupports_mutethrough to the output bridge. - Header-verified, bench read-back pending (§1.4a). Positions are confirmed
by enum counting but not yet by a live
olDaGetSSCapsread-back; seedocs/decisions.md"Output capability positions (WS-B)".
Added (WS-TEST / S0 — hardware test scaffolding + DA-event probe)¶
scripts/bench_probe_ao_wndhandle.py— maintainer-only S0 spike confirming the DA subsystem'sOLDA_WM_*cadence under the window-handle mechanism.tests/hardware/test_dt9806_ao.py,test_dt9806_do.py,test_dt9806_waveform.py— single-value AO/DO confirm-gate + loopback tests and the continuous-play()waveform recovery / 60 s zero-underrun soak. Gated byDTOLLIB_ENABLE_OUTPUT_TESTS=1+ thehardware_outputmarker (loopback halves additionally byDTOLLIB_LOOPBACK_AO0_AI0/DTOLLIB_LOOPBACK_DOUT_DIN).
Fixed (hardware bring-up — DT9806 output path, 2026-05-28)¶
- Single-value AO
write()was broken on hardware.add_channelissuedolDaSetGainListEntryfor analog-output channels, but the DT9806 D/A has no programmable gain and rejects it withOLNOTSUPPORTED(ec=36), failingconfigure(). AO now tolerates that error (AI still sets the gain list to select its range); the single-value put writes the code directly. The AO confirm-gate hardware tests pass on the DT9806 after the fix. play()now fails loud on a single-value-only D/A. Bench-confirmed that the DT9806 D/A reportsOLSSC_SUP_CONTINUOUS=0(no FIFO, no wrap modes) — it cannot stream waveforms.play()checkscapabilities.supports_continuousafterconfigure()and raisesDtolCapabilityErrorpointing atwrite(), instead of dying mid-startup atolDaConfig. Theplay()software path stays unit-tested against the (idealised, continuous-capable) fake D/A.- WS-A0 confirmed on hardware.
record()continuous AI delivers blocks on a live DT9805 through theolDaSetWndHandle+ message-pump bridge. - WS-B capability positions bench-verified against a live
olDaGetSSCapsread-back (scripts/bench_probe_da_caps.py); seedocs/decisions.md.
Fixed (docs)¶
- Reconciled
docs/design.md§11.3 / §11.5 / §12.0 / §12.3.2 and the Phase-1/Phase-3 bound-function lists from the deadNOTIFY_PROCmechanism to the implementedolDaSetWndHandle+ hidden message-window + pump-thread mechanism; the file tree now lists_message_window.py,_bridge_common.py, and_output_callback_bridge.py.
Added (Phase 5 — counter/timer, tachometer, quadrature, simultaneous start)¶
- Counter channel specs —
CounterEdgeCount,CounterFrequency,CounterEdgeToEdge,QuadratureDecoder,Tachometer(inputs) andPulseTrainOutput,OneShotOutput,RepetitiveOneShotOutput(outputs), plus theCounterMode/GateType/PulseType/QuadratureDecodeModeenums. All exported at the top level and registered withchannel_from_dict. DtolSession.read_events()andDtolSession.measure_frequency()— on-demand counter reads packaged asDaqReading(joining siblings on(device, t_mono_ns)). Counter/quadrature/tachometer tasks route through a newTaskBuilder.configure_counterpath (C/T mode set first).RetriggerSpecgained real fields (mode,multiscan_count,frequency_hz,source) and is now wired into the continuous builder viaolDaSetTriggeredScanUsage/olDaSetMultiscanCount/olDaSetRetriggerMode.DtolManager.start_synchronized(names)— runs the four-step SDK simultaneous-start sequence (olDaGetSSList→olDaPutDassToSSList× N →olDaSimultaneousPreStart→olDaSimultaneousStart), always releasing the list. Single-board, single-backend scope; cross-board Sync Bus is Phase 7.- SDK bindings —
olDaSetCTMode,olDaSetCTClockSource,olDaSetCTClockFrequency,olDaSetGateType,olDaSetPulseType,olDaSetPulseWidth,olDaSetMeasureStartEdge/StopEdge,olDaSetCascadeMode,olDaReadEvents,olDaMeasureFrequency,olDaSetTriggeredScanUsage,olDaSetMultiscanCount,olDaSetRetriggerMode,olDaSetRetrigger,olDaSetRetriggerFrequency,olDaGetSSList,olDaPutDassToSSList,olDaSimultaneousPreStart,olDaSimultaneousStart,olDaReleaseSSList, with matchingOpenLayersApimethods. - Backend —
DtolBackendProtocol +DataAcqBackend+FakeDtolBackendgain the counter/retrigger/simultaneous-start surface. The fake enforces C/T-mode-first ordering, counter-subsystem-only setters, RUNNING-before-read, and the SS-list ordering invariants;make_fake_dt9806()now exposes QUAD + TACH subsystems. - Docs: new
docs/counter-timer.md,docs/synchronized.md. - C/T selector constants (OQ-5a) — bench-verified (2026-05-28, SDK
V7.0.0.7).
OL_CTMODE_*/OL_GATE_*/OL_PLS_*/OL_EDGE_*/OL_CT_CASCADE/OL_CT_SINGLEare transcribed fromOLDADEFS.H(line cited per symbol) and read back against the live DT9805/06 C/T. The earlier provisional 1400-family was wrong in value and name (OL_PULSETYPE_*→OL_PLS_*; gates gained underscores).olDaSetCascadeModetakes a UINT selector, not a BOOL. - Quadrature / tachometer / MEASURE gated off (OQ-5b). The DT9805/06 expose
no quadrature or tachometer subsystem and the C/T reports
CTMODE_MEASURE/QUADRATURE_DECODERas 0;CounterMode.QUADRATURE/.TACHOMETER/.MEASUREnow raiseDtolCapabilityErrorat configure time, driven by the runtime capability query. The fake reports support so the software path stays unit-tested. Seedocs/decisions.md. - Binding fixes:
olDaSimultaneousPrestart(header spelling, not…PreStart); the C/T clock uses the genericolDaSetClockSource/olDaSetClockFrequency(noolDaSetCTClock*export exists);olDaMute/olDaUnMutebound optionally (header-declared, not exported by V7.0.0.7).
Added (Phase 4 — outputs and digital I/O, DT9806)¶
- Output channel specs —
AnalogOutputVoltage(withsafe_min/safe_maxsafe-band +requires_confirm),DigitalOutputLine(safe_value+requires_confirm),DigitalInputLine. All exported at the top level. channel_from_dict+ akind → classregistry indtollib.channels, reversingChannelSpec.to_dictfor all five channel kinds. (to_dictno longer deep-copies, fixing amappingproxypickling error.)DtolSession.write(values, *, confirm=False)— single-value AO / DO writes with the §18 safety gate: out-of-device-range →DtolValidationError(always); out-of-safe-band orrequires_confirmwithoutconfirm=True→DtolConfirmationRequiredError. Validation is atomic and pre-SDK — one bad value writes nothing. Dispatches to simultaneous (olDaPutSingleValues) or per-channel (olDaPutSingleValue) writes onsupports_simultaneous_da.open_device(..., confirm_start=...)is now wired: autostarting a task that drives arequires_confirmoutput raisesDtolConfirmationRequiredErrorunlessconfirm_start=True.- SDK bindings —
olDaPutSingleValue,olDaPutSingleValues,olDaMute,olDaUnMute,olDaSetSynchronousDigitalIOUsage,olDaSetDigitalIOListEntry,olDmCopyToBuffer,olDmCopyBuffer, with matchingOpenLayersApimethods. Signatures verified against the installed C headers (SDK V7.0.0.7) — see docs/decisions.md. - Backend output surface —
DtolBackendProtocol +DataAcqBackend+FakeDtolBackendgainput_single_value/put_single_values/mute/unmute/set_synchronous_digital_io_usage/set_digital_io_list_entry/copy_to_buffer/copy_buffer. The fake enforces output-subsystem-only writes, the simultaneous-D/A capability, and committed-state ordering. dtol-readanddtol-infoCLIs — promoted from stubs.dtol-read --board NAME --channel N --range MIN,MAX [--json]does a one-shot scalar read;dtol-info [--board NAME] [--json]dumps the full per-boardCapabilitySet.- Docs: new
docs/safety.md;docs/channels.mdextended with AO / DIN / DOUT.
Deferred (Phase 4, follow-up commit)¶
- Continuous AO waveform output (
play()+ the output callback bridge). Requires a threaded Win32 message-pump analog of the §12.3.2 input bridge and bench read-back on real DT9806 hardware to validate underrun/refill semantics — see docs/implementation-plan.md §6.5.DtolSession.writerejects continuous data-flow with a pointer toplay().
Fixed (hardware bring-up — DT9805 / DT9806, SDK V7.0.0.7)¶
- Continuous-mode
ErrorPolicy.RAISEcrash. A sustained SDK overrun underRAISEsegfaulted (or deadlocked) on real hardware: the drainer raised inside the anyio task group, racing the shielded SDK/pool teardown. The drainer now captures the error, unwinds cleanly, and the bridge re-raises it only after the ordered shutdown — surfacing a cleanDtolBufferOverrunErrorinstead of aBaseExceptionGroup. - Cancelled session leaked the subsystem.
DtolSession.close()is now shielded against cancellation, so a timed-out / cancelledrecord()still releases the HDASS and terminates the HDRVR (was leaving the A/D reserved — ECODE 20 "Subsystem in use"). - Voltage input range on the DT9805/06.
olDaSetChannelRangeis unsupported (ECODE 36) on these fixed-range boards;add_channelnow falls back to subsystem-wideolDaSetRange(then native range + gain). UnblocksAnalogInputVoltagereads anddtol-capture. olDaEnumSSCaps/olDaEnumChannelCapscallbacks. Corrected the ctypes typedefs to the real SDKCAPSPROC/CHANNELCAPSPROCshapes (the value rides in the DBL params, not the first arg);CapabilitySetnow reports realranges/gains(e.g. DT9806 gains 1/10/100/500) instead of empty tuples.
Added (Phase 1 — C boundary, discovery, diagnostics)¶
dtollib.capipackage — three-layer C boundary (prototypes →OpenLayersApi→DataAcqBackend) per docs/design.md §10.3.dtollib.capi.loader.load_openlayers()— two-DLL discovery (oldaapi*.dll+olmem*.dll) with explicit-path > env-var > default-path > bare-name resolution. Pre-checks bitness beforeWinDLLis called.dtollib.capi.types— opaque handle aliases (HDRVR,HDASS,HBUF,HLIST,HSSLIST), pointer-sizedWPARAM/LPARAMfromwintypes, six SDK callback typedefs including the Phase-3NOTIFY_PROC(declared early so the typedef has exactly one home).dtollib.capi.prototypes— boundargtypes/restypeon the 15 Phase-1 SDK functions (per docs/design.md §26 Phase 1): versions, error-strings, board enumeration, device lifecycle, subsystem enumeration, capability queries.dtollib.capi.constants— SDK-side numeric IDs (OLSS_*,OLSSC_*,OL_DF_*,OLDA_WM_*), kept in a binding-internal namespace separate from the user-facingdtollib.constants.dtollib.capi.errors— ECODE → typed-exception classification via per-code table + range fallback (docs/design.md §17.4). Singlecheck()seam through which every SDK call routes; AST-level regression test asserts the invariant.dtollib.capi.callbacks—SdkEventKindenum andevent_kind_from_messagehelper for the Phase-3 callback bridge; callback typedef re-exports.dtollib.capi.api.OpenLayersApi— typed wrapper exposing one method per Phase-1 SDK function with output-pointer extraction and_check-routed error wrapping.dtollib.backendpackage —DtolBackendProtocol,DataAcqBackend(real SDK, Phase-1 subset),FakeDtolBackend(cross-platform in-memory fake enforcing the same ordering and capability rules).dtollib.systempackage —find_devices(),find_subsystems(), immutableBoardInfo/SubsystemInfo/DeviceInfodataclasses,CapabilitySet(typed view over the four SDK capability-query functions).dtollib.testing—make_fake_dt9805(),make_fake_dt9806(),make_fake_backend()with realistic capability sets.dtollib.utils— NIST ITS-90 forward + inverse polynomials for Type K and Type J thermocouples (others ship operating-range helpers only), CJC compensation, rectangular + delta strain rosette transforms.dtol-diagCLI — real implementation: DLL load + version report, board enumeration, common-failure diagnostics,--jsonoutput.dtol-discoverCLI — real implementation: multi-board summary,--board NAMEsingle-board drill-in,--jsonoutput.scripts/gen_openlayers.py— maintainer-only header-diff tool.- Bench-confirmed on DT9805 + DT9806 against SDK V7.0.0.7 + olmem V2.00.01 — see docs/decisions.md for the verified findings.
Added (Phase 0 scaffold)¶
- Phase 0 scaffold — package installs and imports cleanly on Windows, Linux, and macOS with no DataAcq SDK present.
DtolConfig+config_from_env— process-wide settings with full env-var coercion (DTOLLIB_DEFAULT_TIMEOUT_S,DTOLLIB_DEFAULT_BUFFERS,DTOLLIB_OLDAAPI_DLL,DTOLLIB_OLMEM_DLL, ...).- Full
DtolErrorhierarchy +ErrorContext— every subclass from docs/design.md §17.3 declared, none raised by Phase 0 code. - Public
StrEnumsurface —DataFlow,SubsystemType,SubsystemState,BufferState,IOType,SensorStatus,Edge,WrapMode,QueueStrategy,ClockSource,RetriggerMode. SyncPortal— blocking portal for the Phase 2 sync facade.- Sink Protocols (
ReadingSink,BlockSink) +InMemorySink. - Streaming policy types (
ErrorPolicy,OverflowPolicy,AcquisitionSummary,Recording[T]). - CI lanes (lint, typecheck, test matrix on Win/Ubuntu/macOS × py3.13 + py3.14, build, docs, release).
- Five CLI entry-point stubs (
dtol-discover,dtol-diag,dtol-capture,dtol-read,dtol-info) that print a "not yet implemented — see Phase N" message and exit 2.
Not yet (deferred to later phases)¶
capi/package and any SDK function binding. Phase 1.DtolBackendProtocol,DataAcqBackend,FakeDtolBackend. Phase 1.find_devices(),dtol-discoverreal output,dtol-diagreal checks. Phase 1.TaskSpec,ChannelSpec,ThermocoupleInput,open_device,DtolSession,DtolManager, scalarpoll(). Phase 2.- Continuous AI, the §12.3.2 callback bridge,
BufferPlan,record(), durable sinks. Phase 3.