Симулятор реального железа. Этот проект предоставляет сервер, способный создавать несколько имитируемых устройств и одновременно обслуживать запросы.
Этот проект предоставляет только необходимую инфраструктуру для запуска сервера из файла конфигурации (YAML, TOML или json) и средства для регистрации сторонних плагинов устройств через механизм точки входа Python.
На данный момент проект предоставляет транспорт для TCP, UDP и последовательной линии. Поддержка новых видов транспорта (например, USB, GPIB или SPI) реализуется по мере необходимости.
Пиар приветствуется!
( 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>
Файл конфигурации описывает, какие устройства должен создавать сервер, а также ряд опций, таких как транспорт(ы), на которых каждое устройство прослушивает запросы.
Представьте, что вам нужно смоделировать два GE Pace 5000, каждый из которых доступен через порт TCP, и CryoCon 24C, доступный через последовательную линию.
Сначала убедитесь, что зависимости установлены с помощью:
$ pip install gepace[simulator] cryoncon[simulator]
Теперь мы можем подготовить конфигурацию YAML под названием simulator.yml
:
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
Вот и все! Теперь вы сможете подключиться к любому устройству Pace через TCP или CryoCon, используя локальную эмулируемую последовательную линию.
Давайте попробуем подключиться к первому Pace с помощью инструмента командной строки Linux nc (он же netcat) и запросить хорошо известный *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-адрес представляет собой специальный файл, который создается симулятором для имитации последовательной линии, доступной как файл последовательной линии Linux /dev/ttyS0
.
Эта функция доступна только в 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
Симулятор предоставляет консоль Python с бэкдором gevent, которую вы можете активировать, если хотите удаленно получить доступ к работающему процессу симулятора. Чтобы активировать эту функцию, просто добавьте на верхний уровень конфигурации следующее:
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
является частью пакета Python под названием myproject
, второе, что нужно сделать, — это зарегистрировать плагин симулятора в файле 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
при запуске. Кроме того, если начальное значение не настроено, по умолчанию оно должно быть «ВЫКЛ».
Сначала давайте добавим это в наш пример конфигурации:
# 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