Bench and Device Discovery¶
The goal of this guide is to provide a deep understanding of the concept and design patterns used in secbench.
To get the best from this tutorial, here are some recommendations:
The tutorial is written so that each section can be read (or executed) linearly.
We recommend you not to rush this notebook, take the necessary time to understand the concepts. It will save you a lot of time.
If possible, run this tutorial in a notebook and try to play with the examples, modify them and see how they behave.
Logging Setup¶
In the secbench framework, many events are logged through the logging
module. By default those messages would not be visible in an application, since logging is not configured. Enabling logging is a good way to inspect what is going on. For most application, we recommend using logging.INFO
or logging.WARNING
. The logging.DEBUG
mode is very verbose and can be helpful when things do not work.
The following will initialize logging with logging.INFO
level.
import logging
logging.basicConfig(level=logging.INFO)
Note that we can also enable logging for specific parts of the framework. For example to enable exclusively debug messages in the secbench.api.backend
function, we can use:
logging.getLogger("secbench.api.backend").setLevel(logging.DEBUG)
There are many other possiblities with logging, please refer to the logging module documentation for more information.
Creating a Bench¶
The Bench
is defined in secbench.api
module. It is a central concept in Secbench. Put shortly, a Bench
is a helper to discover and instanciate lab hardware such as scopes, XYZ-Tables.
from secbench.api import get_bench, Discoverable, Bench, HardwareInfo
import numpy as np
A bench can be created as follows:
local_bench = Bench()
local_bench
INFO:secbench.api.bench:SECBENCH_USER_CONFIG not defined, no user configuration loaded
INFO:secbench.api.bench:SECBENCH_SCOPENET not defined, skipping registration of VxiScanner
INFO:secbench.api.bench:SECBENCH_PYVISA_BACKEND not defined, skipping registration of PyVisaScanner
<secbench.api.bench.Bench at 0x7f9be6d51a10>
Alternatively, you can also call secbench.api.get_bench()
, which implements a singleton pattern. It returns a reference to a internal bench.
bench = get_bench()
INFO:secbench.api.bench:SECBENCH_USER_CONFIG not defined, no user configuration loaded
INFO:secbench.api.bench:SECBENCH_SCOPENET not defined, skipping registration of VxiScanner
INFO:secbench.api.bench:SECBENCH_PYVISA_BACKEND not defined, skipping registration of PyVisaScanner
Futher calls to get_bench
will always return the same bench object:
print(bench)
print(get_bench())
<secbench.api.bench.Bench object at 0x7f9be6b19a50>
<secbench.api.bench.Bench object at 0x7f9be6b19a50>
Unless you have specific needs (e.g., you must have two different benches in your process), we recommend you to use the secbench.api.get_bench()
method.
Hardware Discovery¶
A bench allows discovery of any hardware in the current scope that implements the secbench.api.Discoverable
interface.
Let’s define a trivial Discoverable
class for a fake instrument.
class StrangeHardware:
pass
class MyDummyInstrument(Discoverable, StrangeHardware):
def __init__(self, usb_port: str):
self.usb_port = usb_port
def arm(self):
pass
def wait(self):
pass
def get_data(self):
return np.random.random(size=(10, 100))
# Discoverable interface
@classmethod
def discover(cls, hardware_info: HardwareInfo):
# Since the dummy hardware is always
# available, we always return it.
yield "/dev/fooUSB0"
@classmethod
def build(cls, hardware_info: HardwareInfo, dev_path: str):
return cls(dev_path)
Now, this MyDummyInstrument
should be available for discovery.
An instrument can be discovered by calling secbench.api.Bench.get()
with the expected hardware type.
obj = bench.get(MyDummyInstrument)
print(obj, obj.usb_port)
<__main__.MyDummyInstrument object at 0x7f9be6d0fad0> /dev/fooUSB0
Note that if you call again bench.get
for the same type of hardware, the function will give you the same Python object.
print(bench.get(MyDummyInstrument))
<__main__.MyDummyInstrument object at 0x7f9be6d0fad0>
If you don’t want this behavior and force the instanciation of a new object, you can add cache=False
to the Bench.get
.
print(bench.get(MyDummyInstrument, cache=False))
<__main__.MyDummyInstrument object at 0x7f9be6b26a90>
print(bench.get(MyDummyInstrument))
<__main__.MyDummyInstrument object at 0x7f9be6d0fad0>
You can also look for all subclasses. This is useful if you want to get any hardware that implements an interface, such as secbench.api.Instrument.Scope
.
print(bench.get(StrangeHardware))
<__main__.MyDummyInstrument object at 0x7f9be6d0fad0>
If needed, you can clear all cached hardware:
bench.clear_cache()
Common Hardware Abstractions¶
In the secbench.api
package, we define abstract interfaces for typical hardware. For example:
The actual drivers (e.g., Picoscope, NewAE, Tektronix, R&S, …) are implemented in their own packages (e.g., secbench.picoscope
).
To see what an instrument driver looks like, you can take a look at the code:
Lets say you want to load a scope, here is how you would do. For this example, we will add the argument required=False
so that this notebook can still execute without any scope connected.
from secbench.api.instrument import Scope, Afg, Pulser
scope = bench.get(Scope, required=False)
afg = bench.get(Afg, required=False)
pulser = bench.get(Pulser, required=False)
print("scope:", scope)
print("afg:", afg)
print("pulser", pulser)
INFO:secbench.picoscope.ps2000a:no ps2000a device found: <PicoscopeApiError: 3 PICO_NOT_FOUND)>, leaving...
INFO:secbench.picoscope.ps3000a:no ps3000a device found: <PicoscopeApiError: 3 PICO_NOT_FOUND)>, leaving...
INFO:secbench.picoscope.ps6000:no ps6000 device found: <PicoscopeApiError: 3 PICO_NOT_FOUND)>, leaving...
WARNING:secbench.api.discovery:Afg does not implement Discoverable and does not have subclasses, not matches found.
WARNING:secbench.api.discovery:Pulser does not implement Discoverable and does not have subclasses, not matches found.
scope: None
afg: None
pulser None
For common instrument types, the Bench
has methods to load them without needing an additional import.
scope = bench.get(Scope, required=False)
# Is the same as:
scope = bench.get_scope(required=False)
INFO:secbench.picoscope.ps2000a:no ps2000a device found: <PicoscopeApiError: 3 PICO_NOT_FOUND)>, leaving...
INFO:secbench.picoscope.ps3000a:no ps3000a device found: <PicoscopeApiError: 3 PICO_NOT_FOUND)>, leaving...
INFO:secbench.picoscope.ps6000:no ps6000 device found: <PicoscopeApiError: 3 PICO_NOT_FOUND)>, leaving...
INFO:secbench.picoscope.ps2000a:no ps2000a device found: <PicoscopeApiError: 3 PICO_NOT_FOUND)>, leaving...
INFO:secbench.picoscope.ps3000a:no ps3000a device found: <PicoscopeApiError: 3 PICO_NOT_FOUND)>, leaving...
INFO:secbench.picoscope.ps6000:no ps6000 device found: <PicoscopeApiError: 3 PICO_NOT_FOUND)>, leaving...
Registering Custom Hardware (Bypassing Discovery)¶
There are two common situations where you want to bypass discovery:
When you want to make accessible hardware that cannot implement the
Discoverable
interface.When multiple instances of the same hardware (e.g., 2 thermal sensors) are available and you want to use a specific one.
In both cases, you want the secbench.api.Bench.get()
method to return a specific hardware.
To achieve that, all you need to do is register your hardware in the bench as follows.
obj = MyDummyInstrument("A-CUSTOM-TTY")
obj
<__main__.MyDummyInstrument at 0x7f9be6b62b90>
If we call get
, a different new MyDummyInstrument
instance will be created.
obj_2 = bench.get(MyDummyInstrument)
print("Same objects?", obj_2 == obj)
Same objects? False
Which is not what we want. To avoid that, we register obj
manually.
# Clear the cache (otherwise, we would get obj_2)
bench.clear_cache()
bench.register(obj)
Now the get
method behaves as expected:
obj_2 = bench.get(MyDummyInstrument)
print("Same objects?", obj_2 == obj)
Same objects? True