User Guide#
This guide covers common usage patterns and best practices for SECoP-Ophyd.
Device Connection#
Basic Connection#
Connect to a SECoP node using its IP address and port:
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:
# 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 SECoPNodeDevice constructor accepts the following parameters:
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’sequipment_idproperty.loglevel(optional): Control logging verbosity. Options:"DEBUG","INFO","WARNING","ERROR","CRITICAL"logdir(optional): Directory path for storing log files. IfNone, the default logdir.secop-ophyd/is set.
Examples:
# 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:
# 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:
# 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 directoryLog 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 troubleshootingINFO: General operational information (default)WARNING: Warning messages onlyERROR: Error messages onlyCRITICAL: Critical errors only
Device Structure#
Understanding the Hierarchy#
A SECoP-Ophyd device follows this structure:
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:
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:
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:
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
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:
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.
# 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:
# 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:
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#
# 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#
# 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:
# 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:
# 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:
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))