Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Codex Instructions

## Scope

These instructions apply to the whole `bec_docs` workspace.

## Project Overview

- This repository contains the BEC documentation site.
- It combines the documentation for three repositories:
- `bec` (the main BEC codebase): https://github.com/bec-project/bec
- `ophyd_devices` (ophyd device components for BEC): https://github.com/bec-project/ophyd_devices
- `bec_widgets` (BEC-specific widgets for the UI): https://github.com/bec-project/bec_widgets
- Main documentation sources live under `docs/`.
- Navigation is configured in `zensical.toml`.
- The site is built with Zensical and follows a diataxis structure.

## Editing Guidelines

- Follow the diataxis structure when adding or editing pages:
- Getting Started: high-level tutorials for new users, focused on teaching concepts and workflows without overwhelming details and lots of explanations.
- How-To: focused guides for specific tasks. Should be concise and practical, with just enough explanation to understand the steps. Reference learning pages for deeper dives into concepts. A user coming back to the how-to page should not be bombarded with information that is not directly relevant to the task at hand.
- Learning: in-depth explanations of concepts and features
- Reference: API documentation and technical details, treated as a lookup resource rather than a narrative
- Prefer small, focused documentation edits that preserve the existing structure and tone.
- Match the wording and formatting already used in nearby pages.
- When adding tables that need fixed column widths, prefer inline HTML tables over Markdown tables.
- Keep links relative and consistent with the surrounding docs.
- Do not remove or overwrite user changes outside the requested scope.
- If you need to make a larger structural change, please discuss it first before implementing.

## Admonitions
- Getting-started / tutorial pages must start with a info admonition "Goal" that clearly states the learning outcomes for the page and end with a success admonition "What you have learned" that summarizes the key takeaways.
- How-to pages must start with an info admonition "Overview" that explains the purpose of the page and end with a success admonition "Congratulations!" that celebrates the completion of the task.
- Learning pages must provide a `!!! info "What to remember"` for end-of-page takeaways.

## Content Conventions

- Use sentence case in prose and keep headings consistent with neighboring pages.
- Prefer short examples that reflect real BEC usage.
- When appropriate, add related links to other documentation pages at the top of the page in a `related` section.

## Validation

- If you change navigation-relevant docs, check whether `zensical.toml` also needs an update.
- If you delete or merge pages, update internal links so no stale references remain.
- When practical, verify links and references with fast text searches before finishing.
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
---
related:
- title: Custom ophyd devices
url: ../../../learn/devices/custom-ophyd-devices.md
- title: Write a custom ophyd device
url: write-a-custom-device.md
- title: BEC signals for custom devices
url: ../../../learn/devices/bec-signals.md
---

# Use status objects in a custom ophyd device

!!! Info "Overview"
Use `CompareStatus` and `TransitionStatus` in custom ophyd device hooks when the device action starts immediately but completes only after a signal reaches the expected state.

## Prerequisites

- You already have a custom device class based on `PSIDeviceBase`.
- Your device exposes signal values that indicate readiness, acquisition, or completion.
- You know whether success is best described as one target value or as a sequence of transitions.

!!! learn "[Learn when custom devices should return status objects](../../../learn/devices/custom-ophyd-devices.md#status-objects-and-asynchronous-work){ data-preview }"

## Use `CompareStatus` in `on_pre_scan()`

`CompareStatus` is a good fit when pre-scan preparation should complete as soon as one signal reaches one target value.

For example, assume a detector has:

- an `arm` signal that starts preparation
- a `ready` signal that becomes `1` once arming is complete
- a `state` signal that may go to `"error"` if arming fails

In that case, `on_pre_scan()` can start arming and return a `CompareStatus`:

```py
from ophyd_devices.utils.psi_device_base_utils import CompareStatus


def on_pre_scan(self):
self.arm.put(1)
status = CompareStatus(
signal=self.ready,
value=1,
operation_success="==",
timeout=5,
)
self.cancel_on_stop(status)
return status
```

If the device has an explicit error state, include that as a failure value:

```py
def on_pre_scan(self):
self.arm.put(1)
status = CompareStatus(
signal=self.state,
value="armed",
operation_success="==",
failure_value=["error", "fault"],
timeout=5,
)
self.cancel_on_stop(status)
return status
```

Use this pattern when one value is enough to decide whether the device is ready.

## Use `TransitionStatus` in `on_complete()`

`TransitionStatus` is a better fit when completion is expressed as a sequence of states.

A common detector pattern is:

- `acquire = 1` while acquisition is running
- `acquire = 0` again once acquisition is complete

If the signal should explicitly transition back to idle, `on_complete()` can return:

```py
from ophyd_devices.utils.psi_device_base_utils import TransitionStatus


def on_complete(self):
status = TransitionStatus(
signal=self.acquire,
transitions=[1, 0],
strict=True,
timeout=10,
)
self.cancel_on_stop(status)
return status
```

This tells BEC to wait until acquisition is first observed as active and then observed as idle again.

If the controller may pass through an error code, fail early with `failure_states`:

```py
def on_complete(self):
status = TransitionStatus(
signal=self.acquire,
transitions=[1, 0],
strict=True,
failure_states=[-1],
timeout=10,
)
self.cancel_on_stop(status)
return status
```

Use this pattern when completion depends on a state transition rather than one static value.

## Choose the right helper

- Use `CompareStatus` when success means “the signal reached this value”.
- Use `TransitionStatus` when success means “the signal moved through these values in order”.

## Common pitfalls

- Returning `None` even though the device is still changing state asynchronously.
- Forgetting `cancel_on_stop(status)`, which can leave BEC waiting on a status after an abort.
- Using `CompareStatus` when the real requirement is to observe a full transition sequence.
- Using `TransitionStatus` with an incomplete transition list, for example waiting only for `0` when the real completion pattern is `1 -> 0`.

!!! success "Congratulations!"
You have used status objects to describe asynchronous device behavior in a way that BEC can wait for reliably during scans.
175 changes: 175 additions & 0 deletions docs/how-to/devices/development/write-a-custom-device.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
---
related:
- title: Custom ophyd devices in BEC
url: ../../../learn/devices/custom-ophyd-devices.md
- title: Introduction to ophyd
url: ../../../learn/devices/introduction-to-ophyd.md
- title: BEC signals for custom devices
url: ../../../learn/devices/bec-signals.md
- title: Device config in BEC
url: ../../../learn/devices/device-config-in-bec.md
---

# Write a custom ophyd device

!!! Info "Overview"
Create a reusable ophyd device for BEC by subclassing `PSIDeviceBase`, declaring the device signals as ophyd components, implementing the scan hooks you need, and exposing the class in your device config.

## Prerequisites

- You have a beamline plugin repository or a local checkout of `ophyd_devices`.
- You are comfortable editing Python classes and reloading the BEC device config.
- You already know the control-system endpoints you need, for example EPICS PV suffixes.

!!! learn "[Learn how custom ophyd devices fit into BEC](../../../learn/devices/custom-ophyd-devices.md){ data-preview }"

## 1. Create the device class

In BEC, custom devices should usually inherit from `ophyd_devices.PSIDeviceBase`. It wraps the normal ophyd `Device` lifecycle with BEC-specific hooks such as `on_connected()`, `on_stage()`, `on_pre_scan()`, `on_trigger()`, and `on_stop()`.

Create a new module in your plugin, for example `<bec_plugin>/devices/beam_stop_shutter.py`:

```py
from ophyd import Component as Cpt
from ophyd import EpicsSignal, EpicsSignalRO, StatusBase

from ophyd_devices import PSIDeviceBase


class BeamStopShutter(PSIDeviceBase):
"""Simple shutter device that can be prepared automatically for scans."""

open_cmd = Cpt(EpicsSignal, "OPEN", kind="omitted")
close_cmd = Cpt(EpicsSignal, "CLOSE", kind="omitted")
state = Cpt(EpicsSignalRO, "STATE", kind="hinted")
ready = Cpt(EpicsSignalRO, "READY", kind="normal")
config = Cpt(EpicsSignalRO, "CONFIG", kind="config")

def __init__(self, prefix: str, name: str, ready_timeout: float = 5.0, **kwargs) -> None:
super().__init__(prefix=prefix, name=name, **kwargs)
self._ready_timeout = ready_timeout
```

This keeps the device reusable: the class defines the structure once, and each BEC config entry supplies the concrete `prefix` and optional configuration values.

## 2. Set defaults in `on_connected()`

`on_connected()` is called by the BEC device manager after the device and its signals are connected. Use it for default setup that depends on live signals.

```py
class BeamStopShutter(PSIDeviceBase):
...

def on_connected(self) -> None:
self.config.set(0).wait(timeout=3)

```

This is a good place to:

- register callbacks
- apply beamline-specific default values

## 3. Implement the scan hooks you need

You do not need to override every hook. Only implement the ones that match the device behavior you want in BEC.

For a shutter that must be opened automatically before the first trigger, `on_pre_scan()` is often enough:

```py

from ophyd_devices import CompareStatus

class BeamStopShutter(PSIDeviceBase):
...

def on_pre_scan(self) -> StatusBase:
status = CompareStatus(self.state, "open", timeout=self._ready_timeout)
self.open_cmd.put(1)
self.cancel_on_stop(status)
return status

def on_stop(self) -> None:
self.close_cmd.put(1)

def on_unstage(self) -> None:
self.close_cmd.put(1)
```

Important details:

- Return a status object from `on_pre_scan()` that resolves when the shutter is open and ready. That way, BEC will wait for the shutter to be ready before starting the scan.
- Register long-running statuses with `cancel_on_stop(...)` so BEC can fail them cleanly when a scan is interrupted.

!!! related "[How to use CompareStatus in a hook](../../../how-to/devices/development/use-status-objects-in-a-custom-device.md){ data-preview }"

## 4. Use `scan_info` when scan context matters

`PSIDeviceBase` keeps the current scan metadata on `self.scan_info`. That lets your device adjust its behavior to the active scan.

For example:

```py
def on_stage(self):
exp_time = self.scan_info.msg.scan_parameters["exp_time"]
self.exposure_time.put(exp_time)
```

This is useful when the device needs information such as exposure time, number of frames, scan name, or other request parameters before acquisition starts.

## 5. Export the class

Expose the new class in the `__init__.py` of the devices module, this makes it easier to reference in the BEC config:

```py
from .beam_stop_shutter import BeamStopShutter
```

## 6. Add the device to the BEC config

Reference the class in your device config and pass constructor arguments through `deviceConfig`:

```yaml
beamstop:
readoutPriority: baseline
description: Beam stop shutter
deviceClass: <bec_plugin>.devices.BeamStopShutter
deviceConfig:
prefix: "X01DA-FE-OPEN:"
ready_timeout: 5.0
deviceTags:
- shutter
enabled: true
readOnly: false
softwareTrigger: false
```

The keys inside `deviceConfig` must match the constructor signature of your device class.

!!! learn "[Learn more about device config in BEC](../../../learn/devices/device-config-in-bec.md){ data-preview }"

## 7. Reload and verify the device

Reload the plugin repository and the YAML config and verify that:

- the device appears in the client as `dev.beamstop`
- `dev.beamstop.read()` returns the signals you expect (`state` and `ready` in this example)
- `dev.beamstop.read_configuration()` returns the config signals you expect (`config` in this example)
- a scan that uses the device reaches `on_pre_scan()` and opens the shutter
- stopping the scan closes the shutter and cancels any outstanding status objects

## Common pitfalls

- Initializing live signal defaults in `__init__()` instead of `on_connected()`.
- Returning `None` from an asynchronous hook even though the device still has work to finish.
- Forgetting `cancel_on_stop(status)` for long-running `pre_scan`, `trigger`, or `kickoff` logic.
- Polling without `check_stopped=True`, which can leave the device hanging after a scan stop.
- Forgetting to export the class from the package used by `deviceClass`.

## Next steps

<!-- TODO- Add tests with `patched_device(...)` from `ophyd_devices.tests.utils` so you can exercise the device without a real IOC. -->
- If the device streams files, previews, or async data, use the BEC-specific signal classes documented in [BEC signals for custom devices](../../../learn/devices/bec-signals.md).

!!! success "Congratulations!"
You have written a custom ophyd device for BEC, connected it to the BEC lifecycle, and exposed it through the device config so it can participate in scans like any other device.
Loading
Loading