From 3149d22d9c6c80fffea94307a10de77dcb328b87 Mon Sep 17 00:00:00 2001 From: Peter Braun Date: Thu, 19 Feb 2026 12:14:45 +0100 Subject: [PATCH] update docs to new conn strategy --- docs/source/getting_started.rst | 14 ++--- docs/source/tutorial.rst | 29 +++++---- docs/source/user_guide.rst | 103 ++++++++++++++++++++++++++++++-- pyproject.toml | 6 +- uv.lock | 18 +++--- 5 files changed, 136 insertions(+), 34 deletions(-) diff --git a/docs/source/getting_started.rst b/docs/source/getting_started.rst index 1b60103..1696766 100644 --- a/docs/source/getting_started.rst +++ b/docs/source/getting_started.rst @@ -46,7 +46,7 @@ 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 ------------ @@ -54,7 +54,7 @@ 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 @@ -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 `). Next Steps ---------- diff --git a/docs/source/tutorial.rst b/docs/source/tutorial.rst index 575a5b8..2851154 100644 --- a/docs/source/tutorial.rst +++ b/docs/source/tutorial.rst @@ -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:** @@ -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:: @@ -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. diff --git a/docs/source/user_guide.rst b/docs/source/user_guide.rst index a62509c..031fae3 100644 --- a/docs/source/user_guide.rst +++ b/docs/source/user_guide.rst @@ -285,6 +285,8 @@ Capture command return values: +.. _class-file-generation: + Class File Generation --------------------- @@ -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 `_) + - **Format** declares if the signal represents a **read** or a **config** signal (`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 ~~~~~~~~~~~~~~~~~~~~~~~ @@ -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. + @@ -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 diff --git a/pyproject.toml b/pyproject.toml index 29a6de1..40958b3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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', ] diff --git a/uv.lock b/uv.lock index 0ab2b19..12afe3e 100644 --- a/uv.lock +++ b/uv.lock @@ -645,11 +645,11 @@ wheels = [ [[package]] name = "filelock" -version = "3.24.2" +version = "3.24.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/02/a8/dae62680be63cbb3ff87cfa2f51cf766269514ea5488479d42fec5aa6f3a/filelock-3.24.2.tar.gz", hash = "sha256:c22803117490f156e59fafce621f0550a7a853e2bbf4f87f112b11d469b6c81b", size = 37601, upload-time = "2026-02-16T02:50:45.614Z" } +sdist = { url = "https://files.pythonhosted.org/packages/73/92/a8e2479937ff39185d20dd6a851c1a63e55849e447a55e798cc2e1f49c65/filelock-3.24.3.tar.gz", hash = "sha256:011a5644dc937c22699943ebbfc46e969cdde3e171470a6e40b9533e5a72affa", size = 37935, upload-time = "2026-02-19T00:48:20.543Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e7/04/a94ebfb4eaaa08db56725a40de2887e95de4e8641b9e902c311bfa00aa39/filelock-3.24.2-py3-none-any.whl", hash = "sha256:667d7dc0b7d1e1064dd5f8f8e80bdac157a6482e8d2e02cd16fd3b6b33bd6556", size = 24152, upload-time = "2026-02-16T02:50:44Z" }, + { url = "https://files.pythonhosted.org/packages/9c/0f/5d0c71a1aefeb08efff26272149e07ab922b64f46c63363756224bd6872e/filelock-3.24.3-py3-none-any.whl", hash = "sha256:426e9a4660391f7f8a810d71b0555bce9008b0a1cc342ab1f6947d37639e002d", size = 24331, upload-time = "2026-02-19T00:48:18.465Z" }, ] [[package]] @@ -2602,10 +2602,10 @@ dev = [ [package.metadata] requires-dist = [ - { name = "autoflake", specifier = ">=2.3.2" }, - { name = "black", specifier = ">=23.0.0" }, + { name = "autoflake" }, + { name = "black" }, { name = "frappy-core", specifier = "==0.20.4" }, - { name = "jinja2", specifier = ">=3.1.6" }, + { name = "jinja2" }, { name = "ophyd-async", specifier = ">=0.10.0" }, ] @@ -3056,16 +3056,16 @@ wheels = [ [[package]] name = "virtualenv" -version = "20.37.0" +version = "20.36.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "distlib" }, { name = "filelock" }, { name = "platformdirs" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c1/ef/d9d4ce633df789bf3430bd81fb0d8b9d9465dfc1d1f0deb3fb62cd80f5c2/virtualenv-20.37.0.tar.gz", hash = "sha256:6f7e2064ed470aa7418874e70b6369d53b66bcd9e9fd5389763e96b6c94ccb7c", size = 5864710, upload-time = "2026-02-16T16:17:59.42Z" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/a3/4d310fa5f00863544e1d0f4de93bddec248499ccf97d4791bc3122c9d4f3/virtualenv-20.36.1.tar.gz", hash = "sha256:8befb5c81842c641f8ee658481e42641c68b5eab3521d8e092d18320902466ba", size = 6032239, upload-time = "2026-01-09T18:21:01.296Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/42/4b/6cf85b485be7ec29db837ec2a1d8cd68bc1147b1abf23d8636c5bd65b3cc/virtualenv-20.37.0-py3-none-any.whl", hash = "sha256:5d3951c32d57232ae3569d4de4cc256c439e045135ebf43518131175d9be435d", size = 5837480, upload-time = "2026-02-16T16:17:57.341Z" }, + { url = "https://files.pythonhosted.org/packages/6a/2a/dc2228b2888f51192c7dc766106cd475f1b768c10caaf9727659726f7391/virtualenv-20.36.1-py3-none-any.whl", hash = "sha256:575a8d6b124ef88f6f51d56d656132389f961062a9177016a50e4f507bbcc19f", size = 6008258, upload-time = "2026-01-09T18:20:59.425Z" }, ] [[package]]