การเขียนโปรแกรม 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 มีขั้นตอนดังนี้

  1. เชื่อมต่อสาย LAN จาก Rigol DS2072A เข้ากับเครือข่ายเดียวกับคอมพิวเตอร์ของผู้ใช้
  2. ไปที่เมนู Utility > I/O Setting > LAN Config เพื่อดูหรือกำหนดค่า IP Address ของออสซิลโลสโคป
  3. ตรวจสอบว่า IP Address ของ DS2072A สามารถคำสั่ง ping ได้จากเครื่องคอมพิวเตอร์

รูป: การตรวจสอบโมเดลของสโคป

รูป: การตรวจสอบการตั้งค่าสำหรับการเชื่อมต่อเครือข่าย

การตั้งค่าการใช้งาน DS2072A โดยการส่งคำสั่งผ่าน LXI สามารถจำแนกตามฟังก์ชันการใช้งาน เช่น

  • การกำหนดโหมดการควบคุมระหว่าง Local (ควบคุมจากปุ่มกดหน้าจอของเครื่อง) และ Remote (ควบคุมจากระยะไกลผ่านเครือข่าย)
  • การตั้งค่าเชิงเวลา (Timebase) ได้แก่:
    • สเกลเชิงเวลา (Timescale หรือ Time/Div) เพื่อกำหนดความละเอียดของแกนเวลา เช่น 0.001s/div
    • ออฟเซตเวลา (Time Offset) ซึ่งหมายถึงการเลื่อนศูนย์กลางของคลื่นสัญญาณบนหน้าจอ ไปทางซ้ายหรือขวา
  • การตั้งค่าสำหรับแต่ละช่องสัญญาณอินพุต (CH1 และ CH2):
    • การเลือกค่า Probe Attenuation Factor ตามสายวัด (โพรบ) ที่ใช้งาน เช่น 1X และ 10X
    • การตั้งค่าแรงดันไฟฟ้า:
      • Voltage Scale (แรงดันต่อหนึ่งช่องบนหน้าจอภาพ) เช่น 1V/div
      • Voltage Offset (ค่าออฟเซตจากระดับ GND) ซึ่งใช้เลื่อนระดับของคลื่นสัญญาณบนหน้าจอภาพ ขึ้นหรือลง
  • การตั้งค่าสำหรับทริกเกอร์ (Trigger Settings):
    • Trigger Source: ช่องสัญญาณอินพุตที่ใช้สำหรับการทริกเกอร์ เช่น CH1 และ CH2
    • Trigger Mode (หรือที่เรียกว่า Sweep Mode): โหมดการจับสัญญาณ ได้แก่ AUTO, NORMAL, SINGLE
    • Trigger Type: ประเภทของเงื่อนไขของทริกเกอร์ เช่น ขอบสัญญาณ (EDGE), ความกว้างของพัลส์ (PULSE), หรืออื่น ๆ
    • Trigger Slope: กำหนดเหตุการณ์ของทริกเกอร์บนขอบของสัญญาณ เช่น RISING (ขอบขาขึ้น) หรือ FALLING (ขอบขาลง)
  • การกำหนดขนาดของหน่วยความจำ (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