A universal reader, analyzer and viewer for oscilloscope .bin files — the SPBXDS
container format used by OWON, Hanmatek, AKIP and other scopes built on the same OEM
platform.
This project is a fork of RobThree/OwonBinfileReader, the original C# library for OWON scope binfiles (still used here as-is, and still published under its original name on NuGet). It grew into ScopeBinReader after discovering that the
SPBXDScontainer is shared across vendors, but the metadata schema inside it is not. The .NET library remains the parsing core (Python package name:scopebin); a Python layer adds multi-vendor support, analysis tools and a GUI on top of it. Much of the format knowledge here is reverse engineered — don't use it to build a nuclear power plant or launch people into space!
Just want to open a .bin file? Grab the latest Windows build — no Python, no .NET, no
setup:
➡️ Download the latest release
— download scopebin-view.exe, double-click it, File → Open .bin…. That's it.
(Building from source, the CLI, and the C# library are documented below for developers.)
- 🧩 One container, many vendors. A single schema-agnostic parser reads the
SPBXDScontainer; small pluggable profiles interpret each vendor's JSON metadata and calibrate raw samples into real volts and seconds. - 🔎 Auto-detection. The right profile is chosen automatically from the file's
IDN/MODELand the shape of its JSON. - 🖥️ Oscilloscope-style GUI. Dark theme, per-channel colors, draggable time & voltage cursors with live ΔT / 1÷ΔT / ΔV readouts, zoom & pan.
- 🛠️ CLI for
inspect,csv,plot,overlayandfft. - 📦 Single-file Windows
.exe— bundles the .NET bridge, so it runs with nothing installed. - 🧪 Cross-validated: 27 C# tests + 6 Python tests, covering both vendor schemas.
.
├── dotnet/ # Everything C#
│ ├── OwonBinfileReader/ # Core library (NuGet) — container + OWON schema
│ │ └── Raw/ # Schema-agnostic RawContainerReader (the parsing core)
│ ├── OwonBinfileReader.Tests/
│ ├── BinfileDump/ # Tiny CLI: dumps a .bin into raw parts (the Py↔C# bridge)
│ └── TestApp/ # Upstream console demo (CSV export)
├── python/ # Everything Python
│ ├── scopebin/ # The scopebin package
│ │ ├── bridge.py # Invokes BinfileDump, loads segments into numpy
│ │ ├── profiles/ # akip.py · owon.py · generic.py (+ auto-detect)
│ │ ├── api.py · model.py # read() → normalized Waveform
│ │ ├── cli.py · gui.py # command line + Tk/matplotlib viewer
│ │ └── units.py
│ ├── app.py # PyInstaller entry point (the viewer)
│ └── tests/
├── samples/ # Example capture (150-1.bin, an AKIP-4122/7 pulse)
├── scripts/ # build_windows_exe.ps1
└── README.md
Download the latest release
(or build it yourself — see Building the Windows exe) and just run
it — no Python, no .NET required. File → Open .bin…, or drop a path on the command line:
scopebin-view.exe samples\150-1.bin
- Drag the vertical (time) and horizontal (voltage) cursors to measure; the side panel
shows the values, ΔT, frequency
1÷ΔT, ΔV and each channel's value at both cursors. - Scroll to zoom the time axis, Shift+Scroll to zoom voltage, or use the matplotlib toolbar (box-zoom / pan / home).
- Toggle channels with the checkboxes.
conda create -n scopebin python=3.11 numpy pandas matplotlib pytest
conda activate scopebin
pip install -e python
scopebin view samples/150-1.bin # GUI
scopebin inspect samples/150-1.bin # metadata + channel summary
scopebin csv samples/150-1.bin -o out.csv
scopebin plot samples/150-1.bin -o out.png
scopebin overlay a.bin b.bin c.bin # several captures on one time axis
scopebin fft samples/150-1.bin # magnitude spectrum of a channelIn development mode the Python side shells out to the
BinfileDump.NET tool (building it on first use), so a .NET 8 SDK is needed then. The packaged.exebundles it instead.
import scopebin
wf = scopebin.read("samples/150-1.bin")
print(wf.vendor, wf.model, wf.idn) # -> akip 320202103 AKIP,AKIP-4122/7,...
ch = wf.channels[0]
ch.t, ch.v # numpy arrays: time (s), value (V)The .NET core is still usable standalone for OWON/Hanmatek files:
using OwonBinfileReader;
var bf = await new BinfileReader().ReadAsync(@"/path/to/file.bin");
// bf.MetaData -> scope settings
// bf.Measurements -> ReadOnlyDictionary<int, double[]> per displayed channelFor files whose JSON schema BinfileReader doesn't understand, use the vendor-agnostic
container reader and interpret the metadata yourself:
using OwonBinfileReader.Raw;
var raw = await new RawContainerReader().ReadAsync(@"/path/to/file.bin");
// raw.RawJson -> the metadata JSON, untouched
// raw.Segments -> one raw Int16[] per data segment
// raw.Tail -> optional trailing INFO block ┌──────────────────────────────────────────────────────────────┐
│ C# core (single source of truth for the binary container) │
│ RawContainerReader → BinfileDump CLI: │
│ magic · JSON header · N int16 segments · INFO tail │
└───────────────────────────────┬──────────────────────────────┘
raw JSON + raw int16 segments (subprocess bridge)
┌───────────────────────────────▼──────────────────────────────┐
│ Python (scopebin) │
│ profile auto-detect → calibrate → normalized Waveform │
│ akip · owon · generic (t in seconds, v in volts) │
│ → CLI · GUI · CSV · FFT │
└──────────────────────────────────────────────────────────────┘
The container is the same everywhere; the calibration differs per vendor, which is why the raw-parse and the interpretation are split:
| Vendor | JSON channel key | Sample → value formula |
|---|---|---|
| OWON | CHANNEL (DISPLAY, Current_Ratio, Current_Rate) |
value = raw₁₆ × (Current_Ratio ÷ Current_Rate) |
| AKIP | channel (Display_Switch, Reference_Zero, Voltage_Rate) |
value = (raw₁₆ ÷ 16 − Reference_Zero) × Voltage_Rate |
| unknown | (heuristic) | reuses whichever known fields it finds, else raw uncalibrated codes |
Drop a module in python/scopebin/profiles/ exposing two functions and register it:
NAME = "myvendor"
def matches(meta: dict) -> bool:
... # True if this profile recognizes the JSON schema
def build_channels(meta: dict, segments: list[np.ndarray]) -> list[ChannelWaveform]:
... # calibrate raw int16 segments into t (s) / v (V)Add it to _VENDOR_PROFILES in profiles/__init__.py. generic.py is always the last-resort
fallback, so an unknown file still opens (uncalibrated if need be) rather than failing.
The .bin file is a binary container with this structure:
| Offset | Bytes | Value | Meaning |
|---|---|---|---|
| 0 | 6 | SPBXDS |
Magic header |
| 6 | 4 | JSON length | Int32 length of the following JSON metadata |
| 10 | Json Length | JSON data | Metadata about the capture (schema varies per vendor) |
| 10 + Json Length | 4 | Data Length | Int32 length of the data segment (bytes) |
| 14 + Json Length | Data length | Data | Int16 little-endian samples, Data Length ÷ 2 of them |
The Data Length + Data block repeats for each captured channel. An optional segment may
follow the data (End Of Data):
| Offset | Bytes | Value | Meaning |
|---|---|---|---|
| EOD | 4 | INFO |
Additional information marker |
| EOD + 4 | 8 | Unknown | Two Int32 values |
| EOD + 12 | 19 | Date | ISO8601 date (YYYY-MM-DD HH:MM:SS) |
| EOD + 31 | 11 | Unknown | Usually zeroes |
RawContainerReader captures this whole trailing block verbatim as Tail.
OWON / Hanmatek uses uppercase keys and per-channel Current_Ratio/Current_Rate:
{
"TIMEBASE": { "SCALE": "200ms", "HOFFSET": 120 },
"SAMPLE": { "DATALEN": 1520, "SAMPLERATE": "(2.5MS/s)", "DEPMEM": "10M" },
"CHANNEL": [
{ "NAME": "CH1", "DISPLAY": "ON", "COUPLING": "DC", "PROBE": "10X",
"SCALE": "50.0mV", "OFFSET": -201, "Current_Rate": 10000, "Current_Ratio": 0.78125 }
],
"IDN": "OWON,XDS3104AE,2308149,V4.0.0", "MODEL": "310401101"
}AKIP uses a lowercase channel array with Reference_Zero + Voltage_Rate, and may keep
data for channels whose Display_Switch is OFF:
{
"MODEL": "320202103", "IDN": "AKIP,AKIP-4122/7,24230571,V7.6.0",
"channel": [
{ "Index": "CH1", "Display_Switch": "OFF", "Sample_Rate": "(2.5MS/s)",
"Hscale": "200us", "Vscale": "500mV", "Reference_Zero": "-29",
"Adc_Data_Time": "0.400000us", "Voltage_Rate": "1.250000mv", "Data_Length": "10000" }
]
}AKIP samples are an 8-bit ADC code shifted left by 4 bits (i.e. stored int16 values are multiples of 16), which is why the AKIP formula divides by 16 before applying the zero reference. Note also that only the active channel tends to be written to the file, even if several were on screen — worth knowing if you expected two traces and got one.
The Python layer always returns SI base units: seconds for time and volts for value.
The C# BinfileReader similarly normalizes to base units (mV→V, µs→s, …) and parses
what it can into enums / bools / timespans, represented as double.
From an environment that has scopebin and pyinstaller installed, and with a .NET 8
SDK available:
conda activate scopebin
pip install pyinstaller
scripts\build_windows_exe.ps1This does two things:
- Publishes the bridge (
BinfileDump) as a self-contained, single-filewin-x64executable — the .NET runtime is embedded, so no .NET install is needed to run. - Runs PyInstaller to bundle the Python viewer + that bridge into one file:
dist/scopebin-view.exe.
Useful switches:
| Switch | Effect |
|---|---|
| (default) | one file, with console (startup errors visible) |
-OneDir |
one folder in dist/scopebin-view/ — faster startup, easier to inspect |
-Windowed |
no console window (nicer for double-click, hides stdout) |
-Python <path> |
pick a specific interpreter (e.g. the env's python.exe) |
The one-file build is ~215 MB because it embeds the .NET runtime, numpy, matplotlib and Tk.
-OneDirproduces the same total size split across a folder and starts faster.
Sanity-check a fresh build without touching your PATH:
dist\scopebin-view.exe --self-test samples\150-1.bin out.png# C# — 27 tests
dotnet test dotnet/OwonBinfileReader.sln
# Python — 6 tests (rebuilds the bridge on first run; needs the .NET 8 SDK)
python -m pytest python/testsThe OWON schema has been verified with a Hanmatek DSO1102 (a rebranded OWON SDS1102), an
OWON XDS3104AE, and various .bin files found online. The AKIP profile was built and
validated against an AKIP-4122/7 (firmware V7.6.0). Other scopes on the same platform are
likely to work — if not, a new profile is only a few lines.
Original C# library by RobThree. The logo is composed of:
MIT — see LICENSE.
