Skip to content

Instantly share code, notes, and snippets.

@hansemro
Last active August 9, 2023 04:26
Show Gist options
  • Save hansemro/4b8733cf5a217bcee7a5796f8eff2726 to your computer and use it in GitHub Desktop.
Save hansemro/4b8733cf5a217bcee7a5796f8eff2726 to your computer and use it in GitHub Desktop.
[WIP] Siglent BIN waveform viewer
#!/usr/bin/env python3
# SPDX-License-Identifier: MIT
# Copyright (c) 2023 Hansem Ro
# Siglent BIN waveform viewer
# Tasks:
# - [ ] Support v0/v1 waveform
# - [ ] Process analog waveform
# - [ ] Process digital waveform
# - [x] Support v2 waveform
# - [x] Process analog waveform
# - [x] Process math waveform
# - [x] Process digital waveform
# - untested, assumes little endian bit order
# - [x] Parse code_per_div from bin file
# - Tested with SDS2000X+ 1.3.9R12
# - 8-bit/10-bit ADC modes supported
# - [x] Support v4 waveform
# - [x] Process analog waveform
# - [x] Process math waveform
# - [x] Process digital waveform
# - [ ] Process memory waveform?
# - [ ] Process zoom waveform?
# - [ ] Refactor code
# Usage:
# 0. Install dependencies:
# `pip install matplotlib`
# 1. Run script with a bin file:
# `python3 siglent_bin_viewer.py <BIN_FILE>`
from argparse import ArgumentParser
import matplotlib.pyplot as plt
import struct
class SiglentChannel:
def __init__(self, name:str, enabled:bool):
self.name = name
self.enabled = enabled
class SiglentAnalogChannel(SiglentChannel):
def __init__(self, name:str, enabled:bool, volt_div_val:float, vert_offset:float, probe_factor:float, code_per_div:float):
super().__init__(name, enabled)
self.volt_div_val = volt_div_val
self.vert_offset = vert_offset
self.probe_factor = probe_factor
self.code_per_div = code_per_div
class SiglentDigitalChannel(SiglentChannel):
def __init__(self, name:str, enabled:bool):
super().__init__(name, enabled)
class SiglentMathChannel(SiglentChannel):
def __init__(self, name:str, enabled:bool, vdiv_val:float, vert_offset:float, wave_length:int, f_time:float):
super().__init__(name, enabled)
self.vdiv_val = vdiv_val
self.vert_offset = vert_offset
self.wave_length = wave_length
self.f_time = f_time
class SiglentBIN:
"""
Siglent BIN waveform
"""
def __init__(self, bin_file:str):
self.bin_file = bin_file
self.version = None
self.analog_channels = []
self.digital_on = False
self.digital_channels = []
self.math_channels = []
self.time_div = None
self.time_delay = None
self.wave_length = None
self.s_rate = None
self.digital_wave_length = None
self.digital_s_rate = None
self.data_width = None
self.byte_order = None
self.num_hori_div = None
self.parse_bin()
def __parse_version_2__(self, data:bytearray):
"""
Spec details starting at page 23 of E02A document.
E02A: https://web.archive.org/web/20230730072643/https://www.siglenteu.com/wp-content/uploads/2021/08/How-to-Extract-Data-from-the-Binary-File.pdf
Model+FW that exports version 2 binary data:
- SDS5000X
- FW: 0.8.6 to 0.9.6
- SDS2000X Plus
- FW: 1.2.6 to 1.3.9RX
- SDS6000A
- FW older than 1.4.1.0
- SDS6000 H10 Pro
- FW older than 1.4.1.0
- SDS6000 H12 Pro
- FW older than 1.4.1.0
"""
version = struct.unpack('<l', data[0x0:0x4])[0]
assert version == 2
for i in range(0,4):
# enable
# ch1: [0x4:0x4+4]
# ch2: [0x8:0x8+4]
# ch3: [0xc:0xc+4]
# ch4: [0x10:0x10+4]
ch_on = struct.unpack('<l', data[i*4+0x4:(i+1)*4+0x4])[0] == 1
print(f"ch{i+1}_on: {ch_on}")
# volt_div_val
# ch1: [0x14:0x14+8]
# ch2: [0x3c:0x3c+8]
# ch3: [0x64:0x64+8]
# ch4: [0x8c:0x8c+8]
ch_volt_div_val = struct.unpack('<d', data[i*0x28+0x14:i*0x28+0x14+8])[0]
print(f"ch{i+1}_volt_div_val: {ch_volt_div_val}")
# vert_offset
# ch1: [0xb4:0xb4+8]
# ch2: [0xdc:0xdc+8]
# ch3: [0x104:0x104+8]
# ch4: [0x12c:0x12c+8]
ch_vert_offset = struct.unpack('<d', data[i*0x28+0xb4:i*0x28+0xb4+8])[0]
print(f"ch{i+1}_vert_offset: {ch_vert_offset}")
# probe_factor
# ch1: [0x240:0x240+8]
# ch2: [0x248:0x248+8]
# ch3: [0x250:0x250+8]
# ch4: [0x258:0x258+8]
ch_probe = struct.unpack('<d', data[i*8+0x240:(i+1)*8+0x240])[0]
print(f"ch{i+1}_probe: {ch_probe}")
# code_per_div
# ch1: [0x26c:0x26c+4]
# ch2: [0x270:0x270+4]
# ch3: [0x274:0x274+4]
# ch4: [0x278:0x278+4]
ch_code_per_div = struct.unpack('<l', data[i*4+0x26c:(i+1)*4+0x26c])[0]
print(f"ch{i+1}_code_per_div: {ch_code_per_div}")
ch = SiglentAnalogChannel(name=f"C{i+1}", enabled=ch_on,
volt_div_val=ch_volt_div_val,
vert_offset=ch_vert_offset,
probe_factor=ch_probe,
code_per_div=ch_code_per_div)
self.analog_channels += [ch]
self.digital_on = struct.unpack('<l', data[0x154:0x158])[0] == 1
print(f"digital_on: {self.digital_on}")
for i in range(0, 16):
start = i * 4 + 0x158
end = start + 4
d_ch_on = struct.unpack('<l', data[start:end])[0] == 1
d_ch = SiglentDigitalChannel(name=f"D{i}", enabled=d_ch_on)
self.digital_channels += [d_ch]
print(f"d{i}_on: {d_ch_on}")
self.time_div = struct.unpack('<d', data[0x198:0x1a0])[0]
self.time_delay = struct.unpack('<d', data[0x1c0:0x1c8])[0]
self.wave_length = struct.unpack('<l', data[0x1e8:0x1ec])[0]
self.s_rate = struct.unpack('<d', data[0x1ec:0x1f4])[0]
self.digital_wave_length = struct.unpack('<l', data[0x214:0x218])[0]
# typo in E02A: digital_s_rate is not between 0x208-0x20f
self.digital_s_rate = struct.unpack('<d', data[0x218:0x220])[0]
print(f"time_div: {self.time_div}")
print(f"time_delay: {self.time_delay}")
print(f"wave_length: {self.wave_length}")
print(f"s_rate: {self.s_rate}")
print(f"digital_wave_length: {self.digital_wave_length}")
print(f"digital_s_rate: {self.digital_s_rate}")
data_width = struct.unpack('<B', data[0x260:0x261])[0]
if data_width == 0:
print("data_width: byte")
self.data_width = 1
elif data_width == 1:
print("data_width: word (2 bytes)")
self.data_width = 2
else:
print(f"unknown data_width: {data_width}")
# byte order of waveform data
byte_order = struct.unpack('<B', data[0x261:0x262])[0]
if byte_order == 0:
self.byte_order = 'little'
else:
self.byte_order = 'big'
print(f"byte_order: {self.byte_order}")
self.num_hori_div = struct.unpack('<l', data[0x268:0x26c])[0]
print(f"num_hori_div: {self.num_hori_div}")
for i in range(0, 4):
# math_on:
# math1: [0x27c:0x27c+4]
# math2: [0x280:0x280+4]
# math3: [0x284:0x284+4]
# math4: [0x288:0x288+4]
math_on = struct.unpack('<l', data[i*4+0x27c:(i+1)*4+0x27c])[0] == 1
print(f"math{i+1}_on: {math_on}")
# math_vdiv_val
# math1: [0x28c:0x28c+8]
# math2: [0x2b4:0x2b4+8]
# math3: [0x2dc:0x2dc+8]
# math4: [0x304:0x304+8]
math_vdiv_val = struct.unpack('<d', data[i*0x28+0x28c:i*0x28+0x28c+8])[0]
print(f"math{i+1}_vdiv_val: {math_vdiv_val}")
# math_vpos_val
# math1: [0x32c:0x32c+8]
# math2: [0x354:0x354+8]
# math3: [0x37c:0x37c+8]
# math4: [0x3a4:0x3a4+8]
math_vpos_val = struct.unpack('<d', data[i*0x28+0x32c:i*0x28+0x32c+8])[0]
print(f"math{i+1}_vpos_val: {math_vpos_val}")
# math_store_len (wave_length)
# math1: [0x3cc:0x3cc+4]
# math2: [0x3d0:0x3d0+4]
# math3: [0x3d4:0x3d4+4]
# math4: [0x3d8:0x3d8+4]
math_store_len = struct.unpack('<l', data[i*0x28+0x3cc:i*0x28+0x3cc+4])[0]
print(f"math{i+1}_store_len: {math_store_len}")
# math_f_time (time between samples)
# math1: [0x3dc:0x3dc+8]
# math2: [0x3e4:0x3e4+8]
# math3: [0x3ec:0x3ec+8]
# math4: [0x3f4:0x3f4+8]
math_f_time = struct.unpack('<d', data[i*8+0x3dc:(i+1)*8+0x3dc])[0]
print(f"math{i+1}_f_time: {math_f_time}")
math_ch = SiglentMathChannel(name=f"Math{i+1}", enabled=math_on,
vdiv_val=math_vdiv_val,
vert_offset=math_vpos_val,
wave_length=math_store_len,
f_time=math_f_time)
self.math_channels += [math_ch]
# math_code_per_div
self.math_code_per_div = struct.unpack('<l', data[0x3fc:0x400])[0]
print(f"math_code_per_div: {self.math_code_per_div}")
wave_data = data[0x800:]
print(f"len of wave_data: {len(wave_data)}")
center = 2**(8*self.data_width - 1) - 1
wave_idx = 0
for ch in self.analog_channels:
if ch.enabled:
start = wave_idx * self.data_width * self.wave_length
end = (wave_idx+1) * self.data_width * self.wave_length
ch_wave_data = wave_data[start:end]
print(f"{ch.name} Waveform:")
print(f"\tstart: {start}")
print(f"\tend: {end}")
time = []
voltages = []
v_gain = ch.volt_div_val * ch.probe_factor / ch.code_per_div
print(f"\tv_gain: {v_gain}")
# process data
for i in range(0, self.wave_length):
value = int.from_bytes(ch_wave_data[i*self.data_width:(i+1)*self.data_width], byteorder=self.byte_order, signed=False)
volt = float(value - center) * v_gain - ch.vert_offset
t = i / self.s_rate
time += [t]
voltages += [volt]
# plot waveform
plt.figure()
plt.xlabel('time')
plt.ylabel('V')
plt.plot(time, voltages)
plt.show()
wave_idx += 1
analog_end = end
wave_idx = 0
for ch in self.math_channels:
if ch.enabled:
start = wave_idx * self.data_width * ch.wave_length + analog_end
end = (wave_idx+1) * self.data_width * ch.wave_length + analog_end
ch_wave_data = wave_data[start:end]
print(f"{ch.name} Waveform:")
print(f"\tstart: {start}")
print(f"\tend: {end}")
time = []
voltages = []
v_gain = ch.vdiv_val / self.math_code_per_div
print(f"\tv_gain: {v_gain}")
# process data
for i in range(0, ch.wave_length):
value = int.from_bytes(ch_wave_data[i*self.data_width:(i+1)*self.data_width], byteorder=self.byte_order, signed=False)
volt = float(value - center) * v_gain - ch.vert_offset
t = i * ch.f_time
time += [t]
voltages += [volt]
# plot waveform
plt.figure()
plt.xlabel('time')
plt.ylabel('V')
plt.plot(time, voltages)
plt.show()
wave_idx += 1
math_end = end
wave_idx = 0
for ch in self.digital_channels:
if ch.enabled:
start = (wave_idx * self.digital_wave_length // 8) + math_end
end = ((wave_idx+1) * self.digital_wave_length // 8) + math_end
ch_wave_data = wave_data[start:end]
print(f"{ch.name} Waveform:")
print(f"\tstart: {start}")
print(f"\tend: {end}")
time = []
values = []
# process data
for i in range(0, self.wave_length, 8):
byte_value = int.from_bytes(ch_wave_data[i:i+1], byteorder=self.byte_order, signed=False)
for j in range(0, 8):
value = byte_value & 0x1
byte_value = byte_value >> 1
t = (i*8 + j) / self.digital_s_rate
time += [t]
values += [value]
# plot waveform
plt.figure()
plt.xlabel('time')
plt.ylabel('value')
plt.plot(time, values)
plt.show()
wave_idx += 1
def __parse_version_4__(self, data:bytearray):
"""
Spec details starting at page 37 of E02A document.
E02A: https://web.archive.org/web/20230730072643/https://www.siglenteu.com/wp-content/uploads/2021/08/How-to-Extract-Data-from-the-Binary-File.pdf
Mostly the same as version 2, except a 4-byte data offset field was
inserted at 0x4, shifting everything down by 4 bytes. Waveform data
begins at 0x1000.
Model+FW that exports version 4 binary data:
- SDS5000X
- FW newer than 0.9.6
- SDS2000X Plus
- FW newer than 1.4.0
- SDS2000X HD
- FW: 1.2.1.1 (?)
- SDS6000A
- FW newer than 1.4.1.0
- SDS6000 H10 Pro
- FW newer than 1.4.1.0
- SDS6000 H12 Pro
- FW newer than 1.4.1.0
"""
version = struct.unpack('<l', data[0x0:0x4])[0]
assert version == 4
data_offset_byte = struct.unpack('<l', data[0x4:0x8])[0]
print(f"data_offset_byte: {data_offset_byte}")
for i in range(0,4):
# enable
# ch1: [0x8:0x8+4]
# ch2: [0xc:0xc+4]
# ch3: [0x10:0x10+4]
# ch4: [0x14:0x14+4]
ch_on = struct.unpack('<l', data[i*4+0x8:(i+1)*4+0x8])[0] == 1
print(f"ch{i+1}_on: {ch_on}")
# volt_div_val
# ch1: [0x18:0x18+8]
# ch2: [0x40:0x40+8]
# ch3: [0x68:0x68+8]
# ch4: [0x90:0x90+8]
ch_volt_div_val = struct.unpack('<d', data[i*0x28+0x18:i*0x28+0x18+8])[0]
print(f"ch{i+1}_volt_div_val: {ch_volt_div_val}")
# vert_offset
# ch1: [0xb8:0xb8+8]
# ch2: [0xe0:0xe0+8]
# ch3: [0x108:0x108+8]
# ch4: [0x130:0x130+8]
ch_vert_offset = struct.unpack('<d', data[i*0x28+0xb8:i*0x28+0xb8+8])[0]
print(f"ch{i+1}_vert_offset: {ch_vert_offset}")
# probe_factor
# ch1: [0x244:0x244+8]
# ch2: [0x24c:0x24c+8]
# ch3: [0x254:0x254+8]
# ch4: [0x25c:0x25c+8]
ch_probe = struct.unpack('<d', data[i*8+0x244:(i+1)*8+0x244])[0]
print(f"ch{i+1}_probe: {ch_probe}")
# code_per_div
# ch1: [0x270:0x270+4]
# ch2: [0x274:0x274+4]
# ch3: [0x278:0x278+4]
# ch4: [0x27c:0x27c+4]
ch_code_per_div = struct.unpack('<l', data[i*4+0x26c:(i+1)*4+0x26c])[0]
print(f"ch{i+1}_code_per_div: {ch_code_per_div}")
ch = SiglentAnalogChannel(name=f"C{i+1}", enabled=ch_on,
volt_div_val=ch_volt_div_val,
vert_offset=ch_vert_offset,
probe_factor=ch_probe,
code_per_div=ch_code_per_div)
self.analog_channels += [ch]
self.digital_on = struct.unpack('<l', data[0x158:0x15c])[0] == 1
print(f"digital_on: {self.digital_on}")
for i in range(0, 16):
d_ch_on = struct.unpack('<l', data[i*4 + 0x15c:(i+1)*4+0x15c])[0] == 1
d_ch = SiglentDigitalChannel(name=f"D{i}", enabled=d_ch_on)
self.digital_channels += [d_ch]
print(f"d{i}_on: {d_ch_on}")
self.time_div = struct.unpack('<d', data[0x19c:0x19c+8])[0]
self.time_delay = struct.unpack('<d', data[0x1c4:0x1c4+8])[0]
self.wave_length = struct.unpack('<l', data[0x1ec:0x1ec+4])[0]
self.s_rate = struct.unpack('<d', data[0x1f0:0x1f0+8])[0]
self.digital_wave_length = struct.unpack('<l', data[0x218:0x218+4])[0]
self.digital_s_rate = struct.unpack('<d', data[0x21c:0x21c+8])[0]
print(f"time_div: {self.time_div}")
print(f"time_delay: {self.time_delay}")
print(f"wave_length: {self.wave_length}")
print(f"s_rate: {self.s_rate}")
print(f"digital_wave_length: {self.digital_wave_length}")
print(f"digital_s_rate: {self.digital_s_rate}")
data_width = struct.unpack('<B', data[0x264:0x265])[0]
if data_width == 0:
print("data_width: byte")
self.data_width = 1
elif data_width == 1:
print("data_width: word (2 bytes)")
self.data_width = 2
else:
print(f"unknown data_width: {data_width}")
# byte order of waveform data
byte_order = struct.unpack('<B', data[0x265:0x266])[0]
if byte_order == 0:
self.byte_order = 'little'
else:
self.byte_order = 'big'
print(f"byte_order: {self.byte_order}")
self.num_hori_div = struct.unpack('<l', data[0x26c:0x26c+4])[0]
print(f"num_hori_div: {self.num_hori_div}")
for i in range(0, 4):
# math_on:
# math1: [0x280:0x280+4]
# math2: [0x284:0x284+4]
# math3: [0x288:0x288+4]
# math4: [0x28c:0x28c+4]
math_on = struct.unpack('<l', data[i*4+0x280:(i+1)*4+0x280])[0] == 1
print(f"math{i+1}_on: {math_on}")
# math_vdiv_val
# math1: [0x290:0x290+8]
# math2: [0x2b8:0x2b8+8]
# math3: [0x2e0:0x2e0+8]
# math4: [0x308:0x308+8]
math_vdiv_val = struct.unpack('<d', data[i*0x28+0x290:i*0x28+0x290+8])[0]
print(f"math{i+1}_vdiv_val: {math_vdiv_val}")
# math_vpos_val
# math1: [0x330:0x330+8]
# math2: [0x358:0x358+8]
# math3: [0x380:0x380+8]
# math4: [0x3a8:0x3a8+8]
math_vpos_val = struct.unpack('<d', data[i*0x28+0x330:i*0x28+0x330+8])[0]
print(f"math{i+1}_vpos_val: {math_vpos_val}")
# math_store_len (wave_length)
# math1: [0x3d0:0x3d0+4]
# math2: [0x3d4:0x3d4+4]
# math3: [0x3d8:0x3d8+4]
# math4: [0x3dc:0x3dc+4]
math_store_len = struct.unpack('<l', data[i*0x28+0x3d0:i*0x28+0x3d0+4])[0]
print(f"math{i+1}_store_len: {math_store_len}")
# math_f_time (time between samples)
# math1: [0x3e0:0x3e0+8]
# math2: [0x3e8:0x3e8+8]
# math3: [0x3f0:0x3f0+8]
# math4: [0x3f8:0x3f8+8]
math_f_time = struct.unpack('<d', data[i*8+0x3e0:(i+1)*8+0x3e0])[0]
print(f"math{i+1}_f_time: {math_f_time}")
math_ch = SiglentMathChannel(name=f"Math{i+1}", enabled=math_on,
vdiv_val=math_vdiv_val,
vert_offset=math_vpos_val,
wave_length=math_store_len,
f_time=math_f_time)
self.math_channels += [math_ch]
# math_code_per_div
self.math_code_per_div = struct.unpack('<l', data[0x400:0x400+4])[0]
print(f"math_code_per_div: {self.math_code_per_div}")
wave_data = data[0x1000:]
print(f"len of wave_data: {len(wave_data)}")
center = 2**(8*self.data_width - 1) - 1
wave_idx = 0
for ch in self.analog_channels:
if ch.enabled:
start = wave_idx * self.data_width * self.wave_length
end = (wave_idx+1) * self.data_width * self.wave_length
ch_wave_data = wave_data[start:end]
print(f"{ch.name} Waveform:")
print(f"\tstart: {start}")
print(f"\tend: {end}")
time = []
voltages = []
v_gain = ch.volt_div_val * ch.probe_factor / ch.code_per_div
print(f"\tv_gain: {v_gain}")
# process data
for i in range(0, self.wave_length):
value = int.from_bytes(ch_wave_data[i*self.data_width:(i+1)*self.data_width], byteorder=self.byte_order, signed=False)
volt = float(value - center) * v_gain - ch.vert_offset
t = i / self.s_rate
time += [t]
voltages += [volt]
# plot waveform
plt.figure()
plt.xlabel('time')
plt.ylabel('V')
plt.plot(time, voltages)
plt.show()
wave_idx += 1
analog_end = end
wave_idx = 0
for ch in self.math_channels:
if ch.enabled:
start = wave_idx * self.data_width * ch.wave_length + analog_end
end = (wave_idx+1) * self.data_width * ch.wave_length + analog_end
ch_wave_data = wave_data[start:end]
print(f"{ch.name} Waveform:")
print(f"\tstart: {start}")
print(f"\tend: {end}")
time = []
voltages = []
v_gain = ch.vdiv_val / self.math_code_per_div
print(f"\tv_gain: {v_gain}")
# process data
for i in range(0, ch.wave_length):
value = int.from_bytes(ch_wave_data[i*self.data_width:(i+1)*self.data_width], byteorder=self.byte_order, signed=False)
volt = float(value - center) * v_gain - ch.vert_offset
t = i * ch.f_time
time += [t]
voltages += [volt]
# plot waveform
plt.figure()
plt.xlabel('time')
plt.ylabel('V')
plt.plot(time, voltages)
plt.show()
wave_idx += 1
math_end = end
wave_idx = 0
for ch in self.digital_channels:
if ch.enabled:
start = (wave_idx * self.digital_wave_length // 8) + math_end
end = ((wave_idx+1) * self.digital_wave_length // 8) + math_end
ch_wave_data = wave_data[start:end]
print(f"{ch.name} Waveform:")
print(f"\tstart: {start}")
print(f"\tend: {end}")
time = []
values = []
# process data
for i in range(0, self.wave_length, 8):
byte_value = int.from_bytes(ch_wave_data[i:i+1], byteorder=self.byte_order, signed=False)
for j in range(0, 8):
value = byte_value & 0x1
byte_value = byte_value >> 1
t = (i*8 + j) / self.digital_s_rate
time += [t]
values += [value]
# plot waveform
plt.figure()
plt.xlabel('time')
plt.ylabel('value')
plt.plot(time, values)
plt.show()
wave_idx += 1
def parse_bin(self):
with open(self.bin_file, "rb") as bf:
d = bf.read()
print(f"bin length: {len(d)}")
version = struct.unpack('<l', d[0x0:0x4])[0]
print(f"version: {version}")
if version == 2:
self.__parse_version_2__(d)
elif version == 4:
self.__parse_version_4__(d)
else:
print(f"unsupported version: {version}")
def create_parser():
parser = ArgumentParser()
parser.add_argument('bin',
type=str,
help='Input Siglent BIN waveform file')
return parser
if __name__ == '__main__':
args = create_parser().parse_args()
sb = SiglentBIN(args.bin)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment