Skip to content

Commit

Permalink
Merge pull request #436 from DiamondLightSource/435_create_webcam_device
Browse files Browse the repository at this point in the history
Create webcam device for robot load snapshots on i03
  • Loading branch information
DominicOram authored Apr 22, 2024
2 parents fed0040 + acad9cf commit cee1627
Show file tree
Hide file tree
Showing 5 changed files with 134 additions and 0 deletions.
3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ dependencies = [
"aioca", # Required for CA support with ophyd-async.
"p4p", # Required for PVA support with ophyd-async.
"numpy",
"aiofiles",
"aiohttp",
]

dynamic = ["version"]
Expand Down Expand Up @@ -54,6 +56,7 @@ dev = [
"types-requests",
"types-mock",
"types-PyYAML",
"types-aiofiles",
]

[project.urls]
Expand Down
17 changes: 17 additions & 0 deletions src/dodal/beamlines/i03.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
from dodal.devices.synchrotron import Synchrotron
from dodal.devices.undulator import Undulator
from dodal.devices.undulator_dcm import UndulatorDCM
from dodal.devices.webcam import Webcam
from dodal.devices.xbpm_feedback import XBPMFeedback
from dodal.devices.xspress3_mini.xspress3_mini import Xspress3Mini
from dodal.devices.zebra import Zebra
Expand Down Expand Up @@ -447,3 +448,19 @@ def robot(
wait_for_connection,
fake_with_ophyd_sim,
)


def webcam(
wait_for_connection: bool = True, fake_with_ophyd_sim: bool = False
) -> Webcam:
"""Get the i03 webcam, instantiate it if it hasn't already been.
If this is called when already instantiated in i03, it will return the existing object.
"""
return device_instantiation(
Webcam,
"webcam",
"",
wait_for_connection,
fake_with_ophyd_sim,
url="http://i03-webcam1/axis-cgi/jpg/image.cgi",
)
34 changes: 34 additions & 0 deletions src/dodal/devices/webcam.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
from pathlib import Path

import aiofiles
from aiohttp import ClientSession
from bluesky.protocols import Triggerable
from ophyd_async.core import AsyncStatus, StandardReadable

from dodal.devices.ophyd_async_utils import create_soft_signal_rw
from dodal.log import LOGGER


class Webcam(StandardReadable, Triggerable):
def __init__(self, name, prefix, url):
self.url = url
self.filename = create_soft_signal_rw(str, "filename", "webcam")
self.directory = create_soft_signal_rw(str, "directory", "webcam")
self.last_saved_path = create_soft_signal_rw(str, "last_saved_path", "webcam")

self.set_readable_signals([self.last_saved_path])
super().__init__(name=name)

@AsyncStatus.wrap
async def trigger(self) -> None:
filename = await self.filename.get_value()
directory = await self.directory.get_value()

async with ClientSession() as session:
async with session.get(self.url) as response:
response.raise_for_status()
path = Path(f"{directory}/{filename}.png").as_posix()
LOGGER.info(f"Saving webcam image from {self.url} to {path}")
async with aiofiles.open(path, "wb") as file:
await file.write((await response.read()))
await self.last_saved_path.set(path)
79 changes: 79 additions & 0 deletions tests/devices/unit_tests/test_webcam.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
from unittest.mock import AsyncMock, MagicMock, patch

import pytest
from bluesky.run_engine import RunEngine

from dodal.beamlines import i03
from dodal.devices.webcam import Webcam


@pytest.fixture
def webcam() -> Webcam:
RunEngine()
return i03.webcam(fake_with_ophyd_sim=True)


async def test_given_last_saved_path_when_device_read_then_returns_path(webcam: Webcam):
await webcam.last_saved_path.set("test")
read = await webcam.read()
assert read["webcam-last_saved_path"]["value"] == "test"


@pytest.mark.parametrize(
"directory, filename, expected_path",
[
("/tmp", "test", "/tmp/test.png"),
("/tmp/", "other", "/tmp/other.png"),
],
)
@patch("dodal.devices.webcam.aiofiles", autospec=True)
@patch("dodal.devices.webcam.ClientSession.get", autospec=True)
async def test_given_filename_and_directory_when_trigger_and_read_then_returns_expected_path(
mock_get: MagicMock,
mock_aiofiles,
directory,
filename,
expected_path,
webcam: Webcam,
):
mock_get.return_value.__aenter__.return_value = AsyncMock()
await webcam.filename.set(filename)
await webcam.directory.set(directory)
await webcam.trigger()
read = await webcam.read()
assert read["webcam-last_saved_path"]["value"] == expected_path


@patch("dodal.devices.webcam.aiofiles", autospec=True)
@patch("dodal.devices.webcam.ClientSession.get", autospec=True)
async def test_given_data_returned_from_url_when_trigger_then_data_written(
mock_get: MagicMock, mock_aiofiles, webcam: Webcam
):
mock_get.return_value.__aenter__.return_value = (mock_response := AsyncMock())
mock_response.read.return_value = (test_web_data := "TEST")
mock_open = mock_aiofiles.open
mock_open.return_value.__aenter__.return_value = (mock_file := AsyncMock())
await webcam.filename.set("file")
await webcam.directory.set("/tmp")
await webcam.trigger()
mock_open.assert_called_once_with("/tmp/file.png", "wb")
mock_file.write.assert_called_once_with(test_web_data)


@patch("dodal.devices.webcam.aiofiles", autospec=True)
@patch("dodal.devices.webcam.ClientSession.get", autospec=True)
async def test_given_response_throws_exception_when_trigger_then_exception_rasied(
mock_get: MagicMock, mock_aiofiles, webcam: Webcam
):
class MyException(Exception):
pass

def _raise():
raise MyException()

mock_get.return_value.__aenter__.return_value = (mock_response := AsyncMock())
mock_response.raise_for_status = _raise
await webcam.filename.set("file")
await webcam.directory.set("/tmp")
with pytest.raises(MyException):
await webcam.trigger()
1 change: 1 addition & 0 deletions tests/plans/test_topup_plan.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

@pytest.fixture
def synchrotron() -> Synchrotron:
RunEngine()
return i03.synchrotron(fake_with_ophyd_sim=True)


Expand Down

0 comments on commit cee1627

Please sign in to comment.