ControllerPlugin¶
Controller plugins provide device-specific control widgets.
Purpose¶
Use ControllerPlugin when you want to:
Create custom UIs for specific device types
Provide optimized controls for beamline hardware
Implement device-pattern matching for automatic widget selection
Base Class¶
from lightfall.plugins.controller_plugin import ControllerPlugin
Class Attributes¶
Attribute |
Value |
Description |
|---|---|---|
|
|
Plugin type identifier |
|
|
Plugin is singleton (creates widgets on demand) |
Required Methods¶
name (property)¶
Unique identifier for this controller.
@property
def name(self) -> str:
return "my_controller"
can_control(items)¶
Check if this controller handles the given device items.
def can_control(self, items: list[DeviceTreeItem]) -> int | None:
"""Check if this controller can handle the selected items.
Args:
items: List of selected DeviceTreeItems.
Returns:
Priority value (higher = preferred) or None if not applicable.
"""
if len(items) != 1:
return None
item = items[0]
if self._matches_device(item):
return 150 # Higher than default
return None
create_widget(parent)¶
Create a new widget instance for controlling devices.
def create_widget(self, parent: QWidget | None = None) -> QWidget:
"""Create the control widget.
Args:
parent: Parent widget.
Returns:
A new widget instance.
"""
return MyControlWidget(parent)
Optional Methods¶
display_name (property)¶
Human-readable name for the widget selector. Defaults to title-cased name.
@property
def display_name(self) -> str:
return "My Custom Controller"
Priority Guidelines¶
can_control() returns a priority value indicating how well this controller matches:
Priority Range |
Meaning |
|---|---|
|
Exact device/prefix match |
|
Device class match |
|
Category match |
|
Generic fallback |
|
Cannot control |
Higher priorities take precedence when multiple controllers match.
Lifecycle¶
Plugin is instantiated on load
Plugin is registered with
ControllerPluginRegistryWhen user selects devices:
All registered controllers’
can_control()is calledController with highest priority is selected
create_widget()creates the control widget
Widget is configured with selected devices via
set_items()
Complete Example¶
Motor Controller Plugin¶
"""Custom motor controller plugin."""
from PySide6.QtWidgets import QWidget
from lightfall.plugins.controller_plugin import ControllerPlugin
class MotorControllerPlugin(ControllerPlugin):
"""Controller for motor devices."""
@property
def name(self) -> str:
return "motor_controller"
@property
def display_name(self) -> str:
return "Motor Control"
def can_control(self, items) -> int | None:
"""Match motor devices."""
if len(items) != 1:
return None
item = items[0]
if not item.device_info:
return None
# Check if it's a motor
from lightfall.devices.types import DeviceCategory
if item.device_info.category == DeviceCategory.MOTOR:
return 100 # Device class match
return None
def create_widget(self, parent: QWidget | None = None) -> QWidget:
from my_package.widgets.motor_widget import MotorControlWidget
return MotorControlWidget(parent)
Motor Control Widget¶
"""Motor control widget implementation."""
from PySide6.QtWidgets import (
QDoubleSpinBox,
QFormLayout,
QGroupBox,
QHBoxLayout,
QPushButton,
QVBoxLayout,
QWidget,
)
class MotorControlWidget(QWidget):
"""Widget for controlling a single motor."""
def __init__(self, parent=None):
super().__init__(parent)
self._device = None
self._setup_ui()
def _setup_ui(self):
layout = QVBoxLayout(self)
# Position group
pos_group = QGroupBox("Position")
pos_layout = QFormLayout(pos_group)
self._position_spin = QDoubleSpinBox()
self._position_spin.setDecimals(4)
self._position_spin.setRange(-1000, 1000)
pos_layout.addRow("Target:", self._position_spin)
# Movement buttons
btn_layout = QHBoxLayout()
self._move_btn = QPushButton("Move")
self._move_btn.clicked.connect(self._on_move)
self._stop_btn = QPushButton("Stop")
self._stop_btn.clicked.connect(self._on_stop)
btn_layout.addWidget(self._move_btn)
btn_layout.addWidget(self._stop_btn)
pos_layout.addRow(btn_layout)
layout.addWidget(pos_group)
def set_items(self, items):
"""Configure the widget with device items.
Args:
items: List of DeviceTreeItems (should be single motor).
"""
if items and len(items) == 1:
self._device = items[0].device_info
self._update_from_device()
def _update_from_device(self):
"""Update widget from device state."""
if self._device:
# Read current position
# self._position_spin.setValue(self._device.position)
pass
def _on_move(self):
"""Handle move button click."""
if self._device:
target = self._position_spin.value()
# self._device.move(target)
def _on_stop(self):
"""Handle stop button click."""
if self._device:
# self._device.stop()
pass
Prefix-based Matching¶
Match devices by PV prefix:
class SlitControllerPlugin(ControllerPlugin):
"""Controller for slit devices by prefix."""
@property
def name(self) -> str:
return "slit_controller"
def can_control(self, items) -> int | None:
if len(items) != 1:
return None
item = items[0]
if not item.device_info:
return None
# Match by prefix
prefix = item.device_info.prefix
if prefix and prefix.startswith("BL701:SLIT"):
return 200 # Exact match - high priority
return None
def create_widget(self, parent=None):
from my_package.widgets import SlitWidget
return SlitWidget(parent)
Multi-device Controller¶
Handle multiple devices together:
class MultiMotorControllerPlugin(ControllerPlugin):
"""Controller for coordinated multi-motor control."""
@property
def name(self) -> str:
return "multi_motor"
@property
def display_name(self) -> str:
return "Coordinated Motors"
def can_control(self, items) -> int | None:
"""Match when 2-4 motors are selected."""
if not (2 <= len(items) <= 4):
return None
from lightfall.devices.types import DeviceCategory
# All items must be motors
for item in items:
if not item.device_info:
return None
if item.device_info.category != DeviceCategory.MOTOR:
return None
return 80 # Category match for multiple
def create_widget(self, parent=None):
from my_package.widgets import CoordinatedMotorWidget
return CoordinatedMotorWidget(parent)
Registration¶
PluginEntry(
type_name="controller",
name="motor_controller",
import_path="my_package.plugins:MotorControllerPlugin",
),
Widget Interface¶
Control widgets should implement set_items() to receive selected devices:
class MyControlWidget(QWidget):
def set_items(self, items: list[DeviceTreeItem]) -> None:
"""Configure widget with selected device items.
Args:
items: List of selected DeviceTreeItems.
"""
self._items = items
self._update_display()
Controller Selection Flow¶
User selects device(s) in the device tree
System calls
can_control(items)on all registered controllersControllers return priority (int) or
NoneController with highest priority is selected
create_widget()creates a new widget instancewidget.set_items(items)configures the widget