Lチカで始めるテスト自動化(19)Webカメラの映像を録画しながらテストスクリプトを実行する

##1. はじめに
テスト対象のデバイスの様子をWebカメラで撮影し録画しながらテストスクリプトを実行できるようにします。Webカメラの映像に1.時刻、2.実行中のスクリプト行番号、3.スクリプトのコマンドを重畳しテストの経過を追えるようにします。

テストスクリプト実行の様子をWebカメラで撮影している実行例を以下に示します。
![実行例](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221758/a05d7fc9-4e83-347f-b9ea-2ad62ce77f47.png)

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">Lチカで始めるテスト自動化のテストランナーを改修:-)<br><br>・Webカメラの映像を録画しながらテストスクリプトを実行できるよう、録画のスレッドを追加<br>・時刻、実行中のスクリプト行番号、スクリプトのコマンドを映像の上部にオーバーレイ<br>・ランダムスリープをスリープ時間を表示するように修正 <a href="https://t.co/lIMfcNVrft">pic.twitter.com/lIMfcNVrft</a></p>&mdash; ka’s (@pbjpkas) <a href="https://twitter.com/pbjpkas/status/1433814656611618820?ref_src=twsrc%5Etfw">September 3, 2021</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script> 

##2. 設計
録画を行う関数をスレッド化しテストスクリプト実行と非同期に録画を行います。録画のシーケンスを以下に示します。

```plantuml
@startuml

title test-runner Video Recording Diagram

participant "command input" as cmd order 10 #White
participant "main()" as main order 20
participant "video_rec()" as video_rec order 30 #LightGreen
participant "set_video_txt()" as set_video_txt order 40 #LightCyan
participant "get_video_txt()" as get_video_txt order 50 #LightCyan
participant "set_video_rec()" as set_video_rec order 60 #LightSteelBlue
participant "get_video_rec()" as get_video_rec order 70 #LightSteelBlue

== initialize ==
cmd -> main : open_cam (omission)

== start recording==
cmd -> main : rec_start

    main -> set_video_txt : "rec_start"

    main -> set_video_rec : True

    main ->> video_rec : start video_rec() thread.
    activate video_rec

        video_rec ->  get_video_txt
        video_rec <-- get_video_txt : "rec_start"

        video_rec ->  get_video_rec
        video_rec <-- get_video_rec : True

== stop recording ==
cmd -> main : rec_stop

    main -> set_video_txt : "rec_stop"

    main -> set_video_rec : False

        video_rec ->  get_video_txt
        video_rec <-- get_video_txt : "rec_stop"

        video_rec ->  get_video_rec
        video_rec <-- get_video_rec : False

    main -> main : wait for the thread termination.
    deactivate video_rec

@enduml
```
[^99]: ![test-runner Video Recording Diagram](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221758/6b340088-0208-83be-16fb-a6629128a1d0.png)

- video_rec()はWebカメラの映像をファイルへ記録する関数です。
- set_video_txt()、get_video_txt()は映像に重畳する文字列を扱う関数です。
- set_video_rec()、get_video_rec()は録画をする/しないのフラグを扱う関数です。

###2.1 initialize
open_camコマンドでWebカメラをオープンしハンドラを取得します。詳細は「[Lチカで始めるテスト自動化(8)HDMIビデオキャプチャデバイスの組込み](https://qiita.com/pbjpkas/items/58f84c347e44fb9d5635)」の[3. テストランナーの改修](https://qiita.com/pbjpkas/items/58f84c347e44fb9d5635#3-テストランナーの改修)をご覧ください。

###2.2 start recording
rec_startコマンドを受け取るとmain()は以下の処理を行います。

1. set_video_txt()にスクリプトのコマンド文字列(rec_start)を渡す
1. set_video_rec()に録画フラグ=Trueを渡す
1. video_rec()のスレッドを開始する

video_rec()は以下のような動作を行います。

1. カメラデバイスから画像データを読み出す
1. 現在時刻を取得する
1. 画像に重畳する文字列をget_video_txt()で取得する
1. 2で取得した現在時刻と3で取得した文字列を画像に重畳しファイルに出力する
1. 録画フラグをget_video_rec()で取得し、Trueなら1へ戻って録画を継続、Falseなら録画を終了する

以後、録画フラグがTrueのあいだvideo_rec()スレッドがループを繰り返して画像をファイルに出力します。

テストスクリプトの処理が進むとmain()はその都度set_video_txt()でコマンド文字列を上書きし、画像に重畳される文字列に反映されます。

###2.3 stop recording
rec_stopコマンドを受け取るとmain()は以下の処理を行います。

1. set_video_txt()にスクリプトのコマンド文字列(rec_stop)を渡す
1. set_video_rec()に録画フラグ=Falseを渡す
1. video_rec()のスレッドが終了するのを待つ

##3. 実装
全部入りのソースは付録.Aをご覧ください。

###3.1 threadingモジュールのインポート
```python
import threading
```

###3.2 set_video_txt() / get_video_txt()
gVideoTxtは画像に重畳する文字列を代入するグローバル変数です。グローバル変数をあちこちの関数から直接アクセスするのは避けたいのでset/getの関数を用意します。

```python
# Text over Video Frame
gVideoTxt = "null"

def set_video_txt(str):
    global gVideoTxt
    gVideoTxt = str

def get_video_txt():
    global gVideoTxt
    return gVideoTxt
```

###3.3 set_video_rec() / get_video_rec()
gVideoRecは録画フラグを代入するグローバル変数です。gVideoTxt同様、set/getの関数を用意します。

```python
# Video Recording Thread flag
gVideoRec = False

def set_video_rec(flag):
    global gVideoRec
    if flag:
        gVideoRec = True
    else:
        gVideoRec = False

def get_video_rec():
    global gVideoRec
    return gVideoRec
```

###3.4 video_rec()
- カメラデバイスのハンドラと出力先のファイル名を引数に取ります。ファイル名は拡張子.mp4を含めてください。
- 動画の幅、高さ、フレームレートは[VideoCaptureProperties](https://docs.opencv.org/master/d4/d15/group__videoio__flags__base.html#gaeb8dd9c89c10a5c63c139bf7c4f5704d)から取得して自動設定します。
- 録画フラグがFalseの場合だけでなく、Webカメラの映像を表示しているウィンドウで"q"を押下することでも録画を終了します。

```python
def video_rec(cam, filename):
    if cam == UNINITIALIZED:
        return False

    width  = int(cam.get(cv2.CAP_PROP_FRAME_WIDTH))
    height = int(cam.get(cv2.CAP_PROP_FRAME_HEIGHT))
    fps    = cam.get(cv2.CAP_PROP_FPS)
    print(str(width) + "x" + str(height) + "," + str(fps) + "fps")
    fourcc = cv2.VideoWriter_fourcc('m','p','4','v')
    out = cv2.VideoWriter(filename, fourcc, fps, (width,height))

    while True:
        ret, img = cam.read()
        if ret == True:
            timestamp = datetime.datetime.now().strftime("%Y/%m/%d %H:%M:%S")
            videotxt  = get_video_txt()
            cv2.rectangle(img, (5,5), (width-5,25), (255,255,255), thickness=-1)
            cv2.putText(img, timestamp, ( 10,20), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0,0,0), 1, cv2.LINE_AA)
            cv2.putText(img, videotxt,  (200,20), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0,0,0), 1, cv2.LINE_AA)
            out.write(img)
            cv2.imshow('img', img)

        if (cv2.waitKey(1) == ord('q')) or (get_video_rec() == False):
            print("### rec stop ###")
            break

    out.release()
    return True
```

###3.5 main()の改修
「[Lチカで始めるテスト自動化(14)sleepの時間をランダムに設定する](https://qiita.com/pbjpkas/items/4f023917f31f6096eb6b)」の[test-runner.py](https://qiita.com/pbjpkas/items/4f023917f31f6096eb6b#付録a-test-runnerpyのソース)との差分を示します。

####3.5.1 初期化
変数cmds(set_video_txt()に渡す文字列の作成に用いるローカル変数)、録画フラグ、ダミーのコマンド文字列を設定します。

```python:before
def main():
    is_passed = True
    val = str(UNINITIALIZED)
    fval = 0.0
    uart = UNINITIALIZED
    dso = UNINITIALIZED
    cam = UNINITIALIZED
    ocr = UNINITIALIZED
```

```python:after
def main():
    is_passed = True
    val = str(UNINITIALIZED)
    fval = 0.0
    uart = UNINITIALIZED
    dso = UNINITIALIZED
    cam = UNINITIALIZED
    ocr = UNINITIALIZED
    cmds = ""
    set_video_rec(False)
    set_video_txt("null")
```

####3.5.2 実行中のスクリプト行番号およびコマンド文字列の生成

```python:before
            for cmd in script:
                timestamp = datetime.datetime.now().strftime("%Y/%m/%d %H:%M:%S")
                print(cmd)
```

```python:after
            script_line = 0
            for cmd in script:
                timestamp = datetime.datetime.now().strftime("%Y/%m/%d %H:%M:%S")
                script_line += 1
                print("##### " + timestamp + " " + str(script_line) + " #####")

                cmds = str(script_line) + ":"
                for i in range(len(cmd)):
                    cmds += cmd[i] + ","
                set_video_txt(cmds)
                print(cmds)
```

####3.5.3 ランダムスリープrusleepコマンドの改修
スリープ時間を画像に表示できるようrusleepコマンドを改修します。

```python:before
                elif cmd[0] == "rusleep":
                    fval = random.uniform(float(cmd[1]), float(cmd[2]))
                    cmd.append(str(fval))
                    sleep(fval)
```

```python:after
                elif cmd[0] == "rusleep":
                    fval = random.uniform(float(cmd[1]), float(cmd[2]))
                    cmd.append(str(fval))
                    cmds += str(fval)
                    set_video_txt(cmds)
                    print(cmds)
                    sleep(fval)
```

####3.5.4 rec_startコマンド/rec_stopコマンドの作成

|コマンド|引数|機能|
|--------+----+----|
|rec_start|録画ファイルの名前。<br>拡張子.mp4を付けること。|録画を開始する。|
|rec_stop |なし。|録画を終了する。|

video_rec()はデーモンスレッドにはしません。video_rec()内部のrelease()処理を経て関数が終了するのをjoin()で待ちます。
> 注釈: デーモンスレッドは終了時にいきなり停止されます。デーモンスレッドで使われたリソース (開いているファイル、データベースのトランザクションなど) は適切に解放されないかもしれません。きちんと (gracefully) スレッドを停止したい場合は、スレッドを非デーモンスレッドにして、Event のような適切なシグナル送信機構を使用してください。
出典：[threading --- スレッドベースの並列処理 -> Thread オブジェクト](https://docs.python.org/ja/3/library/threading.html#thread-objects)


```python:after
                elif cmd[0] == "rec_start":
                    if get_video_rec() == False:
                        set_video_rec(True)
                        thread1 = threading.Thread(target=video_rec, args=(cam,cmd[1],))
                        thread1.start()
                    else:
                        print("Already Recording.")

                elif cmd[0] == "rec_stop":
                    if get_video_rec() == True:
                        set_video_rec(False)
                        thread1.join()
                    else:
                        print("No Recording.")
```

####3.5.6 終了処理
テストスクリプトが終了する際に自動で録画を終了するようにします。rec_stopコマンド相当の処理を追加します。

```python:before
                else:
                    cmd.append("NG")
                    cmd.insert(0,timestamp)
                    print(cmd)
                    result.writerow(cmd)
                    close_uart(uart)
                    close_cam(cam)
                    print("FAIL")
                    sys.exit(1)

    if is_passed == True:
        close_uart(uart)
        close_cam(cam)
        print("PASS")
        sys.exit(0)
```

```python:after
                else:
                    cmd.append("NG")
                    cmd.insert(0,timestamp)
                    print(cmd)
                    result.writerow(cmd)
                    if get_video_rec() == True:
                        set_video_rec(False)
                        thread1.join()
                    #else:
                    #    print("No Recording.")
                    close_uart(uart)
                    close_cam(cam)
                    print("FAIL")
                    sys.exit(1)

    if is_passed == True:
        if get_video_rec() == True:
            set_video_rec(False)
            thread1.join()
        #else:
        #    print("No Recording.")
        close_uart(uart)
        close_cam(cam)
        print("PASS")
        sys.exit(0)
```

##4. 実行例
1章の実行例のテストスクリプトと実行結果を以下に示します。

```sh:テストスクリプト(video.csv)
open_cam,1
rec_start,video.mp4
sleep,1.0
sleep,1.5
sleep,2.0
rusleep,1,2
rusleep,1,2
rusleep,1,2
```

```sh:実行結果(video_result.csv)
2021/09/03 23:30:35,open_cam,1,OK
2021/09/03 23:30:35,rec_start,video.mp4,OK
2021/09/03 23:30:35,sleep,1.0,OK
2021/09/03 23:30:36,sleep,1.5,OK
2021/09/03 23:30:38,sleep,2.0,OK
2021/09/03 23:30:40,rusleep,1,2,1.98797858918717,OK
2021/09/03 23:30:42,rusleep,1,2,1.8228758657682014,OK
2021/09/03 23:30:43,rusleep,1,2,1.0566791437142018,OK
```

##5. おわりに
- Webカメラの映像を録画しながらテストスクリプトを実行できるようになりました。
  - 録画スレッドはmain()とは非同期に処理を行えばよく排他制御やアトミック操作もないため、スレッドを1つ起動するだけでできました。
- Webカメラの映像に時刻や実行中のスクリプト行番号、スクリプトのコマンドを重畳できました。

##6. 参考資料
この記事を作成するにあたって参考にさせていただいた記事です。

- [[Python] スレッドで実装する](https://qiita.com/tchnkmr/items/b05f321fa315bbce4f77)
- [Pythonのthreadingとmultiprocessingを完全理解](https://qiita.com/kaitolucifer/items/e4ace07bd8e112388c75)
- [Python3.4 でマルチスレッドを試した](https://qiita.com/narupo/items/2c13c7156565370139f8)
- [threading --- スレッドベースの並列処理](https://docs.python.org/ja/3/library/threading.html)
- [Python：グローバル変数](https://qiita.com/New_enpitsu_15/items/8c9ff420133df1fed53f)
- [OpenCVの動画プロパティを取得する](https://qiita.com/takahiro_itazuri/items/e740f0a1b5165fafa6ac)
- [Flags for video I/O](https://docs.opencv.org/master/d4/d15/group__videoio__flags__base.html)

##付録.A test-runner.pyのソース
```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 random
import sys
import codecs
import csv
import datetime
import serial
import pyvisa as visa
import cv2
from PIL import Image
import pyocr
import pyocr.builders
import platform
import subprocess
from subprocess import PIPE
import threading

UNINITIALIZED = 0xdeadbeef
NUM_OF_SERVO = 6

# Text over Video Frame
gVideoTxt = "null"

def set_video_txt(str):
    global gVideoTxt
    gVideoTxt = str

def get_video_txt():
    global gVideoTxt
    return gVideoTxt

# Video Recording Thread flag
gVideoRec = False

def set_video_rec(flag):
    global gVideoRec
    if flag:
        gVideoRec = True
    else:
        gVideoRec = False

def get_video_rec():
    global gVideoRec
    return gVideoRec

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

def close_uart(h):
    if h != UNINITIALIZED:
        h.close()
    else:
        #print("UART Not Initialized.")
        pass

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 open_cam(camera_number, width, height):
    h = cv2.VideoCapture(camera_number)
    h.set(cv2.CAP_PROP_FRAME_WIDTH, width)
    h.set(cv2.CAP_PROP_FRAME_HEIGHT, height)
    return h

def close_cam(cam):
    if cam != UNINITIALIZED:
        cam.release()

def capture_cam(cam, filename):
    if cam != UNINITIALIZED:
        _, img = cam.read()
        cv2.imwrite(filename, img)
        return True
    else:
        print("CAM Not Ready.")
        return False

def crop_img(filename_in, v, h, filename_out):
    img = cv2.imread(filename_in, cv2.IMREAD_COLOR)
    v0 = int(v.split(':')[0])
    v1 = int(v.split(':')[1])
    h0 = int(h.split(':')[0])
    h1 = int(h.split(':')[1])
    img2 = img[v0:v1, h0:h1]
    cv2.imwrite(filename_out, img2)
    return True

def video_rec(cam, filename):
    if cam == UNINITIALIZED:
        return False

    width  = int(cam.get(cv2.CAP_PROP_FRAME_WIDTH))
    height = int(cam.get(cv2.CAP_PROP_FRAME_HEIGHT))
    fps    = cam.get(cv2.CAP_PROP_FPS)
    print(str(width) + "x" + str(height) + "," + str(fps) + "fps")
    fourcc = cv2.VideoWriter_fourcc('m','p','4','v')
    out = cv2.VideoWriter(filename, fourcc, fps, (width,height))

    while True:
        ret, img = cam.read()
        if ret == True:
            timestamp = datetime.datetime.now().strftime("%Y/%m/%d %H:%M:%S")
            videotxt  = get_video_txt()
            cv2.rectangle(img, (5,5), (width-5,25), (255,255,255), thickness=-1)
            cv2.putText(img, timestamp, ( 10,20), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0,0,0), 1, cv2.LINE_AA)
            cv2.putText(img, videotxt,  (200,20), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0,0,0), 1, cv2.LINE_AA)
            out.write(img)
            cv2.imshow('img', img)

        if (cv2.waitKey(1) == ord('q')) or (get_video_rec() == False):
            print("### rec stop ###")
            break

    out.release()
    return True

def open_ocr():
    ocr = pyocr.get_available_tools()
    if len(ocr) != 0:
        ocr = ocr[0]
    else:
        ocr = UNINITIALIZED
        print("OCR Not Ready.")
    return ocr

def exec_ocr(ocr, filename):
    try:
        txt = ocr.image_to_string(
            Image.open(filename),
            lang = "eng",
            builder = pyocr.builders.TextBuilder()
        )
    except:
        print("OCR Fail.")
    else:
        return txt

def exec_labelimg(filename, label_string):
    if platform.system() == "Windows" :
        python = "python"
        grep = "findstr"
    else:
        python = "python3"
        grep = "grep"

    cmd = python + \
          " label_image.py \
          --graph=c:\\tmp\\output_graph.pb \
          --labels=c:\\tmp\\output_labels.txt \
          --input_layer=Placeholder \
          --output_layer=final_result \
          --image=" + filename \
          + "|" + grep + " " + label_string
    print(cmd)
    log = subprocess.run(cmd, stdout=subprocess.PIPE, shell=True)
    ret = log.stdout.strip().decode("utf-8").split(" ")[1]
    return ret

def set_servo(uart, servo_id, servo_pos):
    if servo_id < 0 or servo_id >= NUM_OF_SERVO:
        print("Invalid Servo ID")
        return False

    if servo_pos < 0 or servo_pos > 180:
        print("Invalid Servo Position")
        return False

    if uart != UNINITIALIZED:
        serial_write(uart, "servoread")

        servo_position = uart.readline().strip().decode('utf-8')
        print(servo_position)

        # discard "OK"
        devnull = uart.readline().strip().decode('utf-8')

        current_pos = int(servo_position.split(' ')[servo_id])
        #print(servo_id, current_pos)

        if current_pos < servo_pos:
            start = current_pos +1
            stop  = servo_pos +1
            step  = 1
        else:
            start = current_pos -1
            stop  = servo_pos -1
            step  = -1

        for i in range(start, stop, step):
            command = "servo " + str(servo_id) + " " + str(i)
            print(command)
            serial_write(uart, command)
            # discard "OK"
            devnull = uart.readline().strip().decode('utf-8')
            sleep(0.2) # sec.

        return True
    else:
        print("UART Not Initialized.")
        return False

def main():
    is_passed = True
    val = str(UNINITIALIZED)
    fval = 0.0
    uart = UNINITIALIZED
    dso = UNINITIALIZED
    cam = UNINITIALIZED
    ocr = UNINITIALIZED
    cmds = ""
    set_video_rec(False)
    set_video_txt("null")

    if len(sys.argv) == 2:
        script_file_name = sys.argv[1] + ".csv"
        result_file_name = sys.argv[1] + "_result.csv"
    else:
        script_file_name = "script.csv"
        result_file_name = "result.csv"

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

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

            script_line = 0
            for cmd in script:
                timestamp = datetime.datetime.now().strftime("%Y/%m/%d %H:%M:%S")
                script_line += 1
                print("##### " + timestamp + " " + str(script_line) + " #####")

                cmds = str(script_line) + ":"
                for i in range(len(cmd)):
                    cmds += cmd[i] + ","
                set_video_txt(cmds)
                print(cmds)

                if "#" in cmd[0]:
                    pass

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

                elif cmd[0] == "rusleep":
                    fval = random.uniform(float(cmd[1]), float(cmd[2]))
                    cmd.append(str(fval))
                    cmds += str(fval)
                    set_video_txt(cmds)
                    print(cmds)
                    sleep(fval)

                elif cmd[0] == "open_uart":
                    if len(cmd) == 2:
                        dsrdtr_val = 1
                    else:
                        dsrdtr_val = int(cmd[2])
                    try:
                        uart = serial.Serial(cmd[1], 115200, timeout=1.0, dsrdtr=dsrdtr_val)
                    except:
                        is_passed = False

                elif cmd[0] == "send":
                    ret = serial_write(uart, cmd[1])
                    if ret == False:
                        is_passed = False

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

                elif cmd[0] == "open_dso":
                    try:
                        dso = open_dso()
                    except:
                        is_passed = False

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

                elif cmd[0] == "open_cam":
                    if len(cmd) == 2:
                        cam = open_cam(int(cmd[1]), 640, 480)
                    else:
                        cam = open_cam(int(cmd[1]), int(cmd[2]), int(cmd[3]))

                elif cmd[0] == "close_cam":
                    close_cam(cam)
                    cam = UNINITIALIZED

                elif cmd[0] == "capture_cam":
                    ret = capture_cam(cam, cmd[1])
                    if ret == False:
                        is_passed = False

                elif cmd[0] == "rec_start":
                    if get_video_rec() == False:
                        set_video_rec(True)
                        thread1 = threading.Thread(target=video_rec, args=(cam,cmd[1],))
                        thread1.start()
                    else:
                        print("Already Recording.")

                elif cmd[0] == "rec_stop":
                    if get_video_rec() == True:
                        set_video_rec(False)
                        thread1.join()
                    else:
                        print("No Recording.")

                elif cmd[0] == "crop_img":
                    crop_img(cmd[1], cmd[2], cmd[3], cmd[4])

                elif cmd[0] == "open_ocr":
                    ocr = open_ocr()
                    if ocr == UNINITIALIZED:
                        is_passed = False

                elif cmd[0] == "exec_ocr":
                    try:
                        val = exec_ocr(ocr, cmd[1])
                    except:
                        is_passed = False
                    else:
                        cmd.append(str(val))

                elif cmd[0] == "exec_labelimg":
                    try:
                        val = exec_labelimg(cmd[1], cmd[2])
                    except:
                        is_passed = False
                    else:
                        cmd.append(str(val))

                elif cmd[0] == "set_servo":
                    ret = set_servo(uart, int(cmd[1]), int(cmd[2]))
                    if ret == False:
                        is_passed = False

                elif cmd[0] == "run":
                    ret = subprocess.run(cmd[1], shell=True, stdout=PIPE, stderr=PIPE, universal_newlines=True)
                    val = ret.stdout.strip()
                    print(ret)
                    if ret.returncode != 0:
                        is_passed = False

                elif cmd[0] == "eval_str_eq":
                    if str(val) != str(cmd[1]):
                        is_passed = False

                elif cmd[0] == "eval_int_eq":
                    if int(val) != int(cmd[1]):
                        is_passed = False

                elif cmd[0] == "eval_int_gt":
                    if int(val) < int(cmd[1]):
                        is_passed = False

                elif cmd[0] == "eval_int_lt":
                    if int(val) > int(cmd[1]):
                        is_passed = False

                elif cmd[0] == "eval_dbl_eq":
                    if float(val) != float(cmd[1]):
                        is_passed = False

                elif cmd[0] == "eval_dbl_gt":
                    if float(val) < float(cmd[1]):
                        is_passed = False

                elif cmd[0] == "eval_dbl_lt":
                    if float(val) > float(cmd[1]):
                        is_passed = False

                else:
                    cmd.append("#")

                if is_passed == True:
                    cmd.append("OK")
                    cmd.insert(0,timestamp)
                    print(cmd)
                    result.writerow(cmd)
                else:
                    cmd.append("NG")
                    cmd.insert(0,timestamp)
                    print(cmd)
                    result.writerow(cmd)
                    if get_video_rec() == True:
                        set_video_rec(False)
                        thread1.join()
                    #else:
                    #    print("No Recording.")
                    close_uart(uart)
                    close_cam(cam)
                    print("FAIL")
                    sys.exit(1)

    if is_passed == True:
        if get_video_rec() == True:
            set_video_rec(False)
            thread1.join()
        #else:
        #    print("No Recording.")
        close_uart(uart)
        close_cam(cam)
        print("PASS")
        sys.exit(0)

main()
```

##付録.B シーケンス図のPlantUMLソース

```
@startuml

title test-runner Video Recording Diagram

participant "command input" as cmd order 10 #White
participant "main()" as main order 20
participant "video_rec()" as video_rec order 30 #LightGreen
participant "set_video_txt()" as set_video_txt order 40 #LightCyan
participant "get_video_txt()" as get_video_txt order 50 #LightCyan
participant "set_video_rec()" as set_video_rec order 60 #LightSteelBlue
participant "get_video_rec()" as get_video_rec order 70 #LightSteelBlue

== initialize ==
cmd -> main : open_cam (omission)

== start recording==
cmd -> main : rec_start

    main -> set_video_txt : "rec_start"

    main -> set_video_rec : True

    main ->> video_rec : start video_rec() thread.
    activate video_rec

        video_rec ->  get_video_txt
        video_rec <-- get_video_txt : "rec_start"

        video_rec ->  get_video_rec
        video_rec <-- get_video_rec : True

== stop recording ==
cmd -> main : rec_stop

    main -> set_video_txt : "rec_stop"

    main -> set_video_rec : False

        video_rec ->  get_video_txt
        video_rec <-- get_video_txt : "rec_stop"

        video_rec ->  get_video_rec
        video_rec <-- get_video_rec : False

    main -> main : wait for the thread termination.
    deactivate video_rec

@enduml
```

##付録C. Lチカで始めるテスト自動化・記事一覧
1. [Lチカで始めるテスト自動化](https://qiita.com/pbjpkas/items/fd4bd2689deda2d793aa)
2. [Lチカで始めるテスト自動化(2)テストスクリプトの保守性向上](https://qiita.com/pbjpkas/items/337bce392281ee5f0526)
3. [Lチカで始めるテスト自動化(3)オシロスコープの組込み](https://qiita.com/pbjpkas/items/b80b7e199a5aed4688a4)
4. [Lチカで始めるテスト自動化(4)テストスクリプトの保守性向上(2)](https://qiita.com/pbjpkas/items/a02c4128baf5a3f2a46c)
5. [Lチカで始めるテスト自動化(5)WebカメラおよびOCRの組込み](https://qiita.com/pbjpkas/items/9af9e6441e1e20c5909e)
6. [Lチカで始めるテスト自動化(6)AI(機械学習)を用いたPass/Fail判定](https://qiita.com/pbjpkas/items/405d55f809da1221b867)
7. [Lチカで始めるテスト自動化(7)タイムスタンプの保存](https://qiita.com/pbjpkas/items/8e3c4330f4a81d30bd53)
8. [Lチカで始めるテスト自動化(8)HDMIビデオキャプチャデバイスの組込み](https://qiita.com/pbjpkas/items/58f84c347e44fb9d5635)
9. [Lチカで始めるテスト自動化(9)6DoFロボットアームの組込み](https://qiita.com/pbjpkas/items/7a4cb4c59886b1fd2cf9)
10. [Lチカで始めるテスト自動化(10)6DoFロボットアームの制御スクリプトの保守性向上](https://qiita.com/pbjpkas/items/c011d7ad6f98bc1458d5)
11. [Lチカで始めるテスト自動化(11)ロボットアームのコントローラ製作](https://qiita.com/pbjpkas/items/fa6d2956ea3dd18b3584)
12. [Lチカで始めるテスト自動化(12)書籍化の作業メモ](https://qiita.com/pbjpkas/items/e76a763de7f496d5430c)
13. [Lチカで始めるテスト自動化(13)外部プログラムの呼出し](https://qiita.com/pbjpkas/items/717b2aba39798d8cab1d)
14. [Lチカで始めるテスト自動化(14)sleepの時間をランダムに設定する](https://qiita.com/pbjpkas/items/4f023917f31f6096eb6b)
15. [Lチカで始めるテスト自動化(15)Raspberry Pi Zero WHでテストランナーを動かして秋月のIoT学習HATキットに進捗を表示する](https://qiita.com/pbjpkas/items/a2e7551ae10f7dce2505)
16. [Lチカで始めるテスト自動化(16)秋月のIoT学習HATキットにBME280を接続してテスト実行環境の温度・湿度・気圧を取得する](https://qiita.com/pbjpkas/items/0a48ca71a2de0042e188)
17. [Lチカで始めるテスト自動化(17)コマンド制御のBLE Keyboard & MouseをM5Stackで製作しiOSアプリをテストスクリプトで操作する](https://qiita.com/pbjpkas/items/3df58743644128a11b9e)
18. [Lチカで始めるテスト自動化(18)秋月のIoT学習HATキットの圧電ブザーでテスト終了時にpass/failに応じてメロディを流す](https://qiita.com/pbjpkas/items/43a609581cc68b7ea2fd)

電子書籍化したものを[技術書典](https://techbookfest.org/)で頒布しています。
1. [Lチカで始めるテスト自動化](https://techbookfest.org/product/5661312486998016)
2. [Lチカで始めるテスト自動化 -2- リレー駆動回路の設計](https://techbookfest.org/product/5299462253576192) (※書き下ろし)
3. [Lチカで始めるテスト自動化 -3- Raspberry Pi Zero WHとIoT学習HATキットで作るテストランナー](https://techbookfest.org/product/6323836549070848)