{ "cells": [ { "cell_type": "markdown", "id": "6418033e", "metadata": {}, "source": [ "(sec:secbench-essential-concepts)=\n", "# Bench and Device Discovery\n", "\n", "The goal of this guide is to provide a deep understanding of the concept and design patterns used in secbench.\n", "\n", "To get the best from this tutorial, here are some recommendations:\n", "- The tutorial is written so that each section can be read (or executed) linearly.\n", "- We recommend you not to rush this notebook, take the necessary time to understand the concepts. It will save you a lot of time.\n", "- If possible, run this tutorial in a notebook and try to play with the examples, modify them and see how they behave." ] }, { "cell_type": "markdown", "id": "f14b0734", "metadata": {}, "source": [ "## Logging Setup\n", "\n", "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.\n", "\n", "The following will initialize logging with `logging.INFO` level." ] }, { "cell_type": "code", "execution_count": null, "id": "05073161", "metadata": {}, "outputs": [], "source": [ "import logging\n", "\n", "logging.basicConfig(level=logging.INFO)" ] }, { "cell_type": "markdown", "id": "02b24994", "metadata": {}, "source": [ "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:\n", "\n", "```python\n", "logging.getLogger(\"secbench.api.backend\").setLevel(logging.DEBUG)\n", "```\n", "\n", "There are many other possiblities with logging, please refer to the [logging module](https://docs.python.org/3/library/logging.html) documentation for more information." ] }, { "cell_type": "markdown", "id": "4107df13", "metadata": {}, "source": [ "(sec:understanting-the-bench)=\n", "\n", "## Creating a Bench\n", "\n", "The {py:class}`~secbench.api.Bench` is defined in {py:mod}`secbench.api` module. It is a central concept in *Secbench*. Put shortly, a {py:class}`~secbench.api.Bench` is a helper to discover and instanciate lab hardware such as scopes, XYZ-Tables." ] }, { "cell_type": "code", "execution_count": null, "id": "1dfaa64d", "metadata": {}, "outputs": [], "source": [ "from secbench.api import get_bench, Discoverable, Bench, HardwareInfo\n", "\n", "import numpy as np" ] }, { "cell_type": "markdown", "id": "d74012a0", "metadata": {}, "source": [ "A bench can be created as follows:" ] }, { "cell_type": "code", "execution_count": null, "id": "5fd80bef", "metadata": {}, "outputs": [], "source": [ "local_bench = Bench()\n", "local_bench" ] }, { "cell_type": "markdown", "id": "17b1d2d3", "metadata": {}, "source": [ "Alternatively, you can also call {py:func}`secbench.api.get_bench`, which implements a singleton pattern. It returns a reference to a internal bench." ] }, { "cell_type": "code", "execution_count": null, "id": "ce8f251a", "metadata": {}, "outputs": [], "source": [ "bench = get_bench()" ] }, { "cell_type": "markdown", "id": "146d2267", "metadata": {}, "source": [ "Futher calls to `get_bench` will always return the same bench object:" ] }, { "cell_type": "code", "execution_count": null, "id": "ad00fb89", "metadata": {}, "outputs": [], "source": [ "print(bench)\n", "print(get_bench())" ] }, { "cell_type": "markdown", "id": "5e93b7f3", "metadata": {}, "source": [ "Unless you have specific needs (e.g., you must have two different benches in your process), we recommend you to use the {py:func}`secbench.api.get_bench` method." ] }, { "cell_type": "markdown", "id": "442dd8b9", "metadata": {}, "source": [ "## Hardware Discovery" ] }, { "cell_type": "markdown", "id": "4472c81b", "metadata": {}, "source": [ "A bench allows discovery of any hardware **in the current scope** that implements the {py:class}`secbench.api.Discoverable` interface.\n", "\n", "Let's define a trivial {py:class}`~secbench.api.Discoverable` class for a fake instrument." ] }, { "cell_type": "code", "execution_count": null, "id": "544af12e", "metadata": {}, "outputs": [], "source": [ "class StrangeHardware:\n", " pass" ] }, { "cell_type": "code", "execution_count": null, "id": "0b909a69", "metadata": {}, "outputs": [], "source": [ "class MyDummyInstrument(Discoverable, StrangeHardware):\n", " def __init__(self, usb_port: str):\n", " self.usb_port = usb_port\n", " \n", " def arm(self):\n", " pass\n", " \n", " def wait(self):\n", " pass\n", " \n", " def get_data(self):\n", " return np.random.random(size=(10, 100))\n", " \n", " # Discoverable interface\n", " \n", " @classmethod\n", " def discover(cls, hardware_info: HardwareInfo):\n", " # Since the dummy hardware is always \n", " # available, we always return it.\n", " yield \"/dev/fooUSB0\"\n", " \n", " @classmethod\n", " def build(cls, hardware_info: HardwareInfo, dev_path: str):\n", " return cls(dev_path)" ] }, { "cell_type": "markdown", "id": "2200c301", "metadata": {}, "source": [ "Now, this `MyDummyInstrument` should be available for discovery.\n", "\n", "An instrument can be discovered by calling {py:meth}`secbench.api.Bench.get` with the expected hardware type." ] }, { "cell_type": "code", "execution_count": null, "id": "74128d3d", "metadata": {}, "outputs": [], "source": [ "obj = bench.get(MyDummyInstrument)" ] }, { "cell_type": "code", "execution_count": null, "id": "8257c68c", "metadata": {}, "outputs": [], "source": [ "print(obj, obj.usb_port)" ] }, { "cell_type": "markdown", "id": "9039c2d3", "metadata": {}, "source": [ "Note that if you call again `bench.get` for the same type of hardware, the function will give you the same Python object." ] }, { "cell_type": "code", "execution_count": null, "id": "dd039834", "metadata": {}, "outputs": [], "source": [ "print(bench.get(MyDummyInstrument))" ] }, { "cell_type": "markdown", "id": "a1c9ae3b", "metadata": {}, "source": [ "If you don't want this behavior and force the instanciation of a new object, you can add `cache=False` to the `Bench.get`." ] }, { "cell_type": "code", "execution_count": null, "id": "a5a567fc", "metadata": {}, "outputs": [], "source": [ "print(bench.get(MyDummyInstrument, cache=False))" ] }, { "cell_type": "code", "execution_count": null, "id": "36d6fe31", "metadata": {}, "outputs": [], "source": [ "print(bench.get(MyDummyInstrument))" ] }, { "cell_type": "markdown", "id": "3ac58dc0", "metadata": {}, "source": [ "You can also look for all subclasses. This is useful if you want to get any hardware that implements an interface, such as {py:class}`secbench.api.Instrument.Scope`." ] }, { "cell_type": "code", "execution_count": null, "id": "d2842ea5", "metadata": {}, "outputs": [], "source": [ "print(bench.get(StrangeHardware))" ] }, { "cell_type": "markdown", "id": "e60888ed", "metadata": {}, "source": [ "If needed, you can clear all cached hardware:" ] }, { "cell_type": "code", "execution_count": null, "id": "21f07233", "metadata": {}, "outputs": [], "source": [ "bench.clear_cache()" ] }, { "cell_type": "markdown", "id": "ed057590", "metadata": {}, "source": [ "## Common Hardware Abstractions" ] }, { "cell_type": "markdown", "id": "ba5608d3", "metadata": {}, "source": [ "In the {py:mod}`secbench.api` package, we define abstract interfaces for typical hardware. For example:\n", "\n", "- {py:class}`~secbench.api.instrument.Scope` for oscilloscopes\n", "- {py:class}`~secbench.api.instrument.Pulser` for fault injection benches\n", "- {py:class}`~secbench.api.instrument.Afg` for arbitrary function generators\n", "\n", "The actual drivers (e.g., Picoscope, NewAE, Tektronix, R&S, ...) are implemented in their own packages (e.g., {py:mod}`secbench.picoscope`).\n", "\n", "To see what an instrument driver looks like, you can take a look at the code:\n", "\n", "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." ] }, { "cell_type": "code", "execution_count": null, "id": "9468de0e", "metadata": {}, "outputs": [], "source": [ "from secbench.api.instrument import Scope, Afg, Pulser" ] }, { "cell_type": "code", "execution_count": null, "id": "3f55e706", "metadata": {}, "outputs": [], "source": [ "scope = bench.get(Scope, required=False)\n", "afg = bench.get(Afg, required=False)\n", "pulser = bench.get(Pulser, required=False)\n", "print(\"scope:\", scope)\n", "print(\"afg:\", afg)\n", "print(\"pulser\", pulser)" ] }, { "cell_type": "markdown", "id": "63d278b4", "metadata": {}, "source": [ "For common instrument types, the {py:class}`~secbench.api.Bench` has methods to load them without needing an additional import." ] }, { "cell_type": "code", "execution_count": null, "id": "2839a877", "metadata": {}, "outputs": [], "source": [ "scope = bench.get(Scope, required=False)\n", "# Is the same as:\n", "scope = bench.get_scope(required=False)" ] }, { "cell_type": "markdown", "id": "a6e1a168", "metadata": {}, "source": [ "### Registering Custom Hardware (Bypassing Discovery)\n", "\n", "There are two common situations where you want to bypass discovery:\n", "\n", "- When you want to make accessible hardware that cannot implement the `Discoverable` interface. \n", "- When multiple instances of the same hardware (e.g., 2 thermal sensors) are available and you want to use a specific one.\n", "\n", "In both cases, you want the {py:meth}`secbench.api.Bench.get` method to return a specific hardware.\n", "\n", "To achieve that, all you need to do is register your hardware in the bench as follows." ] }, { "cell_type": "code", "execution_count": null, "id": "a9454886", "metadata": {}, "outputs": [], "source": [ "obj = MyDummyInstrument(\"A-CUSTOM-TTY\")\n", "obj" ] }, { "cell_type": "markdown", "id": "512c58d4", "metadata": {}, "source": [ "If we call `get`, a different new `MyDummyInstrument` instance will be created." ] }, { "cell_type": "code", "execution_count": null, "id": "cfcb52a6", "metadata": {}, "outputs": [], "source": [ "obj_2 = bench.get(MyDummyInstrument)\n", "print(\"Same objects?\", obj_2 == obj)" ] }, { "cell_type": "markdown", "id": "a214ae80", "metadata": {}, "source": [ "Which is not what we want. To avoid that, we register `obj` manually." ] }, { "cell_type": "code", "execution_count": null, "id": "3f2db13f", "metadata": {}, "outputs": [], "source": [ "# Clear the cache (otherwise, we would get obj_2)\n", "bench.clear_cache()\n", "\n", "bench.register(obj)" ] }, { "cell_type": "markdown", "id": "f21f1092", "metadata": {}, "source": [ "Now the `get` method behaves as expected:" ] }, { "cell_type": "code", "execution_count": null, "id": "90aa454c", "metadata": {}, "outputs": [], "source": [ "obj_2 = bench.get(MyDummyInstrument)\n", "print(\"Same objects?\", obj_2 == obj)" ] }, { "cell_type": "code", "execution_count": null, "id": "fa3fda2e", "metadata": {}, "outputs": [], "source": [] } ], "metadata": { "jupytext": { "default_lexer": "ipython3", "formats": "ipynb,md:myst" }, "kernelspec": { "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.11.14" } }, "nbformat": 4, "nbformat_minor": 5 }