Skip to content
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
14 changes: 7 additions & 7 deletions docs/source/getting_started.rst
Original file line number Diff line number Diff line change
Expand Up @@ -46,15 +46,15 @@ Here's a minimal example to get you started:
with init_devices():
node = SECoPNodeDevice('localhost:10800')

# The device tree is now automatically built from the node description
# The device tree is now automatically built from introspecting the SEC node description

Key Concepts
------------

SECoP Nodes and Modules
~~~~~~~~~~~~~~~~~~~~~~~

A **SECoP node** represents a complete hardware device or service. Each node contains one or more
A **SEC node** can represent any number of hardware devices or services. Each node contains one or more
**modules**, which are individual functional units (e.g., a temperature controller, a pressure sensor).

SECoPNodeDevice
Expand All @@ -68,18 +68,18 @@ ophyd-async devices from SECoP nodes. It:
- Creates ophyd-async signals and devices for all parameters and modules
- Exposes SECoP commands as Bluesky plan methods

Dynamic Device Generation
~~~~~~~~~~~~~~~~~~~~~~~~~~
Ophyd Device from introspection
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Unlike most other ophyd devices that must be statically declared, SECoP-Ophyd devices are **dynamically
generated** at connection time. This means:
SECoP-Ophyd devices are **dynamically
generated** at connection time, by introspecting the SECoP node's metadata. This means:

- No manual device class definition is needed
- The device structure matches the structure defined in the SEC node
- Changes to the SECoP node are automatically reflected

However, for better development experience with type hints and autocompletion, you can generate
static class files (see :doc:`tutorial`).
declarative device class files (see :ref:`user guide section <class-file-generation>`).

Next Steps
----------
Expand Down
29 changes: 19 additions & 10 deletions docs/source/tutorial.rst
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,8 @@ from the node's description upon connection:
.. code-block:: python

with init_devices():
gas_dosing = SECoPNodeDevice('localhost:10801', loglevel="DEBUG")
reactor_cell = SECoPNodeDevice('localhost:10802', loglevel="INFO")
gas_dosing_introspected = SECoPNodeDevice('localhost:10801', loglevel="DEBUG")
reactor_cell_introspected = SECoPNodeDevice('localhost:10802', loglevel="INFO")

**Log Levels:**

Expand All @@ -93,14 +93,21 @@ Generate class files with:

.. code-block:: python

gas_dosing.class_from_instance()
reactor_cell.class_from_instance()
gas_dosing_introspected.class_from_instance()
reactor_cell_introspected.class_from_instance()

This creates ``genNodeClass.py`` in your current working directory. You can specify a custom path:

.. code-block:: python

gas_dosing.class_from_instance('/path/to/output/directory')
gas_dosing_introspected.class_from_instance('/path/to/output/directory')


The generated class file contains declarative device definitions that match the
structure of the SECoP node. Signals are annotated with their types,
and commands are exposed as method stubs that get overwritten on ``.connect()``.



.. warning::

Expand All @@ -110,15 +117,17 @@ This creates ``genNodeClass.py`` in your current working directory. You can spec
Using the Generated Classes
----------------------------

Import the generated classes and type-cast your devices:
Import the generated classes and instantiate your devices from the generated class definitions:

.. code-block:: python

from genNodeClass import *
from genNodeClass import Gas_dosing, Reactor_cell

# once the class files are generated, instantiate your devices using the generated classes
with init_devices():
gas_dosing = Gas_dosing('localhost:10801', loglevel="DEBUG")
reactor_cell = Reactor_cell('localhost:10802', loglevel="INFO")

# Type cast for IDE support
gas_dosing: Gas_dosing = gas_dosing
reactor_cell: Reactor_cell = reactor_cell

Now your IDE will provide autocompletion and type checking for all device attributes.

Expand Down
103 changes: 98 additions & 5 deletions docs/source/user_guide.rst
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,8 @@ Capture command return values:



.. _class-file-generation:

Class File Generation
---------------------

Expand Down Expand Up @@ -312,6 +314,90 @@ Generating Class Files
# Or specify output directory
device.class_from_instance('/path/to/output')


The generated code will look similar to the example below, with signals annotated with
their types and commands as method stubs. Enums are generated for parameters with the
SECoP enum type.

Enum classes are namespaced under the module device class and
parameter name. Should multipe enums have the same name, theyre members are merged,
and the class will be derived from ``SupersetEnum`` instead of ``StrictEnum``.

.. code-block:: python

from typing import Annotated as A

from ophyd_async.core import SignalR, SignalRW, StandardReadableFormat as Format, StrictEnum

from numpy import ndarray

from secop_ophyd.SECoPDevices import ParameterType as ParamT, PropertyType as PropT, SECoPMoveableDevice, SECoPNodeDevice


class Cryostat_Mode_Enum(StrictEnum):
"""mode enum for `Cryostat`."""

RAMP = "ramp"
PID = "pid"
OPENLOOP = "openloop"


class Cryostat(SECoPMoveableDevice):
"""A simulated cc cryostat with heat-load, specific heat for the sample and a temperature dependent heat-link between sample and regulation."""

# Module Properties
group: A[SignalR[str], PropT()]
description: A[SignalR[str], PropT()]
implementation: A[SignalR[str], PropT()]
interface_classes: A[SignalR[ndarray], PropT()]
features: A[SignalR[ndarray], PropT()]

# Module Parameters
value: A[SignalR[float], ParamT(), Format.HINTED_SIGNAL] # regulation temperature; Unit: (K)
status: A[SignalR[ndarray], ParamT()] # current status of the module
target: A[SignalRW[float], ParamT(), Format.HINTED_SIGNAL] # target temperature; Unit: (K)
ramp: A[SignalRW[float], ParamT()] # ramping speed of the setpoint; Unit: (K/min)
setpoint: A[SignalR[float], ParamT()] # current setpoint during ramping else target; Unit: (K)
mode: A[SignalRW[Cryostat_Mode_Enum], ParamT()] # mode of regulation
maxpower: A[SignalRW[float], ParamT()] # Maximum heater power; Unit: (W)
heater: A[SignalR[float], ParamT()] # current heater setting; Unit: (%)
heaterpower: A[SignalR[float], ParamT()] # current heater power; Unit: (W)
pid: A[SignalRW[ndarray], ParamT()] # regulation coefficients
p: A[SignalRW[float], ParamT()] # regulation coefficient 'p'; Unit: (%/K)
i: A[SignalRW[float], ParamT()] # regulation coefficient 'i'
d: A[SignalRW[float], ParamT()] # regulation coefficient 'd'
tolerance: A[SignalRW[float], ParamT()] # temperature range for stability checking; Unit: (K)
window: A[SignalRW[float], ParamT()] # time window for stability checking; Unit: (s)
timeout: A[SignalRW[float], ParamT()] # max waiting time for stabilisation check; Unit: (s)


class Cryo_7_frappy_demo(SECoPNodeDevice):
"""short description

This is a very long description providing all the gory details about the stuff we are describing."""

# Module Devices
cryo: Cryostat

# Node Properties
equipment_id: A[SignalR[str], PropT()]
firmware: A[SignalR[str], PropT()]
description: A[SignalR[str], PropT()]
_interfaces: A[SignalR[ndarray], PropT()]


.. note::

Annotations for signals include:

- **Signal type** (e.g., ``SignalR``, ``SignalRW``)
- **Signal data type** (e.g., ``float``, ``str``,... (`SignalDatatype <https://blueskyproject.io/ophyd-async/main/_api/ophyd_async/ophyd_async.core.html#ophyd_async.core.SignalDatatypeT>`_)
- **Format** declares if the signal represents a **read** or a **config** signal (`StandardReadableFormat <https://blueskyproject.io/ophyd-async/main/_api/ophyd_async/ophyd_async.core.html#ophyd_async.core.StandardReadableFormat>`_)
- **SECoP attribute type** ``ParamT`` the signal represents a SECoP parameter (data is dynamic at runtime) or ``PropT`` the signal represents a SECoP property (static metadata at runtime, can only change on reconnect)




Using Generated Classes
~~~~~~~~~~~~~~~~~~~~~~~

Expand All @@ -320,16 +406,23 @@ Using Generated Classes
# Import generated classes
from genNodeClass import MyDevice

# Type-cast for IDE support
device: MyDevice = device
# Instatiate device using generated class
with init_devices():
cryo_node = MyDevice('localhost:10800')

# Now you have autocompletion!
device.temperature. # IDE shows: value, target, ramp, status, etc.
cryo_node.cryo. # IDE shows: value, target, ramp, status, etc.






.. warning::

Regenerate class files whenever the static metadata of a SECoP node changes!
THis can happen on every new connection to a SEC node.
This can happen on every new connection to a SEC node.




Expand All @@ -339,7 +432,7 @@ Best Practices
Connection Management
~~~~~~~~~~~~~~~~~~~~~

Always use ``init_devices()`` context manager:
With or without a runengine involved always use ``init_devices()`` context manager:

.. code-block:: python

Expand Down
6 changes: 3 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ description = "An Interface between bluesky and SECoP, using ophyd and frappy-cl
dependencies = [
'ophyd-async >= 0.10.0',
'frappy-core == 0.20.4',
'black >= 23.0.0',
'Jinja2 >= 3.1.6',
'autoflake >= 2.3.2',
'black',
'Jinja2',
'autoflake',

]

Expand Down
18 changes: 9 additions & 9 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.