pyVISA でオシロスコープからデータを取得する

# はじめに

PyVISA を使うことで、オシロスコープからデータを取得することができます。インストール方法は、[PythonでVISA - Qiita](https://qiita.com/ryota23/items/cebaae35f1ed15d1d1fd) を参照してください。また、私の場合 PyVISA を使う際にエラーが出てしまったので、[【備忘録】pyvisa.errors.VisaIOError: VI_ERROR_INV_OBJECT (-1073807346): The given session or object reference is invalid.が出たとき - Qiita](https://qiita.com/grinpeaceman/items/9a580c137f1cbbfe5ba7) を参考にして NI-VISA をインストールしました。

[追記]
この記事は現在工事中です。

[工事ノート：
ascii でデータを取得するより、binary で取得する方がかなり早いです。
その際、オシロスコープのバイナリのエンコード指定については [How is binary data represented on Tektronix oscilloscopes? | テクトロニクス](https://jp.tek.com/support/faqs/how-binary-data-represented-tektronix-oscilloscopes) などを参照してください。query_binary_values() 関数に渡す is_big_endian 引数の値が分かります。また、引数 datatype の値は、DATA:WIDTH に指定したバイト数で、エンコードで指定したデータ型を用いてください。型の指定に使う文字は、 [struct — Interpret bytes as packed binary data — Python 3.8.6 documentation](https://docs.python.org/3/library/struct.html#format-characters) を参照してください。例えば、`DATA:WIDTH 2` で `DATA:ENC SRIBINARY` を指定した場合、2 byte の符号付き整数が、little endian で転送されてくるため、`query_binary_values('CURV?', datatype='h', is_big_endian=False, container=np.array)`のように指定します。ここで、`datatype='h'` が、2 byte の符号付き整数型(short) を意味しています。

pyvisa が頻繁にタイムアウトのエラーを返すときは、`query_binary_values('CURV?', datatype='h', is_big_endian=False, container=np.array, delay=0.1)` のように、`delay` 引数を指定すると良いかもしれません。この例ではオシロスコープに query を write してから、0.1 秒待った後に read します。 

オシロスコープへの詳細な設定は、各オシロスコープのプログラマーマニュアルを参照してください。例：[オシロスコープ(MSO2000B, DPO2000B, MSO2000 and DPO2000Series Oscilloscopes) のプログラマーマニュアル](http://sdpha2.ucsd.edu/Lab_Equip_Manuals/tek_dpo2000b_programming_0770738_1.pdf) プログラマーマニュアルには、オシロスコープからのデータをもとのデータに直す方法が書かれていると思います。また、pyvisa のリファレンスは [User guide — PyVISA documentation](https://pyvisa.readthedocs.io/en/latest/introduction/index.html) です。

また、以下のコードは冗長になっているため、適宜書き直していただければ幸いです。
-- ノート終わり]

# Python のコード
下記に、コードと説明のコメントを書きました。

```python
import numpy as np
import visa

rm = visa.ResourceManager()

# この場所では、rm.list_resources() の中から
# 目的の機器を選択して rm.open_resource() に渡してください
inst = rm.open_resource(rm.list_resources()[0])

# timeout の設定
scope.timeout = 30000 # 30 秒

# Setting source as Channel 1
scope.write('DATA:SOU CH1') 
scope.write('DATA:WIDTH 1') 
scope.write('DATA:ENC ASCI')

# オシロスコープから、FULL にデータを取得する設定です
# 私の環境では、この場所をコメントアウトするとデータ数が削減されるようです
scope.write('DATA:RESOLUTION FULL')

# Getting axis info
# データを取得する処理は、
# How to save Data from Oscilloscope using Python in Linux - Tech For Curious
# <https://techforcurious.website/how-to-save-data-from-oscilloscope-using-python-in-linux/#more-348> や、
# How do I get a waveform using the Instrument Control Toolbox in Matlab? | Tektronix
# <https://techforcurious.website/how-to-save-data-from-oscilloscope-using-python-in-linux/#more-348>
# を参考にしました
ymult = float(scope.query('WFMPRE:YMULT?')) # y-axis least count
yzero = float(scope.query('WFMPRE:YZERO?')) # y-axis zero error
yoff = float(scope.query('WFMPRE:YOFF?')) # y-axis offset
xincr = float(scope.query('WFMPRE:XINCR?')) # x-axis least count
xoff = float(scope.query('WFMP:PT_OFF?')) # x-axis offset
xzero = float(scope.query('WFMPRE:XZERO?')) # x-axis least count

# データを取得します。私の環境では、約 6.5 秒かかりました
ADC_wave = scope.query_ascii_values('CURV?', container=np.array)

# データ
volts = (ADC_wave - yoff) * ymult + yzero
time = np.arange(xzero, xincr * (len(Volts) - xoff) + xzero, xincr)

```

上のコードで、データをリアルタイムに取得・表示し、セーブできるようにしたものをここに載せます。

```python
import numpy as np
import matplotlib.pyplot as plt
import visa
from matplotlib import _pylab_helpers
import tkinter
from tkinter import filedialog
import threading
import csv

# セーブするかどうかを伝えるフラグ
# この変数を変更する際は、スレッドをロックしてください
is_save_requested = False

def gui(lock):
    global is_save_requested

    def change_title():
        global is_save_requested
        lock.acquire()
        is_save_requested = True
        lock.release()

    root = tkinter.Tk()

    #ボタン
    button1 = tkinter.Button(
        master=root,
        text="Save",          #初期値
        width=30,               #幅
        bg="lightblue",         #色
        command=change_title    #クリックに実行する関数
        )
    button1.pack()

    root.mainloop()

def get_data_from_inst(rm , scope):

    scope.timeout = 30000

    # Setting source as Channel 1
    scope.write('DATA:SOU CH1') 
    scope.write('DATA:WIDTH 1') 
    scope.write('DATA:ENC ASCI')
    scope.write('DATA:RESOLUTION FULL')

    # Getting axis info
    ymult = float(scope.query('WFMPRE:YMULT?')) # y-axis least count
    yzero = float(scope.query('WFMPRE:YZERO?')) # y-axis zero error
    yoff = float(scope.query('WFMPRE:YOFF?')) # y-axis offset
    xincr = float(scope.query('WFMPRE:XINCR?')) # x-axis least count
    xoff = float(scope.query('WFMP:PT_OFF?')) # x-axis offset
    xzero = float(scope.query('WFMPRE:XZERO?')) # x-axis least count

    ADC_wave = scope.query_ascii_values('CURV?', container=np.array)

    Volts = (ADC_wave - yoff) * ymult + yzero
    Time = np.arange(xzero, xincr * (len(Volts) - xoff) + xzero, xincr)
    return (Time, Volts)

def main():

    rm = visa.ResourceManager()
    inst = rm.open_resource(rm.list_resources()[0])

    # Save をトリガーする GUI のセットアップ
    global is_save_requested
    lock = threading.Lock()
    t1 = threading.Thread(target=gui, args=(lock,))
    t1.start()
    
    data = get_data_from_inst(rm, inst)
    
    x, y = data
	
    x = x * 1e6 # sec to micro sec
    y = y * 1e3 # V to mV
    
    fig, ax = plt.subplots(1, 1)
    ax.set_xlabel('time(μs)')
    ax.set_ylabel('Intensity (mV)')

    lines, = ax.plot(x, y)
    
    while True:
	
        manager = _pylab_helpers.Gcf.get_active()
        if manager is None: break
	
        # plot データの更新
        data = get_data_from_inst(rm, inst)

        x, y = data

        x = x * 1e6 # sec to micro sec
        y = y * 1e3 # V to mV

        # data update
        lines.set_data(x, y)

        plt.pause(.01)

        # Save 処理
        lock.acquire()
        if is_save_requested:
            file =  filedialog.asksaveasfilename(initialfile='', title = "保存場所を選択",filetypes = [("csv ファイル", ".csv")])

            if file:
                with open(file, mode='w',encoding="utf-8") as f:
                    writer = csv.writer(f, lineterminator='\n')
                    writer.writerows(np.array(data).T)

            is_save_requested = False

        lock.release()

if __name__ == "__main__":
    main()
```

# おわりに
pyVISA はとても便利ですね！