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:

  • Scope for oscilloscopes

  • Pulser for fault injection benches

  • Afg for arbitrary function generators

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