Source code for dodal.devices.webcam
from collections.abc import ByteString
from io import BytesIO
from pathlib import Path
import aiofiles
from aiohttp import ClientSession
from bluesky.protocols import Triggerable
from ophyd_async.core import (
AsyncStatus,
StandardReadable,
StandardReadableFormat,
soft_signal_rw,
)
from PIL import Image
from dodal.log import LOGGER
PLACEHOLDER_IMAGE_SIZE = (1024, 768)
IMAGE_FORMAT = "png"
[docs]
def create_placeholder_image() -> ByteString:
image = Image.new("RGB", PLACEHOLDER_IMAGE_SIZE)
image.save(buffer := BytesIO(), format=IMAGE_FORMAT)
return buffer.getbuffer()
[docs]
class Webcam(StandardReadable, Triggerable):
def __init__(self, name, prefix, url):
self.url = url
self.filename = soft_signal_rw(str, name="filename")
self.directory = soft_signal_rw(str, name="directory")
self.last_saved_path = soft_signal_rw(str, name="last_saved_path")
self.add_readables(
[self.last_saved_path], format=StandardReadableFormat.HINTED_SIGNAL
)
super().__init__(name=name)
async def _write_image(self, file_path: str, image: ByteString):
async with aiofiles.open(file_path, "wb") as file:
await file.write(image)
async def _get_and_write_image(self, file_path: str):
async with ClientSession() as session:
async with session.get(self.url) as response:
if not response.ok:
LOGGER.warning(
f"Webcam responded with {response.status}: {response.reason}. Attempting to read anyway."
)
try:
data = await response.read()
LOGGER.info(f"Saving webcam image from {self.url} to {file_path}")
except Exception as e:
LOGGER.warning(
f"Failed to read data from {self.url} ({e}). Using placeholder image."
)
data = create_placeholder_image()
await self._write_image(file_path, data)
@AsyncStatus.wrap
async def trigger(self) -> None:
filename = await self.filename.get_value()
directory = await self.directory.get_value()
file_path = Path(f"{directory}/{filename}.{IMAGE_FORMAT}").as_posix()
await self._get_and_write_image(file_path)
await self.last_saved_path.set(file_path)