実際のハードウェアのシミュレータ。このプロジェクトは、複数のシミュレートされたデバイスを生成し、リクエストを同時に処理できるサーバーを提供します。
このプロジェクトは、構成ファイル (YAML、TOML、または json) からサーバーを起動するために必要なインフラストラクチャと、Python エントリ ポイント メカニズムを通じてサードパーティのデバイス プラグインを登録する手段のみを提供します。
これまでのところ、このプロジェクトは TCP、UDP、シリアル回線のトランスポートを提供しています。新しいトランスポート (USB、GPIB、SPI など) のサポートは、必要に応じて実装されています。
PRの方も大歓迎です!
( TL;DR : pip install sinstruments[all]
)
お気に入りの Python 環境内から:
$ pip install sinstruments
さらに、YAML 構成ファイルを YAML で作成する場合は、次のようにします。
$ pip install sinstruments[yaml]
...または、TOML ベースの構成の場合:
$ pip install sinstruments[toml]
インストールすると、サーバーは次のように実行できます。
$ sinstruments-server -c <config file name>
構成ファイルには、サーバーがインスタンス化する必要があるデバイスと、各デバイスがリクエストをリッスンするトランスポートなどの一連のオプションが記述されています。
それぞれ TCP ポート経由でアクセス可能な 2 つの GE Pace 5000 と、シリアル回線経由でアクセス可能な CryoCon 24C をシミュレートする必要があると想像してください。
まず、依存関係が以下を使用してインストールされていることを確認します。
$ pip install gepace[simulator] cryoncon[simulator]
これで、 simulator.yml
という YAML 構成を準備できます。
devices :
- class : Pace
name : pace-1
transports :
- type : tcp
url : :5000
- class : Pace
name : pace-2
transports :
- type : tcp
url : :5001
- class : CryoCon
name : cryocon-1
transports :
- type : serial
url : /tmp/cryocon-1
これでサーバーを起動する準備が整いました。
$ sinstruments-server -c simulator.yml
それでおしまい!これで、ローカルのエミュレートされたシリアル回線を使用して、TCP または CryoCon 経由で任意の Pace デバイスに接続できるようになります。
nc (別名 netcat) Linux コマンド ライン ツールを使用して最初の Pace に接続して、よく知られている*IDN?
を尋ねてみましょう。 SCPIコマンド:
$ nc localhost 5000
*IDN?
GE,Pace5000,204683,1.01A
これは、独自のシミュレータを提供する既知のサードパーティ計測ライブラリの概要です。
公的に入手可能なデバイスを作成した場合は、PR を作成して上記のリストを自由に記入してください。
ヒント: sinstruments-server ls
利用可能なプラグインのリストが表示されます。
構成ファイルは、以下の説明を含む辞書に変換される限り、YAML、TOML、または JSON ファイルにすることができます。
この章では、YAML を参考例として使用します。
ファイルには、少なくともdevices
と呼ばれるトップレベルのキーが含まれている必要があります。値はデバイスの説明のリストである必要があります。
devices :
- class : Pace
name : pace-1
transports :
- type : tcp
url : :5000
各デバイスの説明には次のものが含まれている必要があります。
tcp
、 udp
、 serial
です)各デバイスに指定されたその他のオプションは、実行時に特定のプラグイン オブジェクトに直接渡されます。各プラグインは、サポートする追加オプションとその使用方法を説明する必要があります。
TCP および UDP トランスポートの場合、 URL は<host>:<port>
形式になります。
(上記の例のような) 空のホストは、 0.0.0.0
(すべてのネットワーク インターフェイスでリッスンすることを意味します) として解釈されます。ホストが127.0.0.1
またはlocalhost
の場合、デバイスはシミュレーターが実行されているマシンからのみアクセスできます。
ポート値 0 は、空きポート (テスト スイートの実行に便利) を割り当てるように OS に要求することを意味します。それ以外の場合は、有効な TCP または UDP ポートを指定する必要があります。
URL は、 /dev/ttyS0
Linux シリアル ライン ファイルのようにアクセス可能なシリアル ラインをシミュレートするためにシミュレーターによって作成される特別なファイルを表します。
この機能は、Linux および擬似端末pty
が Python で実装されているシステムでのみ使用できます。
URLはオプションです。シミュレーターは常に/dev/pts/4
のような非決定的な名前を作成し、アクセスする必要がある場合に備えてこの情報をログに記録します。この機能は、テスト スイートを実行する場合に最も役立ちます。
シミュレーターにシンボリック ファイルを作成する権限があることが確認されている限り、任意のURLパス (例: /dev/ttyRP10
) を自由に選択できます。
トランスポート (TCP、UDP、およびシリアル ライン) のいずれについても、追加のbaudrate
パラメーターを構成に提供することで、通信チャネル速度の基本的なシミュレーションを実行できます。例:
- class : CryoCon
name : cryocon-1
transports :
- type : serial
url : /tmp/cryocon-1
baudrate : 9600
シミュレーターには、実行中のシミュレーター プロセスにリモートでアクセスする場合にアクティブ化できる gevent バック ドア Python コンソールが用意されています。この機能を有効にするには、構成の最上位に次の内容を追加するだけです。
backdoor : ["localhost": 10001]
devices :
- ...
他の TCP ポートとバインド アドレスを自由に選択できます。このバックドアは認証を提供せず、リモート ユーザーが実行できることを制限しようとしないことに注意してください。サーバーにアクセスできる人は誰でも、実行中の Python プロセスが実行できるアクションを実行できます。したがって、任意のインターフェイスにバインドできますが、セキュリティ上の理由から、ローカル マシンにのみアクセスできるインターフェイス (例: 127.0.0.1/localhost) にバインドすることをお勧めします。
使用法
バックドアが設定され、サーバーが実行されたら、別の端末で次のように接続します。
$ nc 127.0.0.1 10001
Welcome to Simulator server console.
You can access me through the 'server()' function. Have fun!
>>> print(server())
...
新しいデバイスの作成は簡単です。 SCPI オシロスコープをシミュレートしたいと考えてみましょう。必要なのは、BaseDevice から継承するクラスを作成し、デバイスでサポートされているさまざまなコマンドを処理するhandle_message(self, message)
を実装することだけです。
# myproject/simulator.py
from sinstruments . simulator import BaseDevice
class Oscilloscope ( BaseDevice ):
def handle_message ( self , message ):
self . _log . info ( "received request %r" , message )
message = message . strip (). decode ()
if message == "*IDN?" :
return b"ACME Inc,O-3000,23l032,3.5A"
elif message == "*RST" :
self . _log . info ( "Resetting myself!" )
...
常にbytes
返すことを忘れないでください。シミュレーターはstr
エンコードする方法を推測しません。
このファイルsimulator.py
myproject
という Python パッケージの一部であると仮定すると、次に行うべきことは、setup.py にシミュレーター プラグインを登録することです。
setup (
...
entry_points = {
"sinstruments.device" : [
"Oscilloscope=myproject.simulator:Oscilloscope"
]
}
)
これで、構成ファイルを作成してシミュレータを起動できるようになります。
# oscilo.yml
devices :
- class : Oscilloscope
name : oscilo-1
transports :
- type : tcp
url : :5000
次にサーバーを起動します
$ sinstruments-server -c oscillo.yml
そして、次のように接続できるはずです。
$ nc localhost 5000
*IDN?
ACME Inc,O-3000,23l032,3.5A
デフォルトでは、 eol
n
に設定されます。次のコマンドを使用して任意の文字に変更できます。
class Oscilloscope ( BaseDevice ):
newline = b" r "
デバイスが 1 つのリクエストに対して複数の (遅延する可能性がある) 応答を返すプロトコルを実装している場合は、 handle_message()
ジェネレーターに変換することでこれをサポートできます。
class Oscilloscope ( BaseDevice ):
def handle_message ( self , message ):
self . _log . info ( "received request %r" , message )
message = message . strip (). decode ()
if message == "*IDN?" :
yield b"ACME Inc,O-3000,23l032,3.5A"
elif message == "*RST" :
self . _log . info ( "Resetting myself!" )
elif message == "GIVE:ME 10" :
for i in range ( 1 , 11 ):
yield f"Here's { i } n " . encode ()
...
常にbytes
与えることを忘れないでください。シミュレーターはstr
エンコードする方法を推測しません。
シミュレートされたデバイスに追加の構成が必要な場合は、同じ YAML ファイルを通じて提供できます。
起動時にデバイスがCONTROL
モードであるかどうかを設定できるようにしたいとします。さらに、初期値が設定されていない場合は、デフォルトで「OFF」にする必要があります。
まず、これを構成例に追加してみましょう。
# oscilo.yml
devices :
- class : Oscilloscope
name : oscilo-1
control : ON
transports :
- type : tcp
url : :5000
次に、オシロスコープ__init__()
再実装して、この新しいパラメータをインターセプトし、 handle_message()
で処理します。
class Oscilloscope ( BaseDevice ):
def __init__ ( self , name , ** opts ):
self . _control = opts . pop ( "control" , "OFF" ). upper ()
super (). __init__ ( name , ** opts )
def handle_message ( self , message ):
...
elif message == "CONTROL" :
return f"CONTROL { self . _control } n " . encode ()
...
予約キーname
、 class
、 transports
と競合しない限り、オプションを必要なだけ自由に追加できます。
一部の機器は、EOL ベースのメッセージ プロトコルによって適切に管理されないプロトコルを実装しています。
シミュレーターを使用すると、独自のメッセージ プロトコルを作成できます。以下に例を示します。
from sinstruments . simulator import MessageProtocol
class FixSizeProtocol ( MessageProtocol ):
Size = 32
def read_messages ( self ):
transport = self . transport
buff = b''
while True :
buff += transport . read ( self . channel , size = 4096 )
if not buff :
return
for i in range ( 0 , len ( buff ), self . Size ):
message = buff [ i : i + self . Size ]
if len ( message ) < self . Size :
buff = message
break
yield message
class Oscilloscope ( BaseDevice ):
protocol = FixSizeProtocol
...
ソケットまたはシリアル回線を介してアクセスできる計測器へのアクセスを提供する Python ライブラリを開発していて、そのためのシミュレータを作成した場合は、シミュレータに対してライブラリをテストすることに興味があるかもしれません。
instruments は、別のスレッドでシミュレーターを生成する pytest ヘルパーのペアを提供します。
server_context
最初の使用法は、 server_context
ヘルパーを単純に使用することです。実際には、このヘルパーについて pytest に特化したものは何もないので、他のシナリオでも同様に使用することを想像できます。
以下に例を示します。
import pytest
from sinstruments . pytest import server_context
cfg = {
"devices" : [{
"name" : "oscillo-1" ,
"class" : "Oscilloscope" ,
"transports" : [
{ "type" : "tcp" , "url" : "localhost:0" }
]
}]
}
def test_oscilloscope_id ():
with server_context ( cfg ) as server :
# put here code to perform your tests that need to communicate with
# the simulator. In this example an oscilloscope client
addr = server . devices [ "oscillo-1" ]. transports [ 0 ]. address
oscillo = Oscilloscope ( addr )
assert oscillo . idn (). startswith ( "ACME Inc,O-3000" )
この構成ではポート0
使用していることに気づくかもしれません。これは、OS が提供する空きポートをリッスンするようにシミュレーターに指示します。
実際のテストでは、OS によって割り当てられた現在のアドレスを取得し、それをテストで使用します。
ご覧のとおり、テストは 1 つの特定のポートの可用性に依存していないため、移植性が高くなります。
ここでは、 server_context
ヘルパーを使用して独自のフィクスチャを作成する方法についての提案を示します。目的は、テストを作成するために必要な定型コードの量を減らすことでした。
@ pytest . fixture
def oscillo_server ():
with server_context ( config ) as server :
server . oscillo1 = server . devices [ "oscillo-1" ]
server . oscillo1 . addr = server . oscillo1 . transports [ 0 ]. address
yield server
def test_oscilloscope_current ( oscillo_server ):
oscillo = Oscilloscope ( oscillo_server . oscillo1 . addr )
assert .05 < oscillo . current () < 0.01
server
2 番目のヘルパーはserver
フィクスチャです。このフィクスチャは、モジュールに存在する必要がある既存のconfig
機能に依存します。前のコードに続く例を次に示します。
from sinstruments . pytest import server
@ pytest . fixture
def config ()
yield cfg
def test_oscilloscope_voltage ( server ):
addr = server . devices [ "oscillo-1" ]. transports [ 0 ]. address
oscillo = Oscilloscope ( addr )
assert 5 < oscillo . voltage () < 10