A Julia package for reading TwinCAT 3 Scope View .svb binary recording files.
.svb is a binary export format of the TwinCAT 3 Scope View application (Beckhoff Automation). This package is a Julia port of the Python python-tcscopeview which itself was influenced by CagTayFabry/pytcs library, replacing pandas/xarray with DataFrames.jl.
Scope: Only SVB file reading is implemented.
[NOTE] This package was mostly generated with AI assistance (GitHub Copilot / Claude). Code has been reviewed and tested by the maintainer, but may contain errors or incomplete edge-case handling. Contributions and bug reports are welcome.
Julia 1.9 or newer is required (tested on 1.12).
| Package | Role |
|---|---|
DataFrames≥ 1.0 | Multi-channel tabular output (as_dataframe) |
Dates | stdlib — DateTime timestamps |
Printf | stdlib — display formatting |
Only files exported with the default BIN-Properties are supported:
| Export option | Required state |
|---|---|
| Standard binary header | ✅ Enabled — header must be embedded in the same .svb file |
| Exclude header to separate file | ❌ Disabled — separate header files are not supported |
| Scale values before export | ❌ Disabled — raw integer values must be stored; scaling is applied by this library using the Offset and Scalefactor fields in the channel header |
| Exclude time information | ❌ Disabled — every data point must include its UInt32 relative timestamp |
# From the Julia REPL, with this package on a local path:using Pkg
Pkg.develop(path ="/path/to/twincat-scope-view-jl")
# Or, once registered:
Pkg.add("TcScopeView")using TcScopeView
# Open a file — reads headers only, no sample data loaded yet
svb =SvbFile("recording.svb")
show(svb)
# <SvbFile 'recording'># file : recording.svb# start : 2024-03-15T10:30:00.123# end : 2024-03-15T10:30:01.123# runtime : 1 second# channels (21):# [REAL32] var_REAL32: (unloaded) @ 1.000 ms# [INT16 ] var_INT16: (unloaded) @ 1.000 ms# ...# Load all channel data into memoryload!(svb)
# Or load only specific channelsload!(svb; channels = ["var_REAL32", "var_INT16"])# Number of channelslength(svb) # 21# Check membership"var_REAL32"in svb # true# Iterate channel names (in file order)for name in svb
println(name)
end# Access a single channel by name
ch = svb["var_REAL32"]
name(ch) # "var_REAL32"data_type(ch) # "REAL32"sample_time(ch) # 1.0 (milliseconds)sample_time_ms(ch) # 1.0 (alias)# Iterate (name, channel) pairsfor (n, ch) inchannel_items(svb)
println("$n$(length(ch.values)) samples")
end# Iterate channels onlyfor ch inchannel_values(svb)
println(name(ch))
end# Dict{String, SvbChannel} for direct named access
d =channels(svb) # Dict{String, SvbChannel}
d["var_REAL32"] # SvbChannelch = svb["var_REAL32"]
# Raw vectors (after load!)
ch.time_ms # Vector{Float64} — time in ms from recording start
ch.values # AbstractVector — Float32, Int16, … or Float64 if scaled# Named tuple view (same vectors, no copy)
s =as_series(ch)
s.name # "var_REAL32"
s.time_ms # same object as ch.time_ms
s.values # same object as ch.valuesas_dataframe outer-joins all channels onto a shared time axis. Gaps where a channel
has no sample at a given time are filled with missing.
# ── :timestamp (default) ─────────────────────────────────────────────────────# Time column is DateTime (UTC), binned to 1 ms.# Channels sampled faster than 1 ms are decimated (last sample in each ms bin).
df =as_dataframe(svb)
# 1000×22 DataFrame# Row │ time var_REAL32 var_INT16 …# │ DateTime Union{Float32,Mis… Union{Int1…# ────┼─────────────────────────────────────────────────────────# 1 │ 2024-03-15T10:30:00.123 0.0 0# 2 │ 2024-03-15T10:30:00.124 1.0 1# …# ── :timedelta ───────────────────────────────────────────────────────────────# Time column is Float64 milliseconds from recording start.# Sub-millisecond precision is preserved; row count is driven by the# highest-rate channel.
df =as_dataframe(svb; time_fmt =:timedelta)
# 4996×22 DataFrame (driven by a 0.2 ms channel)# Row │ time var_REAL32 var_INT16 …# │ Float64 Union{Float32,Mis… Union{Int16,Missin…# ────┼─────────────────────────────────────────────────# 1 │ 0.0 0.0 0# 2 │ 0.2 missing missing# 3 │ 0.4 missing missing# 4 │ 0.6 missing missing# 5 │ 0.8 missing missing# 6 │ 1.0 1.0 1# …# ── subset of channels ───────────────────────────────────────────────────────
df =as_dataframe(svb; channels = ["var_INT16", "var_REAL32"])
# 1000×3 DataFrame — time + 2 columnsd =as_dict(svb)
# Keys: "name", "file", "start_time", "run_time", "channels"
d["name"] # recording name string
d["start_time"] # DateTime
d["run_time"] # Millisecond# Channel sub-dict
ch_dict = d["channels"] # Dict{String, Dict{String, Any}}
ch_dict["var_REAL32"] # Dict with "time_ms", "values", header fields, …using Dates
# Convert a Windows FILETIME integer to DateTimefiletime_to_dt(130_000_000_000_000_000) # DateTime(2013, 5, 9, …)# Format a duration for displayfmt_duration(3672.5) # "1 hour 1 minute"fmt_duration(svb.run_time) # e.g. "1 second"# Convert a time_ms vector to absolute DateTime valuesto_datetime_from_ms(ch.time_ms, svb.start_time) # Vector{DateTime}Raw sample values are stored in the type declared in the channel header. Engineering-
unit scaling (eng = raw × Scalefactor + Offset) is applied automatically at load time
when either field is non-identity.
SVB DataType | Julia type (unscaled) | Julia type (scaled) |
|---|---|---|
BIT, UINT8 | UInt8 | Float64 |
INT8 | Int8 | Float64 |
INT16 | Int16 | Float64 |
INT32 | Int32 | Float64 |
INT64 | Int64 | Float64 |
UINT16 | UInt16 | Float64 |
UINT32 | UInt32 | Float64 |
UINT64 | UInt64 | Float64 |
REAL32 | Float32 | Float64 |
REAL64 | Float64 | Float64 |
In a DataFrame, each channel column has element type Union{T, Missing} to
accommodate the outer-join gaps from mixed sample rates.
| Symbol | Description |
|---|---|
SvbFile(path) | Open an SVB file; read headers only |
load!(sf; channels) | Load sample data; channels can be a String, Vector{String}, or nothing (all) |
sf[name] | Access a SvbChannel by name; throws KeyError if not found |
name in sf | Membership test |
length(sf) | Number of channels |
iterate(sf) | Iterate channel names in file order |
channel_items(sf) | Iterator of (name, SvbChannel) pairs |
channel_values(sf) | Iterator of SvbChannel values |
channels(sf) | Dict{String, SvbChannel} of all channels |
as_dataframe(sf; channels, time_fmt) | Outer-joined DataFrame; time_fmt is :timestamp (default) or :timedelta |
as_dict(sf) | Nested Dict{String, Any} |
| Symbol | Description |
|---|---|
name(ch) | Channel name string |
data_type(ch) | SVB data type string, e.g. "REAL32" |
sample_time(ch) | Nominal sample period in ms |
sample_time_ms(ch) | Alias for sample_time |
as_series(ch) | NamedTuple with time_ms, values, name; throws if not loaded |
ch.time_ms | Vector{Float64} of sample times in ms (after load!) |
ch.values | AbstractVector of sample values (after load!) |
ch.header | ChannelHeader with all metadata fields |
cd twincat-scope-view-jl
julia --project=. test/runtests.jlExpected output: 521 passed.