真实硬件的模拟器。该项目提供了一个能够生成多个模拟设备并同时处理请求的服务器。
该项目仅提供从配置文件(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