0
0
mirror of https://github.com/bec-project/bec_widgets.git synced 2025-07-13 19:21:50 +02:00

docs(widget tutorial): step by step guide added

This commit is contained in:
2024-08-27 18:15:33 +02:00
committed by wyzula_j
parent d0e5643d4f
commit b32ced85ff
3 changed files with 248 additions and 2 deletions

View File

@ -34,7 +34,7 @@ integrated with the BEC system by providing:
dictionaries, JSON, or YAML formats, allowing for persistent storage and retrieval of widget states. dictionaries, JSON, or YAML formats, allowing for persistent storage and retrieval of widget states.
4. **RPC Registration**: Widgets derived 4. **RPC Registration**: Widgets derived
from [`BECWidget`](https://bec.readthedocs.io/projects/bec-widgets/en/latest/api_reference/_autosummary/bec_widgets.utils.bec_widget.BECWidget.html#bec_widgets.utils.bec_widget.BECWidget) from [`BECConnector`](https://bec.readthedocs.io/projects/bec-widgets/en/latest/api_reference/_autosummary/bec_widgets.utils.bec_connector.BECConnector.html#bec_widgets.utils.bec_connector.BECConnector)
are automatically registered with are automatically registered with
the [`RPCRegister`](https://bec.readthedocs.io/projects/bec-widgets/en/latest/api_reference/_autosummary/bec_widgets.cli.rpc_register.RPCRegister.html#bec_widgets.cli.rpc_register.RPCRegister), the [`RPCRegister`](https://bec.readthedocs.io/projects/bec-widgets/en/latest/api_reference/_autosummary/bec_widgets.cli.rpc_register.RPCRegister.html#bec_widgets.cli.rpc_register.RPCRegister),
enabling them to handle remote procedure calls (RPCs) efficiently. This allows the widget to be controlled remotely enabling them to handle remote procedure calls (RPCs) efficiently. This allows the widget to be controlled remotely
@ -135,7 +135,7 @@ BEC system through convenient shortcuts:
self.client.shutdown() self.client.shutdown()
``` ```
### Example: `PositionerBox` Widget ### Example: [`PositionerBox`](user.widgets.positioner_box) Widget
Lets look at an example of a widget that leverages Lets look at an example of a widget that leverages
the [`BECWidget`](https://bec.readthedocs.io/projects/bec-widgets/en/latest/api_reference/_autosummary/bec_widgets.utils.bec_widget.BECWidget.html#bec_widgets.utils.bec_widget.BECWidget) the [`BECWidget`](https://bec.readthedocs.io/projects/bec-widgets/en/latest/api_reference/_autosummary/bec_widgets.utils.bec_widget.BECWidget.html#bec_widgets.utils.bec_widget.BECWidget)

View File

@ -11,4 +11,5 @@ hidden: false
bec_dispatcher bec_dispatcher
widget_base_class widget_base_class
widget_tutorial
``` ```

View File

@ -0,0 +1,245 @@
(developer.widget_development.widget_tutorial)=
# Tutorial: Creating a New BEC-Connected Widget
In this tutorial, we'll create a BEC-connected widget that allows you to control a motor by setting its position. The
widget will demonstrate how to retrieve data from BEC, prompt an action in BEC (like moving a motor), and expose an RPC
interface for remote control. By the end of this tutorial, you'll have a functional widget that can interact with the
BEC system both through a graphical interface and via command-line control.
We'll break the tutorial into the following steps:
1. **Creating the Basic Widget Layout**: Well design a simple UI with a `QLabel`, `QDoubleSpinBox`, and
a `QPushButton`.
2. **Connecting to BEC**: Well integrate our widget with the BEC system using
the [`BECWidget`](https://bec.readthedocs.io/projects/bec-widgets/en/latest/api_reference/_autosummary/bec_widgets.utils.bec_widget.BECWidget.html#bec_widgets.utils.bec_widget.BECWidget)
base class
and [`BECDispatcher`](https://bec.readthedocs.io/projects/bec-widgets/en/latest/api_reference/_autosummary/bec_widgets.utils.bec_dispatcher.BECDispatcher.html#bec_widgets.utils.bec_dispatcher.BECDispatcher).
3. **Implementing RPC for Remote Control**: Well set up an RPC interface to allow remote control of the widget via CLI.
4. **Running the Widget**: Well create a small script to run the widget in a `QApplication`.
## Step 1: Creating the Basic Widget Layout
First, let's start by creating the basic layout of our widget. Well add a `QLabel` to display the current coordinates
of the motor, a `QDoubleSpinBox` to input the desired coordinates, and a `QPushButton` to initiate the motor movement.
```python
from qtpy.QtWidgets import QWidget, QLabel, QDoubleSpinBox, QPushButton, QVBoxLayout
class MotorControlWidget(QWidget):
def __init__(self, motor_name: str, parent=None):
super().__init__(parent)
self.motor_name = motor_name
# Initialize UI elements
self.label_top = QLabel("Current Position:", self)
self.label = QLabel(f"{self.motor_name} - N/A", self)
self.spin_box = QDoubleSpinBox(self)
self.spin_box.setRange(-10000, 10000)
self.spin_box.setDecimals(3)
self.spin_box.setSingleStep(0.1)
self.move_button = QPushButton("Move Motor", self)
# Set up the layout
layout = QVBoxLayout(self)
layout.addWidget(self.label_top)
layout.addWidget(self.label)
layout.addWidget(self.spin_box)
layout.addWidget(self.move_button)
self.setLayout(layout)
# Connect button click to move motor
self.move_button.clicked.connect(self.move_motor)
def move_motor(self):
# Placeholder method for motor movement
print(f"Moving motor {self.motor_name} to {self.spin_box.value()}")
```
## Step 2: Connecting to BEC
Now that we have the basic layout, let's connect our widget to the BEC system using
the [`BECWidget`](https://bec.readthedocs.io/projects/bec-widgets/en/latest/api_reference/_autosummary/bec_widgets.utils.bec_widget.BECWidget.html#bec_widgets.utils.bec_widget.BECWidget)
base class
and [`BECDispatcher`](https://bec.readthedocs.io/projects/bec-widgets/en/latest/api_reference/_autosummary/bec_widgets.utils.bec_dispatcher.BECDispatcher.html#bec_widgets.utils.bec_dispatcher.BECDispatcher).
Well modify the widget to inherit
from [`BECWidget`](https://bec.readthedocs.io/projects/bec-widgets/en/latest/api_reference/_autosummary/bec_widgets.utils.bec_widget.BECWidget.html#bec_widgets.utils.bec_widget.BECWidget),
pass the motor name to the widget, and use `get_bec_shortcuts` to access BEC services.
```python
from qtpy.QtCore import Slot
from qtpy.QtWidgets import QWidget, QLabel, QDoubleSpinBox, QPushButton, QVBoxLayout
from bec_lib.endpoints import MessageEndpoints
from bec_widgets.utils.bec_widget import BECWidget
class MotorControlWidget(BECWidget, QWidget):
def __init__(self, motor_name: str, parent=None, *args, **kwargs):
super().__init__(*args, **kwargs)
QWidget.__init__(self, parent)
self.motor_name = motor_name
# Initialize BEC shortcuts
self.get_bec_shortcuts()
# Initialize UI elements
self.label_top = QLabel(f"Current Position:", self)
self.label = QLabel(f"{self.motor_name} - N/A", self)
self.spin_box = QDoubleSpinBox(self)
self.spin_box.setRange(-10000, 10000)
self.spin_box.setDecimals(3)
self.spin_box.setSingleStep(0.1)
self.move_button = QPushButton("Move Motor", self)
# Set up the layout
layout = QVBoxLayout(self)
layout.addWidget(self.label_top)
layout.addWidget(self.label)
layout.addWidget(self.spin_box)
layout.addWidget(self.move_button)
self.setLayout(layout)
# Connect button click to move motor
self.move_button.clicked.connect(self.move_motor)
# Register BECDispatcher to listen for motor position updates
self.bec_dispatcher.connect_slot(
self.on_motor_update, MessageEndpoints.device_readback(self.motor_name)
)
@Slot()
def move_motor(self):
target_position = self.spin_box.value()
self.dev[self.motor_name].move(target_position)
print(f"Commanding motor {self.motor_name} to move to {target_position}")
@Slot(dict, dict)
def on_motor_update(self, msg_content, metadata):
position = msg_content.get("signals", {}).get(self.motor_name, {}).get("value", "N/A")
self.label.setText(f"{self.motor_name} : {round(position, 2)}")
```
## Step 3: Implementing RPC for Remote Control
Next, well set up an RPC interface to allow remote control of the widget from the command line via
the `BECIPythonClient`. Well expose a method that allows changing the motor name through CLI commands.
```python
from qtpy.QtCore import Slot
from qtpy.QtWidgets import QWidget, QLabel, QDoubleSpinBox, QPushButton, QVBoxLayout
from bec_lib.endpoints import MessageEndpoints
from bec_widgets.utils.bec_widget import BECWidget
class MotorControlWidget(BECWidget, QWidget):
USER_ACCESS = ["change_motor"]
def __init__(self, motor_name: str, parent=None, *args, **kwargs):
super().__init__(*args, **kwargs)
QWidget.__init__(self, parent)
self.motor_name = motor_name
# Initialize BEC shortcuts
self.get_bec_shortcuts()
# Initialize UI elements
self.label_top = QLabel(f"Current Position:", self)
self.label = QLabel(f"{self.motor_name} - N/A", self)
self.spin_box = QDoubleSpinBox(self)
self.spin_box.setRange(-10000, 10000)
self.spin_box.setDecimals(3)
self.spin_box.setSingleStep(0.1)
self.move_button = QPushButton("Move Motor", self)
# Set up the layout
layout = QVBoxLayout(self)
layout.addWidget(self.label_top)
layout.addWidget(self.label)
layout.addWidget(self.spin_box)
layout.addWidget(self.move_button)
self.setLayout(layout)
# Connect button click to move motor
self.move_button.clicked.connect(self.move_motor)
# Register BECDispatcher to listen for motor position updates
self.bec_dispatcher.connect_slot(
self.on_motor_update, MessageEndpoints.device_readback(self.motor_name)
)
@Slot()
def move_motor(self):
target_position = self.spin_box.value()
self.dev[self.motor_name].move(target_position)
print(f"Commanding motor {self.motor_name} to move to {target_position}")
@Slot(dict, dict)
def on_motor_update(self, msg_content, metadata):
position = msg_content.get("signals", {}).get(self.motor_name, {}).get("value", "N/A")
self.label.setText(f"{self.motor_name} : {round(position, 2)}")
def change_motor(self, motor_name):
"""RPC method to change the motor being controlled."""
# Disconnect from previous motor
self.bec_dispatcher.disconnect_slot(
self.on_motor_update, MessageEndpoints.device_readback(self.motor_name)
)
# Update motor name and reconnect to new motor
self.motor_name = motor_name
self.label.setText(f"{self.motor_name} - N/A")
self.bec_dispatcher.connect_slot(
self.on_motor_update, MessageEndpoints.device_readback(self.motor_name)
)
```
```{warning}
After implementing an RPC method, you must run the `cli/generate_cli.py` script to update the CLI commands for `BECIPythonClient`. This script generates the necessary command-line interface bindings, ensuring that your RPC method can be accessed and controlled remotely.
```
```{note}
In this tutorial, we used the @Slot decorator from QtCore to mark methods as slots for signals. This decorator ensures that the connected methods are treated as slots by the Qt framework, which can be connected to signals. Its a best practice to use the @Slot decorator to clearly indicate which methods are intended to handle signal events with correct argument signatures.
```
## Step 4: Running the Widget
Finally, lets create a script to run our widget within a `QApplication`. This script can be used to test the widget
independently. You can pass different motor names to control different motors using the same widget class.
```python
import sys
from qtpy.QtWidgets import QApplication
if __name__ == "__main__":
app = QApplication(sys.argv)
widget = MotorControlWidget(motor_name='samx')
widget.show()
sys.exit(app.exec_())
```
## Conclusion
In this tutorial, we've created a BEC-connected widget that allows you to control a motor. We started by designing the
UI, then connected it to the BEC system using
the [`BECWidget`](https://bec.readthedocs.io/projects/bec-widgets/en/latest/api_reference/_autosummary/bec_widgets.utils.bec_widget.BECWidget.html#bec_widgets.utils.bec_widget.BECWidget)
base class
and [`BECDispatcher`](https://bec.readthedocs.io/projects/bec-widgets/en/latest/api_reference/_autosummary/bec_widgets.utils.bec_dispatcher.BECDispatcher.html#bec_widgets.utils.bec_dispatcher.BECDispatcher).
We also implemented an RPC interface, allowing remote control of the widget through the CLI. Finally, we tested our
widget by running it in a `QApplication`.
This widget demonstrates a simplified version of the [`PositionerBox`](user.widgets.positioner_box), showcasing the
power and flexibility of
the [`BECWidget`](https://bec.readthedocs.io/projects/bec-widgets/en/latest/api_reference/_autosummary/bec_widgets.utils.bec_widget.BECWidget.html#bec_widgets.utils.bec_widget.BECWidget)
base class
and [`BECDispatcher`](https://bec.readthedocs.io/projects/bec-widgets/en/latest/api_reference/_autosummary/bec_widgets.utils.bec_dispatcher.BECDispatcher.html#bec_widgets.utils.bec_dispatcher.BECDispatcher),
making it easy to integrate with the BEC system and enabling robust, interactive control of devices directly from the
GUI or the command line.