VISA計測器制御で周波数vs振幅特性の自動計測をするのに、オブジェクト指向的には継承の使い時だという例②

# 概要
ファンクションジェネレータの周波数設定を変化させ、それに伴うオシロスコープの観測波形の画面キャプチャとPeak to Peakの計測値をロギングする作業をPythonで自動化したときの例を紹介します

[前回の記事](https://qiita.com/takeru0x5569/items/cc9b78467924be1f8e16)からの続きになります。


# オシロスコープの派生クラス

## ポイント
* 画面をキャプチャしたい場合は`HARDCopy STARt`を送って、`read_raw()`するとBitmapのバイナリが取得できる
* Bitmapのバイナリを画像ファイルとして保存するのはPython側のお仕事
* Bitmapのバイナリをそのまま保存するとBitmapなのでファイルサイズは大きい
* PILのImageOpsを経由して画像保存するとファイルサイズを小さくできる
* オシロによっては`インクセーバー`なる機能があって、背景が白くなった画面データを取得できる
* TBS1000Cはインクセーバー機能はあるがVISAのコマンドには対応していない
* PILのほうで色反転して保存することでインクセーバーと似たような機能を実装した

※インクセーバーとは・・・　オシロの画面は背景が黒いので紙に印刷するとトナーの消費が激しい、色反転することで背景が白くなって紙に印刷した際の節約ができるよという機能です

## コード

```py:TBS1000C.py
#//////////////////////////////////////////////////////////////////////
# Tektronikオシロスコープ（TBS1000Cシリーズ）用
#//////////////////////////////////////////////////////////////////////
from VisaBase import VisaBase  # Visa計測器の基底クラス
from datetime import datetime   # ファイルタイムスタンプ用
from PIL import Image, ImageOps #スクリーンキャプチャ用
import io

class TBS1000C(VisaBase):
    def __init__(self,connection_type="USB"):
        # "TEKTRONIX"をキーワードにしてリソース名を検索
        resource_list = VisaBase.serch("TEKTRONIX", connection_type)
        if not resource_list:
            raise Exception("該当する計測器が見つかりません")
        super().__init__(resource_list[0])  # ここでリソース名を渡す
    #------------------------------------------------------
    #スクリーンキャプチャ
    def capture_image(self, capture_file_name="",is_ink_save=True):
        # ハードコピーコマンドを送って、RAWデータを読み取る,RAWデータはBMP形式
        self.inst.write("HARDCopy STARt")
        __raw_data = self.inst.read_raw()
        # BMPデータをメモリ上で開き、色を反転
        __bmp_stream = io.BytesIO(__raw_data)
        img = Image.open(__bmp_stream)
        #インクセーブOptionnがTrueなら色を反転して背景を
        if is_ink_save:
            __save_image = ImageOps.invert(img.convert("RGB"))  # 色を反転
        else:
            __save_image = img.convert("RGB")   # 色をそのままだがコンバートした方が生BMP形式よりファイルサイズが圧縮される
        # ファイル名を作成:引数の後ろにタイムスタンプを付加する
        __dt = datetime.now()
        __file_name = capture_file_name + __dt.strftime("_%Y%m%d_%H%M%S.png")
        # PNGで保存
        __save_image.save(__file_name, "PNG")
    #------------------------------------------------------
    #設定コマンドをまとめて送る
    def setup(self):
        # ピーク検出の設定を行う
        command =["MEASUrement:MEAS1:SOUrce1 CH1"
                ,"MEASUrement:MEAS1:TYPe PK2Pk"
                ,"MEASUrement:MEAS1:STATe ON"
        ]
        self.write(command)
    #計測CH１の値を取得
    def MeasValue(self,ch=1):
        self.wait(f"Get pk-pk", 0.5) #安定待ち
        # ピーク検出の値を取得（コマンド直書）
        return self.inst.query(f"MEASUrement:MEAS{ch}:VALue?").strip()

################################################################
#テストコード、計測CH1に入力CHのPk-PK計測を割り当てて読込＆スクショ実行
if __name__ == "__main__":
    tek = TBS1000C("USB")
    tek.setup()
    print(f"Pk = {tek.MeasValue(1)}")
    tek.capture_image("InkSave_")
    tek.capture_image("Nomal_",False)
```

## インクセーバーONで取得した画像

![InkSave__20250527_174816.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/204268/43f48409-16f7-44a3-88e2-9177a325a63a.png)

## インクセーバーOFFで取得した画像

![Nomal__20250527_174818.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/204268/d1b0fce4-2d7c-4c56-b151-d8c772d1e6bd.png)


# 周波数vs振幅特性の計測

```py:
import TBS1000C 
import WF1968 
import time
import matplotlib.pyplot as plt

osc = TBS1000C.TBS1000C("USB") #オシロスコープのインスタンスを作成
fg = WF1968.WF1968("USB")      #ファンクションジェネレータのインスタンスを作成
fg.Output = "ON"    # ファンクションジェネレータの出力をONに設定
osc.setup()  # オシロスコープの設定を行う
startFreq = 100000  # 初期周波数100kHz
stepFreq = 10000  # 周波数ステップ10kHz
result = []  # 結果を格納するリスト
for i in range(0, 11):
    freq  = 100000 + (i *stepFreq)
    fg.Frequency = freq  # 周波数を設定
    osc.capture_image(f"Capture_{freq}Hz_") # スクリーンキャプチャを実行
    pk2pk = osc.MeasValue(1)  # CH1のPk-Pk値を計測
    result.append((freq, pk2pk))  # 結果をリストに追加
    time.sleep(1)  # 1秒待機
fg.Output = "OFF"    

print(result)

# グラフ化
freqs = [x[0] for x in result]
pk2pks = [float(x[1]) for x in result]

plt.plot(freqs, pk2pks, marker="o")
plt.xlabel("Frequency [Hz]")
plt.ylabel("Pk-Pk Value")
plt.title("Frequency vs Pk-Pk Value")
plt.grid(True)
plt.show()
```
# 実行結果の例
スクショした複数画像を合体してアニメーションGIFにするとこんな感じです。
![Capture.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/204268/4a3b1598-383c-4655-ab55-41cf39e1ee24.gif)

で、読み出したPk-Pk値をグラフプロットした結果がこんな感じになりました。
今回はファンクションジェネレータの出力に **ハイパスフィルタ―を入れています** のでこのような結果となっております。

![Figure_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/204268/82b1294b-462d-465c-b984-2dc9533edf6f.png)

ということで、１０点分の計測と波形保存がおおよそ30秒程度で済むようになりました。
というのと、VISAの規定クラスを継承することで、冗長な記述が減らせますということの紹介でした。

# 注意事項
今回はスクショのタイミングでトリガをStopさせていません。よって、スクショのPk-Pk値表示と、コマンドで読んだPk-Pkが完全一致していません。　多少時間が長くなりますが、実際の計測では **トリガ停止を入れてからスクショをして計測値読み出しをすることをお勧め** いたいます。