Skip to content

Latest commit

History

2 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

TcScopeView.jl

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.


Requirements

Julia version

Julia 1.9 or newer is required (tested on 1.12).

Dependencies

PackageRole
DataFrames≥ 1.0Multi-channel tabular output (as_dataframe)
Datesstdlib — DateTime timestamps
Printfstdlib — display formatting

TwinCAT export settings

Only files exported with the default BIN-Properties are supported:

Export optionRequired state
Standard binary headerEnabled — header must be embedded in the same .svb file
Exclude header to separate fileDisabled — separate header files are not supported
Scale values before exportDisabled — raw integer values must be stored; scaling is applied by this library using the Offset and Scalefactor fields in the channel header
Exclude time informationDisabled — every data point must include its UInt32 relative timestamp

Installation

# 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")

Quick Start

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"])

Usage

Inspecting channels

# 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"] # SvbChannel

Working with a single channel

ch = 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.values

Getting a DataFrame

as_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 columns

Getting a plain Dict

d =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, …

Helper utilities

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}

Data Types

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 DataTypeJulia type (unscaled)Julia type (scaled)
BIT, UINT8UInt8Float64
INT8Int8Float64
INT16Int16Float64
INT32Int32Float64
INT64Int64Float64
UINT16UInt16Float64
UINT32UInt32Float64
UINT64UInt64Float64
REAL32Float32Float64
REAL64Float64Float64

In a DataFrame, each channel column has element type Union{T, Missing} to accommodate the outer-join gaps from mixed sample rates.


API Reference

SvbFile

SymbolDescription
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 sfMembership 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}

SvbChannel

SymbolDescription
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_msVector{Float64} of sample times in ms (after load!)
ch.valuesAbstractVector of sample values (after load!)
ch.headerChannelHeader with all metadata fields

Running the Tests

cd twincat-scope-view-jl
julia --project=. test/runtests.jl

Expected output: 521 passed.

About

A Julia package for reading TwinCAT 3 Scope View .svb binary recording files.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Contributors

Languages