การเขียนโปรแกรม Python เชื่อมต่อ Rigol DS2072A (ผ่านเครือข่าย LAN)#
บทความนี้กล่าวถึง ตัวอย่างการตั้งค่าออสซิลโลสโคป RIGOL DS2072A เพื่อใช้งานผ่านระบบเครือข่าย Ethernet / LAN และเขียนโปรแกรมเชื่อมต่อโดยใช้โพรโทคอล TCP/IP ตามมาตรฐานของ LXI
Keywords: Rigol DS2072A, Digital Storage Oscilloscope, LXI, Python Programming
▷ Rigol DS2072A#
ในบทความนี้จะกล่าวถึงการเขียนโค้ด Python เพื่อเชื่อมต่อกับออสซิลโลสโคปของบริษัท Rigol Technologies Inc. รุ่น DS2072A และการใช้งานในระบบเครือข่าย TCP/IP ผ่าน Ethernet/LAN
อุปกรณ์ DS2072A มี 2 ช่องสัญญาณอินพุต และเชื่อมต่อผ่านพอร์ต LAN ได้ รองรับ LXI (LAN eXtensions for Instrumentation) ซึ่งเป็นมาตรฐานการสื่อสารผ่านเครือข่าย Ethernet สำหรับอุปกรณ์เครื่องมือวัด และพัฒนาโดยกลุ่ม LXI Consortium
คุณสมบัติเบื้องต้นของ Rigol DS2072A (ในปัจจุบันถือว่า อุปกรณ์รุ่นนี้ เลิกจำหน่ายแล้ว):
- ช่องสัญญาณอินพุต: 2 ช่อง (CH1 และ CH2)
- Analog Bandwidth: 70 MHz (อัปเกรดได้ถึง 300 MHz)
- Sampling Rate: 2 GSamples/s (Single-channel) | 1GSamples/s (Dual-channel)
- Memory Depth (max.): 14 Megapoints (Standard) | 56 Megapoints (Upgraded)
- Display: 8-inch TFT (800x480) WVGA
- Vertical Resolution: 8-bit
- Interface: USB และ LAN (Ethernet)
การเตรียมการเชื่อมต่อผ่าน LAN มีขั้นตอนดังนี้
- เชื่อมต่อสาย LAN จาก Rigol DS2072A เข้ากับเครือข่ายเดียวกับคอมพิวเตอร์ของผู้ใช้
- ไปที่เมนู Utility > I/O Setting > LAN Config เพื่อดูหรือกำหนดค่า IP Address ของออสซิลโลสโคป
- ตรวจสอบว่า IP Address ของ DS2072A สามารถคำสั่ง
ping
ได้จากเครื่องคอมพิวเตอร์
รูป: การตรวจสอบโมเดลของสโคป
รูป: การตรวจสอบการตั้งค่าสำหรับการเชื่อมต่อเครือข่าย
การตั้งค่าการใช้งาน DS2072A โดยการส่งคำสั่งผ่าน LXI สามารถจำแนกตามฟังก์ชันการใช้งาน เช่น
- การกำหนดโหมดการควบคุมระหว่าง Local (ควบคุมจากปุ่มกดหน้าจอของเครื่อง) และ Remote (ควบคุมจากระยะไกลผ่านเครือข่าย)
- การตั้งค่าเชิงเวลา (Timebase) ได้แก่:
- สเกลเชิงเวลา (Timescale หรือ Time/Div) เพื่อกำหนดความละเอียดของแกนเวลา เช่น
0.001s/div
- ออฟเซตเวลา (Time Offset) ซึ่งหมายถึงการเลื่อนศูนย์กลางของคลื่นสัญญาณบนหน้าจอ ไปทางซ้ายหรือขวา
- สเกลเชิงเวลา (Timescale หรือ Time/Div) เพื่อกำหนดความละเอียดของแกนเวลา เช่น
- การตั้งค่าสำหรับแต่ละช่องสัญญาณอินพุต (CH1 และ CH2):
- การเลือกค่า Probe Attenuation Factor ตามสายวัด (โพรบ) ที่ใช้งาน เช่น
1X
และ10X
- การตั้งค่าแรงดันไฟฟ้า:
- Voltage Scale (แรงดันต่อหนึ่งช่องบนหน้าจอภาพ) เช่น
1V/div
- Voltage Offset (ค่าออฟเซตจากระดับ GND) ซึ่งใช้เลื่อนระดับของคลื่นสัญญาณบนหน้าจอภาพ ขึ้นหรือลง
- Voltage Scale (แรงดันต่อหนึ่งช่องบนหน้าจอภาพ) เช่น
- การเลือกค่า Probe Attenuation Factor ตามสายวัด (โพรบ) ที่ใช้งาน เช่น
- การตั้งค่าสำหรับทริกเกอร์ (Trigger Settings):
- Trigger Source: ช่องสัญญาณอินพุตที่ใช้สำหรับการทริกเกอร์ เช่น
CH1
และCH2
- Trigger Mode (หรือที่เรียกว่า Sweep Mode): โหมดการจับสัญญาณ ได้แก่
AUTO
,NORMAL
,SINGLE
- Trigger Type: ประเภทของเงื่อนไขของทริกเกอร์ เช่น ขอบสัญญาณ (EDGE), ความกว้างของพัลส์ (PULSE), หรืออื่น ๆ
- Trigger Slope: กำหนดเหตุการณ์ของทริกเกอร์บนขอบของสัญญาณ เช่น
RISING
(ขอบขาขึ้น) หรือFALLING
(ขอบขาลง)
- Trigger Source: ช่องสัญญาณอินพุตที่ใช้สำหรับการทริกเกอร์ เช่น
- การกำหนดขนาดของหน่วยความจำ (Memory Depth) สำหรับการจับและจัดเก็บข้อมูลของคลื่นสัญญาณ ซึ่งมีผลต่อความละเอียดของข้อมูลที่ได้ เช่น
14000
,140000
และ1400000
เป็นต้น หากเปิดใช้งานหนึ่งช่องอินพุต แต่ถ้าเปิดช่องสัญญาณอินพุตพร้อมกัน 2 ช่อง ขนาดจะลดครึ่งหนึ่ง เป็น7000
,70000
และ7000000
ตามลำดับ
คำสั่งตามรูปแบบที่เรียกว่า SCPI และเกี่ยวข้องกับการตั้งค่าใช้งาน DS2072A สามารถศึกษาได้จากเอกสารต่อไปนี้:
▷ ตัวอย่างการเขียนโค้ดภาษา Python#
การเชื่อมต่อกับสโคปตามรูปแบบ LXI โดยใช้ภาษา Python ก็มีไลบรารี เช่น
python-vxi11
ให้ใช้งาน
ดังนั้นให้ทำคำสั่ง pip
เพื่อติดตั้งไลบรารี
$ pip install python-vxi11
เริ่มต้นด้วยโค้ดตัวอย่าง เพื่อลองเชื่อมต่อกับสโคป โดยจะต้องระบุหมายเลข IP ของสโคปที่กำลังเชื่อมต่อกับระบบเครือข่าย
โค้ดตัวอย่างนี้สาธิตการอ่านข้อมูลเกี่ยวกับสโคป เช่น โมเดล และหมายเลขเครื่อง โดยใช้คำสั่ง *IDN?
File: rigol_lxi_idn.py
import time
import vxi11
IP_ADDR = '10.42.0.19'
instr = vxi11.Instrument(IP_ADDR)
instr.timeout = 1.0 # Change timeout to 1.0 second
try:
instr.write('*IDN?')
time.sleep(0.1)
result = instr.read().strip()
values = result.split(',')
if len(values) >= 3:
print( f'Vendor : {values[0]}')
print( f'Model : {values[1]}')
print( f'S/N : {values[2]}')
except OSError as ex:
print( f'Cannot connect to the device at IP: {IP_ADDR}' )
instr.close()
ตัวอย่างข้อความเอาต์พุต
Vendor : RIGOL TECHNOLOGIES
Model : DS2302A
S/N : DS2D154700781
▷ ตัวอย่างการสร้างคลาสในภาษา Python เพื่อใช้งานกับ DS2072A#
ถัดไปเป็นตัวอย่างการสร้างคลาสในภาษา Python เพื่อนำไปใช้งานกับสโคป DS2072A
File: rigol_ds2000.py
import time
import vxi11
import numpy as np
class DS2000A:
def __init__(self, ip_addr):
self.ip_addr = ip_addr
self.instr = vxi11.Instrument(ip_addr)
self.instr.timeout = 1.0
self.remote()
self.run()
self.set_mem_depth(7000)
def set_mem_depth(self, mem_depth):
# Memory depth (dual-channel): 7000 | 70000
self.write(f':ACQ:MDEP {mem_depth}')
_ = self.get_mem_depth()
def get_mem_depth(self):
self.mem_depth = int(self.read(':ACQ:MDEP?'))
return self.mem_depth
def close(self):
self.instr.close()
def write(self, cmd, dly=0.05):
self.instr.write(cmd)
time.sleep(dly)
def read(self, cmd, dly=0.1):
self.write(cmd, dly)
return self.instr.read().strip()
def read_raw(self, cmd, dly=0.1):
self.write(cmd, dly)
return self.instr.read_raw()
def get_idn(self):
return self.read('*IDN?').split(',')
def begin(self):
idn = self.get_idn()
if idn[1] != 'DS2302A':
print('Expected the model DS2302A')
self.remote()
def remote(self):
self.write('SYSTem:REMote')
def config_channel(self, chan, config):
if chan not in [1, 2]:
print('Invalid channel number')
return
self.write(f':CHAN{chan}:COUP {config["coupling"]}')
self.write(f':CHAN{chan}:PROB {int(config["probe_ratio"])}')
self.write(f':CHAN{chan}:SCAL {float(config["scale"])}')
self.write(f':CHAN{chan}:OFFS {float(config["offset"])}')
status = 'ON' if config['enabled'] else 'OFF'
self.write(f':CHAN{chan}:DISP {status}')
def config_timebase(self, config):
self.write(f':TIM:OFFS {config["offset"]}')
self.write(f':TIM:SCAL {config["timescale"]}')
def config_trigger(self, config):
self.write(f':TRIG:EDG:SOUR CHAN{config["chan"]}')
mode = config["mode"].upper()
self.write(f':TRIG:MODE {mode}')
if mode == 'EDGE':
self.write(f':TRIG:EDG:LEV {config["level"]}')
self.write(f':TRIG:EDG:SLOP {config["slope"]}')
self.write(f':TRIG:SWE {config["sweep"]}')
def get_waveform_params(self, chan=1):
self.write( f':WAV:SOUR CHAN{chan}' )
# Get waveform preamble information
preamble = self.read(':WAV:PRE?')
p = preamble.split(',')
if len(p) != 10:
print('Waveform preamble parse error!')
return None, None
npoints = int(p[2])
xinc, xorg, xref = float(p[4]), float(p[5]), float(p[6])
yinc, yorg, yref = float(p[7]), float(p[8]), float(p[9])
params = {
'npoints': npoints,
'xinc': xinc, 'xorg': xorg, 'xref': xref,
'yinc': yinc, 'yorg': yorg, 'yref': yref
}
return params
def get_waveform(self, chan=1, mode='RAW'):
mode = mode.upper()
if mode not in ['RAW', 'NORMAL']:
print( 'Expected mode: RAW or NORMAL')
return None, None
self.write(f':WAV:SOUR CHAN{chan}')
self.write(':WAV:FORM BYTE')
if mode == 'RAW':
self.write(':WAV:MODE RAW')
self.write(':WAV:STAR 1')
self.write(f':WAV:STOP {self.mem_depth}')
else:
self.write(':WAV:MODE NORM')
self.write(':WAV:POIN 1400')
self.write(':WAV:RES')
self.write(':WAV:BEG')
params = self.get_waveform_params(chan)
yinc = params['yinc']
yref = params['yref']
xinc = params['xinc']
if mode == 'RAW':
retries = 0
done = False
while True:
status, points = self.read(':WAV:STAT?').strip().split(',')
if status == 'IDLE':
done = True
break
else:
retries += 1
if retries > 20:
break
print('Waiting for data reading..' )
time.sleep(0.5)
if done:
rawdata = self.read_raw(':WAV:DATA?', 0.2)
self.write(':WAV:END') # stop waveform reading
num_bytes = int( rawdata[2:11] )
rawdata = rawdata[11:-1]
data = np.frombuffer( rawdata, 'B' )
data = (data - yref) * yinc
npoints = len(data)
xstart = -(npoints/2) * xinc
xstop = (npoints/2) * xinc
else:
print('Invalid or missing waveform data!')
return None, None
else: # Normal mode
rawdata = self.read_raw(':WAV:DATA?')
self.write(':WAV:END')
if rawdata and rawdata.startswith(b'#'):
header_len = int(rawdata[1:2])
byte_count = int(rawdata[2:2 + header_len])
wav_data = rawdata[2+header_len : 2+header_len+byte_count]
data = np.frombuffer(wav_data, dtype='B')
data = (data - yref) * yinc
npoints = len(data)
xstart = params['xref'] + params['xorg']
xstop = xstart + (npoints * xinc)
else:
print('Invalid or missing waveform data!')
return None, None
ts = np.linspace(xstart, xstop, num=npoints, endpoint=False )
return ts, data
def show_settings(self):
mem_depth = int(self.read(':ACQ:MDEP?'))
sample_rate = float(self.read(':ACQ:SRAT?'))
time_per_div = float(self.read(':TIM:SCAL?'))
time_offset = float(self.read(':TIM:OFFS?'))
ch1_volt_per_div = float(self.read(':CHAN1:SCAL?'))
ch1_vert_offset = float(self.read(':CHAN1:OFFS?'))
ch2_volt_per_div = float(self.read(':CHAN2:SCAL?'))
ch2_vert_offset = float(self.read(':CHAN2:OFFS?'))
print( 'Memory Depth : ', mem_depth )
print( 'Sample Rate : ', sample_rate/1e6, 'MHz' )
print( 'Time/Div : ', time_per_div )
print( 'Time Offset : ', time_offset )
print( 'Volt/Div CH1 : ', ch1_volt_per_div )
print( 'Volt Offset CH1 : ', ch1_vert_offset )
print( 'Volt/Div CH2 : ', ch2_volt_per_div )
print( 'Volt Offset CH2 : ', ch2_vert_offset )
def run(self):
self.write(':RUN')
def is_stopped(self):
return self.read('TRIG:STAT?') == 'STOP'
def stop(self):
self.write(':STOP')
def local(self):
self.write('SYST:LOC')
โค้ดต่อไปนี้สาธิต การใช้คลาส DS2072A
เพื่อตั้งค่าการใช้งานออสซิลโลสโคป DS2072A
และอ่านข้อมูลจากสโคป เพื่อนำมาแสดงรูปกราฟ จำนวน 2 ช่องสัญญาณพร้อมกัน
File: test_ds2072a.py
# Date: 2025-04-24
import time
import matplotlib.pyplot as plt
from rigol_ds2000 import DS2000A
def main():
# Connect to the RIGOL scope at the specified IPv4 address.
scope = DS2000A('10.42.0.19')
try:
scope.begin()
# Set memory depth
scope.set_mem_depth( 70000 )
# Configure the time base
scope.config_timebase({
'offset': 0.000, 'timescale': 5e-4,
})
# Configure the trigger
scope.config_trigger({
'chan': 1, 'mode': 'EDGE', 'slope': 'NEG', 'level': 0.5,
'sweep': 'NORMAL',
})
# Configure CH1 and CH2
cfg = {'enabled': True,
'probe_ratio': 1.0, 'coupling': 'DC',
'scale': 1.0, 'offset': 0.0 }
cfg1 = cfg.copy(); cfg1['offset'] = 1.0
cfg2 = cfg.copy(); cfg2['offset'] = -3.0
scope.config_channel(1, cfg1)
scope.config_channel(2, cfg2)
scope.show_settings()
# Run the scope
scope.run()
time.sleep(2)
# Stop the scope
scope.stop()
mode = 'RAW' # use RAW mode for waveform data
# Read waveform data for CH1
ts, ch1_data = scope.get_waveform(1, mode)
# Read waveform data for CH2
_, ch2_data = scope.get_waveform(2, mode)
if (ts[-1] < 1e-3):
ts = ts * 1e6
ts_unit = "usec"
elif (ts[-1] < 1.0):
ts = ts * 1e3
ts_unit = "msec"
else:
ts_unit = "sec"
npoints = len(ch1_data)
plt.figure(figsize=(7,4))
plt.plot(ts, ch1_data, ts, ch2_data)
plt.title(f'Waveform Plot (#samples = {npoints})')
plt.xlabel(f'Time [{ts_unit}]')
plt.ylabel('Voltage [V]')
plt.grid(True)
plt.tight_layout()
plt.savefig('plot.png', dpi=200)
plt.show()
print('Done.')
finally:
scope.run()
scope.local()
scope.close()
if __name__ == '__main__':
main()
ถัดไปเป็นตัวอย่างคลื่นสัญญาณ (Waveform) แสดงผลบนหน้าจอภาพของสโคป
ซึ่งจะเห็นได้ว่า ช่องสัญญาณ CH1
และ CH2
ใช้สัญญาณทดสอบเหมือนกัน
และมีการตั้งค่าช่องสัญญาณเหมือนกัน เช่น 1V / Div และ 500usec / Div
แต่ต่างกันที่การตั้งค่าออฟเซตของแรงดันไฟฟ้าสำหรับการแสดงผลบนหน้าจอภาพ
(CH1
เลื่อนขึ้นไปอยู่ที่ระดับ +1V และ CH2
เลื่อนลงไปอยู่ที่ระดับ -3V)
สัญญาณทดสอบเป็นคลื่นพัลส์ที่มีความถี่ 1kHz หรือ คาบเท่ากับ 1000usec หรือ 1msec มีระดับแรงดันไฟฟ้าอยู่ระหว่าง 0V กับ 3V
รูป: ตัวอย่างรูปคลื่นสัญญาณที่บันทึกเป็นไฟล์ .png ใน USB Flash Device ซึ่งได้จากการกดปุ่ม Quick Print ของสโคป
รูป: ตัวอย่างรูปคลื่นสัญญาณที่แสดงผลโดยการทำงานของโค้ด Python (มีจำนวนข้อมูล 70,000 ตัวเลข สำหรับแต่ละช่องสัญญาณ)
อีกกรณีหนึ่งเป็นตัวอย่างการเปิดใช้ช่องสัญญาณ CH1
เพียงช่องเดียว
ตั้งค่าสำหรับเงื่อนไขทริกเกอร์เป็นขอบขาขึ้น มีการตั้งค่าสเกลเวลา (Time/Div)
เท่ากับ 100usec ต่อหนึ่งช่อง สโคปจะปรับอัตราสุ่มสัญญาณ (Sample Rate)
เป็น 100.0MHz
รูป: ตัวอย่างรูปคลื่นสัญญาณหนึ่งช่อง
รูป: ตัวอย่างรูปคลื่นสัญญาณหนึ่งช่อง (จำนวนข้อมูล 140,000) ซึ่งได้จากการทำงานของโค้ด Python
▷ ตัวอย่างการสร้าง GUI สำหรับควบคุมการใช้งานสโคป#
ถัดไปเป็นตัวอย่างการเขียนโค้ดโดยใช้ Python Tk เพื่อใช้งานแบบ GUI ในลักษณะเป็น Control Panel เพื่อให้ผู้ใช้ตั้งค่าการใช้งานสโคปได้ง่ายขึ้น
File: rigol_control_panel.py
import tkinter as tk
from tkinter import ttk, messagebox
from functools import partial
from rigol_ds2000 import DS2000A
DEFAULT_IP = '10.42.0.19'
class ScopeControlPanel:
def __init__(self, root):
self.root = root
self.root.title("Rigol DS2072A Control Panel")
self.scope = None
# Configure root grid weights for resizing
self.root.columnconfigure(0, weight=1)
self.root.columnconfigure(1, weight=1)
for i in range(5):
self.root.rowconfigure(i, weight=1)
self.create_connection_block()
self.create_timebase_block()
self.create_channel_block()
self.create_trigger_block()
def create_connection_block(self):
frame = ttk.Frame(self.root)
frame.grid(row=0, column=0, columnspan=2,
sticky='ew', padx=10, pady=10)
frame.columnconfigure(0, weight=1)
self.ip_entry = ttk.Entry(frame)
self.ip_entry.insert(0, DEFAULT_IP)
self.ip_entry.grid(row=0, column=0,
sticky='ew', padx=(0, 10))
self.connect_btn = ttk.Button(frame, text="Connect",
command=self.connect)
self.connect_btn.grid(row=0, column=1)
def connect(self):
ip = self.ip_entry.get()
try:
self.scope = DS2000A(ip)
self.scope.begin()
messagebox.showinfo("Connection", f"Connected to {ip}")
except Exception as e:
messagebox.showerror("Connection Failed", str(e))
def create_timebase_block(self):
frame = ttk.LabelFrame(self.root, text="Time Base")
frame.grid(row=1, column=0, columnspan=2,
sticky='ew', padx=10, pady=5)
frame.columnconfigure((0, 1), weight=1)
time_div_label = ttk.Label(frame, text="Scale (s/div):")
time_div_label.grid(row=0, column=0, sticky='e', padx=5, pady=2)
self.time_scale = ttk.Entry(frame)
self.time_scale.insert(0, "0.001")
self.time_scale.grid(row=0, column=1, sticky='ew', padx=5, pady=2)
time_offset_label = ttk.Label(frame, text="Offset (s):")
time_offset_label.grid(row=1, column=0, sticky='e', padx=5, pady=2)
self.time_offset = ttk.Entry(frame)
self.time_offset.insert(0, "0")
self.time_offset.grid(row=1, column=1, sticky='ew', padx=5, pady=2)
apply_btn = ttk.Button(frame, text="Apply", command=self.set_timebase)
apply_btn.grid(row=2, column=0, columnspan=2, pady=5)
def create_channel_block(self):
for ch in [1, 2]:
frame = ttk.LabelFrame(self.root, text=f"Channel {ch}")
frame.grid(row=2, column=ch - 1, sticky='nsew', padx=10, pady=5)
for i in range(2):
frame.columnconfigure(i, weight=1)
setattr(self, f'ch{ch}_enabled', tk.BooleanVar(value=True))
check_btn = ttk.Checkbutton(frame, text="Enabled",
variable=getattr(self,f'ch{ch}_enabled'))
check_btn.grid(row=0, column=0, columnspan=2, sticky='w', pady=(0,5))
self.create_labeled_entry(frame, "Coupling", "DC",
row=1, col=0, var_name=f'ch{ch}_coupling')
self.create_labeled_entry(frame, "Scale (V/div)", "0.5",
row=2, col=0, var_name=f'ch{ch}_scale')
self.create_labeled_entry(frame, "Offset (V)", "0",
row=3, col=0, var_name=f'ch{ch}_offset')
self.create_labeled_entry(frame, "Probe Ratio", "1",
row=4, col=0, var_name=f'ch{ch}_probe')
apply_btn = ttk.Button(frame, text="Apply",
command=partial(self.set_channel, ch))
apply_btn.grid(row=5, column=0, columnspan=2, pady=5)
def create_trigger_block(self):
frame = ttk.LabelFrame(self.root, text="Trigger")
frame.grid(row=3, column=0, columnspan=2, sticky='ew', padx=10, pady=5)
frame.columnconfigure((0, 1), weight=1)
self.create_labeled_entry(frame, "Channel (1/2)", "1",
row=0, col=0, var_name='trig_chan')
self.create_labeled_entry(frame, "Mode", "EDGE",
row=1, col=0, var_name='trig_mode')
self.create_labeled_entry(frame, "Level (V)", "0",
row=2, col=0, var_name='trig_level')
self.create_labeled_entry(frame, "Slope (POS/NEG)", "POS",
row=3, col=0, var_name='trig_slope')
self.create_labeled_entry(frame, "Sweep (AUTO/NORM/SING)", "AUTO",
row=4, col=0, var_name='trig_sweep')
apply_btn = ttk.Button(frame, text="Apply", command=self.set_trigger)
apply_btn.grid(row=5, column=0, columnspan=2, pady=5)
def create_labeled_entry(self, frame, label, default, row, col, var_name):
label = ttk.Label(frame, text=label + ":")
label.grid(row=row, column=col, sticky='e', padx=5, pady=2)
entry = ttk.Entry(frame)
entry.insert(0, default)
entry.grid(row=row, column=col + 1, sticky='ew', padx=5, pady=2)
setattr(self, var_name, entry)
def set_timebase(self):
if not self.scope:
return
config = {
'timescale': float(self.time_scale.get()),
'offset': float(self.time_offset.get())
}
self.scope.config_timebase(config)
def set_channel(self, ch):
if not self.scope:
return
config = {
'enabled': getattr(self, f'ch{ch}_enabled').get(),
'coupling': getattr(self, f'ch{ch}_coupling').get(),
'scale': float(getattr(self, f'ch{ch}_scale').get()),
'offset': float(getattr(self, f'ch{ch}_offset').get()),
'probe_ratio': float(getattr(self, f'ch{ch}_probe').get())
}
self.scope.config_channel(ch, config)
def set_trigger(self):
if not self.scope:
return
config = {
'chan': int(self.trig_chan.get()),
'mode': self.trig_mode.get(),
'level': float(self.trig_level.get()),
'slope': self.trig_slope.get(),
'sweep': self.trig_sweep.get()
}
self.scope.config_trigger(config)
if __name__ == "__main__":
root = tk.Tk()
root.geometry("560x680")
app = ScopeControlPanel(root)
root.mainloop()
รูป: ตัวอย่างการใช้งานในรูปแบบ GUI
▷ ตัวอย่างการสร้าง Web App#
ถัดไปเป็นตัวอย่างการสร้างแอปพลิเคชันอย่างง่าย เพื่อเชื่อมต่อกับสโคป เพื่อตั้งค่าใช้งานและอ่านข้อมูลคลื่นสัญญาณ เพื่อนำไปแสดงรูปกราฟบนหน้าเว็บ โดยได้ลองใช้ Plotly Dash ซึ่งเป็นเฟรมเวิร์กแบบโอเพ่นซอร์สที่ใช้สร้างเว็บแอปพลิเคชันเชิงโต้ตอบ (Interactive Web Apps) เขียนโค้ดด้วยภาษา Python และใช้ Plotly.js React.js และ Flask Web Framework ในการพัฒนา ผู้ที่สนใจสามารถศึกษารายละเอียดและตัวอย่างการใช้งานได้จาก: Dash Python User Guide
ในการใช้งาน Plotly Dash จะต้องทำคำสั่งติดตั้งไลบรารีก่อน
$ pip install dash plotly
File: rigol_web_plot.py
import time
import dash
from dash import dcc, html
from dash.dependencies import Input, Output
import plotly.graph_objects as go
from rigol_ds2000 import DS2000A
scope = None
def init_scope():
global scope
scope = DS2000A('10.42.0.19')
scope.begin()
scope.set_mem_depth(2 * 700000)
scope.config_timebase({'offset': 0.000, 'timescale': 1e-3})
scope.config_trigger(
{'chan': 1, 'sweep': 'AUTO',
'mode': 'EDGE', 'slope': 'POS', 'level': 1.5})
cfg = {'enabled': True, 'probe_ratio': 1.0,
'coupling': 'DC', 'scale': 1.0, 'offset': 0.0}
scope.config_channel(1, cfg)
scope.config_channel(2, {**cfg, 'enabled': False})
def capture_waveform():
scope.run()
time.sleep(1)
scope.stop()
ts, data = scope.get_waveform(1, 'RAW')
if ts[-1] < 1e-3:
ts = ts * 1e6
ts_unit = "μs"
elif ts[-1] < 1.0:
ts = ts * 1e3
ts_unit = "ms"
else:
ts_unit = "s"
return ts, data, ts_unit
# Dash app layout
app = dash.Dash('Rigol Scope Waveform Visualization with Python - Dash')
app.layout = html.Div([
html.H2("Rigol DS2000A Waveform Viewer",
style={'textAlign': 'center'}),
dcc.Graph(id='waveform-plot'),
html.Div([
html.Button("Capture Waveform", id='capture-button', n_clicks=0)
], style={'textAlign': 'center', 'padding': '10px'})
])
@app.callback(
Output('waveform-plot', 'figure'),
Input('capture-button', 'n_clicks')
)
def update_graph(n_clicks):
ts, data, ts_unit = capture_waveform()
fig = go.Figure()
fig.add_trace(go.Scatter(x=ts, y=data, mode='lines', name='CH1'))
fig.update_layout(
title=f'Waveform Plot (#samples = {len(data)})',
xaxis_title=f'Time {ts_unit}',
yaxis_title='Voltage [V]',
template='plotly_white',
height=500,
dragmode='zoom', # Enable zoom tool
xaxis=dict(
fixedrange=False # Allow zoom on x-axis (time)
),
yaxis=dict(
fixedrange=True # Lock zoom on y-axis (voltage)
)
)
return fig
if __name__ == '__main__':
try:
init_scope()
app.run(debug=False)
finally:
if scope:
scope.local()
scope.close()
เมื่อรันโค้ดตัวอย่างได้สำเร็จ ให้เปิดเว็บเบราว์เซอร์ ไปยัง http://127.0.0.1:8050/
และมีตัวอย่างหน้าเว็บดังนี้
รูป: ตัวอย่าง GUI App สร้างโดยใช้ Python Ploty Dash
▷ กล่าวสรุป#
บทความนี้ได้นำเสนอแนวทางการใช้งาน RIGOL DS2072A ออสซิลโลสโคปแบบดิจิทัล ที่รองรับการควบคุมผ่านเครือข่าย LAN ด้วยมาตรฐาน LXI โดยใช้ภาษา Python ในการเขียนโค้ด มีการสาธิตการใช้คำสั่งในรูปแบบที่เรียกว่า SCPI เพื่อกำหนดค่าต่าง ๆ ให้กับสโคป เช่น Trigger, Time/Div และ Volt/Divของแต่ละช่องสัญญาณ
โค้ดตัวอย่างแสดงให้เห็นว่า ผู้ใช้สามารถควบคุมการตั้งค่าและดึงข้อมูลสำหรับ Waveform จากทั้ง 2 ช่องสัญญาณของออสซิลโลสโคป เพื่อนำไปแสดงผลหรือวิเคราะห์บนคอมพิวเตอร์ได้อย่างสะดวก นอกจากนั้นแล้ว ยังมีตัวอย่างที่สาธิตการสร้าง GUI App และ Web App สำหรับการตั้งค่าใช้งานสโคป และการแสดงรูปคลื่นสัญญาณในเบื้องต้น
This work is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License.
Created: 2025-04-23 | Last Updated: 2025-04-24