diff --git a/.github/workflows/check-links.yml b/.github/workflows/check-links.yml index 63a2faf6..1bcfd035 100644 --- a/.github/workflows/check-links.yml +++ b/.github/workflows/check-links.yml @@ -10,5 +10,6 @@ jobs: - uses: actions/checkout@v4 - uses: gaurav-nelson/github-action-markdown-link-check@v1 with: - use-quiet-mode: 'yes' - use-verbose-mode: 'yes' \ No newline at end of file + use-quiet-mode: "yes" + use-verbose-mode: "yes" + config-file: .mlc_config.json diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8bb7b5b2..990c0a40 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,6 +12,7 @@ jobs: with: use-quiet-mode: "yes" use-verbose-mode: "yes" + config-file: .mlc_config.json test: runs-on: ${{ matrix.os }} diff --git a/.mlc_config.json b/.mlc_config.json new file mode 100644 index 00000000..38fc77af --- /dev/null +++ b/.mlc_config.json @@ -0,0 +1,7 @@ +{ + "ignorePatterns": [ + { + "pattern": "^\\.\\./reference/" + } + ] +} diff --git a/docs/hardware.md b/docs/hardware.md new file mode 100644 index 00000000..bd5afa43 --- /dev/null +++ b/docs/hardware.md @@ -0,0 +1,139 @@ +# Hardware + +To work, FINESSE requires connections to a number of devices. At a minimum, this must +include: + +- An interferometer +- A stepper motor for orienting the interferometer's mirror +- Temperature controllers for getting and setting the temperature of the hot and cold + black bodies +- A separate temperature monitor with sensors recording from various angles around the + mirror + +All the code for interfacing with the hardware lives in the +[`finesse.hardware`](../reference/finesse/hardware) module. + +## Plugin architecture + +Code in the `finesse.hardware` module is not imported into the frontend code +([`finesse.gui`](../reference/finesse/gui)) directly. Instead, messages are passed back +and forth using the [PyPubSub](https://pypi.org/project/PyPubSub/) package. + +As we want the user to be able to select which devices to use at runtime, the FINESSE +hardware framework is designed to be modular. This is achieved via a plugin system. Each +device type and device base type (explained below) is represented by a plugin class +residing somewhere in the +[`finesse.hardware.plugins`](../reference/finesse/hardware/plugins) module. To add a new +plugin, it is sufficient just to define this class in a `.py` file and put it somewhere +in the plugins directory hierarchy. + +### Creating a new device type + +A device base type is a class providing a common interface for similar device types +(e.g. a stepper motor). Each device base class must inherit from `Device` and each +device class must inherit from a device base class. + +You can create a new device base like so: + +```py +class MyBaseType( + Device, is_base_type=True, name="my_base_type", description="Example base type" +): + # ... +``` + +The `is_base_type=True` is required to register the class as a device base type. `name` +is the short name for the base type and is used in the topic for PyPubSub messages (see +more below). `description` provides a human-readable name for the base type, which will +be displayed in the GUI. It is additionally possible to provide a list of possible names +for instances of the device, but this is currently only used for temperature controllers +(to distinguish between the hot and cold black body controllers). + +You can create a concrete implementation of `MyBaseType` like so: + +```py +class MyDevice1(MyBaseType, description="An example device"): + # ... +``` + +You may optionally provide a `dict` specifying which parameters should be passed when +the device object is constructed, along with a human-readable description. This provides +a mechanism by which the frontend can know what parameters it can provide for a given +device type, as well as information about their type and default value (if any). Here is +an example: + +```py +class MyDevice2( + MyBaseType, + description="An example device", + parameters={"my_param": "An example parameter"} +): + def __init__(self, my_param: int = 42) -> None: + # ... +``` + +In this case, the frontend will be informed that `MyDevice2` has a parameter, `my_param` +of type `int` with a default value of `42`. The user can then alter this value via a +text box in the GUI. Note that if a default value were *not* provided for this +parameter, the user would be forced to enter one. Subclasses inherit their parents' +device parameters (but can add more of their own). As a result, they *must* also include +these parameters for their constructors. + +You can also provide a `Sequence` of possible values that a parameter can take, e.g.: + +```py +class MyDevice3( + MyBaseType, + description="An example device", + parameters={"my_param": ("An example parameter", range(10))} +): + def __init__(self, my_param: int = 5) -> None: + # ... +``` + +In this case, `my_param` must be a number in the range 0 to 9. The user will be able to +select from among these options in a dropdown box. + +Subclasses can provide different default values for device parameters than their +parents, simply by providing a different default value in their constructors. This is +used by device classes for USB serial devices to choose a default baud rate. For +example: + +```py +class MyUSBDevice(SerialDevice, MyBaseType, description="A USB serial device"): + def __init__(self, port: str, baudrate: int = 9600) -> None: + # ... +``` + +Note that the constructor must have both the `port` and `baudrate` parameters as they +are defined as device parameters by the `SerialDevice` base class. The `SerialDevice` +class must be listed before `MyBaseType` unless `MyUSBDevice` defines its own `close()` +method, otherwise you will get an error about this abstract method not being +implemented. + +### Communicating with devices via PyPubSub + +Many messages for communicating with devices include a string indicating which device +the communication is intended for (prefixed by `device.`). This is composed of the +device base type's name and, if provided, the device's name. For example, this could be +`stepper_motor` for the stepper motor and `temperature_controller.hot_bb` for the hot +black body temperature controller. + +To connect to a device, the frontend should send a `device.open` message, indicating +which device type should be opened, along with any device parameters. If the connection +is successful, a `device.opening.*` message is sent, followed by a `device.opened.*` +one. If the connection fails, a `device.error.*` message is sent instead. +(`device.error.*` messages can also be sent at any point during the device's lifetime to +indicate that an error has occurred.) Similarly, the `device.close` method is used to +close a connection to a device. + +If the frontend sends a `device.list.request` message all of the plugins are loaded and +information about each device type (grouped by base type) is sent to the frontend with +the `device.list.response` message. Note that this step is not required in order to open +a device: if the name of the plugin and values for parameters are known (e.g. if the +user is connecting to a predefined hardware set), it is sufficient to just send the +`device.open` message. + +Device types also need to define their own message types for communication. For example, +the `StepperMotorBase` class allows for setting the current angle of the stepper motor +with a `device.stepper_motor.move.begin` message. diff --git a/mkdocs.yml b/mkdocs.yml index 59c3eb09..13ef09f8 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -36,6 +36,7 @@ plugins: nav: - FINESSE documentation: index.md +- Hardware: hardware.md - Measure scripts: measure_scripts.md # defer to gen-files + literate-nav - Code Reference: reference/