真實硬體的模擬器。該專案提供了一個能夠產生多個模擬設備並同時處理請求的伺服器。
該專案僅提供從設定檔(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 表示要求作業系統分配一個空閒連接埠(對於執行測試套件很有用)。否則必須是有效的 TCP 或 UDP 連接埠。
url代表一個由模擬器創建的特殊文件,用於模擬可存取的串行線路,如/dev/ttyS0
Linux 串行線路檔案。
此功能僅在 Linux 和使用 Python 實作偽終端pty
的系統中可用。
網址是可選的。模擬器將始終建立一個不確定的名稱,例如/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
。這告訴模擬器監聽作業系統提供的任何空閒連接埠。
實際測試檢索作業系統分配的當前位址並在測試中使用它。
正如您所看到的,這些測試並不依賴特定連接埠的可用性,這使得它們具有可移植性。
以下是如何使用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