実験自動化(Lab Automation)のススメ -client software編-

前回記事
https://qiita.com/notori48/items/519eeced7a1d9e7ee735
では、実験自動化(Lab Automation)のための物理NW構成について書きました。
今回は、client softwareの書き方についてまとめます。
Pyhonのpyvisa,telnetlibを使用します。

最初に、pyvsiaを扱います。ここで共通事項についても説明します。

# PyVISA

PyVISAは、VISAを用いてCLIによる装置制御を行うためのライブラリです。
https://pyvisa.readthedocs.io/en/latest/

VISAをたたくことで、装置I/F(Ethernet,GPIB,RS232,USB...)によらず
同じmethodで制御スクリプトを記述できます。

例を見てみます。

```python
import pyvisa
rm = pyvisa.ResourceManager()
rm.list_resources()
('ASRL1::INSTR', 'ASRL2::INSTR', 'GPIB0::12::INSTR')
inst = rm.open_resource('GPIB0::12::INSTR')
print(inst.query("*IDN?"))
```

`rm.list_resources()`　により、PCへ接続されている装置が自動的にサーチされてリストアップされます。
`'ASRL1::INSTR'`　のような文字列は、visaアドレスとかvisaリソースネームと呼ばれ、装置を一意に識別します。
また、'ASRL'は原則USBデバイスを、'GPIB'はGPIBデバイスを意味します。
他にも'TCPIP'などがあります。

`inst = rm.open_resource('GPIB0::12::INSTR')` により、GPIBデバイスであって、GPIBアドレスが12番の装置との接続をopenし、インスタンスinstを生成します。
このクラスのmethodとしては、制御コマンドの送受信をするためのwrite,read,query等があります。
queryは、何らかの問い合わせコマンドに対して使用するもので、writeした後にreadをセットで行ってくれます。
基本的には、writeとqueryのみを使います。

例の `"IDN?"`は装置の型番を問い合わせるコマンドです。
IDNは、SCPIというコマンド標準で定められたコマンドであり、だいたいの装置で使うことができます。

コマンドを送信した後は、`inst.close()`でセッションを終了します。
忘れやすいので、with構文を使用するとよいでしょう。
```python
import pyvisa
rm = pyvisa.ResourceManager()
rm.list_resources()
('ASRL1::INSTR', 'ASRL2::INSTR', 'GPIB0::12::INSTR')
with rm.open_resource('GPIB0::12::INSTR') as inst:
    print(inst.query("*IDN?"))
```

良く引っかかってしまうポイントとして、termination characterがあります。
termination characterは、送信コマンドまたは受信文字列がどこで終わりかを示す文字です。
一般に、装置ごとに異なります。
最も多いものは CRLF "\r\n"で、続いて多いのはCR "\r"、まれにあるのが LF "\n"です。
termination characterは、セッションopenの前または後で指定ができます。
```python
my_instrument.read_termination = '\n'
my_instrument.write_termination = '\n'
```

もし装置から何の応答も帰ってこなかった場合には、termination characterを疑いましょう。

他に引っかかってしまうポイントとして、装置が返す文字列の形式が特殊な場合があります。
先に述べたように、pyvisaのqueryでは、readのtermination characterが来るのを期待して待ちます。
しかし、装置が返してきた文字列にたまたまtermination characterが入っていた場合には、そこで文字列の終端だと解釈して読み込みを打ち切ってしまいます。
通常はこのようなことはないのですが、装置が数値を文字コード(ascii)として返してくるのではなく生の数値(bit表現されたfloatなど)を返してくる使用だった場合に、たまたまそれがtermination characterの文字コードとして解釈できる部分列を含んでおり、受信が打ち切られる場合があります。
（実際に遭遇したことがあります）
このような場合には`read_bytes`　という低級のmethodを用いて、terminationを無視して受信文字列を取り込むなどの工夫が必要です。
```python
inst.write('CURV?')
data = inst.read_bytes(1) # read 1 byte
```

最後に、コマンド標準(SCPI)について説明します。
コマンドは装置ごとに全く異なりますが、電源再起動コマンドぐらいは共通だとうれしいですよね。
また、コマンドの形式(例えば設定対象と設定数値の間を、空白で区切るのか、カンマで区切るのかなど）ぐらいは共通であってほしいですよね。
このような要請に従って生まれたのが SCPI という標準です。
https://www.keysight.com/jp/ja/lib/resources/training-materials/resource-2304680.html

もし装置がSCPI標準に従うコマンド実装していれば、楽に扱えます。
（むろん、従っていない装置もあります）
例えば、SCPIでは以下のようなコマンドは”共通コマンド”とされており、どの装置でも実装されています。
（ただし義務ではないので、実装が欠落していたり、queryの結果が常にTrueになる嘘実装の場合もあります。あまり信用しないようにしましょう。）
共通コマンドは "*"から始まるのが特徴です。

```
*IDN?: 測定器のIDを取得するコマンド。測定器は、以下のようなフォー
マットの文字列で情報を返します。
<製造業者>,<モデル番号>,<シリアル番号>,<ファームウェアのリビジョン>

*RST: 
測定器をリセットするコマンド。

*OPC?	動作完了クエリ・コマンド。実行待ちの動作がすべて終了したら、「1」を返します。用い方の例としては、測定器の設定コマンドを一連送った後、*OPC?を送れば、測定器の設定が終了した場合に、「1」が返りますので、これを見て、次の測定へ移ります。実行待ち動作に時間が掛かる事が予想される場合には、*OPC?のタイムアウト設定を長めに設定しておきます。
```
この中で、*OPC?は特に便利です。
*OPC?を送ると、それまで送ったコマンドが装置側で解釈・反映されるまでの間、ブロック(wait)します。
設定反映後に、Trueが返ってきます。
基本的には、writeで設定を投げた後にはquery("*OPC?")を手癖にするとよいと思います。

SCPIでは、そのほか、コマンドの文字列形式も定められています。
例えばコマンドセットが階層構造を持っており、階層の間は":"で識別します。
（フォルダのパスの "/"　をイメージしてください)
例えば
`:TIMebase:SCALe 0.1`
というコマンドは、TIMebaseという階層の下にあるSCALeというコマンドをたたいています。
またコマンドと数値の間は半角スペース("<white-space>")であることが定められています。
SCALe という設定値を値 "0.1" にセットするコマンドということです。

ここで、コマンド文字列に大文字と小文字が混在しているのが気持ち悪いと思ったかもしれません。
これには意味があります。
実は、このコマンドは、大文字部分だけの文字列
`:TIM:SCAL 0.1`
でも動作します。
これは、ロングフォームとショートフォームの関係と呼ばれます。
`:TIM:SCAL 0.1`は、`:TIMebase:SCALe 0.1`のショートフォームです。
SCPIでは、省略可能な文字は、コマンドマニュアルには小文字で書くと決まっています。
そのほかにもいろいろルールが決まっている（連続コマンドの場合は";"で区切るなど)ので、SCPI標準について一度読んでおくとよいと思います。

最後に、pyvisaでは、たたくVISA(backend)を選択することができます。
デフォルトでは、NI-VISAなどのVISAソフトウェアを入れていればそれが優先されます。
pyvisaライブラリ独自の（インストール不要な)VISAとして pyVISA-py も用意されています。
`visa.ResourceManager('@py')`
とすると使えます。
ただしpyVISA-pyよりもNI-VISAなどのほうが機能が豊富らしいので、原則NI-VISAなどをインストールしておくことを進めます。

# telnetlib

pyvisaとtelnetは、使用感としてはよく似ています。

https://docs.python.org/3/library/telnetlib.html

```python
import getpass
import telnetlib

HOST = "localhost"
user = input("Enter your remote account: ")
password = getpass.getpass()

tn = telnetlib.Telnet(HOST)

tn.read_until(b"login: ")
tn.write(user.encode('ascii') + b"\n")
if password:
    tn.read_until(b"Password: ")
    tn.write(password.encode('ascii') + b"\n")

tn.write(b"ls\n")
tn.write(b"exit\n")

print(tn.read_all().decode('ascii'))
````

pyvisaと異なるのは、いろいろと指定しないといけないことです。
まず、接続先のIPアドレスとポート番号(typical:23)は自動サーチしてくれません。
また、write termination も read termination も、都度つける必要があります。
先の例だと、write時はwrite terminationである"\n"を毎回つけています。
またread時は、例えば "Password: という文字列が来るまで待つ　としています。
login確立後は、プロンプト ">"や"$"を待つことで送受タイミング同期することになります。
もしプロンプトが返ってこない場合は、受信メッセージに含まれる改行文字"\r\n"などを待つという手もあります。

ここで、pyvisaの時と違って、文字列の先頭にいちいち **b** がついていることに気づいたかもしれません。
telnetlibのwriteの引数は、文字列ではなく、文字列の文字コード(ascii)を10進数→2進数にしてさらに1byte単位で固めた**byte列**になっています。
pythonでは、文字列の先頭に"b"をつけることで、文字列をbyte列にできるのです。
手動でやる場合は、str.encode()とすると、byte列にできます。
また、readで得られる値もbyte列です。
こちらは byte_seq.decode() とやることで文字列に戻ります。

なおclose処理はtn.close()で行いますが、pyvisa同様にwith構文にも対応してますので、with文で使うことを推奨します。

もしtelnetlibでうまく動かないときは、debugとしてtertermなどのターミナルソフトではちゃんと入れるか確認してください。
telnetlibはあくまでそのpython実装にすぎませんので、teratermで無理ならば無理です。


# まとめ
pyvisaまたはtelnetlibを使うことで、ほとんどの測定器はPythonで制御できると思います。
ぜひ自動化に挑戦してみてください。