Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Finish setting up an ophyd_async OAV #857

Open
wants to merge 58 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
b2d7b1b
Set up skeleton of async mjpg
noemifrisina Oct 16, 2024
bf59cce
Start filling things in
noemifrisina Oct 16, 2024
579bac1
Try using completed_status
noemifrisina Oct 16, 2024
67e774c
Add SnapshotWithBeamCentre
noemifrisina Oct 16, 2024
dd0e707
Add SnapshotWithBeamCentre
noemifrisina Oct 16, 2024
2a1ed7f
A first try at adding snapshot to oav
noemifrisina Oct 16, 2024
75e83b5
Rename
noemifrisina Oct 17, 2024
e47699b
Start adding async grid overlay
noemifrisina Oct 17, 2024
6d28b69
Actually add file
noemifrisina Oct 17, 2024
866addb
Start adding a test - might be deleted
noemifrisina Oct 17, 2024
07ffa05
Move mjpg to plugins and get trigger working with async
noemifrisina Oct 17, 2024
4d9c485
Remove microns per pixels from mjpg
noemifrisina Oct 17, 2024
76eecae
Pass an instance of soft signal beam_centre and fix imports
noemifrisina Oct 17, 2024
3553bfc
Fix some typing and signals
noemifrisina Oct 17, 2024
a15765c
A bunch of fixmes
noemifrisina Oct 18, 2024
a0092a3
A bunch of fixmes with test
noemifrisina Oct 18, 2024
b5f6e7c
Improving file saving to make async and looking for the problem
noemifrisina Oct 18, 2024
233abc9
Merge branch 'main' into 824_mjpg-async-device
noemifrisina Oct 21, 2024
51d45f7
A passing test
noemifrisina Oct 21, 2024
46c4a33
get test to pass
noemifrisina Oct 21, 2024
f6af967
Tidy up and fix tests
noemifrisina Oct 21, 2024
d321c16
Add a test for snapshot with grid
noemifrisina Oct 21, 2024
d316812
Merge branch 'main' into 824_mjpg-async-device
noemifrisina Oct 21, 2024
13edeca
Fix oav
noemifrisina Oct 21, 2024
126f687
Start breaking things: remove old OAV
noemifrisina Oct 21, 2024
25166a4
Rename oav async file
noemifrisina Oct 21, 2024
a5b800b
Remove old MJPG code and move around some files
noemifrisina Oct 21, 2024
83429ac
Rename
noemifrisina Oct 21, 2024
c6a518b
Instantiate async oav on beamlines
noemifrisina Oct 21, 2024
3294404
Tidy up some tests
noemifrisina Oct 21, 2024
1899d5d
Add commented out test
noemifrisina Oct 21, 2024
35d4af7
Add a sort of similar test for utils
noemifrisina Oct 23, 2024
e0546c4
Remove some prints
noemifrisina Oct 23, 2024
84a7098
Use async oav in system test
noemifrisina Oct 23, 2024
0f44f68
Remove some old tests and add to new ones
noemifrisina Oct 23, 2024
a27afb9
Remove tests for old oav
noemifrisina Oct 23, 2024
dab5697
Remove old OAVConfigParams
noemifrisina Oct 23, 2024
dc3650b
Fix import
noemifrisina Oct 23, 2024
fe599ed
Hopefully fix system test
noemifrisina Oct 23, 2024
caef818
Fix more imports
noemifrisina Oct 23, 2024
acc1bd0
Add small missing test
noemifrisina Oct 23, 2024
c7b78cd
Fix snapshot prefixes
noemifrisina Oct 23, 2024
1b03b35
Fix snapshot prefixes - part 2
noemifrisina Oct 23, 2024
5358486
Merge branch 'main' into 824_mjpg-async-device
noemifrisina Oct 23, 2024
dcaa8fd
Add small test
noemifrisina Oct 23, 2024
b4ec7a2
Try to make codecov happy
noemifrisina Oct 23, 2024
fe259d7
Fix test
noemifrisina Oct 23, 2024
eb32c5a
Add a small cam plugin for oav since we use it
noemifrisina Oct 23, 2024
5a03213
Try to fix test
noemifrisina Oct 23, 2024
6541cdd
Add test for parameters
noemifrisina Oct 24, 2024
9f13205
Add array sizes
noemifrisina Oct 24, 2024
df612d8
Merge branch 'main' into 824_mjpg-async-device
noemifrisina Oct 24, 2024
b2d0dbe
Add check for zoom level and test
noemifrisina Oct 24, 2024
9c8a3cc
Fix linting
noemifrisina Oct 24, 2024
c0914a6
Really fix linting
noemifrisina Oct 24, 2024
bffbd29
Merge branch 'main' into 824_mjpg-async-device
noemifrisina Oct 24, 2024
6f19281
Fix docstring
noemifrisina Oct 24, 2024
f30a519
Change i04 oav instatiation
noemifrisina Oct 25, 2024
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
7 changes: 4 additions & 3 deletions src/dodal/beamlines/i03.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@
from dodal.devices.flux import Flux
from dodal.devices.focusing_mirror import FocusingMirrorWithStripes, MirrorVoltages
from dodal.devices.motors import XYZPositioner
from dodal.devices.oav.oav_detector import OAV, OAVConfigParams
from dodal.devices.oav.oav_detector import OAV
from dodal.devices.oav.oav_parameters import OAVConfig
from dodal.devices.oav.pin_image_recognition import PinTipDetection
from dodal.devices.qbpm import QBPM
from dodal.devices.robot import BartRobot
Expand Down Expand Up @@ -227,7 +228,7 @@ def panda_fast_grid_scan(
def oav(
wait_for_connection: bool = True,
fake_with_ophyd_sim: bool = False,
params: OAVConfigParams | None = None,
params: OAVConfig | None = None,
) -> OAV:
"""Get the i03 OAV device, instantiate it if it hasn't already been.
If this is called when already instantiated in i03, it will return the existing object.
Expand All @@ -238,7 +239,7 @@ def oav(
"",
wait_for_connection,
fake_with_ophyd_sim,
params=params or OAVConfigParams(ZOOM_PARAMS_FILE, DISPLAY_CONFIG),
config=params or OAVConfig(ZOOM_PARAMS_FILE, DISPLAY_CONFIG),
)


Expand Down
11 changes: 8 additions & 3 deletions src/dodal/beamlines/i04.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@
from dodal.devices.i04.transfocator import Transfocator
from dodal.devices.ipin import IPin
from dodal.devices.motors import XYZPositioner
from dodal.devices.oav.oav_detector import OAV, OAVConfigParams
from dodal.devices.oav.oav_detector import OAV
from dodal.devices.oav.oav_parameters import OAVConfig
from dodal.devices.oav.oav_to_redis_forwarder import OAVToRedisForwarder
from dodal.devices.robot import BartRobot
from dodal.devices.s4_slit_gaps import S4SlitGaps
Expand Down Expand Up @@ -344,7 +345,11 @@ def zebra(wait_for_connection: bool = True, fake_with_ophyd_sim: bool = False) -


@skip_device(lambda: BL == "s04")
def oav(wait_for_connection: bool = True, fake_with_ophyd_sim: bool = False) -> OAV:
def oav(
wait_for_connection: bool = True,
fake_with_ophyd_sim: bool = False,
params: OAVConfig | None = None,
) -> OAV:
"""Get the i04 OAV device, instantiate it if it hasn't already been.
If this is called when already instantiated in i04, it will return the existing object.
"""
Expand All @@ -354,7 +359,7 @@ def oav(wait_for_connection: bool = True, fake_with_ophyd_sim: bool = False) ->
"",
wait_for_connection,
fake_with_ophyd_sim,
params=OAVConfigParams(ZOOM_PARAMS_FILE, DISPLAY_CONFIG),
config=params or OAVConfig(ZOOM_PARAMS_FILE, DISPLAY_CONFIG),
)


Expand Down
5 changes: 3 additions & 2 deletions src/dodal/beamlines/i04_1.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
from dodal.devices.backlight import Backlight
from dodal.devices.detector import DetectorParams
from dodal.devices.eiger import EigerDetector
from dodal.devices.oav.oav_detector import OAV, OAVConfigParams
from dodal.devices.oav.oav_detector import OAV
from dodal.devices.oav.oav_parameters import OAVConfig
from dodal.devices.s4_slit_gaps import S4SlitGaps
from dodal.devices.synchrotron import Synchrotron
from dodal.devices.undulator import Undulator
Expand Down Expand Up @@ -75,7 +76,7 @@ def oav(wait_for_connection: bool = True, fake_with_ophyd_sim: bool = False) ->
"",
wait_for_connection,
fake_with_ophyd_sim,
params=OAVConfigParams(ZOOM_PARAMS_FILE, DISPLAY_CONFIG),
config=OAVConfig(ZOOM_PARAMS_FILE, DISPLAY_CONFIG),
)


Expand Down
2 changes: 1 addition & 1 deletion src/dodal/beamlines/i24.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from dodal.devices.i24.i24_detector_motion import DetectorMotion
from dodal.devices.i24.i24_vgonio import VGonio
from dodal.devices.i24.pmac import PMAC
from dodal.devices.oav.oav_async import OAV
from dodal.devices.oav.oav_detector import OAV
from dodal.devices.oav.oav_parameters import OAVConfig
from dodal.devices.zebra import Zebra
from dodal.log import set_beamline as set_log_beamline
Expand Down
31 changes: 31 additions & 0 deletions src/dodal/devices/areadetector/plugins/CAM.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
from enum import IntEnum

from ophyd_async.core import StandardReadable
from ophyd_async.epics.signal import epics_signal_r, epics_signal_rw


class ColorMode(IntEnum):
"""
Enum to store the various color modes of the camera. We use RGB1.
"""

MONO = 0
BAYER = 1
RGB1 = 2
RGB2 = 3
RGB3 = 4
YUV444 = 5
YUV422 = 6
YUV421 = 7


class Cam(StandardReadable):
def __init__(self, prefix: str, name: str = "") -> None:
self.color_mode = epics_signal_rw(ColorMode, "GC_BalRatioSelector")
self.acquire_period = epics_signal_rw(float, "AcquirePeriod")
self.acquire_time = epics_signal_rw(float, "AcquireTime")
self.gain = epics_signal_rw(float, "Gain")

self.array_size_x = epics_signal_r(int, "ArraySizeX_RBV")
self.array_size_y = epics_signal_r(int, "ArraySizeY_RBV")
Comment on lines +24 to +30
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Must: these aren't connecting in dodal connect i03 as they need the PV prefix

super().__init__(name)
160 changes: 55 additions & 105 deletions src/dodal/devices/areadetector/plugins/MJPG.py
Original file line number Diff line number Diff line change
@@ -1,138 +1,88 @@
import os
import threading
from abc import ABC, abstractmethod
from io import BytesIO
from pathlib import Path

import requests
from ophyd import Component, Device, DeviceStatus, EpicsSignal, EpicsSignalRO, Signal
from PIL import Image, ImageDraw
import aiofiles
from aiohttp import ClientConnectionError, ClientSession
from bluesky.protocols import Triggerable
from ophyd_async.core import AsyncStatus, StandardReadable, soft_signal_rw
from ophyd_async.epics.signal import epics_signal_r, epics_signal_rw
from PIL import Image

from dodal.devices.oav.oav_parameters import OAVConfigParams
from dodal.log import LOGGER


class MJPG(Device, ABC):
class MJPG(StandardReadable, Triggerable, ABC):
"""The MJPG areadetector plugin creates an MJPG video stream of the camera's output.

This devices uses that stream to grab images. When it is triggered it will send the
latest image from the stream to the `post_processing` method for child classes to handle.
"""

filename = Component(Signal)
directory = Component(Signal)
last_saved_path = Component(Signal)
url = Component(EpicsSignal, "JPG_URL_RBV", string=True)
x_size = Component(EpicsSignalRO, "ArraySize1_RBV")
y_size = Component(EpicsSignalRO, "ArraySize2_RBV")
input_rbpv = Component(EpicsSignalRO, "NDArrayPort_RBV")
input_plugin = Component(EpicsSignal, "NDArrayPort")
def __init__(self, prefix: str, name: str = "") -> None:
self.filename = soft_signal_rw(str)
self.directory = soft_signal_rw(str)
self.last_saved_path = soft_signal_rw(str)

# scaling factors for the snapshot at the time it was triggered
microns_per_pixel_x = Component(Signal)
microns_per_pixel_y = Component(Signal)
self.url = epics_signal_rw(str, prefix + "JPG_URL_RBV")

oav_params: OAVConfigParams | None = None
self.x_size = epics_signal_r(int, prefix + "ArraySize1_RBV")
self.y_size = epics_signal_r(int, prefix + "ArraySize2_RBV")

Comment on lines +29 to +30
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Must: these aren't connecting in dodal connect i03 as you're missing a colon

KICKOFF_TIMEOUT: float = 30.0
# TODO check type of these two
self.input_rbpv = epics_signal_r(str, prefix + "NDArrayPort_RBV")
self.input_plugin = epics_signal_rw(str, prefix + "NDArrayPort")

Comment on lines +32 to +34
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should: These two PVs aren't used anyway, lets just remove them

def _save_image(self, image: Image.Image):
"""A helper function to save a given image to the path supplied by the directory
and filename signals. The full resultant path is put on the last_saved_path signal
self.KICKOFF_TIMEOUT = 30.0

super().__init__(name)

async def _save_image(self, image: Image.Image):
"""A helper function to save a given image to the path supplied by the \
directory and filename signals. The full resultant path is put on the \
last_saved_path signal
"""
filename_str = self.filename.get()
directory_str: str = self.directory.get() # type: ignore
filename_str = await self.filename.get_value()
directory_str = await self.directory.get_value()

path = Path(f"{directory_str}/{filename_str}.png").as_posix()
if not os.path.isdir(Path(directory_str)):
if not Path(directory_str).is_dir():
LOGGER.info(f"Snapshot folder {directory_str} does not exist, creating...")
os.mkdir(directory_str)
Path(directory_str).mkdir(parents=True)

LOGGER.info(f"Saving image to {path}")
image.save(path)
self.last_saved_path.put(path)

def trigger(self):
buffer = BytesIO()
image.save(buffer, format="png")
async with aiofiles.open(path, "wb") as fh:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could: We use png here and in the path above. Can we pull it into a const?

await fh.write(buffer.getbuffer())
await self.last_saved_path.set(path, wait=True)

@AsyncStatus.wrap
async def trigger(self):
"""This takes a snapshot image from the MJPG stream and send it to the
post_processing method, expected to be implemented by a child of this class.

It is the responsibility of the child class to save any resulting images.
"""
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: by calling _save_image

st = DeviceStatus(device=self, timeout=self.KICKOFF_TIMEOUT)
url_str = self.url.get()

assert isinstance(
self.oav_params, OAVConfigParams
), "MJPG does not have valid OAV parameters"
self.microns_per_pixel_x.set(self.oav_params.micronsPerXPixel)
self.microns_per_pixel_y.set(self.oav_params.micronsPerYPixel)

def get_snapshot():
try:
response = requests.get(url_str, stream=True)
response.raise_for_status()
with Image.open(BytesIO(response.content)) as image:
self.post_processing(image)
st.set_finished()
except requests.HTTPError as e:
st.set_exception(e)

threading.Thread(target=get_snapshot, daemon=True).start()

return st
url_str = await self.url.get_value()

async with ClientSession() as session:
async with session.get(url_str) as response:
if not response.ok:
LOGGER.error(
f"OAV responded with {response.status}: {response.reason}."
)
raise ClientConnectionError(
f"OAV responded with {response.status}: {response.reason}."
)
try:
Comment on lines +72 to +78
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should: I think we can just do response.raise_for_status() or actually ClientSession(raise_for_status=True)

data = await response.read()
with Image.open(BytesIO(data)) as image:
await self.post_processing(image)
except Exception as e:
LOGGER.warning(f"Failed to create snapshot. \n {e}")

Comment on lines +83 to +84
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should: I actually think if we don't manage to make snapshots we should fail and not just warn so don't catch the exception at all

@abstractmethod
def post_processing(self, image: Image.Image):
async def post_processing(self, image: Image.Image):
pass


class SnapshotWithBeamCentre(MJPG):
"""A child of MJPG which, when triggered, draws an outlined crosshair at the beam
centre in the image and saves the image to disk."""

CROSSHAIR_LENGTH_PX = 20
CROSSHAIR_OUTLINE_COLOUR = "Black"
CROSSHAIR_FILL_COLOUR = "White"

def post_processing(self, image: Image.Image):
assert (
self.oav_params is not None
), "Snapshot device does not have valid OAV parameters"
beam_x = self.oav_params.beam_centre_i
beam_y = self.oav_params.beam_centre_j

SnapshotWithBeamCentre.draw_crosshair(image, beam_x, beam_y)

self._save_image(image)

@classmethod
def draw_crosshair(cls, image: Image.Image, beam_x: int, beam_y: int):
draw = ImageDraw.Draw(image)
OUTLINE_WIDTH = 1
HALF_LEN = cls.CROSSHAIR_LENGTH_PX / 2
draw.rectangle(
[
beam_x - OUTLINE_WIDTH,
beam_y - HALF_LEN - OUTLINE_WIDTH,
beam_x + OUTLINE_WIDTH,
beam_y + HALF_LEN + OUTLINE_WIDTH,
],
fill=cls.CROSSHAIR_OUTLINE_COLOUR,
)
draw.rectangle(
[
beam_x - HALF_LEN - OUTLINE_WIDTH,
beam_y - OUTLINE_WIDTH,
beam_x + HALF_LEN + OUTLINE_WIDTH,
beam_y + OUTLINE_WIDTH,
],
fill=cls.CROSSHAIR_OUTLINE_COLOUR,
)
draw.line(
((beam_x, beam_y - HALF_LEN), (beam_x, beam_y + HALF_LEN)),
fill=cls.CROSSHAIR_FILL_COLOUR,
)
draw.line(
((beam_x - HALF_LEN, beam_y), (beam_x + HALF_LEN, beam_y)),
fill=cls.CROSSHAIR_FILL_COLOUR,
)
Loading
Loading