diff --git a/src/openflexure_microscope_server/things/camera/__init__.py b/src/openflexure_microscope_server/things/camera/__init__.py index e2299737..32f3c889 100644 --- a/src/openflexure_microscope_server/things/camera/__init__.py +++ b/src/openflexure_microscope_server/things/camera/__init__.py @@ -319,7 +319,6 @@ class BaseCamera(lt.Thing): @lt.action def grab_jpeg( self, - portal: lt.deps.BlockingPortal, stream_name: Literal["main", "lores"] = "main", ) -> JPEGBlob: """Acquire one image from the preview stream and return as blob of JPEG data. @@ -337,13 +336,12 @@ class BaseCamera(lt.Thing): stream = ( self.lores_mjpeg_stream if stream_name == "lores" else self.mjpeg_stream ) - frame = portal.call(stream.grab_frame) + frame = self._thing_server_interface.call_async_task(stream.grab_frame) return JPEGBlob.from_bytes(frame) @lt.action def grab_as_array( self, - portal: lt.deps.BlockingPortal, stream_name: Literal["main", "lores"] = "main", ) -> ArrayModel: """Acquire one image from the preview stream and return as an array. @@ -361,7 +359,7 @@ class BaseCamera(lt.Thing): tries = 0 while tries < 3: try: - frame = portal.call(stream.grab_frame) + frame = self._thing_server_interface.call_async_task(stream.grab_frame) return np.asarray(Image.open(io.BytesIO(frame))) except OSError: tries += 1 @@ -370,14 +368,13 @@ class BaseCamera(lt.Thing): @lt.action def grab_jpeg_size( self, - portal: lt.deps.BlockingPortal, stream_name: Literal["main", "lores"] = "main", ) -> int: """Acquire one image from the preview stream and return its size.""" stream = ( self.lores_mjpeg_stream if stream_name == "lores" else self.mjpeg_stream ) - return portal.call(stream.next_frame_size) + return self._thing_server_interface.call_async_task(stream.next_frame_size) def capture_image( self, @@ -705,13 +702,13 @@ class BaseCamera(lt.Thing): ) @lt.action - def image_is_sample(self, portal: lt.deps.BlockingPortal) -> tuple[bool, str]: + def image_is_sample(self) -> tuple[bool, str]: """Label the current image as either background or sample.""" - current_image = self.grab_as_array(portal, stream_name="lores") + current_image = self.grab_as_array(stream_name="lores") return self.active_detector.image_is_sample(current_image) @lt.action - def set_background(self, portal: lt.deps.BlockingPortal) -> None: + def set_background(self) -> None: """Grab an image, and use its statistics to set the background. This should be run when the microscope is looking at an empty region, @@ -720,7 +717,7 @@ class BaseCamera(lt.Thing): future images to the distribution, to determine if each pixel is foreground or background. """ - background = self.grab_as_array(portal, stream_name="lores") + background = self.grab_as_array(stream_name="lores") self.active_detector.set_background(background) # Manually save settings as the setter is not called. self.save_settings() diff --git a/src/openflexure_microscope_server/things/camera/picamera.py b/src/openflexure_microscope_server/things/camera/picamera.py index ac3078a1..7ed77a52 100644 --- a/src/openflexure_microscope_server/things/camera/picamera.py +++ b/src/openflexure_microscope_server/things/camera/picamera.py @@ -71,10 +71,8 @@ class PicameraStreamOutput(Output): def __init__(self, stream: lt.outputs.MJPEGStream) -> None: """Create an output that puts frames in an MJPEGStream. - We need to pass the stream object, and also the blocking portal, because - new frame notifications happen in the anyio event loop and frames are - sent from a thread. The blocking portal enables thread-to-async - communication. + We need to pass the stream object, because new frame notifications happen in + the anyio event loop and frames are sent from a thread. """ Output.__init__(self) self.stream = stream @@ -744,7 +742,7 @@ class StreamingPiCamera2(BaseCamera): self._initialise_picamera() @lt.action - def full_auto_calibrate(self, portal: lt.deps.BlockingPortal) -> None: + def full_auto_calibrate(self) -> None: """Perform a full auto-calibration. This function will call the other calibration actions in sequence: @@ -763,7 +761,7 @@ class StreamingPiCamera2(BaseCamera): for _i in range(3): try: time.sleep(self._sensor_info.long_pause) - self.set_background(portal) + self.set_background() # Return if background is set return except ChannelBlankError: diff --git a/src/openflexure_microscope_server/things/camera/simulation.py b/src/openflexure_microscope_server/things/camera/simulation.py index 79a4cff8..e067f8dc 100644 --- a/src/openflexure_microscope_server/things/camera/simulation.py +++ b/src/openflexure_microscope_server/things/camera/simulation.py @@ -350,7 +350,7 @@ class SimulatedCamera(BaseCamera): return self.generate_frame() @lt.action - def full_auto_calibrate(self, portal: lt.deps.BlockingPortal) -> None: + def full_auto_calibrate(self) -> None: """Perform a full auto-calibration. For the simulation microscope the process is: @@ -361,7 +361,7 @@ class SimulatedCamera(BaseCamera): """ self.remove_sample() time.sleep(0.2) - self.set_background(portal) + self.set_background() time.sleep(0.2) self.load_sample() diff --git a/tests/test_autofocus.py b/tests/test_autofocus.py index 4c02146c..58a5f577 100644 --- a/tests/test_autofocus.py +++ b/tests/test_autofocus.py @@ -6,6 +6,8 @@ This doesn't check the behaviour of the JPEG shaprness monitor. import numpy as np import pytest +from labthings_fastapi.testing import create_thing_without_server + from openflexure_microscope_server.things.autofocus import ( AutofocusThing, NoFocusFoundError, @@ -77,7 +79,7 @@ def test_looping_autofocus(start_z, max_loc, centre, attempts_expected, passes, sharpness_monitor.move_data.side_effect = return_sharpness - autofocus_thing = AutofocusThing() + autofocus_thing = create_thing_without_server(AutofocusThing) if passes: autofocus_thing.looping_autofocus( stage=stage, diff --git a/tests/test_camera.py b/tests/test_camera.py index 628f2a09..7c0b1654 100644 --- a/tests/test_camera.py +++ b/tests/test_camera.py @@ -1,7 +1,6 @@ """Use the Simulation camera to test base camera functionality.""" import tempfile -from contextlib import contextmanager import numpy as np import pytest @@ -10,29 +9,30 @@ from PIL import Image import labthings_fastapi as lt -from openflexure_microscope_server.things.camera.simulation import SimulatedCamera - -@contextmanager -def camera_server(camera: SimulatedCamera) -> lt.ThingClient: +@pytest.fixture +def camera_server() -> lt.ThingClient: """Add the camera to a ThingServer and start a TestClient application. - The test client application is needed for the camera to have a blocking portal. + The test client will be needed for the camera to run async frame generation code. """ with tempfile.TemporaryDirectory() as tmpdir: - server = lt.ThingServer(settings_folder=tmpdir) - server.add_thing(camera, "camera") - with TestClient(server.app): - yield server + conf = { + "camera": "openflexure_microscope_server.things.camera.simulation:SimulatedCamera", + "stage": "openflexure_microscope_server.things.stage.dummy:DummyStage", + } + + server = lt.ThingServer(things=conf, settings_folder=tmpdir) + yield server -def test_handle_broken_frame(): +def test_handle_broken_frame(camera_server): """Monkey patch the the mjpeg steam so 1 in 5 frames are broken, then test operation. This simulates the very occasional broken frames that can occur when grabbing directly from the MJPEG stream. """ - camera = SimulatedCamera() + camera = camera_server.things["camera"] # Money patch the mjpeg_stream grab_frame to break 1 in 5 frames. frame_number = 0 @@ -50,7 +50,8 @@ def test_handle_broken_frame(): return frame camera.mjpeg_stream.grab_frame = flaky_grabber - with camera_server(camera): + + with TestClient(camera_server.app): # Check that this does cause broken frames. # The noqa is because we don't know exactly when the error is thrown so we # can't have a single simple statement in the pytest raises. @@ -68,10 +69,10 @@ def test_handle_broken_frame(): assert isinstance(array, np.ndarray) -def test_simulation_cam_calibration(): +def test_simulation_cam_calibration(camera_server): """Test that the simulated camera can be calibrated and reports calibration correctly.""" - camera = SimulatedCamera() - with camera_server(camera): + camera = camera_server.things["camera"] + with TestClient(camera_server.app): assert camera.calibration_required camera.full_auto_calibrate() assert not camera.calibration_required