From 29838e60d4d75314418d6ae54351d4aa21238fe9 Mon Sep 17 00:00:00 2001 From: Callum Forrester Date: Fri, 24 Jan 2025 15:35:50 +0000 Subject: [PATCH 1/3] Start on plans for controlling backlight --- pyproject.toml | 8 ++-- src/htss_rig_bluesky/plans/backlight.py | 47 ++++++++++++++++++++++++ tests/unit_tests/plans/test_backlight.py | 44 ++++++++++++++++++++++ 3 files changed, 95 insertions(+), 4 deletions(-) create mode 100644 src/htss_rig_bluesky/plans/backlight.py create mode 100644 tests/unit_tests/plans/test_backlight.py diff --git a/pyproject.toml b/pyproject.toml index 5aa10ee..75f963a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,7 +27,7 @@ dependencies = [ "pandablocks", "scipy", "tiled==0.1.0a91", - "ophyd_async>=0.3.4", + "ophyd_async>=0.9.0a2", ] dynamic = ["version"] license.file = "LICENSE" @@ -44,6 +44,7 @@ dev = [ "pydata-sphinx-theme>=0.12", "pytest", "pytest-cov", + "pytest-asyncio", "ruff", "sphinx-autobuild", "sphinx-copybutton", @@ -80,9 +81,8 @@ addopts = """ filterwarnings = "error" # Doctest python code in docs, python code in src docstrings, test functions in tests testpaths = "docs tests" -env= [ - "BEAMLINE=", -] +env = ["BEAMLINE="] +asyncio_mode = "auto" [tool.coverage.run] data_file = "/tmp/htss_rig_bluesky.coverage" diff --git a/src/htss_rig_bluesky/plans/backlight.py b/src/htss_rig_bluesky/plans/backlight.py new file mode 100644 index 0000000..f732de3 --- /dev/null +++ b/src/htss_rig_bluesky/plans/backlight.py @@ -0,0 +1,47 @@ +import bluesky.plan_stubs as bps +from bluesky.utils import MsgGenerator +from ophyd_async.fastcs.panda import HDFPanda + + +def set_backlight_intensity( + panda: HDFPanda, + intensity: float, + wait: bool = True, + group: str | None = None, +) -> MsgGenerator[None]: + if 0.0 <= intensity <= 1.0: + duty_cycle = int(round(intensity * 100.0)) + if duty_cycle == 0: + port = "ZERO" + elif duty_cycle == 100: + port = "ONE" + else: + port = yield from create_pwm(panda, duty_cycle, wait, group) + + yield from set_backlight_control_port(panda, port, wait, group) + else: + raise ValueError(f"Given intensity {intensity} should be between 0.0 and 1.0") + + +def set_backlight_control_port( + panda: HDFPanda, + port: str, + wait: bool = True, + group: str | None = None, +) -> MsgGenerator[None]: + ttl_out_2 = panda.ttlout[2] + yield from bps.abs_set( + ttl_out_2.val, + port, + wait=wait, + group=group, + ) + + +def create_pwm( + panda: HDFPanda, + duty_cycle: int, + wait: bool = True, + group: str | None = None, +) -> MsgGenerator[str]: + return NotImplemented diff --git a/tests/unit_tests/plans/test_backlight.py b/tests/unit_tests/plans/test_backlight.py new file mode 100644 index 0000000..a04fd9a --- /dev/null +++ b/tests/unit_tests/plans/test_backlight.py @@ -0,0 +1,44 @@ +import pytest +from bluesky import RunEngine +from dodal.beamlines import training_rig as training_rig +from ophyd_async.core import ( + Device, + DeviceVector, +) +from ophyd_async.epics.core import epics_signal_rw +from ophyd_async.fastcs.panda import HDFPanda +from ophyd_async.plan_stubs import ensure_connected + +from htss_rig_bluesky.plans.backlight import ( + set_backlight_control_port, +) + + +@pytest.fixture +def run_engine() -> RunEngine: + return RunEngine() + + +@pytest.fixture +async def mock_panda( + run_engine: RunEngine, +): + class TtlOutBlock(Device): + def __init__(self, name: str = ""): + self.val = epics_signal_rw(str, "VAL") + super().__init__(name) + + mock_panda = training_rig.panda() + mock_panda.phase_1_signal_units = epics_signal_rw(int, "") + mock_panda.ttlout = DeviceVector({i: TtlOutBlock() for i in range(1, 5)}) + + run_engine(ensure_connected(mock_panda, mock=True)) + yield mock_panda + + +async def test_set_control_port_sets_control_port( + run_engine: RunEngine, mock_panda: HDFPanda +): + assert (await mock_panda.ttlout[2].val.get_value()) == "" + run_engine(set_backlight_control_port(mock_panda, "FOO"), wait=True) + assert (await mock_panda.ttlout[2].val.get_value()) == "FOO" From 9f047bf6cb6b9a5f1080a8600f4dc46e16f8e14a Mon Sep 17 00:00:00 2001 From: Callum Forrester Date: Fri, 24 Jan 2025 15:39:46 +0000 Subject: [PATCH 2/3] More tests --- tests/unit_tests/plans/test_backlight.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/unit_tests/plans/test_backlight.py b/tests/unit_tests/plans/test_backlight.py index a04fd9a..dd24792 100644 --- a/tests/unit_tests/plans/test_backlight.py +++ b/tests/unit_tests/plans/test_backlight.py @@ -11,6 +11,7 @@ from htss_rig_bluesky.plans.backlight import ( set_backlight_control_port, + set_backlight_intensity, ) @@ -42,3 +43,14 @@ async def test_set_control_port_sets_control_port( assert (await mock_panda.ttlout[2].val.get_value()) == "" run_engine(set_backlight_control_port(mock_panda, "FOO"), wait=True) assert (await mock_panda.ttlout[2].val.get_value()) == "FOO" + + +@pytest.mark.parametrize("intensity,port_value", [(0.0, "ZERO"), (1.0, "ONE")]) +async def test_constant_port_value( + run_engine: RunEngine, + mock_panda: HDFPanda, + intensity: float, + port_value: str, +): + run_engine(set_backlight_intensity(mock_panda, intensity), wait=True) + assert (await mock_panda.ttlout[2].val.get_value()) == port_value From 336cdd70a752e3ced5ce24d5d1d3f2192d451e2f Mon Sep 17 00:00:00 2001 From: Callum Forrester Date: Thu, 6 Feb 2025 11:34:22 +0000 Subject: [PATCH 3/3] Create PWM loop to manage backlight --- src/htss_rig_bluesky/plans/backlight.py | 64 +++++++++++++++++++------ 1 file changed, 49 insertions(+), 15 deletions(-) diff --git a/src/htss_rig_bluesky/plans/backlight.py b/src/htss_rig_bluesky/plans/backlight.py index f732de3..cdff820 100644 --- a/src/htss_rig_bluesky/plans/backlight.py +++ b/src/htss_rig_bluesky/plans/backlight.py @@ -1,6 +1,6 @@ import bluesky.plan_stubs as bps from bluesky.utils import MsgGenerator -from ophyd_async.fastcs.panda import HDFPanda +from ophyd_async.fastcs.panda import HDFPanda, TimeUnits def set_backlight_intensity( @@ -10,38 +10,72 @@ def set_backlight_intensity( group: str | None = None, ) -> MsgGenerator[None]: if 0.0 <= intensity <= 1.0: - duty_cycle = int(round(intensity * 100.0)) + group = group or set_backlight_intensity.__name__ + duty_cycle = int(round(intensity * 100)) + if duty_cycle == 0: port = "ZERO" elif duty_cycle == 100: port = "ONE" else: - port = yield from create_pwm(panda, duty_cycle, wait, group) + port = yield from create_pwm(panda, intensity, wait=False, group=group) + + yield from set_backlight_control_port(panda, port, wait=False, group=group) - yield from set_backlight_control_port(panda, port, wait, group) + if wait: + yield from bps.wait(group=group) else: raise ValueError(f"Given intensity {intensity} should be between 0.0 and 1.0") +def create_pwm( + panda: HDFPanda, + duty_cycle: float, + period: float = 100.0, + wait: bool = True, + group: str | None = None, +) -> MsgGenerator[str]: + live_time = period * duty_cycle + dead_time = period - live_time + + clock = panda.clock[2] + pulse = panda.pulse[2] + + group = group or create_pwm.__name__ + + for signal, value in { + # Set units first + clock.period_units: TimeUnits.US, + pulse.delay_units: TimeUnits.US, + pulse.width_units: TimeUnits.US, + # Then set all other values + clock.enable: "ONE", + clock.enable_delay: 0.0, + clock.period: period, + pulse.enable: "ONE", + pulse.trig: "CLOCK2.OUT", + pulse.delay: dead_time, + pulse.width: live_time, + pulse.trig_edge: "Rising", + }.items(): + yield from bps.abs_set(signal, value, wait=False, group=group) + + if wait: + yield from bps.wait(group) + + return "PULSE2.OUT" + + def set_backlight_control_port( panda: HDFPanda, port: str, wait: bool = True, group: str | None = None, ) -> MsgGenerator[None]: - ttl_out_2 = panda.ttlout[2] + output_port = panda.ttlout[2] yield from bps.abs_set( - ttl_out_2.val, + output_port.val, port, wait=wait, group=group, ) - - -def create_pwm( - panda: HDFPanda, - duty_cycle: int, - wait: bool = True, - group: str | None = None, -) -> MsgGenerator[str]: - return NotImplemented