실제 하드웨어를 위한 시뮬레이터입니다. 이 프로젝트는 여러 개의 시뮬레이션된 장치를 생성하고 동시에 요청을 처리할 수 있는 서버를 제공합니다.
이 프로젝트는 구성 파일(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 "
장치가 단일 요청에 대해 여러 개의(잠재적으로 지연된) 응답으로 응답하는 프로토콜을 구현하는 경우, 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 라이브러리를 개발 중이고 이에 대한 시뮬레이터를 작성한 경우 시뮬레이터에 대해 라이브러리를 테스트하는 데 관심이 있을 수 있습니다.
sinstruments는 별도의 스레드에서 시뮬레이터를 생성하는 한 쌍의 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에서 할당한 현재 주소를 검색하여 테스트에 사용합니다.
보시다시피 테스트는 이식성을 제공하는 특정 포트의 가용성에 의존하지 않습니다.
다음은 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
두 번째 도우미는 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