User Guide ========== This guide covers common usage patterns and best practices for SECoP-Ophyd. .. contents:: Table of Contents :local: :depth: 2 Device Connection ----------------- Basic Connection ~~~~~~~~~~~~~~~~ Connect to a SECoP node using its IP address and port: .. code-block:: python from ophyd_async.core import init_devices from secop_ophyd.SECoPDevices import SECoPNodeDevice with init_devices(): device = SECoPNodeDevice('192.168.1.100:10767') The device tree is automatically built from the node's descriptive data during the ``init_devices()`` context. **Important:** The connection must be established within an ``init_devices()`` context manager, which handles the asynchronous connection setup. Device Naming ~~~~~~~~~~~~~ By default, devices use the SECoP node's ``equipment_id`` property as their name. You can customize this: .. code-block:: python # Default: uses equipment_id from SECoP node device = SECoPNodeDevice('192.168.1.100:10767') print(device.name) # e.g., "CRYO-01" # Custom name device = SECoPNodeDevice('192.168.1.100:10767', name='my_cryostat') print(device.name) # "my_cryostat" # With prefix (useful for multiple similar devices) device = SECoPNodeDevice( '192.168.1.100:10767', prefix='beamline_A_', name='cryo' ) print(device.name) # "beamline_A_cryo" Connection Parameters ~~~~~~~~~~~~~~~~~~~~~ The :class:`~secop_ophyd.SECoPDevices.SECoPNodeDevice` constructor accepts the following parameters: .. code-block:: python SECoPNodeDevice( sec_node_uri: str, # Required: 'host:port' format prefix: str = "", # Optional: prefix for device name name: str = "", # Optional: custom device name loglevel: str = "INFO", # Optional: logging level logdir: str | None = None # Optional: log file directory ) **Parameters explained:** - ``sec_node_uri`` (required): Connection string in ``'host:port'`` format - ``'localhost:10767'`` - Local SECoP node - ``'192.168.1.100:10767'`` - Network device - ``'device.example.com:10767'`` - Domain name - ``prefix`` (optional): String prefix added to the device name. Useful when managing multiple devices. - ``name`` (optional): Custom name for the device. If not provided, the device uses the SECoP node's ``equipment_id`` property. - ``loglevel`` (optional): Control logging verbosity. Options: ``"DEBUG"``, ``"INFO"``, ``"WARNING"``, ``"ERROR"``, ``"CRITICAL"`` - ``logdir`` (optional): Directory path for storing log files. If ``None``, the default logdir ``.secop-ophyd/`` is set. **Examples:** .. code-block:: python # Minimal connection device = SECoPNodeDevice('192.168.1.100:10767') # With custom name device = SECoPNodeDevice('192.168.1.100:10767', name='cryo_controller') # With prefix and logging to file device = SECoPNodeDevice( '192.168.1.100:10767', prefix='lab1_', loglevel='DEBUG', logdir='/var/log/secop' ) Logging Configuration ~~~~~~~~~~~~~~~~~~~~~ SECoP-Ophyd provides default logging for debugging and monitoring. Logs are written to rotating hourly log files (48 hours retention). **Default Logging:** .. code-block:: python # Logs written to .secop-ophyd/secop-ophyd.log (default) device = SECoPNodeDevice('localhost:10767') # Change log level device = SECoPNodeDevice('localhost:10767', loglevel="DEBUG") **Custom Log Directory:** .. code-block:: python # Write logs to custom directory device = SECoPNodeDevice( 'localhost:10767', loglevel="DEBUG", logdir="/var/log/secop" ) # Logs written to: /var/log/secop/secop-ophyd.log **Log File Behavior:** - Default location: ``.secop-ophyd/`` in current working directory - Log files rotate hourly with 48-hour retention - Multiple devices can share the same log file - Logs use UTC timestamps for consistency **Log Levels:** - ``DEBUG``: Detailed protocol messages, useful for troubleshooting - ``INFO``: General operational information (default) - ``WARNING``: Warning messages only - ``ERROR``: Error messages only - ``CRITICAL``: Critical errors only Device Structure ---------------- Understanding the Hierarchy ~~~~~~~~~~~~~~~~~~~~~~~~~~~ A SECoP-Ophyd device follows this structure: .. code-block:: text SECoPNodeDevice (Node) ├── SECoPMoveableDevice (Module) │ ├── parameter_a (Signal) │ ├── parameter_b (Signal) │ └── command_x (Method) └── SECoPReadableDevice (Module) ├── parameter_c (Signal) └── command_y (Method) Example with a temperature controller: .. code-block:: text device ├── temperature_controller │ ├── value # Current temperature (readable) │ ├── target # Target temperature (readable/writable) │ ├── ramp # Ramp rate (readable/writable) │ ├── status # Device status │ └── reset() # Reset command └── pressure_sensor └── value # Current pressure (readable) Working with Signals -------------------- Reading Signals ~~~~~~~~~~~~~~~ Read single values: .. code-block:: python from bluesky.plan_stubs import rd def read_temperature(): value = yield from rd(device.temperature.value) print(f"Temperature: {value}") return value RE(read_temperature()) Setting Signals ~~~~~~~~~~~~~~~ There's an important distinction when setting values on movable devices (SECoP modules with the ``Drivable`` interface class): **Module-level vs Signal-level Setting** For a movable device like a temperature controller, you can set the target it should drive toward at two levels: 1. **Module-level** (preferred): ``device.temperature_controller.set(value)`` - Sets the target parameter AND waits for the device to reach the setpoint - Returns a status that completes only when the module returns to IDLE state - Use this when you want to wait for the operation to complete 2. **Signal-level**: ``device.temperature_controller.target.set(value)`` - Only sets the target parameter value - Returns immediately once the parameter is written - Use this for setting config parameters **Examples:** .. code-block:: python from bluesky.plan_stubs import mv, abs_set # Module-level set - waits until temperature is reached RE(abs_set(device.temperature_controller, 300)) # Signal-level set - returns immediately after setting target RE(abs_set(device.temperature_controller.target, 300)) # Explicitly control wait behavior RE(abs_set(device.temperature_controller, 300, wait=True)) # Wait for completion RE(abs_set(device.temperature_controller, 300, wait=False)) # Don't wait .. note:: For ``Drivable`` modules, prefer module-level ``set()`` to ensure operations complete before proceeding. For simple parameter updates on ``Readable`` or ``Writable`` modules, use signal-level ``set()``. Using SECoP Commands -------------------- Command Basics ~~~~~~~~~~~~~~ SECoP commands are wrapped as bluesky plans. Use ``RE()`` to execute them. Commands should return immediately. For some long-running operations, the device will go into a BUSY state, and you may want to wait until the device returns to IDLE state. .. code-block:: python # Call a command without arguments RE(device.module.reset()) # Call a command with arguments RE(device.module.configure(arg = {'mode': 'auto', 'setpoint': 100})) All commands plans accept a ``wait_for_idle`` parameter: .. code-block:: python # Return immediately (default) RE(device.module.command(arg=value, wait_for_idle=False)) # Wait for device to return to IDLE state RE(device.module.command(arg=value, wait_for_idle=True)) .. note:: For commands that trigger long operations, set ``wait_for_idle=True`` to ensure the plan waits until the operation completes. Return Values ~~~~~~~~~~~~~ Capture command return values: .. code-block:: python def get_status(): result = yield from device.module.get_info() print(f"Device info: {result}") return result info = RE(get_status()) Class File Generation --------------------- Why Generate Class Files? ~~~~~~~~~~~~~~~~~~~~~~~~~~ While SECoP-Ophyd works without them, class files provide: - **IDE autocompletion** - See available modules and parameters - **Type hints** - Catch errors before runtime - **Documentation** - Understand device structure Generating Class Files ~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: python # Connect to device with init_devices(): device = SECoPNodeDevice('localhost:10800') # Generate class file in current directory device.class_from_instance() # Or specify output directory device.class_from_instance('/path/to/output') Using Generated Classes ~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: python # Import generated classes from genNodeClass import MyDevice # Type-cast for IDE support device: MyDevice = device # Now you have autocompletion! device.temperature. # 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. Best Practices -------------- Connection Management ~~~~~~~~~~~~~~~~~~~~~ Always use ``init_devices()`` context manager: .. code-block:: python # Good with init_devices(): device = SECoPNodeDevice('localhost:10800') # Device is properly connected and cleaned up # Bad - no cleanup device = SECoPNodeDevice('localhost:10800') Wait Strategies ~~~~~~~~~~~~~~~ Choose appropriate wait strategies for commands: .. code-block:: python # Quick commands - no wait needed RE(device.module.get_status(wait_for_idle=False)) # Long operations - wait for completion RE(device.motor.move_to(100, wait_for_idle=True)) Advanced Topics --------------- Mixed Device Ecosystems ~~~~~~~~~~~~~~~~~~~~~~~ Use SECoP devices alongside EPICS, Tango, or other protocols: .. code-block:: python from ophyd import EpicsMotor from ophyd_async.tango import TangoDevice # Mix different device types with init_devices(): secop_temp = SECoPNodeDevice('localhost:10800') epics_motor = EpicsMotor('XF:31IDA-OP{Tbl-Ax:X1}Mtr', name='motor') tango_detector = TangoDevice('sys/tg_test/1') # Use together in plans RE(scan([tango_detector], epics_motor, 0, 10, 11)) RE(mv(secop_temp.target, 300)) Further Resources ----------------- - `SECoP Specification `_ - `Ophyd-async Documentation `_ - `Bluesky Documentation `_ - `Frappy (SECoP Framework) `_