Pythonで始めるテストツール製作(2)オシロスコープの操作

## 1. はじめに
測定のたびにオシロスコープへ手を伸ばしてボタンを操作するのは一日に何度も繰り返すと疲れてきますよね。そこで「[Pythonで始めるテストツール製作](https://qiita.com/pbjpkas/items/0b8a32e2b0e126fab8bc)」で紹介した[Menu Based CLI](https://www.google.co.jp/search?q=Menu+Based+CLI)(メニュー形式のコマンドラインインターフェース)をカスタマイズしてオシロスコープをPCで操作できるようにします。

## 2. 要件
- 以下の操作ができること
  - 機器IDの読み出し
  - 垂直軸スケールの取得と設定
  - 水平軸スケールの取得と設定
  - 波形取り込みの開始(RUN)、停止(STOP)、単発波形の取り込み(SINGLE)
  - Vmaxの測定
  - 周波数の測定
- [Pythonで始めるテストツール製作](https://qiita.com/pbjpkas/items/0b8a32e2b0e126fab8bc)のmyTools.pyから呼び出して実行できること

## 3. 実装
- [pyvisa](https://pyvisa.readthedocs.io/en/latest/)をimportします。
- VISAコマンドはRIGOL [DS1000Z](https://jp.rigol.com/products/oscilloscopes/ds1000z.html)[^1][^2]のProgramming Manualを参照しています。
- open_visa()、close_visa()、exec_visa()、menu()の4つの関数で構成しています[^3]。
- モジュールとして呼び出せるようファイル名に"-"ではなく"_"を使用します。

2023/1/2改修
- Raspberry Pi Zero WH対応
  - /dev/ttyAMA0(BluetoothモジュールのUART)をrm.open_resource()しないようopen_visa()にガードを追加。

```python:myTools_visa.py
# myTools_visa.py by ka's@pbjpkas 2022
# python 3.x
# MIT License
import sys
import pyvisa

UNINITIALIZED = 0xdeadbeef


def open_visa():
    rm = pyvisa.ResourceManager()
    #print(rm)
    resources = rm.list_resources()
    #print(resources)
    for resource in resources:
        #print(resource)
        if resource.find("USB0",0,4) == 0: #文字列がUSB0で始まれば0、そうでない場合は-1
            try:
                    instrument = rm.open_resource(resource)
                    print("open %s" % resource)
            except:
                instrument = UNINITIALIZED
                print(resource, "Not Found.")
                print(sys.exc_info())
            else:
                print(resource, "Detected.")
        else:
            print("%s is not DSO" % resource)

    #print(type(rm))
    #print(type(instrument))
    return rm, instrument


def close_visa(rm, instrument):
    if instrument == UNINITIALIZED:
        pass
        #print("Not Initialized Instrument.")
    else:
        instrument.close()
    
    if rm == UNINITIALIZED:
        pass
        #print("Not Initialized Resource Manager.")
    else:
        rm.close()


def exec_visa(instrument, cmd):
    if instrument == UNINITIALIZED:
        print("Not Initialized Instrument.")
    else:
        print(cmd)
        try:
            if "?" in cmd:
                val = instrument.query(cmd).rstrip().replace(",", "-")
                print(val)
            else:
                instrument.write(cmd)
        except:
            print(sys.exc_info())


def main():
    rm = UNINITIALIZED
    instrument = UNINITIALIZED

    while True:
        print("== myTools VISA ==")
        print("a: open VISA instrument")
        print("b: *IDN?")
        print("c: Get Channel Scale")
        print("d: Set Channel Scale")
        print("e: Get Timebase Scale")
        print("f: Set Timebase Scale")
        print("g: :RUN")
        print("h: :STOP")
        print("i: :SINGle")
        print("j: Measure VMAX")
        print("k: Measure Freqency")
        print("x: exit")

        s = input(">")

        if s == "a":
            rm, instrument = open_visa()

        if s == "b":
            cmd = "*IDN?"
            exec_visa(instrument, cmd)

        if s == "c":
            ch  = input("ch(1-4)>")
            cmd = ":CHANnel" + ch + ":SCALe?"
            exec_visa(instrument, cmd)

        if s == "d":
            ch  = input("ch(1-4)>")
            scl = input("scale  >")
            cmd = ":CHANnel" + ch + ":SCALe " + scl
            exec_visa(instrument, cmd)

        if s == "e":
            cmd = ":TIMebase:SCALe?"
            exec_visa(instrument, cmd)

        if s == "f":
            scl = input("scale>")
            cmd = ":TIMebase:SCALe " + scl
            exec_visa(instrument, cmd)

        if s == "g":
            cmd = ":RUN"
            exec_visa(instrument, cmd)

        if s == "h":
            cmd = ":STOP"
            exec_visa(instrument, cmd)

        if s == "i":
            cmd = ":SINGle"
            exec_visa(instrument, cmd)

        if s == "j":
            ch  = input("ch(1-4)>")
            cmd = ":MEASure:VMAX? CHANnel" + ch
            exec_visa(instrument, cmd)

        if s == "k":
            ch  = input("ch(1-4)>")
            cmd = ":MEASure:FREQuency? CHANnel" + ch
            exec_visa(instrument, cmd)

        if s == "x":
            if __name__ == "__main__":
                print("Bye.")
            close_visa(rm, instrument)
            return


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

## 4. 実行例
あらかじめDS1000Z本体右下のプローブ補償信号出力端子の信号をChannel 1に入力し、本体のAUTOキー押下にてオシロの設定を自動設定します(筆者のDS1104Zでは垂直軸：500mV/div、水平軸：200us/divに設定されました)。

コマンドプロンプトを起動しpythonで実行します。
※Raspberry Pi OSではsudoを付けます。

```cmd
python myTools_visa.py
```

以下のようにメニューとプロンプト(>)が表示されればOKです。

```
== myTools VISA ==
a: open VISA instrument
b: *IDN?
c: Get Channel Scale
d: Set Channel Scale
e: Get Timebase Scale
f: Set Timebase Scale
g: :RUN
h: :STOP
i: :SINGle
j: Measure VMAX
k: Measure Freqency
x: exit
>
```

### 4.1 open_visa()の実行

```
>a
USB0::0x1AB1::0x04CE::XXXXXXXXXXXXXX::0::INSTR Detected.
(メニューは省略、以下同)
```

### 4.2 *IDN?の発行
exec_visa()内でVISAコマンドをprintしています。

```
>b
*IDN?
RIGOL TECHNOLOGIES-DS1104Z-XXXXXXXXXXXXXX-00.04.04.SP3
```

### 4.3 垂直軸スケールの取得と設定
自動設定にて500mV/divとなっています。これを1V/divに設定します。

```
>c
ch(1-4)>1            // チャンネルを入力します
:CHANnel1:SCALe?
5.000000e-01         // 0.5V/divとなっているのを確認

>d
ch(1-4)>1
scale  >1.0
:CHANnel1:SCALe 1.0  // 1.0V/divを指定

>c
ch(1-4)>1
:CHANnel1:SCALe?
1.000000e+00         // 1.0V/divとなっているのを確認
```

### 4.4 水平軸スケールの取得と設定
自動設定にて200us/divとなっています。これを500us/divに設定します。

```
>e
:TIMebase:SCALe?
2.0000000e-04

>f
scale>5.0e-4
:TIMebase:SCALe 5.0e-4

>e
:TIMebase:SCALe?
5.0000000e-04
```

### 4.5 RUN/STOP/SINGLE
メニュー操作に応じてオシロスコープ本体のLEDが点灯して波形が取り込まれればOKです。

```
>g
:RUN

>h
:STOP

>i
:SINGle
```

### 4.6 Vmaxの測定
あらかじめSTOPまたはSINGLEで波形を取り込みします。

```
>j
ch(1-4)>1            // チャンネルを入力します
:MEASure:VMAX? CHANnel1
3.080000e+00
```

3.08Vでした。

### 4.7 周波数の測定
4.6同様、あらかじめSTOPまたはSINGLEで波形を取り込みします。

```
>k
ch(1-4)>1            // チャンネルを入力します
:MEASure:FREQuency? CHANnel1
9.999999e+02
```

ほぼ1kHzでした。

### 4.8 exit
"Bye."を表示して終了します。

```
>x
Bye.
```

## 5. myTools.pyへの組み込み

### 5.1 myTools.pyの改修
myTools.pyを3か所編集します。

```python:myTools(Ver.4).py
# myTools.py by ka's@pbjpkas 2022
# python 3.x
# MIT License
import sys
import cv2
import myTools_visa              # 1.myTools_visaをimportする

# (略)

def main():
    while True:
        print("== myTools ==")
        print("a: comparison of old and new value")
        print("b: crop image")
        print("c: VISA menu")    # 2.メニューにVISA menuを追加する
        print("x: exit")

        s = input(">")

        if s == "a":
            comparison_value()

        if s == "b":
            crop_image_menu()

        if s == "c":             # 3.myTools_visa.main()を呼び出す
            myTools_visa.main()

        if s == "x":
            print("Bye.")
            sys.exit()
```

### 5.2 実行例
コマンドプロンプトを起動しpythonで実行します。

```cmd
python myTools.py
```

メニューにVISAメニューが追加されています。

```
== myTools ==
a: comparison of old and new value
b: crop image
c: VISA menu
x: exit
>
```

プロンプトでcを入力するとVISAメニューに移動します。

```
>c
== myTools VISA ==
a: open VISA instrument
b: *IDN?
c: Get Channel Scale
d: Set Channel Scale
e: Get Timebase Scale
f: Set Timebase Scale
g: :RUN
h: :STOP
i: :SINGle
j: Measure VMAX
k: Measure Freqency
x: exit
>
```

VISAメニューのプロンプトでxを入力すると"Bye."の表示をせずに上位のメニューに戻ります。

```
>x
== myTools ==
a: comparison of old and new value
b: crop image
c: VISA menu
x: exit
>
```

## 6. おわりに
- オシロスコープのRUN/STOPボタンやSINGLEボタンを腕を伸ばしながら操作すると押すのに意外と力が必要でだんだん疲れてくるのですがPCから操作できるようになることで負担が減る期待があります。
- myTools_visa.py単体での実行に加えてmyTools.pyからモジュールとして呼び出す方法も併せてご紹介しました。モジュール化することでよく使う機能やライブラリを整理しながら組み込むことができ、テストツールの機能性、使用性を向上できます。

## 付録A. 環境構築

1. オシロスコープに付属のVISA、あるいは[Keysight IO Libraries Suite](https://www.keysight.com/find/iosuiteproductcounter)などでVISAライブラリをインストールします。
1. コマンドプロンプトで以下のpipコマンドを実行します。

```cmd
pip install pyvisa
pip install pyvisa-py
```

## 付録B. 動作確認環境
筆者のPC環境はWindows10 64bit バージョン21H1です。Pythonやライブラリのバージョンを以下に示します。

```cmd
python --version
Python 3.10.0

pip list
Package               Version
--------------------- --------
numpy                 1.21.4
opencv-contrib-python 4.5.4.60
opencv-python         4.5.4.60
pip                   21.2.3
PyVISA                1.11.3
PyVISA-py             0.5.2
setuptools            57.4.0
typing_extensions     4.0.1
```


[^1]: [DS1000Z(中国語)](https://www.rigol.com/products/detail/DS1000Z.html)
[^2]: [DS1000Z(英語)](https://int.rigol.com/products/detail/DS1000Z.html)
[^3]: open_visa()とexec_visa()は[Lチカで始めるテスト自動化(3)オシロスコープの組込み](https://qiita.com/pbjpkas/items/b80b7e199a5aed4688a4)のopen_dso()やdsoコマンド部分を移植して改修しています。