Arduino治具とオシロスコープをRaspberry Piに接続してテストベンチを組み、Jenkinsでテストを自動化する

## 1. はじめに
Raspberry PiのUSBにArduino治具とオシロスコープを接続し、Arduino治具にコマンドを送って生成した矩形波パルスをオシロスコープへ入力するとともにオシロスコープで周波数を測定し、その値をPyVISAで取得するというのをJenkinsのパイプラインで行います。テストベンチの構成を以下に示します。

![jenkins-rpi-arduino-dso-with-comment.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221758/7d1336a0-2b9a-5526-dcde-02c08bf4aa93.png)

実行の様子です。
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">Raspberry PiのUSBにArduino治具とオシロを接続し、Arduino治具にコマンドを送って生成した1kHzの矩形波パルスをオシロへ入力し、オシロで測定した周波数をPyVISAで取得するというのをJenkinsのパイプラインでできた😃 <a href="https://t.co/aS0aqJ75hF">pic.twitter.com/aS0aqJ75hF</a></p>&mdash; ka’s (@pbjpkas) <a href="https://twitter.com/pbjpkas/status/1142758774014238721?ref_src=twsrc%5Etfw">June 23, 2019</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script> 

## 2. 補足
- この記事は「[マイコンのUARTコマンド制御をJenkins、Raspberry Pi、Pythonで行う](https://qiita.com/pbjpkas/items/f2f8eeab76dda95891d6)」の続編です。Raspberry PiとJenkinsを組合わせたテスト自動化の先行事例の話や、PySerialやPyVISA、Jenkinsの環境構築の手順などは[前編](https://qiita.com/pbjpkas/items/f2f8eeab76dda95891d6)をご参照ください。
- 「[ArduinoとオシロスコープをPythonで制御して測定する](https://qiita.com/pbjpkas/items/bd483d4773468e26414c)」ではWindowsにPythonの環境を組みインタラクティブシェルで実行しましたが、今回は1.テストスクリプト(機器の制御コマンドを記述したCSVファイル)、2.テスト実行プログラム、3.テスト実行結果ファイルの3ファイルでテストウェアを構成しRaspberry Piで実行します。

## 3. Arduino Leonardo治具
詳しくは「[Arduino Leonardoで多目的ツールの製作](https://qiita.com/pbjpkas/items/97dbf835b0aab6725e94)」をご参照ください。

- コマンド制御
  - Arduino LeonardoのDigital Pin #0、#1に接続しているUSBシリアル変換器(秋月電子[AE-TTL-232R](http://akizukidenshi.com/catalog/g/gK-09951))を経由して行う
  - 通信速度は9600[bps]
  - 起動後、コマンドではない任意の文字(例:改行コード)の受信を以ってコマンド制御に遷移
- 搭載コマンド→[4.3コマンドモード](https://qiita.com/pbjpkas/items/97dbf835b0aab6725e94#43-%E3%82%B3%E3%83%9E%E3%83%B3%E3%83%89%E3%83%A2%E3%83%BC%E3%83%89)
  - Digital Pin #10に矩形波パルスを出力：toneon \<frequency\>
  - Digital Pin #10の矩形波パルスを停止：toneoff
- ソースコード→[7.スケッチ](https://qiita.com/pbjpkas/items/97dbf835b0aab6725e94#7-%E3%82%B9%E3%82%B1%E3%83%83%E3%83%81)

## 4. テストウェア
### 4.1 テストスクリプト
- テストスクリプトはテスト実行プログラムへ与える命令を記述したCSVファイルです。
- ファイル名は script.csv です。
- CSVファイルの第一カラムに命令、第二カラムにパラメータを記述します。
- #で始まる行はコメントです。

|命令   |動作|
|-------+----|
|sendcmd|Arduino治具へ自作コマンドを送信する|
|dso    |Digital Storage OscilloscopeへSCPIコマンドを送信する|
|sleep  |引数で与えられた秒数、sleepする|

```awk:script.csv
### start
sendcmd,""
sendcmd,ver
### LED Blinks
sendcmd,port 1 high
sleep,0.5
sendcmd,port 1 low
sleep,0.5
sendcmd,port 1 high
sleep,0.5
sendcmd,port 1 low
sleep,0.5
### Frequency Measurement
dso,*IDN?
dso,:TIM:SCAL 0.0002
sendcmd,toneon 1500
sleep,5.0
dso,:MEAS:FREQ? CHAN1
sleep,1.0
sendcmd,toneoff
### Unknown command check
undefined,"undefined command"
### end

```

### 4.2 テスト実行プログラム
- 以下のデバイス名は決め打ちです。
  - USB-シリアル変換器のデバイス名
  - オシロスコープの機器ID
- 実行結果は result.csv ファイルへ出力します。
  - script.csvの第一および第二カラムを転記
  - 実行結果を第三カラムへ出力
      - script.csvがコメントなど第一カラムのみの場合は第二カラムへ出力
  - 実行結果の出力内容
      - 命令がコメントの場合は "### COMMENT ###" を出力
      - 未定義の命令の場合は "### UNKNOWN ###" を出力
      - 命令がdsoでパラメータに?を含むものはオシロスコープの応答を出力
      - 上記以外は "OK" を出力

```python:dso_query.py
#!/usr/bin/python

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

def serial_open(portname, baud):
    return serial.Serial(port=portname, baudrate=baud, timeout=1.0)

def serial_write(h, str):
    str = str + '\n'
    h.write(str)

def main():
    serial_h = serial_open("/dev/ttyUSB0", 9600)
    
    rm = visa.ResourceManager()
    print(rm.list_resources())
    dso_h = rm.open_resource('USB0::6833::1230::XXXXXXXXXXXXXX::0::INSTR')
    
    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]:
                    cmd.append("### COMMENT ###")
                    
                elif cmd[0]=="sendcmd":
                    serial_write(serial_h, cmd[1])
                    print(serial_h.read_until('$'))
                    cmd.append("OK")
                    
                elif cmd[0]=="sleep":
                    sleep(float(cmd[1]))
                    cmd.append("OK")
                    
                elif cmd[0]=="dso":
                    if '?' in cmd[1]:
                        string = dso_h.query(cmd[1])
                        values = string.splitlines()
                        cmd.append(values[0])
                    else:
                        dso_h.write(cmd[1])
                        cmd.append("OK")
                    
                else:
                    cmd.append("### UNKNOWN ###")
                
                print(cmd)
                result.writerow(cmd)
    
    dso_h.close()
    

main()
```

### 4.3 テスト実行結果ファイル
テスト実行結果を収めたファイルである result.csv の例を以下に示します。

- Arduino治具へ "toneon 1500" コマンドを送信して1500Hzの矩形波パルスを生成し、オシロスコープの周波数測定値として "1.506024e+03" を得ました。
- コメントや未定義の命令についても設計通りの文字列が格納されているのを確認できました。

```awk:resuly.csv
### start,### COMMENT ###
sendcmd,,OK
sendcmd,ver,OK
### LED Blinks,### COMMENT ###
sendcmd,port 1 high,OK
sleep,0.5,OK
sendcmd,port 1 low,OK
sleep,0.5,OK
sendcmd,port 1 high,OK
sleep,0.5,OK
sendcmd,port 1 low,OK
sleep,0.5,OK
### Frequency Measurement,### COMMENT ###
dso,*IDN?,"RIGOL TECHNOLOGIES,DS1054Z,XXXXXXXXXXXXXX,00.04.04.SP3"
dso,:TIM:SCAL 0.0002,OK
sendcmd,toneon 1500,OK
sleep,5.0,OK
dso,:MEAS:FREQ? CHAN1,1.506024e+03
sleep,1.0,OK
sendcmd,toneoff,OK
### Unknown command check,### COMMENT ###
undefined,undefined command,### UNKNOWN ###
### end,### COMMENT ###
```

## 5. Jenkinsパイプライン設定とコンソール出力
以下にパイプライン設定を示します。

- MasterからSlaveへstash/unstashでテストウェアを転送しテスト実行プログラム dso_query.py をsudoで実行しています。
- SlaveからMasterへstash/unstashでテスト実行結果ファイルを転送しています。

```groovy
node('master') {
    stash name: 'project'
}
node('RPi00') {
    deleteDir()
    unstash 'project'
    sh 'ls -l'
    sh 'sudo chmod 755 *.py'
    sh 'sudo ./dso_query.py'
    sh 'cat result.csv'
    stash name: 'result'
}
node('master') {
    unstash 'result'
}
```
<details><summary>コンソール出力[^1]</summary><div>

```
ユーザー****が実行
Running in Durability level: MAX_SURVIVABILITY
[Pipeline] Start of Pipeline
[Pipeline] node
Running on Jenkins in C:\jenkins\workspace\pipelineSample
[Pipeline] {
[Pipeline] stash
Stashed 4 file(s)
[Pipeline] }
[Pipeline] // node
[Pipeline] node
Running on RPi00 in /home/pi/jenkins/workspace/pipelineSample
[Pipeline] {
[Pipeline] deleteDir
[Pipeline] unstash
[Pipeline] sh
+ ls -l
合計 16
-rw-r--r-- 1 pi pi   42  6月 23 14:27 cmd.txt
-rw-r--r-- 1 pi pi 1957  6月 25 12:57 dso_query.py
-rw-r--r-- 1 pi pi  595  6月 25 13:03 result.csv
-rw-r--r-- 1 pi pi  388  6月 25 13:21 script.csv
[Pipeline] sh
+ sudo chmod 755 dso_query.py
[Pipeline] sh
+ sudo ./dso_query.py
(u'ASRL/dev/ttyUSB0::INSTR', u'ASRL/dev/ttyAMA0::INSTR', u'USB0::6833::1230::XXXXXXXXXXXXXX::0::INSTR')
['### start']
['### start', '### COMMENT ###']
['sendcmd', '']


?
$
['sendcmd', '', 'OK']
['sendcmd', 'ver']
ver

This is arduinoLeonardoToolsV2.ino Build at Jun 23 2018 20:08:09
OK
$
['sendcmd', 'ver', 'OK']
['### LED Blinks']
['### LED Blinks', '### COMMENT ###']
['sendcmd', 'port 1 high']
port 1 high

OK
$
['sendcmd', 'port 1 high', 'OK']
['sleep', '0.5']
['sleep', '0.5', 'OK']
['sendcmd', 'port 1 low']
port 1 low

OK
$
['sendcmd', 'port 1 low', 'OK']
['sleep', '0.5']
['sleep', '0.5', 'OK']
['sendcmd', 'port 1 high']
port 1 high

OK
$
['sendcmd', 'port 1 high', 'OK']
['sleep', '0.5']
['sleep', '0.5', 'OK']
['sendcmd', 'port 1 low']
port 1 low

OK
$
['sendcmd', 'port 1 low', 'OK']
['sleep', '0.5']
['sleep', '0.5', 'OK']
['### Frequency Measurement']
['### Frequency Measurement', '### COMMENT ###']
['dso', '*IDN?']
['dso', '*IDN?', u'RIGOL TECHNOLOGIES,DS1054Z,XXXXXXXXXXXXXX,00.04.04.SP3']
['dso', ':TIM:SCAL 0.0002']
['dso', ':TIM:SCAL 0.0002', 'OK']
['sendcmd', 'toneon 1500']
toneon 1500

OK
$
['sendcmd', 'toneon 1500', 'OK']
['sleep', '5.0']
['sleep', '5.0', 'OK']
['dso', ':MEAS:FREQ? CHAN1']
['dso', ':MEAS:FREQ? CHAN1', u'1.506024e+03']
['sleep', '1.0']
['sleep', '1.0', 'OK']
['sendcmd', 'toneoff']
toneoff

OK
$
['sendcmd', 'toneoff', 'OK']
['### Unknown command check']
['### Unknown command check', '### COMMENT ###']
['undefined', 'undefined command']
['undefined', 'undefined command', '### UNKNOWN ###']
['### end']
['### end', '### COMMENT ###']
[Pipeline] sh
+ cat result.csv
### start,### COMMENT ###
sendcmd,,OK
sendcmd,ver,OK
### LED Blinks,### COMMENT ###
sendcmd,port 1 high,OK
sleep,0.5,OK
sendcmd,port 1 low,OK
sleep,0.5,OK
sendcmd,port 1 high,OK
sleep,0.5,OK
sendcmd,port 1 low,OK
sleep,0.5,OK
### Frequency Measurement,### COMMENT ###
dso,*IDN?,"RIGOL TECHNOLOGIES,DS1054Z,XXXXXXXXXXXXXX,00.04.04.SP3"
dso,:TIM:SCAL 0.0002,OK
sendcmd,toneon 1500,OK
sleep,5.0,OK
dso,:MEAS:FREQ? CHAN1,1.506024e+03
sleep,1.0,OK
sendcmd,toneoff,OK
### Unknown command check,### COMMENT ###
undefined,undefined command,### UNKNOWN ###
### end,### COMMENT ###
[Pipeline] stash
Stashed 4 file(s)
[Pipeline] }
[Pipeline] // node
[Pipeline] node
Running on Jenkins in C:\jenkins\workspace\pipelineSample
[Pipeline] {
[Pipeline] unstash
[Pipeline] }
[Pipeline] // node
[Pipeline] End of Pipeline
Finished: SUCCESS
```

</div></details>

## 6. おわりに
- Pythonで数十行書くだけで機器の操作を自動化できました。複数の機器や測定器にコマンドを送って性能テストを自動化するだけでなく、耐久テストや加速テストといった操作を何度も繰り返して最後に目視で実行結果を確認するようなテストもできそうです。
- 以前「[ArduinoとオシロスコープをExcelで制御して測定する](https://qiita.com/pbjpkas/items/7da7ff99e7226fa84bcb)」でExcel VBAでテスト自動化ツールを作りましたが、ExcelがインストールされているWindows PCをテストベンチの数だけ用意するのと比べるとRaspberry Piでできるこの方法はローコストで置き場所や電源の確保もやりやすいことや、Jenkinsでノード設定すれば台数を増やせるのはメリットと思いました。
- 以下のようなアレンジは追々やってみたいです。
  - 繰り返し回数の指定
  - テスト実行結果にタイムスタンプを入れる
  - デバイス名を自動で解決する、あるいはコマンドの引数で指定する
  - テストスクリプトやテスト実行結果のファイル名をコマンドの引数で指定する
  - テスト実行結果のOK/NG判定

[^1]: ワークスペースのcmd.txtは無視してください。