Ein Simulator für echte Hardware. Dieses Projekt stellt einen Server bereit, der mehrere simulierte Geräte erzeugen und gleichzeitig Anfragen bearbeiten kann.
Dieses Projekt bietet lediglich die erforderliche Infrastruktur zum Starten eines Servers aus einer Konfigurationsdatei (YAML, TOML oder JSON) und eine Möglichkeit, Geräte-Plugins von Drittanbietern über den Python-Einstiegspunktmechanismus zu registrieren.
Bisher stellt das Projekt Transporte für TCP, UDP und serielle Leitungen bereit. Die Unterstützung neuer Transportarten (z. B. USB, GPIB oder SPI) wird je nach Bedarf implementiert.
PRs sind willkommen!
( TL;DR : pip install sinstruments[all]
)
Aus Ihrer bevorzugten Python-Umgebung heraus:
$ pip install sinstruments
Wenn Sie außerdem YAML-Konfigurationsdateien in YAML schreiben möchten:
$ pip install sinstruments[yaml]
...oder für TOML-basierte Konfiguration:
$ pip install sinstruments[toml]
Nach der Installation kann der Server ausgeführt werden mit:
$ sinstruments-server -c <config file name>
Die Konfigurationsdatei beschreibt, welche Geräte der Server instanziieren soll, zusammen mit einer Reihe von Optionen wie den Transport(en), auf denen jedes Gerät auf Anfragen wartet.
Stellen Sie sich vor, Sie müssen zwei GE Pace 5000 simulieren, die jeweils über einen TCP-Port erreichbar sind, und einen CryoCon 24C, der über die serielle Leitung erreichbar ist.
Stellen Sie zunächst sicher, dass die Abhängigkeiten installiert sind mit:
$ pip install gepace[simulator] cryoncon[simulator]
Jetzt können wir eine YAML-Konfiguration namens simulator.yml
vorbereiten:
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
Jetzt können wir den Server starten:
$ sinstruments-server -c simulator.yml
Das ist es! Sie sollten nun in der Lage sein, über TCP oder CryoCon über die lokal emulierte serielle Leitung eine Verbindung zu einem der Pace-Geräte herzustellen.
Versuchen wir, mit dem Linux-Befehlszeilentool nc (auch bekannt als Netcat) eine Verbindung zum ersten Pace herzustellen und nach der bekannten *IDN?
SCPI-Befehl:
$ nc localhost 5000
*IDN?
GE,Pace5000,204683,1.01A
Dies ist eine Zusammenfassung der bekannten Instrumentierungsbibliotheken von Drittanbietern, die ihre eigenen Simulatoren bereitstellen.
Wenn Sie ein öffentlich verfügbares Gerät geschrieben haben, können Sie die obige Liste gerne vervollständigen, indem Sie eine PR erstellen.
Hinweis : sinstruments-server ls
zeigt eine Liste der verfügbaren Plugins.
Die Konfigurationsdatei kann eine YAML-, TOML- oder JSON-Datei sein, solange sie in ein Wörterbuch mit der unten angegebenen Beschreibung übersetzt wird.
In diesem Kapitel verwenden wir YAML als Referenzbeispiel.
Die Datei sollte mindestens einen Schlüssel der obersten Ebene namens devices
enthalten. Der Wert muss eine Liste von Gerätebeschreibungen sein:
devices :
- class : Pace
name : pace-1
transports :
- type : tcp
url : :5000
Jede Gerätebeschreibung muss enthalten:
tcp
, udp
und serial
).Alle anderen Optionen, die jedem Gerät gegeben werden, werden zur Laufzeit direkt an das spezifische Plugin-Objekt übergeben. Jedes Plugin sollte beschreiben, welche zusätzlichen Optionen es unterstützt und wie man diese verwendet.
Für TCP- und UDP-Transporte hat die URL das Format <host>:<port>
.
Ein leerer Host (wie im obigen Beispiel) wird als 0.0.0.0
interpretiert (was bedeutet, dass alle Netzwerkschnittstellen überwacht werden). Wenn der Host 127.0.0.1
oder localhost
ist, kann auf das Gerät nur von dem Computer aus zugegriffen werden, auf dem der Simulator ausgeführt wird.
Ein Portwert von 0 bedeutet, dass das Betriebssystem aufgefordert wird, einen freien Port zuzuweisen (nützlich für die Ausführung einer Testsuite). Andernfalls muss es sich um einen gültigen TCP- oder UDP-Port handeln.
Die URL stellt eine spezielle Datei dar, die vom Simulator erstellt wird, um eine serielle Leitung zu simulieren, auf die wie eine serielle Leitungsdatei /dev/ttyS0
unter Linux zugegriffen werden kann.
Diese Funktion ist nur unter Linux und Systemen verfügbar, für die das Pseudo-Terminal pty
in Python implementiert ist.
Die URL ist optional. Der Simulator erstellt immer einen nicht deterministischen Namen wie /dev/pts/4
und protokolliert diese Informationen für den Fall, dass Sie darauf zugreifen müssen. Diese Funktion ist am nützlichsten, wenn Sie eine Testsuite ausführen.
Sie können einen beliebigen URL -Pfad wählen (z. B. /dev/ttyRP10
), solange Sie sicher sind, dass der Simulator über Berechtigungen zum Erstellen der Symboldatei verfügt.
Für alle Transportarten (TCP, UDP und serielle Leitung) ist es möglich, eine grundlegende Simulation der Kommunikationskanalgeschwindigkeit durchzuführen, indem der Konfiguration ein zusätzlicher baudrate
hinzugefügt wird. Beispiel:
- class : CryoCon
name : cryocon-1
transports :
- type : serial
url : /tmp/cryocon-1
baudrate : 9600
Der Simulator bietet eine gevent-Hintertür-Python-Konsole, die Sie aktivieren können, wenn Sie remote auf einen laufenden Simulatorprozess zugreifen möchten. Um diese Funktion zu aktivieren, fügen Sie einfach auf der obersten Ebene der Konfiguration Folgendes hinzu:
backdoor : ["localhost": 10001]
devices :
- ...
Es steht Ihnen frei, einen anderen TCP-Port und eine andere Bindungsadresse zu wählen. Beachten Sie, dass diese Hintertür keine Authentifizierung bietet und nicht versucht, die Möglichkeiten von Remote-Benutzern einzuschränken. Jeder, der auf den Server zugreifen kann, kann alle Aktionen ausführen, die der laufende Python-Prozess ausführen kann. Sie können sich also zwar an jede beliebige Schnittstelle binden, aus Sicherheitsgründen wird jedoch empfohlen, dass Sie eine Bindung an eine Schnittstelle vornehmen, auf die nur der lokale Rechner zugreifen kann, z. B. 127.0.0.1/localhost.
Verwendung
Sobald die Hintertür konfiguriert ist und der Server läuft, stellen Sie in einem anderen Terminal eine Verbindung her mit:
$ nc 127.0.0.1 10001
Welcome to Simulator server console.
You can access me through the 'server()' function. Have fun!
>>> print(server())
...
Das Schreiben eines neuen Geräts ist einfach. Stellen Sie sich vor, Sie möchten ein SCPI-Oszilloskop simulieren. Das Einzige, was Sie tun müssen, ist, eine Klasse zu schreiben, die von BaseDevice erbt, und handle_message(self, message)
zu implementieren, wo Sie die verschiedenen von Ihrem Gerät unterstützten Befehle verarbeiten sollen:
# 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!" )
...
Vergessen Sie nicht, immer bytes
zurückzugeben! Der Simulator macht keine Vermutungen darüber, wie str
codiert werden soll
Angenommen, diese Datei simulator.py
ist Teil eines Python-Pakets namens myproject
, müssen Sie als Zweites Ihr Simulator-Plugin in Ihrer setup.py registrieren:
setup (
...
entry_points = {
"sinstruments.device" : [
"Oscilloscope=myproject.simulator:Oscilloscope"
]
}
)
Sie sollten nun in der Lage sein, Ihren Simulator zu starten, indem Sie eine Konfigurationsdatei schreiben:
# oscilo.yml
devices :
- class : Oscilloscope
name : oscilo-1
transports :
- type : tcp
url : :5000
Starten Sie nun den Server mit
$ sinstruments-server -c oscillo.yml
und Sie sollten in der Lage sein, eine Verbindung herzustellen mit:
$ nc localhost 5000
*IDN?
ACME Inc,O-3000,23l032,3.5A
Standardmäßig ist eol
auf n
eingestellt. Sie können es in ein beliebiges Zeichen ändern mit:
class Oscilloscope ( BaseDevice ):
newline = b" r "
Wenn Ihr Gerät ein Protokoll implementiert, das mit mehreren (möglicherweise verzögerten) Antworten auf eine einzelne Anfrage antwortet, können Sie dies unterstützen, indem Sie handle_message()
in einen Generator umwandeln:
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 ()
...
Vergessen Sie nicht, immer yield bytes
anzugeben! Der Simulator macht keine Vermutungen darüber, wie str
codiert werden soll
Wenn für Ihr simuliertes Gerät eine zusätzliche Konfiguration erforderlich ist, kann diese über dieselbe YAML-Datei bereitgestellt werden.
Angenommen, Sie möchten konfigurieren können, ob sich Ihr Gerät beim Start im CONTROL
Modus befindet. Wenn außerdem kein Anfangswert konfiguriert ist, sollte er standardmäßig auf „AUS“ stehen.
Fügen wir zunächst Folgendes zu unserem Konfigurationsbeispiel hinzu:
# oscilo.yml
devices :
- class : Oscilloscope
name : oscilo-1
control : ON
transports :
- type : tcp
url : :5000
Dann implementieren wir unser Oszilloskop __init__()
erneut, um diesen neuen Parameter abzufangen, und verarbeiten ihn in 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 ()
...
Es steht Ihnen frei, so viele Optionen hinzuzufügen, wie Sie möchten, solange diese nicht mit den reservierten Schlüsseln name
, class
und transports
in Konflikt stehen.
Einige Instrumente implementieren Protokolle, die von einem EOL-basierten Nachrichtenprotokoll nicht angemessen verwaltet werden.
Mit dem Simulator können Sie Ihr eigenes Nachrichtenprotokoll schreiben. Hier ist ein Beispiel:
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
...
Wenn Sie eine Python-Bibliothek entwickeln, die den Zugriff auf ein Instrument ermöglicht, das über einen Socket oder eine serielle Leitung zugänglich ist, und Sie einen Simulator dafür geschrieben haben, könnten Sie daran interessiert sein, Ihre Bibliothek mit dem Simulator zu testen.
sinstruments bietet ein Paar Pytest-Helfer, die einen Simulator in einem separaten Thread erzeugen.
server_context
Die erste Verwendung besteht einfach darin, den server_context
Helfer zu verwenden. Es gibt eigentlich nichts Pytest-spezifisches an diesem Helfer, sodass Sie sich vorstellen können, ihn auch in anderen Szenarien zu verwenden.
Hier ist ein Beispiel:
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" )
Möglicherweise fällt Ihnen auf, dass wir in der Konfiguration Port 0
verwenden. Dadurch wird der Simulator angewiesen, jeden vom Betriebssystem bereitgestellten freien Port abzuhören.
Der eigentliche Test ruft die vom Betriebssystem zugewiesene aktuelle Adresse ab und verwendet sie im Test.
Wie Sie sehen, sind die Tests nicht von der Verfügbarkeit eines bestimmten Ports abhängig, was sie portabel macht.
Hier ist ein Vorschlag, wie Sie mit dem server_context
Helfer Ihr eigenes Fixture schreiben können. Ziel war es, die Menge an Boilerplate-Code zu reduzieren, die Sie zum Schreiben Ihres Tests benötigen:
@ 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
Ein zweiter Helfer ist die server
. Dieses Fixture hängt von einer vorhandenen config
ab, die in Ihrem Modul vorhanden sein muss. Hier ist ein Beispiel, das dem vorherigen Code folgt:
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