Um simulador para hardware real. Este projeto fornece um servidor capaz de gerar vários dispositivos simulados e atender solicitações simultaneamente.
Este projeto fornece apenas a infraestrutura necessária para iniciar um servidor a partir de um arquivo de configuração (YAML, TOML ou json) e um meio para registrar plug-ins de dispositivos de terceiros por meio do mecanismo de ponto de entrada python.
Até o momento, o projeto fornece transportes para TCP, UDP e linha serial. O suporte para novos transportes (ex: USB, GPIB ou SPI) está sendo implementado conforme a necessidade.
PRs são bem-vindos!
( TL;DR : pip install sinstruments[all]
)
De dentro do seu ambiente python favorito:
$ pip install sinstruments
Além disso, se você quiser gravar arquivos de configuração YAML em YAML:
$ pip install sinstruments[yaml]
...ou, para configuração baseada em TOML:
$ pip install sinstruments[toml]
Depois de instalado, o servidor pode ser executado com:
$ sinstruments-server -c <config file name>
O arquivo de configuração descreve quais dispositivos o servidor deve instanciar, juntamente com uma série de opções, como o(s) transporte(s) em que cada dispositivo escuta solicitações.
Imagine que você precisa simular 2 GE Pace 5000 acessíveis através de uma porta TCP cada e um CryoCon 24C acessível através da linha serial.
Primeiro, certifique-se de que as dependências estejam instaladas com:
$ pip install gepace[simulator] cryoncon[simulator]
Agora podemos preparar uma configuração YAML chamada 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
Agora estamos prontos para iniciar o servidor:
$ sinstruments-server -c simulator.yml
É isso! Agora você deve conseguir se conectar a qualquer um dos dispositivos Pace por meio de TCP ou CryoCon usando a linha serial emulada local.
Vamos tentar conectar-se ao primeiro Pace com a ferramenta de linha de comando nc (também conhecida como netcat) linux e solicitar o conhecido *IDN?
Comando SCPI:
$ nc localhost 5000
*IDN?
GE,Pace5000,204683,1.01A
Este é um resumo das bibliotecas de instrumentação de terceiros conhecidas que fornecem seus próprios simuladores.
Se você escreveu um dispositivo disponível publicamente, sinta-se à vontade para completar a lista acima criando um PR.
Dica : sinstruments-server ls
mostra uma lista de plugins disponíveis.
O arquivo de configuração pode ser um arquivo YAML, TOML ou JSON, desde que seja traduzido para um dicionário com a descrição fornecida abaixo.
Neste capítulo usaremos YAML como exemplo de referência.
O arquivo deve conter pelo menos uma chave de nível superior chamada devices
. O valor precisa ser uma lista de descrições de dispositivos:
devices :
- class : Pace
name : pace-1
transports :
- type : tcp
url : :5000
Cada descrição de dispositivo deve conter:
tcp
, udp
, serial
)Quaisquer outras opções fornecidas a cada dispositivo são passadas diretamente para o objeto específico do plugin em tempo de execução. Cada plugin deve descrever quais opções adicionais ele suporta e como usá-las.
Para transportes TCP e UDP, a url tem o formato <host>:<port>
.
Um host vazio (como no exemplo acima) é interpretado como 0.0.0.0
(o que significa escutar em todas as interfaces de rede). Se o host for 127.0.0.1
ou localhost
o dispositivo só estará acessível na máquina onde o simulador está rodando.
Um valor de porta 0 significa pedir ao sistema operacional para atribuir uma porta livre (útil para executar um conjunto de testes). Caso contrário, deve haver uma porta TCP ou UDP válida.
A url representa um arquivo especial que é criado pelo simulador para simular uma linha serial acessível como um arquivo de linha serial /dev/ttyS0
linux.
Este recurso está disponível apenas em Linux e sistemas para os quais o pseudo terminal pty
é implementado em python.
A URL é opcional. O simulador sempre criará um nome não determinístico como /dev/pts/4
e registrará esta informação caso você precise acessar. Este recurso é mais útil ao executar um conjunto de testes.
Você é livre para escolher qualquer caminho de URL que desejar (ex: /dev/ttyRP10
), desde que tenha certeza de que o simulador tem permissões para criar o arquivo simbólico.
Para qualquer um dos transportes (TCP, UDP e linha serial) é possível fazer simulação básica da velocidade do canal de comunicação fornecendo um parâmetro adicional baudrate
à configuração. Exemplo:
- class : CryoCon
name : cryocon-1
transports :
- type : serial
url : /tmp/cryocon-1
baudrate : 9600
O simulador fornece um console python backdoor gevent que você pode ativar se desejar acessar remotamente um processo do simulador em execução. Para ativar esse recurso basta adicionar ao nível superior da configuração o seguinte:
backdoor : ["localhost": 10001]
devices :
- ...
Você é livre para escolher qualquer outra porta TCP e endereço de ligação. Esteja ciente de que esse backdoor não fornece autenticação e não tenta limitar o que os usuários remotos podem fazer. Qualquer pessoa que possa acessar o servidor pode realizar qualquer ação que o processo python em execução possa realizar. Assim, embora você possa vincular-se a qualquer interface, por motivos de segurança é recomendado que você vincule-se a uma que seja acessível apenas à máquina local, por exemplo, 127.0.0.1/localhost.
Uso
Uma vez configurado o backdoor e o servidor em execução, em outro terminal, conecte-se com:
$ nc 127.0.0.1 10001
Welcome to Simulator server console.
You can access me through the 'server()' function. Have fun!
>>> print(server())
...
Escrever um novo dispositivo é simples. Vamos imaginar que você queira simular um osciloscópio SCPI. A única coisa que você precisa fazer é escrever uma classe herdada de BaseDevice e implementar o handle_message(self, message)
onde você deve manipular os diferentes comandos suportados pelo seu dispositivo:
# 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!" )
...
Não se esqueça de sempre retornar bytes
! O simulador não faz suposições sobre como codificar str
Supondo que este arquivo simulator.py
faça parte de um pacote python chamado myproject
, a segunda coisa a fazer é registrar o plugin do simulador em seu setup.py:
setup (
...
entry_points = {
"sinstruments.device" : [
"Oscilloscope=myproject.simulator:Oscilloscope"
]
}
)
Agora você deve conseguir iniciar seu simulador escrevendo um arquivo de configuração:
# oscilo.yml
devices :
- class : Oscilloscope
name : oscilo-1
transports :
- type : tcp
url : :5000
Agora inicie o servidor com
$ sinstruments-server -c oscillo.yml
e você deve ser capaz de se conectar com:
$ nc localhost 5000
*IDN?
ACME Inc,O-3000,23l032,3.5A
Por padrão, o eol
está definido como n
. Você pode alterá-lo para qualquer caractere com:
class Oscilloscope ( BaseDevice ):
newline = b" r "
Se o seu dispositivo implementa um protocolo que responde com múltiplas respostas (potencialmente atrasadas) a uma única solicitação, você pode suportar isso convertendo handle_message()
em um gerador:
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 ()
...
Não se esqueça de sempre produzir bytes
! O simulador não faz suposições sobre como codificar str
Caso o seu dispositivo simulado exija configuração adicional, ele poderá ser fornecido por meio do mesmo arquivo YAML.
Digamos que você queira configurar se o seu dispositivo está no modo CONTROL
na inicialização. Além disso, se nenhum valor inicial for configurado, o padrão deverá ser 'OFF'.
Primeiro vamos adicionar isto ao nosso exemplo de configuração:
# oscilo.yml
devices :
- class : Oscilloscope
name : oscilo-1
control : ON
transports :
- type : tcp
url : :5000
Então, reimplementamos nosso osciloscópio __init__()
para interceptar esse novo parâmetro e tratamos dele em 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 ()
...
Você é livre para adicionar quantas opções quiser, desde que não entrem em conflito com as chaves reservadas name
, class
e transports
.
Alguns instrumentos implementam protocolos que não são gerenciados adequadamente por um protocolo de mensagens baseado em EOL.
O simulador permite que você escreva seu próprio protocolo de mensagens. Aqui está um exemplo:
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
...
Se você estiver desenvolvendo uma biblioteca python que fornece acesso a um instrumento acessível por meio de soquete ou linha serial e escreveu um simulador para ela, talvez esteja interessado em testar sua biblioteca no simulador.
sinstruments fornece um par de auxiliares pytest que geram um simulador em um thread separado.
server_context
O primeiro uso é simplesmente usar o auxiliar server_context
. Na verdade, não há nada específico do pytest sobre esse auxiliar, então você também pode imaginar usá-lo em outros cenários.
Aqui está um exemplo:
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" )
Você pode notar que na configuração usamos a porta 0
. Isso diz ao simulador para escutar em qualquer porta livre fornecida pelo sistema operacional.
O teste real recupera o endereço atual atribuído pelo sistema operacional e o utiliza no teste.
Como você pode ver, os testes não dependem da disponibilidade de uma porta específica que os torne portáteis.
Aqui está uma sugestão de como você pode escrever seu próprio fixture usando o auxiliar server_context
. O objetivo era reduzir a quantidade de código padrão necessário para escrever seu teste:
@ 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
Um segundo ajudante é o equipamento server
. Este acessório depende de um recurso config
existente que deve estar presente em seu módulo. Aqui está um exemplo seguindo o código anterior:
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