Un simulador de hardware real. Este proyecto proporciona un servidor capaz de generar múltiples dispositivos simulados y atender solicitudes simultáneamente.
Este proyecto proporciona solo la infraestructura necesaria para iniciar un servidor desde un archivo de configuración (YAML, TOML o json) y un medio para registrar complementos de dispositivos de terceros a través del mecanismo de punto de entrada de Python.
Hasta ahora, el proyecto proporciona transportes para TCP, UDP y línea serie. La compatibilidad con nuevos transportes (por ejemplo, USB, GPIB o SPI) se está implementando según sea necesario.
¡Los relaciones públicas son bienvenidos!
( TL;DR : pip install sinstruments[all]
)
Desde tu entorno Python favorito:
$ pip install sinstruments
Además, si desea escribir archivos de configuración YAML en YAML:
$ pip install sinstruments[yaml]
...o, para configuración basada en TOML:
$ pip install sinstruments[toml]
Una vez instalado el servidor se puede ejecutar con:
$ sinstruments-server -c <config file name>
El archivo de configuración describe qué dispositivos debe crear una instancia del servidor junto con una serie de opciones, como los transportes en los que cada dispositivo escucha las solicitudes.
Imagine que necesita simular 2 GE Pace 5000 accesibles a través de un puerto TCP cada uno y un CryoCon 24C accesible a través de la línea serie.
Primero, asegúrese de que las dependencias estén instaladas con:
$ pip install gepace[simulator] cryoncon[simulator]
Ahora podemos preparar una configuración YAML llamada 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
Ahora estamos listos para iniciar el servidor:
$ sinstruments-server -c simulator.yml
¡Eso es todo! Ahora debería poder conectarse a cualquiera de los dispositivos Pace a través de TCP o CryoCon utilizando la línea serie emulada local.
Intentemos conectarnos al primer Pace con la herramienta de línea de comandos de Linux nc (también conocida como netcat) y solicitemos el conocido *IDN?
Comando SCPI:
$ nc localhost 5000
*IDN?
GE,Pace5000,204683,1.01A
Este es un resumen de las bibliotecas de instrumentación de terceros conocidas que proporcionan sus propios simuladores.
Si escribió un dispositivo disponible públicamente, no dude en completar la lista anterior creando un PR.
Sugerencia : sinstruments-server ls
muestra una lista de complementos disponibles.
El archivo de configuración puede ser un archivo YAML, TOML o JSON siempre que se traduzca a un diccionario con la descripción que se proporciona a continuación.
En este capítulo usaremos YAML como ejemplo de referencia.
El archivo debe contener al menos una clave de nivel superior llamada devices
. El valor debe ser una lista de descripciones de dispositivos:
devices :
- class : Pace
name : pace-1
transports :
- type : tcp
url : :5000
La descripción de cada dispositivo debe contener:
tcp
, udp
, serial
)Cualquier otra opción proporcionada a cada dispositivo se pasa directamente al objeto de complemento específico en tiempo de ejecución. Cada complemento debe describir qué opciones adicionales admite y cómo usarlas.
Para transportes TCP y UDP, la URL tiene el formato <host>:<port>
.
Un host vacío (como en el ejemplo anterior) se interpreta como 0.0.0.0
(lo que significa escuchar en todas las interfaces de red). Si el host es 127.0.0.1
o localhost
solo se podrá acceder al dispositivo desde la máquina donde se ejecuta el simulador.
Un valor de puerto de 0 significa pedirle al sistema operativo que asigne un puerto libre (útil para ejecutar un conjunto de pruebas). De lo contrario debe ser un puerto TCP o UDP válido.
La URL representa un archivo especial creado por el simulador para simular una línea serie accesible como un archivo de línea serie de Linux /dev/ttyS0
.
Esta característica solo está disponible en Linux y sistemas para los cuales el pseudo terminal pty
está implementado en Python.
La URL es opcional. El simulador siempre creará un nombre no determinista como /dev/pts/4
y registrará esta información en caso de que necesite acceder. Esta característica es más útil cuando se ejecuta un conjunto de pruebas.
Eres libre de elegir cualquier ruta URL que desees (por ejemplo, /dev/ttyRP10
) siempre que estés seguro de que el simulador tiene permisos para crear el archivo simbólico.
Para cualquiera de los transportes (TCP, UDP y línea serie) es posible realizar una simulación básica de la velocidad del canal de comunicación proporcionando un parámetro de baudrate
adicional a la configuración. Ejemplo:
- class : CryoCon
name : cryocon-1
transports :
- type : serial
url : /tmp/cryocon-1
baudrate : 9600
El simulador proporciona una consola Python de puerta trasera gevent que puede activar si desea acceder de forma remota a un proceso del simulador en ejecución. Para activar esta función simplemente agregue al nivel superior de la configuración lo siguiente:
backdoor : ["localhost": 10001]
devices :
- ...
Eres libre de elegir cualquier otro puerto TCP y dirección de enlace. Tenga en cuenta que esta puerta trasera no proporciona autenticación y no intenta limitar lo que pueden hacer los usuarios remotos. Cualquiera que pueda acceder al servidor puede realizar cualquier acción que pueda realizar el proceso de Python en ejecución. Por lo tanto, si bien puede vincularse a cualquier interfaz, por motivos de seguridad se recomienda que se vincule a una que sólo sea accesible para la máquina local, por ejemplo, 127.0.0.1/localhost.
Uso
Una vez configurado el backdoor y el servidor funcionando, en otro terminal conectar con:
$ nc 127.0.0.1 10001
Welcome to Simulator server console.
You can access me through the 'server()' function. Have fun!
>>> print(server())
...
Escribir un nuevo dispositivo es simple. Imaginemos que desea simular un osciloscopio SCPI. Lo único que debes hacer es escribir una clase que herede de BaseDevice e implementar handle_message(self, message)
donde debes manejar los diferentes comandos admitidos por tu 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!" )
...
¡No olvides devolver siempre bytes
! El simulador no adivina cómo codificar str
Suponiendo que este archivo simulator.py
es parte de un paquete de Python llamado myproject
, lo segundo que debe hacer es registrar el complemento de su simulador en su setup.py:
setup (
...
entry_points = {
"sinstruments.device" : [
"Oscilloscope=myproject.simulator:Oscilloscope"
]
}
)
Ahora deberías poder iniciar tu simulador escribiendo un archivo de configuración:
# oscilo.yml
devices :
- class : Oscilloscope
name : oscilo-1
transports :
- type : tcp
url : :5000
Ahora inicie el servidor con
$ sinstruments-server -c oscillo.yml
y deberías poder conectarte con:
$ nc localhost 5000
*IDN?
ACME Inc,O-3000,23l032,3.5A
Por defecto, el eol
está configurado en n
. Puedes cambiarlo a cualquier personaje con:
class Oscilloscope ( BaseDevice ):
newline = b" r "
Si su dispositivo implementa un protocolo que responde con múltiples respuestas (potencialmente retrasadas) a una sola solicitud, puede admitir esto convirtiendo handle_message()
en un generador:
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 ()
...
¡No olvides producir siempre bytes
! El simulador no adivina cómo codificar str
Si su dispositivo simulado requiere configuración adicional, se puede proporcionar a través del mismo archivo YAML.
Digamos que desea poder configurar si su dispositivo está en modo CONTROL
al inicio. Además, si no se configura ningún valor inicial, el valor predeterminado debería ser 'OFF'.
Primero agreguemos esto a nuestro ejemplo de configuración:
# oscilo.yml
devices :
- class : Oscilloscope
name : oscilo-1
control : ON
transports :
- type : tcp
url : :5000
Luego, volvemos a implementar nuestro Osciloscopio __init__()
para interceptar este nuevo parámetro y lo manejamos en 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 ()
...
Eres libre de agregar tantas opciones como quieras siempre y cuando no entren en conflicto con el name
, class
y transports
de las claves reservadas.
Algunos instrumentos implementan protocolos que no son gestionados adecuadamente por un protocolo de mensajes basado en EOL.
El simulador le permite escribir su propio protocolo de mensajes. Aquí hay un ejemplo:
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
...
Si está desarrollando una biblioteca de Python que proporciona acceso a un instrumento accesible a través de un socket o una línea serie y escribió un simulador para ella, es posible que le interese probar su biblioteca con el simulador.
sinstruments proporciona un par de ayudantes de pytest que generan un simulador en un hilo separado.
server_context
El primer uso es simplemente usar el asistente server_context
. En realidad, no hay nada específico de pytest sobre este asistente, por lo que también podría imaginarse usarlo en otros escenarios.
Aquí hay un ejemplo:
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" )
Podrías notar que en la configuración usamos el puerto 0
. Esto le indica al simulador que escuche en cualquier puerto libre proporcionado por el sistema operativo.
La prueba real recupera la dirección actual asignada por el sistema operativo y la utiliza en la prueba.
Como puede ver, las pruebas no dependen de la disponibilidad de un puerto específico, lo que las hace portátiles.
Aquí hay una sugerencia sobre cómo podrías escribir tu propio dispositivo usando el asistente server_context
. El objetivo era reducir la cantidad de código repetitivo que necesitas para escribir tu prueba:
@ 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
Un segundo ayudante es el dispositivo server
. Este dispositivo depende de una función config
existente que debe estar presente en su módulo. A continuación se muestra un ejemplo siguiendo el 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