Creating Drivers for Instruments¶
In this tutorial, we describe how to integrate your own instruments (scopes, pulsers, AFGs, 3-axis tables) with secbench.api
.
Here, we take the example of Scope
implementation, but the approach would be the same with other type of instruments.
Step 1: Implement an Abstract Instrument¶
If your instrument is based on commands (SCPI or similar), we highly recommend
that you inherit InstrumentMixin
class and implement
its functions. This mixin exposes the functions of the communication backend to
the current hardware, such as query()
, write()
.
Then, you must implement the abstract methods of the Scope
class.
Here’s how the implementation could look like:
from secbench.api import InstrumentMixin, Backend, UserConfig, Discoverable
from secbench.api.instrument import Scope
class MySuperInstrument(InstrumentMixin, Scope):
# ===
# InstrumentMixin
# ===
@property
def backend(self):
return None
@classmethod
def from_backend(cls, backend: Backend, cfg: UserConfig):
...
def has_error(self) -> bool:
...
def pop_next_error(self) -> str | None:
...
# ===
# Scope Interface
# ===
@property
def description(self) -> str:
return "my super instrument"
def horizontal(self, *, interval: float | None = None, duration: float | None = None,
samples: int = None) -> None:
self.write(f"HORIZONTAL:INTERVAL {interval:E}")
self.write(f"HORIZONTAL:SAMPLES {samples}")
def horizontal_duration(self) -> float:
return float(self.query("HORIZONTAL:DURATION?"))
def _wait(self, iterations: int, poll_interval: float) -> float:
t = time.perf_counter()
for _ in range(iterations):
busy = int(self.query("BUSY?"))
if busy == 0:
return time.perf_counter() - t
time.sleep(poll_interval)
raise InstrumentError("Reached timeout")
def _wait_auto(self) -> float:
# Number of seconds to wait
ttw = 3
return self._wait(1000 * ttw, 1e-3)
# ... Other methods ...
Secbench also defines different mixin classes that can be implemented by your instrument, depending on its specific features such as:
WriteManyMixin
: interesting to implement if your instrument supports the reception of multiple SCPI commands at the same time.HasSetupStorage
: defines methods that can load/store the instrument setup.HasWaveformStorage
: some scopes can store waveforms locally, implement this class if you want to retrieve them.
Step 2: Make your Instrument Discoverable¶
The secbench framework already proposes different backends to work with
instruments. The idea is to subclass the “backend-agnostic” instrument
MySuperInstrument
and inherit some mixin what will make it discoverable with
specific communication backends. The following code shows how to make your
device discoverable over serial communication, or VISA backend (e.g. GPIB,
RS232, USB, Ethernet).
Note
It is also possible to manually implement the Discoverable
interface. This would require slightly more code.
from secbench.api.backend import SerialDiscoverableMixin, PyVisaDiscoverableMixin
class MySuperInstrumentOverSerial(SerialDiscoverableMixin, Discoverable, MySuperInstrument):
def _match_serial(cls, idn: str) -> bool:
return idn == "XXAABB"
class MySuperInstrumentOverPyVisa(PyVisaDiscoverableMixin, Discoverable, MySuperInstrument):
def _pyvisa_match_id(cls, rm, path: str) -> bool:
return path == "INSTR:FOO"
Your scope is now discoverable. You should be able to obtain your instrument with the following code:
from secbench.api import get_bench
bench = get_bench()
my_scope = bench.get_scope()
# More specific requests:
my_scope = bench.get(MySuperInstrument)
my_scope = bench.get(MySuperInstrumentOverSerial)
Adding a new Communication Backend¶
If your communication backend is not supported, you can implement your own by inheriting from secbench.api.Backend
.
Currently supported backends for instruments are implemented by the following classes:
from secbench.api import Backend
class MyAwesomeBackend(Backend):
# ===
# Backend interface
# ===
def set_timeout(self, secs: float):
"""
Set timeout for blocking operations. (optional)
"""
...
# ... Other methods ...
class MyAwesomeBackendDiscoverableMixin(abc.ABC):
@classmethod
def discover(cls, hw_info: HardwareInfo):
# Implementation of discover for this class.
...
@classmethod
def build(cls, hardware_info: HardwareInfo, args)
backend = MyAwesomeBackend(args)
return cls.from_backend(backend)
class MySuperInstrumentOverMyAwesomeBackend(MyAwesomeBackendDiscoverableMixin, Discoverable, MySuperInstrument):
pass
# You can load your scope with this
backend = MyAwesomeBackend("FOO")
scope = MySuperInstrument(backend)
# Or discover it from the bench
bench = get_bench()
scope = bench.get(Scope)