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

Merged
merged 81 commits into from
Nov 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
81 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
11826ae
Add connect
noemifrisina Oct 28, 2024
8d0567c
Merge branch 'main' into 824_mjpg-async-device
noemifrisina Oct 28, 2024
91ae0a6
Remove i10_4 again
noemifrisina Oct 28, 2024
9431b34
Fix CAM plugin
noemifrisina Oct 28, 2024
fa965e5
Remove unused signals
noemifrisina Oct 28, 2024
9705c1e
Do no catch exception when failing to create snapshot
noemifrisina Oct 28, 2024
51bf8e8
Put format insto constant
noemifrisina Oct 28, 2024
1bd7ebb
Remove unused oav error file and redefine zoom level error
noemifrisina Oct 28, 2024
83503a4
Fix super
noemifrisina Oct 28, 2024
1da64f3
Fix typo
noemifrisina Oct 28, 2024
05065e4
Remove extra connect
noemifrisina Oct 28, 2024
367c02d
Remove subscription id for snapshot
noemifrisina Oct 28, 2024
ea51f05
Fix string enum
noemifrisina Oct 28, 2024
bcc5d12
Pull draw_crosshair out of class
noemifrisina Oct 28, 2024
1a1c1d9
Mock describe instead of method
noemifrisina Oct 28, 2024
76ccda1
Have a util function for saving image with asyncio
noemifrisina Oct 28, 2024
27c4b64
Use raise_for_status in clientsession
noemifrisina Oct 30, 2024
88ab363
Tidy up mocks in snapshots
noemifrisina Oct 30, 2024
a12ca18
Separate test for mkdir
noemifrisina Oct 30, 2024
eeb8daf
Add readables
noemifrisina Oct 30, 2024
7f4482e
More add_children_as_readables
noemifrisina Nov 1, 2024
8825f18
Use IMG_FORMAT
noemifrisina Nov 4, 2024
598d256
Merge branch 'main' into 824_mjpg-async-device
noemifrisina Nov 4, 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
9 changes: 5 additions & 4 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,18 +228,18 @@ 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.
"""
return device_instantiation(
OAV,
"oav",
"",
"-DI-OAV-01:",
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
13 changes: 9 additions & 4 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,17 +345,21 @@ 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.
"""
return device_instantiation(
OAV,
"oav",
"",
"-DI-OAV-01:",
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
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 Enum

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


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

MONO = "Mono"
BAYER = "Bayer"
RGB1 = "RGB1"
RGB2 = "RGB2"
RGB3 = "RGB3"
YUV444 = "YUV444"
YUV422 = "YUV422"
YUV421 = "YUV421"


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

self.array_size_x = epics_signal_r(int, prefix + "ArraySizeX_RBV")
self.array_size_y = epics_signal_r(int, prefix + "ArraySizeY_RBV")
super().__init__(name)
157 changes: 51 additions & 106 deletions src/dodal/devices/areadetector/plugins/MJPG.py
Original file line number Diff line number Diff line change
@@ -1,138 +1,83 @@
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 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

IMG_FORMAT = "png"

class MJPG(Device, ABC):

async def asyncio_save_image(image: Image.Image, path: str):
buffer = BytesIO()
image.save(buffer, format=IMG_FORMAT)
async with aiofiles.open(path, "wb") as fh:
await fh.write(buffer.getbuffer())


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.url = epics_signal_rw(str, prefix + "JPG_URL_RBV")

# scaling factors for the snapshot at the time it was triggered
microns_per_pixel_x = Component(Signal)
microns_per_pixel_y = Component(Signal)
self.x_size = epics_signal_r(int, prefix + "ArraySize1_RBV")
self.y_size = epics_signal_r(int, prefix + "ArraySize2_RBV")

Comment on lines +34 to +35
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

oav_params: OAVConfigParams | None = None
with self.add_children_as_readables():
self.filename = soft_signal_rw(str)
self.directory = soft_signal_rw(str)
self.last_saved_path = soft_signal_rw(str)

KICKOFF_TIMEOUT: float = 30.0
self.KICKOFF_TIMEOUT = 30.0

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
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)):
path = Path(f"{directory_str}/{filename_str}.{IMG_FORMAT}").as_posix()
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):
await asyncio_save_image(image, path)

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.
It is the responsibility of the child class to save any resulting images by \
calling _save_image.
"""
st = DeviceStatus(device=self, timeout=self.KICKOFF_TIMEOUT)
url_str = self.url.get()
url_str = await self.url.get_value()

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
async with ClientSession(raise_for_status=True) as session:
async with session.get(url_str) as response:
data = await response.read()
with Image.open(BytesIO(data)) as image:
await self.post_processing(image)

@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