Lチカで始めるテスト自動化(3)オシロスコープの組込み

##1. はじめに
Lチカで始めるテスト自動化シリーズ第三弾です。

昨今の省エネ事情を考慮しテスト対象のLEDの点灯方式を[スタティック点灯](https://www.google.co.jp/search?hl=ja&as_q=スタティック点灯)から[ダイナミック点灯](https://www.google.co.jp/search?hl=ja&as_q=ダイナミック点灯)に仕様変更します。そこで、LEDの点灯電圧、点灯周波数をオシロスコープで測定してGo/No-Go判定を自動でできるようテストベンチを改修します。

![IMG_0603s.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221758/28bc724a-f4a2-8458-4d43-7298e781374b.jpeg)

これまでの記事はこちらをご覧ください。

1. [Lチカで始めるテスト自動化](https://qiita.com/pbjpkas/items/fd4bd2689deda2d793aa)
2. [Lチカで始めるテスト自動化(2)テストスクリプトの保守性向上](https://qiita.com/pbjpkas/items/337bce392281ee5f0526)

##2. テスト対象の改修
スタティック点灯から1000Hz、デューティ比50%のダイナミック点灯に変更します。[tone()](https://www.arduino.cc/reference/en/language/functions/advanced-io/tone/)、[noTone()](https://www.arduino.cc/reference/en/language/functions/advanced-io/notone/)で点灯／消灯を制御します。

```c:ArduinoLedLight.ino(変更部分)
void Led::on(void)
{
    //digitalWrite(m_led_pin, HIGH);
    tone(m_led_pin, 1000);
}

void Led::off(void)
{
    //digitalWrite(m_led_pin, LOW);
    noTone(m_led_pin);
}
```

##3. テストランナーの改修
- 下記のコマンドを追加します。

|コマンド   |引数            |機能
|-----------+----------------+----
|open_dso   |なし            |・PyVISAをimportしオシロスコープをopenする
|dso        |VISAコマンド    |・オシロスコープとコマンドを送受信する<br>・受信データに含まれる","は"-"に置換する
|eval_dbl_eq|期待値(double型[^1])|・変数valが期待値の値(double型)と等しいか評価する
|eval_dbl_gt|基準値(double型)|・変数valが基準値(double型)より大きいか評価する
|eval_dbl_lt|基準値(double型)|・変数valが基準値(double型)より小さいか評価する

- [PyVISA](https://pyvisa.readthedocs.io/en/latest/)をインストールしてください[^2]。
- USBバス上のVISA機器は1台だけと仮定し、機器一覧の中から最初にopenできた機器を使用します。

```python:test-runner.py
#!/usr/bin/python3

#
# This software includes the work that is distributed in the Apache License 2.0
#

from time import sleep
import serial
import codecs
import csv
import sys
import visa

UNINITIALIZED = 0xdeadbeef

def serial_write(h, string):
    if h == UNINITIALIZED:
        print("UART Not Initialized.")
        return False
    else:
        string = string + '\n'
        string = str.encode(string)
        h.write(string)
        return True


def open_dso():
    rm = visa.ResourceManager()
    resources = rm.list_resources()
    #print(resources)
    for resource in resources:
        #print(resource)
        try:
            dso = rm.open_resource(resource)
        except:
            print(resource, "Not Found.")
        else:
            print(resource, "Detected.")
            return dso

    #Throw an error to caller if none succeed.
    return dso


def main():
    is_passed = True
    val = str(UNINITIALIZED)
    dso = UNINITIALIZED
    uart = UNINITIALIZED
    arg = [""]

    with codecs.open('script.csv', 'r', 'utf-8') as file:
        script = csv.reader(file, delimiter=',', lineterminator='\r\n', quotechar='"')

        with codecs.open('result.csv', 'w', 'utf-8') as file:
            result = csv.writer(file, delimiter=',', lineterminator='\r\n', quotechar='"')

            for cmd in script:
                print(cmd)

                if "#" in cmd[0]:
                    pass

                elif cmd[0]=="sleep":
                    sleep(float(cmd[1]))
                    cmd.append("OK")

                elif cmd[0]=="open_dso":
                    try:
                        dso = open_dso()
                    except:
                        cmd.append("NG")
                        is_passed = False
                    else:
                        cmd.append("OK")

                elif cmd[0]=="dso":
                    try:
                        if "?" in cmd[1]:
                            val = dso.query(cmd[1]).rstrip().replace(",", "-")
                            cmd.append(val)
                        else:
                            dso.write(cmd[1])
                            cmd.append("OK")
                    except:
                        cmd.append("NG")
                        is_passed = False

                elif cmd[0]=="open_uart":
                    try:
                        uart = serial.Serial(cmd[1], 115200, timeout=1.0, dsrdtr=1)
                        cmd.append("OK")
                    except:
                        cmd.append("NG")
                        is_passed = False

                elif cmd[0]=="send":
                    ret = serial_write(uart, cmd[1])
                    if ret == True:
                        cmd.append("OK")
                    else:
                        cmd.append("NG")
                        is_passed = False

                elif cmd[0]=="rcvd":
                    try:
                        val = uart.readline().strip().decode('utf-8')
                        cmd.append(val)
                        cmd.append("OK")
                    except:
                        cmd.append("NG")
                        is_passed = False

                elif cmd[0]=="eval_str_eq":
                    if str(val) == str(cmd[1]):
                        cmd.append("OK")
                    else:
                        cmd.append("NG")
                        is_passed = False

                elif cmd[0]=="eval_int_eq":
                    if int(val) == int(cmd[1]):
                        cmd.append("OK")
                    else:
                        cmd.append("NG")
                        is_passed = False

                elif cmd[0]=="eval_int_gt":
                    if int(val) > int(cmd[1]):
                        cmd.append("OK")
                    else:
                        cmd.append("NG")
                        is_passed = False

                elif cmd[0]=="eval_int_lt":
                    if int(val) < int(cmd[1]):
                        cmd.append("OK")
                    else:
                        cmd.append("NG")
                        is_passed = False

                elif cmd[0]=="eval_dbl_eq":
                    if float(val) == float(cmd[1]):
                        cmd.append("OK")
                    else:
                        cmd.append("NG")
                        is_passed = False

                elif cmd[0]=="eval_dbl_gt":
                    if float(val) > float(cmd[1]):
                        cmd.append("OK")
                    else:
                        cmd.append("NG")
                        is_passed = False

                elif cmd[0]=="eval_dbl_lt":
                    if float(val) < float(cmd[1]):
                        cmd.append("OK")
                    else:
                        cmd.append("NG")
                        is_passed = False

                else:
                    cmd.append("#")

                print(cmd)
                result.writerow(cmd)

                if is_passed == False:
                    print("FAIL")
                    sys.exit(1)

    if is_passed == True:
        print("PASS")
        sys.exit(0)


main()
```

##4. テストスクリプトおよびスクリプトジェネレータ
###4.1 （単機能）テストスクリプト
以下のテストスクリプトをscript-partsフォルダへ追加します。

- VISAコマンドはRIGOL製オシロスコープの例です。
- setup_dso.csvの機種の文字列はお使いの機種に合わせてください。

```sh:setup_dso.csv
#
# Setup DSO
open_dso,
dso,*IDN?
eval_str_eq,RIGOL TECHNOLOGIES-DS1104Z-XXXXXXXXXXXXXX-00.04.04.SP3
```

```sh:dso_write_timescale_200us.csv
#
# Timebase Scale 200us
dso,:TIMebase:SCALe 0.0002
```

```sh:dso_query_vmax_gt1700-lt1900.csv
#
# 1.80V +/- 0.10V
dso,:MEASure:VMAX? CHANnel1
eval_dbl_gt,1.70
eval_dbl_lt,1.90
```

```sh:dso_query_freq_gt995-lt1005.csv
#
# 1000Hz +/- 5Hz
dso,:MEASure:FREQuency? CHANnel1
eval_dbl_gt,995
eval_dbl_lt,1005
```

###4.2 スクリプトジェネレータ
改修は下記2点です。

- オシロスコープのセットアップを追加
- LED点灯時のチェックをオシロスコープに変更

```bat:script-generator.bat
REM script generator for LED Light Test Bench

set script_parts_dir=script-parts
set script_file_name=script.csv

REM テストベンチのセットアップ
copy /b %script_parts_dir%\setup.csv %script_file_name%

REM デジタルオシロスコープのセットアップ
copy /b %script_file_name% + %script_parts_dir%\setup_dso.csv
copy /b %script_file_name% + %script_parts_dir%\dso_write_timescale_200us.csv

REM LEDが点灯していないこと(LEDの電圧が200mVより小さいこと)
copy /b %script_file_name% + %script_parts_dir%\eval_int_lt0200.csv

REM リレーをONし、
copy /b %script_file_name% + %script_parts_dir%\operation_relay_on.csv
REM 1. LEDの点灯電圧が1700mVより大きく1900mVより小さいこと
copy /b %script_file_name% + %script_parts_dir%\dso_query_vmax_gt1700-lt1900.csv
REM 2. LEDの点灯周波数が995Hzより大きく1005Hzより小さいこと
copy /b %script_file_name% + %script_parts_dir%\dso_query_freq_gt995-lt1005.csv

REM リレーをOFFし、LEDの電圧が200mVより小さいこと
copy /b %script_file_name% + %script_parts_dir%\operation_relay_off.csv
copy /b %script_file_name% + %script_parts_dir%\eval_int_lt0200.csv
```

##5. テスト実行結果

```sh:result.csv
# setup
# wait for if restart Arduino Uno
sleep,2,OK
#
# Open UART
open_uart,COM3,OK
#
# model and version check
send,i,OK
rcvd,Arduino Test Bench Ver.100,OK
eval_str_eq,Arduino Test Bench Ver.100,OK
#
# relay assigned Pin number check
send,p,OK
rcvd,12,OK
eval_int_eq,12,OK
#
# Setup DSO
open_dso,,OK
dso,*IDN?,RIGOL TECHNOLOGIES-DS1104Z-XXXXXXXXXXXXXX-00.04.04.SP3
eval_str_eq,RIGOL TECHNOLOGIES-DS1104Z-XXXXXXXXXXXXXX-00.04.04.SP3,OK
#
# Timebase Scale 200us
dso,:TIMebase:SCALe 0.0002,OK
#
# eval_int_lt0200
send,v,OK
rcvd,0,OK
eval_int_lt,200,OK
#
# relay ON
send,n,OK
sleep,1,OK
#
# 1.80V +/- 0.10V
dso,:MEASure:VMAX? CHANnel1,1.840000e+00
eval_dbl_gt,1.70,OK
eval_dbl_lt,1.90,OK
#
# 1000Hz +/- 5Hz
dso,:MEASure:FREQuency? CHANnel1,9.999999e+02
eval_dbl_gt,995,OK
eval_dbl_lt,1005,OK
#
# relay OFF
send,f,OK
sleep,1,OK
#
# eval_int_lt0200
send,v,OK
rcvd,0,OK
eval_int_lt,200,OK
```

##6. おわりに
オシロスコープの測定結果にもとづいたGo/No-Go判定をPythonで自動化できました＼(^o^)／

[^1]: Pythonのfloatは倍精度浮動小数点数ですが一般的にはfloatは単精度、doubleは倍精度のためここではdouble型と表記しています。
[^2]: 公式サイトではpipコマンドによるインストール手順が提示されています。また、筆者がMiniconda環境にcondaコマンドでインストールした手順は[こちら](https://qiita.com/pbjpkas/items/bd483d4773468e26414c#22-python%E5%AE%9F%E8%A1%8C%E7%92%B0%E5%A2%83)です。