Raw logging (.dt-raw) and replay¶
RawCountsSink is the durable, loss-proof logger unique to dtollib. It
writes the raw int16/int32 codes the SDK handed us — not the
volt-converted floats — to a self-describing .dt-raw file, so a capture is
faithfully preserved exactly as the hardware produced it and can be
re-converted (or re-linearised) later with no loss.
Why a custom format instead of TDMS: no third-party dependency, dead-simple
to read in NumPy / MATLAB / any tool that can np.fromfile, and it preserves
precisely what the SDK gave us.
Design reference: design.md §15.2.
When to use it¶
- Long unattended soaks — fed at the head of the consume loop under
OverflowPolicy.BLOCK, the sink's synchronouswrite_rawback-pressures the SDK buffer queue, so the durable file stays complete even when downstream processing falls behind. - TC / strain captures you may re-process — store raw codes now, linearise later when calibration improves, without re-running the rig.
- Archival — the JSON file header records the channels, rate, units, and encoding needed to reconstruct engineering units offline.
Writing a .dt-raw file¶
import anyio
from dtollib import (
AnalogInputVoltage, BufferPlan, DataFlow, OverflowPolicy, TaskSpec, Timing,
open_device, record,
)
from dtollib.sinks import RawCountsSink
async def main() -> None:
spec = TaskSpec(
name="soak",
channels=[AnalogInputVoltage(physical_channel=0, name="ch0")],
data_flow=DataFlow.CONTINUOUS,
timing=Timing(rate_hz=1000.0),
buffers=BufferPlan(buffers=4, samples_per_buffer=1000),
)
async with (
await open_device(spec, autostart=False) as session,
RawCountsSink("soak.dt-raw") as sink,
# BLOCK overflow makes the consume loop back-pressure the SDK queue
# rather than drop blocks — required for a loss-proof raw capture.
record(session, overflow=OverflowPolicy.BLOCK) as recording,
):
async for block in recording.stream:
await sink.write_raw(block) # durable copy of the raw codes
... # plus any live view / processing
anyio.run(main)
record() has no raw_sink parameter — the sink is fed from the consume
loop. Write each block to the sink first, before any work that could
fall behind, so the durable file stays ahead of slow downstream processing.
This is exactly what dtol-capture does internally.
From the CLI:
File format (.dt-raw v2)¶
file_header_len : uint32 (little-endian)
file_header_json: bytes # channels, rate, units, encoding, dtype
(chunk_record)*
chunk_record = chunk_header_len : uint32
+ chunk_header_json : bytes # block_index, samples, timestamps, error flags
+ chunk_payload : bytes # raw int16/int32 codes, C-order
One chunk record per DaqBlock. A crash mid-run loses at most the chunk
being written.
Replaying¶
dtollib.tools.replay_raw reads a .dt-raw file back into DaqBlocks,
re-applying the conversion captured in the header:
from dtollib.tools.replay_raw import read_file_header, iter_blocks
header = read_file_header("soak.dt-raw")
print(header.channels, header.sample_rate_hz)
for block in iter_blocks("soak.dt-raw"):
print(block.block_index, block.data.shape)
read_file_header(path) -> RawFileHeader— the file-level metadata.iter_blocks(path) -> Iterator[DaqBlock]— stream blocks back in order withraw_codespopulated. Each block'sdatais the stored codes asfloat64(not yet in engineering units); re-apply the channel ranges from the header to recover volts/temperature. Chunkflags(partial,overrun_marker) are preserved viablock.error.
Reading raw codes directly (NumPy / MATLAB)¶
The payloads are plain little-endian integers in C order, so any tool can
read them after skipping the JSON headers — read_file_header reports the
dtype and channel order needed to reshape each chunk to
(n_channels, samples_per_channel).