sinstruments


Namesinstruments JSON
Version 1.4.0 PyPI version JSON
download
home_pagehttps://github.com/tiagocoutinho/sinstruments
SummaryA simulator for real hardware which is accessible via TCP, UDP or serial line
upload_time2025-01-14 05:09:38
maintainerNone
docs_urlNone
authorTiago Coutinho
requires_python>=3.5
licenseNone
keywords sinstruments
VCS
bugtrack_url
requirements No requirements were recorded.
Travis-CI
coveralls test coverage No coveralls.
            # Instrument Simulator

![Pypi python versions][pypi-python-versions]
![Pypi version][pypi-version]
![Pypi status][pypi-status]
![License][license]

A simulator for real hardware. This project provides a server able to spawn
multiple simulated devices and serve requests concurrently.

This project provides only the required infrastructure to launch a server from
a configuration file (YAML, TOML or json) and a means to register third-party device plugins through the python entry point mechanism.

So far, the project provides transports for TCP, UDP and serial line.
Support for new transports (ex: USB, GPIB or SPI) is being implemented on a
need basis.

PRs are welcome!

## Installation

(**TL;DR**: `pip install sinstruments[all]`)

From within your favorite python environment:

```
$ pip install sinstruments
```

Additionally, if you want to write YAML configuration files in YAML:

```
$ pip install sinstruments[yaml]
```

...or, for TOML based configuration:

```
$ pip install sinstruments[toml]
```

## Execution

Once installed the server can be run with:

```
$ sinstruments-server -c <config file name>
```

The configuration file describes which devices the server should instantiate
along with a series of options like the transport(s) each device listens for
requests on.

### Example

Imagine you need to simulate 2 [GE Pace 5000](https://github.com/tiagocoutinho/gepace)
reachable through a TCP port each and a [CryoCon 24C](https://github.com/tiagocoutinho/cryocon) accessible through the serial line.

First, make sure the dependencies are installed with:
```
$ pip install gepace[simulator] cryoncon[simulator]
```

Now we can prepare a YAML configuration called `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
```

We are now ready to launch the server:
```
$ sinstruments-server -c simulator.yml
```

That's it! You should now be able to connect to any of the Pace devices through
TCP or the CryoCon using the local emulated serial line.

Let's try connecting to the first Pace with the *nc* (aka netcat) linux command
line tool and ask for the well known `*IDN?` SCPI command:

```
$ nc localhost 5000
*IDN?
GE,Pace5000,204683,1.01A
```

## Device catalog

This is a summary of the known third-party instrumentation libraries which
provide their own simulators.


* [cryocon](https://github.com/tiagocoutinho/cryocon)
* [fast-spinner](https://github.com/tiagocoutinho/fast-spinner)
* [gepace](https://github.com/tiagocoutinho/gepace)
* [icepap](https://github.com/ALBA-Synchrotron/pyIcePAP)
* [julabo](https://github.com/tiagocoutinho/julabo)
* [vacuubrand](https://github.com/tiagocoutinho/vacuubrand)
* [xia-pfcu](https://github.com/tiagocoutinho/xia-pfcu)
* Mythen detector (from Dectris) - not publicly available yet

If you wrote a publicly available device feel free complete the above list by
creating a PR.

*Hint*: `sinstruments-server ls` shows a list of available plugins.


## Configuration

The configuration file can be a YAML, TOML or JSON file as long as it translates to a dictionary with the description given below.

In this chapter we will use YAML as a reference example.

The file should contain at least a top-level key called `devices`.
The value needs to be a list of device descriptions:

```YAML
devices:
  - class: Pace
    name: pace-1
    transports:
    - type: tcp
      url: :5000
```

Each device description must contain:

* **class**: Each third-party plugin should describe which text
  identify itself
* **name**: a unique name. Each device must be given a unique name at
  your choice
* **transports**: a list of transports from where the device is accessible.
  Most devices provide only one transport.
  * **type**: Each transport must define its type (supported are `tcp`, `udp`, `serial`)
  * **url**: the url where the device is listening on

Any other options given to each device are passed directly to the specific
plugin object at runtime. Each plugin should describe which additional options
it supports and how to use them.

### TCP and UDP

For TCP and UDP transports, the **url** has the `<host>:<port>` format.

An empty host (like in the above example) is interpreted as `0.0.0.0` (which
means listen on all network interfaces). If host is `127.0.0.1` or `localhost`
the device will only be accessible from the machine where the simulator is
running.

A port value of 0 means ask the OS to assign a free port (useful for running
a test suite). Otherwise must be a valid TCP or UDP port.

### Serial line

The **url** represents a special file which is created by the simulator to
simulate a serial line accessible like a `/dev/ttyS0` linux serial line file.

This feature is only available in linux and systems for which the pseudo
terminal `pty` is implemented in python.

The **url** is optional. The simulator will always create a non deterministic
name like `/dev/pts/4` and it will log this information in case you need to
access. This feature is most useful when running a test suite.

You are free to choose any **url** path you like (ex: `/dev/ttyRP10`) as long
as you are sure the simulator has permissions to create the symbolic file.

### Simulating communication delays

For any of the transports (TCP, UDP and serial line) is is possible to do basic
simulation of the communication channel speed by providing an additional
`baudrate` parameter to the configuration. Example:

```YAML
- class: CryoCon
  name: cryocon-1
  transports:
  - type: serial
    url: /tmp/cryocon-1
    baudrate: 9600
```


### Back door

The simulator provides a gevent back door python console which you can activate
if you want to access a running simulator process remotely. To activate this
feature simply add to the top-level of the configuration the following:

```YAML
backdoor: ["localhost": 10001]
devices:
  - ...
```

You are free to choose any other TCP port and bind address. Be aware that this
backdoor provides no authentication and makes no attempt to limit what
remote users can do. Anyone that can access the server can take any action that
the running python process can. Thus, while you may bind to any interface, for
security purposes it is recommended that you bind to one only accessible to the
local machine, e.g., 127.0.0.1/localhost.

**Usage**

Once the backdoor is configured and the server is running, in a another
terminal, connect with:

```
$ nc 127.0.0.1 10001
Welcome to Simulator server console.
You can access me through the 'server()' function. Have fun!
>>> print(server())
...
```

## Develop a new simulator

Writting a new device is simple. Let's imagine you want to simulate a SCPI
oscilloscope. The only thing you need to do is write a class inheriting
from BaseDevice and implement the `handle_message(self, message)` where you
should handle the different commands supported by your device:


```python
# 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!")
        ...
```

Don't forget to always return `bytes`! The simulator doesn't make any guesses
on how to encode `str`

Assuming this file `simulator.py` is part of a python package called `myproject`,
the second thing to do is register your simulator plugin in your setup.py:

```python
setup(
    ...
    entry_points={
        "sinstruments.device": [
            "Oscilloscope=myproject.simulator:Oscilloscope"
        ]
    }
)
```

You should now be able to launch your simulator by writing a configuration
file:

```YAML
# oscilo.yml

devices:
- class: Oscilloscope
  name: oscilo-1
  transports:
  - type: tcp
    url: :5000
```

Now launch the server with
```
$ sinstruments-server -c oscillo.yml
```

and you should be able to connect with:

```
$ nc localhost 5000
*IDN?
ACME Inc,O-3000,23l032,3.5A
```

### Configuring message terminator

By default the `eol` is set to `\n`. You can change it to any character with:

```python
class Oscilloscope(BaseDevice):

    newline = b"\r"

```

### Request with multiple answers

If your device implements a protocol which answers with multiple (potentially
delayed) answers to a single request, you can support this by
converting the `handle_message()` into a generator:

```python
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()
        ...
```
Don't forget to always yield `bytes`! The simulator doesn't make any guesses
on how to encode `str`

### Support for specific configuration options

If your simulated device requires additional configuration, it can be supplied
through the same YAML file.

Let's say you want to be able to configure if your device is in `CONTROL` mode
at startup. Additionally, if no initial value is configured, it should default
to 'OFF'.

First lets add this to our configuration example:

```YAML
# oscilo.yml

devices:
- class: Oscilloscope
  name: oscilo-1
  control: ON
  transports:
  - type: tcp
    url: :5000
```

Then, we re-implement our Oscilloscope `__init__()` to intercept this new
parameter and we handle it in `handle_message()`:

```python
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()
        ...
```

You are free to add as many options as you want as long as they don't conflict
with the reserved keys `name`, `class` and `transports`.

### Writing a specific message protocol

Some instruments implement protocols that are not suitably managed by a EOL
based message protocol.

The simulator allows you to write your own message protocol. Here is an example:

```python
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

    ...
```

## Pytest fixture

If you are developing a python library that provides access to an instrument
accessible through socket or serial line and you wrote a simulator for it, you
might be interested in testing your library against the simulator.

sinstruments provides a pair of pytest helpers that spawn a simulator in a
separate thread.

### `server_context`
The first usage is simply using the `server_context` helper.
There is actually nothing pytest speficic about this helper so you could
imagine using it in other scenarios as well.

Here is an example:

```python
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")
```

You might notice that in the configuration we use port `0`. This is telling
the simulator to listen on any free port provided by the OS.

The actual test retrieves the current address assigned by the OS and uses it in
the test.

As you can see, the tests are not dependent of the availability of one specific
port which makes them portable.

Here is a suggestion on how you could write your own fixture using the
`server_context` helper. The aim was to reduce the amount of boilerplate
code you need to write your test:

```python
@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`

A second helper is the `server` fixture. This fixture depends on an existing
`config` feature that must be present in your module. Here is an example
following the previous code:

```python
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
```

[pypi-python-versions]: https://img.shields.io/pypi/pyversions/sinstruments.svg
[pypi-version]: https://img.shields.io/pypi/v/sinstruments.svg
[pypi-status]: https://img.shields.io/pypi/status/sinstruments.svg
[license]: https://img.shields.io/pypi/l/sinstruments.svg

            

Raw data

            {
    "_id": null,
    "home_page": "https://github.com/tiagocoutinho/sinstruments",
    "name": "sinstruments",
    "maintainer": null,
    "docs_url": null,
    "requires_python": ">=3.5",
    "maintainer_email": null,
    "keywords": "sinstruments",
    "author": "Tiago Coutinho",
    "author_email": "coutinhotiago@gmail.com",
    "download_url": "https://files.pythonhosted.org/packages/94/74/1adab321fb8605d45c9ce4fc9fdaa79bf1f080ae4af05c26207aabc7e575/sinstruments-1.4.0.tar.gz",
    "platform": null,
    "description": "# Instrument Simulator\n\n![Pypi python versions][pypi-python-versions]\n![Pypi version][pypi-version]\n![Pypi status][pypi-status]\n![License][license]\n\nA simulator for real hardware. This project provides a server able to spawn\nmultiple simulated devices and serve requests concurrently.\n\nThis project provides only the required infrastructure to launch a server from\na configuration file (YAML, TOML or json) and a means to register third-party device plugins through the python entry point mechanism.\n\nSo far, the project provides transports for TCP, UDP and serial line.\nSupport for new transports (ex: USB, GPIB or SPI) is being implemented on a\nneed basis.\n\nPRs are welcome!\n\n## Installation\n\n(**TL;DR**: `pip install sinstruments[all]`)\n\nFrom within your favorite python environment:\n\n```\n$ pip install sinstruments\n```\n\nAdditionally, if you want to write YAML configuration files in YAML:\n\n```\n$ pip install sinstruments[yaml]\n```\n\n...or, for TOML based configuration:\n\n```\n$ pip install sinstruments[toml]\n```\n\n## Execution\n\nOnce installed the server can be run with:\n\n```\n$ sinstruments-server -c <config file name>\n```\n\nThe configuration file describes which devices the server should instantiate\nalong with a series of options like the transport(s) each device listens for\nrequests on.\n\n### Example\n\nImagine you need to simulate 2 [GE Pace 5000](https://github.com/tiagocoutinho/gepace)\nreachable through a TCP port each and a [CryoCon 24C](https://github.com/tiagocoutinho/cryocon) accessible through the serial line.\n\nFirst, make sure the dependencies are installed with:\n```\n$ pip install gepace[simulator] cryoncon[simulator]\n```\n\nNow we can prepare a YAML configuration called `simulator.yml`:\n\n```YAML\ndevices:\n- class: Pace\n  name: pace-1\n  transports:\n  - type: tcp\n    url: :5000\n- class: Pace\n  name: pace-2\n  transports:\n  - type: tcp\n    url: :5001\n- class: CryoCon\n  name: cryocon-1\n  transports:\n  - type: serial\n    url: /tmp/cryocon-1\n```\n\nWe are now ready to launch the server:\n```\n$ sinstruments-server -c simulator.yml\n```\n\nThat's it! You should now be able to connect to any of the Pace devices through\nTCP or the CryoCon using the local emulated serial line.\n\nLet's try connecting to the first Pace with the *nc* (aka netcat) linux command\nline tool and ask for the well known `*IDN?` SCPI command:\n\n```\n$ nc localhost 5000\n*IDN?\nGE,Pace5000,204683,1.01A\n```\n\n## Device catalog\n\nThis is a summary of the known third-party instrumentation libraries which\nprovide their own simulators.\n\n\n* [cryocon](https://github.com/tiagocoutinho/cryocon)\n* [fast-spinner](https://github.com/tiagocoutinho/fast-spinner)\n* [gepace](https://github.com/tiagocoutinho/gepace)\n* [icepap](https://github.com/ALBA-Synchrotron/pyIcePAP)\n* [julabo](https://github.com/tiagocoutinho/julabo)\n* [vacuubrand](https://github.com/tiagocoutinho/vacuubrand)\n* [xia-pfcu](https://github.com/tiagocoutinho/xia-pfcu)\n* Mythen detector (from Dectris) - not publicly available yet\n\nIf you wrote a publicly available device feel free complete the above list by\ncreating a PR.\n\n*Hint*: `sinstruments-server ls` shows a list of available plugins.\n\n\n## Configuration\n\nThe configuration file can be a YAML, TOML or JSON file as long as it translates to a dictionary with the description given below.\n\nIn this chapter we will use YAML as a reference example.\n\nThe file should contain at least a top-level key called `devices`.\nThe value needs to be a list of device descriptions:\n\n```YAML\ndevices:\n  - class: Pace\n    name: pace-1\n    transports:\n    - type: tcp\n      url: :5000\n```\n\nEach device description must contain:\n\n* **class**: Each third-party plugin should describe which text\n  identify itself\n* **name**: a unique name. Each device must be given a unique name at\n  your choice\n* **transports**: a list of transports from where the device is accessible.\n  Most devices provide only one transport.\n  * **type**: Each transport must define its type (supported are `tcp`, `udp`, `serial`)\n  * **url**: the url where the device is listening on\n\nAny other options given to each device are passed directly to the specific\nplugin object at runtime. Each plugin should describe which additional options\nit supports and how to use them.\n\n### TCP and UDP\n\nFor TCP and UDP transports, the **url** has the `<host>:<port>` format.\n\nAn empty host (like in the above example) is interpreted as `0.0.0.0` (which\nmeans listen on all network interfaces). If host is `127.0.0.1` or `localhost`\nthe device will only be accessible from the machine where the simulator is\nrunning.\n\nA port value of 0 means ask the OS to assign a free port (useful for running\na test suite). Otherwise must be a valid TCP or UDP port.\n\n### Serial line\n\nThe **url** represents a special file which is created by the simulator to\nsimulate a serial line accessible like a `/dev/ttyS0` linux serial line file.\n\nThis feature is only available in linux and systems for which the pseudo\nterminal `pty` is implemented in python.\n\nThe **url** is optional. The simulator will always create a non deterministic\nname like `/dev/pts/4` and it will log this information in case you need to\naccess. This feature is most useful when running a test suite.\n\nYou are free to choose any **url** path you like (ex: `/dev/ttyRP10`) as long\nas you are sure the simulator has permissions to create the symbolic file.\n\n### Simulating communication delays\n\nFor any of the transports (TCP, UDP and serial line) is is possible to do basic\nsimulation of the communication channel speed by providing an additional\n`baudrate` parameter to the configuration. Example:\n\n```YAML\n- class: CryoCon\n  name: cryocon-1\n  transports:\n  - type: serial\n    url: /tmp/cryocon-1\n    baudrate: 9600\n```\n\n\n### Back door\n\nThe simulator provides a gevent back door python console which you can activate\nif you want to access a running simulator process remotely. To activate this\nfeature simply add to the top-level of the configuration the following:\n\n```YAML\nbackdoor: [\"localhost\": 10001]\ndevices:\n  - ...\n```\n\nYou are free to choose any other TCP port and bind address. Be aware that this\nbackdoor provides no authentication and makes no attempt to limit what\nremote users can do. Anyone that can access the server can take any action that\nthe running python process can. Thus, while you may bind to any interface, for\nsecurity purposes it is recommended that you bind to one only accessible to the\nlocal machine, e.g., 127.0.0.1/localhost.\n\n**Usage**\n\nOnce the backdoor is configured and the server is running, in a another\nterminal, connect with:\n\n```\n$ nc 127.0.0.1 10001\nWelcome to Simulator server console.\nYou can access me through the 'server()' function. Have fun!\n>>> print(server())\n...\n```\n\n## Develop a new simulator\n\nWritting a new device is simple. Let's imagine you want to simulate a SCPI\noscilloscope. The only thing you need to do is write a class inheriting\nfrom BaseDevice and implement the `handle_message(self, message)` where you\nshould handle the different commands supported by your device:\n\n\n```python\n# myproject/simulator.py\n\nfrom sinstruments.simulator import BaseDevice\n\nclass Oscilloscope(BaseDevice):\n\n    def handle_message(self, message):\n        self._log.info(\"received request %r\", message)\n        message = message.strip().decode()\n        if message == \"*IDN?\":\n            return b\"ACME Inc,O-3000,23l032,3.5A\"\n        elif message == \"*RST\":\n            self._log.info(\"Resetting myself!\")\n        ...\n```\n\nDon't forget to always return `bytes`! The simulator doesn't make any guesses\non how to encode `str`\n\nAssuming this file `simulator.py` is part of a python package called `myproject`,\nthe second thing to do is register your simulator plugin in your setup.py:\n\n```python\nsetup(\n    ...\n    entry_points={\n        \"sinstruments.device\": [\n            \"Oscilloscope=myproject.simulator:Oscilloscope\"\n        ]\n    }\n)\n```\n\nYou should now be able to launch your simulator by writing a configuration\nfile:\n\n```YAML\n# oscilo.yml\n\ndevices:\n- class: Oscilloscope\n  name: oscilo-1\n  transports:\n  - type: tcp\n    url: :5000\n```\n\nNow launch the server with\n```\n$ sinstruments-server -c oscillo.yml\n```\n\nand you should be able to connect with:\n\n```\n$ nc localhost 5000\n*IDN?\nACME Inc,O-3000,23l032,3.5A\n```\n\n### Configuring message terminator\n\nBy default the `eol` is set to `\\n`. You can change it to any character with:\n\n```python\nclass Oscilloscope(BaseDevice):\n\n    newline = b\"\\r\"\n\n```\n\n### Request with multiple answers\n\nIf your device implements a protocol which answers with multiple (potentially\ndelayed) answers to a single request, you can support this by\nconverting the `handle_message()` into a generator:\n\n```python\nclass Oscilloscope(BaseDevice):\n\n    def handle_message(self, message):\n        self._log.info(\"received request %r\", message)\n        message = message.strip().decode()\n        if message == \"*IDN?\":\n            yield b\"ACME Inc,O-3000,23l032,3.5A\"\n        elif message == \"*RST\":\n            self._log.info(\"Resetting myself!\")\n        elif message == \"GIVE:ME 10\":\n            for i in range(1, 11):\n                yield f\"Here's {i}\\n\".encode()\n        ...\n```\nDon't forget to always yield `bytes`! The simulator doesn't make any guesses\non how to encode `str`\n\n### Support for specific configuration options\n\nIf your simulated device requires additional configuration, it can be supplied\nthrough the same YAML file.\n\nLet's say you want to be able to configure if your device is in `CONTROL` mode\nat startup. Additionally, if no initial value is configured, it should default\nto 'OFF'.\n\nFirst lets add this to our configuration example:\n\n```YAML\n# oscilo.yml\n\ndevices:\n- class: Oscilloscope\n  name: oscilo-1\n  control: ON\n  transports:\n  - type: tcp\n    url: :5000\n```\n\nThen, we re-implement our Oscilloscope `__init__()` to intercept this new\nparameter and we handle it in `handle_message()`:\n\n```python\nclass Oscilloscope(BaseDevice):\n\n    def __init__(self, name, **opts):\n        self._control = opts.pop(\"control\", \"OFF\").upper()\n        super().__init__(name, **opts)\n\n    def handle_message(self, message):\n        ...\n        elif message == \"CONTROL\":\n            return f\"CONTROL {self._control}\\n\".encode()\n        ...\n```\n\nYou are free to add as many options as you want as long as they don't conflict\nwith the reserved keys `name`, `class` and `transports`.\n\n### Writing a specific message protocol\n\nSome instruments implement protocols that are not suitably managed by a EOL\nbased message protocol.\n\nThe simulator allows you to write your own message protocol. Here is an example:\n\n```python\nfrom sinstruments.simulator import MessageProtocol\n\n\nclass FixSizeProtocol(MessageProtocol):\n\n    Size = 32\n\n    def read_messages(self):\n        transport = self.transport\n        buff = b''\n        while True:\n            buff += transport.read(self.channel, size=4096)\n            if not buff:\n                return\n            for i in range(0, len(buff), self.Size):\n                message = buff[i:i+self.Size]\n                if len(message) < self.Size:\n                    buff = message\n                    break\n                yield message\n\n\nclass Oscilloscope(BaseDevice):\n\n    protocol = FixSizeProtocol\n\n    ...\n```\n\n## Pytest fixture\n\nIf you are developing a python library that provides access to an instrument\naccessible through socket or serial line and you wrote a simulator for it, you\nmight be interested in testing your library against the simulator.\n\nsinstruments provides a pair of pytest helpers that spawn a simulator in a\nseparate thread.\n\n### `server_context`\nThe first usage is simply using the `server_context` helper.\nThere is actually nothing pytest speficic about this helper so you could\nimagine using it in other scenarios as well.\n\nHere is an example:\n\n```python\nimport pytest\n\nfrom sinstruments.pytest import server_context\n\ncfg = {\n    \"devices\": [{\n        \"name\": \"oscillo-1\",\n        \"class\": \"Oscilloscope\",\n        \"transports\": [\n            {\"type\": \"tcp\", \"url\": \"localhost:0\"}\n        ]\n    }]\n}\n\ndef test_oscilloscope_id():\n    with server_context(cfg) as server:\n        # put here code to perform your tests that need to communicate with\n        # the simulator. In this example an oscilloscope client\n        addr = server.devices[\"oscillo-1\"].transports[0].address\n        oscillo = Oscilloscope(addr)\n        assert oscillo.idn().startswith(\"ACME Inc,O-3000\")\n```\n\nYou might notice that in the configuration we use port `0`. This is telling\nthe simulator to listen on any free port provided by the OS.\n\nThe actual test retrieves the current address assigned by the OS and uses it in\nthe test.\n\nAs you can see, the tests are not dependent of the availability of one specific\nport which makes them portable.\n\nHere is a suggestion on how you could write your own fixture using the\n`server_context` helper. The aim was to reduce the amount of boilerplate\ncode you need to write your test:\n\n```python\n@pytest.fixture\ndef oscillo_server():\n    with server_context(config) as server:\n        server.oscillo1 = server.devices[\"oscillo-1\"]\n        server.oscillo1.addr = server.oscillo1.transports[0].address\n        yield server\n\n\ndef test_oscilloscope_current(oscillo_server):\n    oscillo = Oscilloscope(oscillo_server.oscillo1.addr)\n    assert .05 < oscillo.current() < 0.01\n```\n\n### `server`\n\nA second helper is the `server` fixture. This fixture depends on an existing\n`config` feature that must be present in your module. Here is an example\nfollowing the previous code:\n\n```python\nfrom sinstruments.pytest import server\n\n@pytest.fixture\ndef config()\n    yield cfg\n\ndef test_oscilloscope_voltage(server):\n    addr = server.devices[\"oscillo-1\"].transports[0].address\n    oscillo = Oscilloscope(addr)\n    assert 5 < oscillo.voltage() < 10\n```\n\n[pypi-python-versions]: https://img.shields.io/pypi/pyversions/sinstruments.svg\n[pypi-version]: https://img.shields.io/pypi/v/sinstruments.svg\n[pypi-status]: https://img.shields.io/pypi/status/sinstruments.svg\n[license]: https://img.shields.io/pypi/l/sinstruments.svg\n",
    "bugtrack_url": null,
    "license": null,
    "summary": "A simulator for real hardware which is accessible via TCP, UDP or serial line",
    "version": "1.4.0",
    "project_urls": {
        "Homepage": "https://github.com/tiagocoutinho/sinstruments"
    },
    "split_keywords": [
        "sinstruments"
    ],
    "urls": [
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "58558888ff4335db3e5c9b5f8c829fa68a5dcafbe52342cbc5887e79ba02c801",
                "md5": "47662134e16bb202defc92e0321bf9df",
                "sha256": "5fac6fb85f24af67ffdad4289e121efbef88a991703c423b6bc86e33145eea5c"
            },
            "downloads": -1,
            "filename": "sinstruments-1.4.0-py2.py3-none-any.whl",
            "has_sig": false,
            "md5_digest": "47662134e16bb202defc92e0321bf9df",
            "packagetype": "bdist_wheel",
            "python_version": "py2.py3",
            "requires_python": ">=3.5",
            "size": 25823,
            "upload_time": "2025-01-14T05:09:51",
            "upload_time_iso_8601": "2025-01-14T05:09:51.229549Z",
            "url": "https://files.pythonhosted.org/packages/58/55/8888ff4335db3e5c9b5f8c829fa68a5dcafbe52342cbc5887e79ba02c801/sinstruments-1.4.0-py2.py3-none-any.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "94741adab321fb8605d45c9ce4fc9fdaa79bf1f080ae4af05c26207aabc7e575",
                "md5": "e8435adb17c252896d5e0d75fdd9f651",
                "sha256": "2f443af59070bb14228f0842704feca26f0f81d1c9e3028dc6cc42deca6807e7"
            },
            "downloads": -1,
            "filename": "sinstruments-1.4.0.tar.gz",
            "has_sig": false,
            "md5_digest": "e8435adb17c252896d5e0d75fdd9f651",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": ">=3.5",
            "size": 29875,
            "upload_time": "2025-01-14T05:09:38",
            "upload_time_iso_8601": "2025-01-14T05:09:38.942255Z",
            "url": "https://files.pythonhosted.org/packages/94/74/1adab321fb8605d45c9ce4fc9fdaa79bf1f080ae4af05c26207aabc7e575/sinstruments-1.4.0.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2025-01-14 05:09:38",
    "github": true,
    "gitlab": false,
    "bitbucket": false,
    "codeberg": false,
    "github_user": "tiagocoutinho",
    "github_project": "sinstruments",
    "travis_ci": true,
    "coveralls": false,
    "github_actions": false,
    "tox": true,
    "lcname": "sinstruments"
}
        
Elapsed time: 3.33724s