# Repository Guidelines — `ophyd_devices` `ophyd_devices` provides reusable ophyd hardware support and simulation devices for [BEC](https://github.com/bec-project/bec). Prefer focused changes, follow existing local patterns, and verify the smallest relevant test scope. This file is an agent-oriented operating manual. User-facing documentation lives at and is authored in the separate `bec_docs` repository. Treat `pyproject.toml` as the source of truth for dependencies, scripts, and tool configuration. ## Core Rules - Use `PSIDeviceBase` when a device needs BEC lifecycle or business logic. A device that merely exposes signals can use plain `ophyd.Device`, regardless of the communication backend. - Separate reusable device control from beamline-specific business logic in `on_*` hooks. - Prefer repository statuses, signals, and helpers when a BEC-aware counterpart exists. - Return promptly from device-server calls. Represent unfinished work with a status. - Implement safe interruption through `on_stop()` and register cancellable statuses. - Use `bec_signals.py` for BEC live data, progress, and file events. - Check the relevant device protocols when changing interfaces. - Add an example configuration for each new reusable device family. - Do not edit `ophyd_devices/devices/device_list.md`; CI generates it. - Follow existing local patterns before introducing a new abstraction. - Keep diffs focused and preserve unrelated local changes. - Add regression tests for bug fixes. - Do not commit, push, open a PR, or post a review unless explicitly requested. ## First Read Read the affected implementation and its tests. For unfamiliar areas, start here: - `ophyd_devices/interfaces/base_classes/psi_device_base.py` — device lifecycle and BEC integration - `ophyd_devices/interfaces/base_classes/psi_positioner_base.py` — motion - `ophyd_devices/interfaces/base_classes/psi_pseudo_device_base.py` — pseudo devices - `ophyd_devices/interfaces/base_classes/psi_pseudo_motor_base.py` — pseudo motors - `ophyd_devices/interfaces/protocols/bec_protocols.py` — expected device contracts - `ophyd_devices/utils/psi_device_base_utils.py` — statuses, tasks, and file helpers - `ophyd_devices/utils/bec_signals.py` — live data publishing - `tests/conftest.py` and `ophyd_devices/tests/utils.py` — reusable test helpers - `README.md` — project overview ## Repo Layout - `ophyd_devices/interfaces/` — base classes, protocols, and device configuration templates - `ophyd_devices/devices/` — concrete devices and the generated device list - `ophyd_devices/sim/` — simulation devices and data generators - `ophyd_devices/utils/` — shared signals, statuses, controllers, and other helpers - `ophyd_devices/configs/` — example device configurations - `tests/` — unit tests Related but separate repositories: - `bec` — core messaging, scans, services, and the client - `bec_widgets` — GUI widgets - `bec_docs` — published documentation - beamline plugin repositories — beamline-specific devices, scans, and widgets ## Local Overlay If `AGENTS_PERSONAL.md` exists beside this file, read it as an extension of these instructions. Its machine-specific environment and workflow guidance takes precedence over the corresponding sections here. Keep it local and untracked; do not copy its contents into committed files. ## Common Task Routing If you change: - `ophyd_devices/interfaces/base_classes/*`: inspect relevant protocols and affected concrete and simulation devices; test staging, subscriptions, movement, and stop behavior. - `ophyd_devices/utils/psi_device_base_utils.py`: check timeout handling and composition across status subclasses. - `ophyd_devices/utils/bec_signals.py`: check device-server consumers and report compatibility risks for `bec` and `bec_widgets`. - `ophyd_devices/sim/*`: check the affected device and tests consuming its scan data. - `ophyd_devices/devices/*` or vendor integrations: add targeted device tests; include an example configuration and validation notes for a new reusable device family. - `ophyd_devices/configs/*`: run `ophyd_test --config ` and inspect its report. - Documentation or templates: verify referenced paths, commands, examples, and metadata. A broad unit test run is unnecessary when executable behavior is unchanged. Reusable hardware support belongs here. Devices specific to one beamline belong in its plugin repository. Route core messaging, scans, and service changes to `bec`, GUI behavior to `bec_widgets`, and published documentation to `bec_docs`. ## Writing A Device ### Control and business logic The device's base control class defines how to communicate with the hardware: signal definitions, commands, protocol handling, and device state. Keep this layer reusable across beamlines and implement it in `ophyd_devices`. Plain ophyd control classes may be composed into or combined with a class that uses `PSIDeviceBase` when business logic is needed. A device that only exposes a collection of signals needs no business-logic layer; plain `ophyd.Device` is enough. For example, `SLSOperatorMessages` in `ophyd_devices/devices/sls_devices.py` groups operator messages and their dates using dynamic components without scan-specific behavior. This applies equally to EPICS and other communication backends; the need for business logic determines the base class, not the transport. The `on_*` hooks typically describe business logic: how a beamline uses that control interface during a scan. For example, the control class exposes acquisition and trigger-mode commands; a beamline's `on_stage()` chooses the trigger mode and acquisition settings for its experiment, and `on_trigger()` starts acquisition through the control interface. Implement bespoke hook behavior in a subclass in the beamline plugin repository. Keep shared lifecycle behavior generic, and make hooks call reusable control methods rather than duplicating PV definitions or communication code. This lets beamlines share the same hardware support while choosing their own acquisition and scan behavior. ### Base classes and hooks - When lifecycle or business logic is needed, use `PSIDeviceBase` to integrate the control class with BEC's lifecycle, scan information, statuses, and subscriptions. Do not add it solely to expose a collection of signals. - Use the lifecycle hooks provided by the base class rather than replacing its wrappers: `on_init`, `on_connected`, `on_stage`, `on_pre_scan`, `on_trigger`, `on_complete`, `on_kickoff`, `on_unstage`, `on_stop`, and `on_destroy`. - When implementing a device inheriting from `PSIDeviceBase`, copy the entire "Beamline Specific Implementations" section from `ophyd_devices/interfaces/base_classes/psi_device_base.py`, including its separator, all ten hooks, signatures, and docstrings. Keep the hooks together in that order, including unused hooks, so readers can quickly identify the device's business logic. - Put helper methods outside that section under a separate separator, for example: ```python ######################################## # Beamline Specific Implementations # ######################################## # All ten on_* hooks belong here. ######################################## # Helper Methods # ######################################## ``` - When replacing a hook in a subclass, preserve any required parent behavior with `super()`; copied hook stubs must not silently disable an inherited implementation. - Constructors and `on_init()` must not communicate with devices. Use `on_init()` only for local initialization; do not read hardware state or send commands there. - `on_connected()` is the first hook allowed to communicate with devices and send instructions, such as setting hardware defaults. The device server calls it once an enabled device has connected; plain instantiation, unit tests, and `ophyd_test` do not, so call it explicitly in tests that rely on it. It is not a per-scan hook. Use the scan lifecycle hooks for scan-specific instructions and access scan parameters through `self.scan_info.msg`. - Check the relevant protocols when adding or changing a device interface. Read the base-class implementation for hook return semantics; the protocol signatures alone do not describe them. ### Completion and interruption - Acquisition, staging, completion, and movement must return promptly to the device server. Represent unfinished work with a status; do not wait, sleep, or poll on the calling thread. - Import statuses from `ophyd_devices.utils.psi_device_base_utils`, including `DeviceStatus`, `MoveStatus`, and `StatusBase`. These provide BEC timeout diagnostics and status composition. Prefer repository helpers whenever a BEC-aware counterpart exists. - Hooks such as `on_stage()` and `on_complete()` may return a status or `None`. Return `None` only when there is no outstanding work. In particular, `complete()` converts `None` into an already-finished status; return a pending status while acquisition or file writing continues. - Register statuses that must fail on interruption with `self.cancel_on_stop(status)`. Implement hardware interruption in `on_stop()` and make repeated calls safe. Cancelling a status does not by itself stop hardware or a background task. - Preserve base-class stop and destroy behavior. Release subscriptions, threads, sockets, and other owned resources in `on_destroy()`; worker code must be able to exit on interruption. ### Signals and configuration - Set signal `kind` deliberately. `normal` and `hinted` signals appear in `read()`; `hinted` also selects default BEC scan readouts. `config` signals appear in `read_configuration()`; `omitted` signals appear in neither. Treat changes to kinds and names as data-interface changes. - Use the signals in `ophyd_devices/utils/bec_signals.py` for BEC live data, progress, and file events. Reuse their message formats instead of publishing custom Redis messages. - Add an example configuration under `ophyd_devices/configs/` for a new reusable device family. - Give each device class a docstring with a useful first-line description. CI uses it to generate `ophyd_devices/devices/device_list.md`; do not edit that generated file by hand. ### USER_ACCESS - Prefer methods over properties for functionality exposed through `USER_ACCESS`. - Give exposed methods verb-based names that describe the operation, such as `set_velocity()` or `get_velocity()`, rather than noun-only names such as `velocity()`. ## Validation Add regression tests for bug fixes. New device tests must cover instantiation, the relevant protocol, and safe `stop()` behavior. For asynchronous changes, also exercise completion, failures, and interruption while work is pending. Use mocked EPICS and sockets for unit tests. Reuse `get_mock_scan_info` from `ophyd_devices/tests/utils.py` and existing fixtures; use simulation devices when a working device is needed. Keep tests independent of execution order. Run the smallest relevant test target first, with `--random-order` when available: ```bash python -m pytest --random-order tests/test_psi_device_base.py ``` For a full unit test or coverage run: ```bash python -m pytest --random-order tests coverage run --source=./ophyd_devices --omit='*/ophyd_devices/tests/*' \ -m pytest --random-order tests coverage report ``` `ophyd_test` writes reports to `./device_test_reports` by default. Use `--connect` only when the user explicitly requests hardware validation and the target is reachable. Report whether device validation used mocks, simulation, or real hardware; include the model and firmware when known. ## Running BEC Locally Full service validation needs Redis, usually at `localhost:6379`, and an environment with BEC services installed. Unit tests normally use mocks and need no hardware. When service management is part of the requested validation, start services and open the client: ```bash bec-server start bec ``` Run the client in another shell. Restart the device server after changing code it has already loaded; otherwise a running session may use stale code. ## Style And Change Hygiene - Use Python 3.11-compatible syntax, four-space indentation, and a 100-character line limit. - Use f-strings and `pathlib`. - Type-annotate new public functions and methods; document public APIs. - Follow existing naming and docstring conventions. - Run Black and isort on changed Python files using `pyproject.toml` configuration. - Avoid formatting unrelated files and introducing new Pylint warnings. ## Development Environment Use an existing suitable environment when available. To create one, use Python 3.11 or newer: ```bash python -m venv .venv source .venv/bin/activate python -m pip install -e '.[dev]' python -c "import ophyd_devices; print(ophyd_devices.__file__)" ``` Verify imports resolve to the checkout under test. Separate worktrees need separate editable installations; do not repoint another task's environment. The `dev` extra includes `bec-server`. ## Platform Notes Keep code portable across macOS and Linux. Windows is unsupported and untested. ## Commit And PR Notes Branch from `main` for new work. Continue an existing PR on its current branch. The manual PR template lives at `.github/PULL_REQUEST_TEMPLATE/pull_request_template.md`. When writing a description: - Lead with the concrete problem and resulting behavior. For a bug fix, explain the trigger and before/after result. Describe the final change for a reviewer who has not followed the work. - Keep detail proportional to the change. Replace prompts and remove unused sections; avoid a file-by-file recap. Put lengthy examples in a `
` block and label before/after output. - Use `Closes #123` for resolved issues and `Related to #123` for partial work. Link companion PRs and `bec_docs` updates, including any required merge or deployment order. - Give exact test commands or manual steps and expected outcomes. Separate instructions for reviewers from checks already performed; report results and material limitations honestly. - Explain compatibility changes, affected consumers, defaults, migrations, and remaining limits. Include hardware/simulation validation and configuration-check results where applicable. - State whether documentation was updated or why it is unnecessary. Add design tradeoffs and follow-ups only when they help assess the change. Update the description when scope changes. Use Conventional Commit titles: `(): `. Allowed types are `build`, `chore`, `ci`, `docs`, `feat`, `fix`, `perf`, `refactor`, `style`, and `test`. Mark breaking changes with `!` or a `BREAKING CHANGE:` footer. For reviews, use `.github/pull_request_review_template.md`. Separate findings introduced or exposed by the change from inherited issues and optional suggestions. Identify the reviewed revision, give concrete evidence, and state validation limits. Return the review in chat unless posting to GitHub is explicitly requested.