Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add developer documentation for hardware framework #379

Merged
merged 4 commits into from
Nov 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions .github/workflows/check-links.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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'
use-quiet-mode: "yes"
use-verbose-mode: "yes"
config-file: .mlc_config.json
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ jobs:
with:
use-quiet-mode: "yes"
use-verbose-mode: "yes"
config-file: .mlc_config.json

test:
runs-on: ${{ matrix.os }}
Expand Down
7 changes: 7 additions & 0 deletions .mlc_config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"ignorePatterns": [
{
"pattern": "^\\.\\./reference/"
}
]
}
139 changes: 139 additions & 0 deletions docs/hardware.md
Original file line number Diff line number Diff line change
@@ -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.
Comment on lines +137 to +139
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It will be useful, I think, to indicate somewhere the list of all possible messages a particular device type handles - either sending or receiving. In the end, pubsub topics are implicit function calls with their own parameters and they should be documented in detail as we do with any other function.

We have never done it before, but given the complexity of the messaging system of FINESSE, it might be worth considering doing something like this https://pypubsub.readthedocs.io/en/v4.0.3/usage/usage_advanced_maintain.html#specify-topic-tree-def, although as most topics are created implicitely based on the device names, I'm not sure how far we can go down this route...

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense.

The doc you link to looks interesting. I'll see if we can incorporate that way of doing things 😄

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a good idea, but I think we should do it later. I've opened an issue for it: #432

1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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/