diff --git a/picamera_coverage.zip b/picamera_coverage.zip index 3dd24bdf..777a12e1 100644 Binary files a/picamera_coverage.zip and b/picamera_coverage.zip differ diff --git a/src/openflexure_microscope_server/server/legacy_api.py b/src/openflexure_microscope_server/server/legacy_api.py index 69996f52..3de031ae 100644 --- a/src/openflexure_microscope_server/server/legacy_api.py +++ b/src/openflexure_microscope_server/server/legacy_api.py @@ -1,4 +1,4 @@ -"""Provide endpoints that mimic the v2 API for OpenFlexure Connect discoverability.""" +"""Provide endpoints that mimic the v2 API for legacy discoverability.""" from socket import gethostname @@ -22,12 +22,14 @@ class JPEGResponse(Response): def add_v2_endpoints(thing_server: lt.ThingServer) -> None: - """Add the v2 API endpoints for OpenFlexure Connect discoverability.""" + """Add the v2 API endpoints for legacy discoverability. + + Old versions of OpenFlexure Connect use the v2 endpoints to check for microscopes. + This provides these routes so the microscope can be discovered by old versions of + connect. + """ app = thing_server.app - # The endpoints below fool OpenFlexure Connect into thinking we are a - # v2 microscope, so we show up correctly. - # This is necessary until Connect is rebuilt. See #557. @app.get("/routes") def routes_stub() -> dict[str, dict]: """Return a stub list of routes. diff --git a/src/openflexure_microscope_server/things/camera/__init__.py b/src/openflexure_microscope_server/things/camera/__init__.py index fa32d6d6..b85b970e 100644 --- a/src/openflexure_microscope_server/things/camera/__init__.py +++ b/src/openflexure_microscope_server/things/camera/__init__.py @@ -20,6 +20,7 @@ from typing import Any, Literal, Mapping, Optional, Self import numpy as np import piexif +from fastapi import Response from PIL import Image from pydantic import BaseModel @@ -259,6 +260,21 @@ class BaseCamera(OFMThing, ABC): """Close hardware connection when the Thing context manager is closed.""" pass + @lt.endpoint( + "get", + "snapshot", + responses={ + 200: { + "description": "A snapshot of the microscope stream", + "content": {"image/jpeg": {}}, + }, + }, + ) + async def snapshot(self) -> Response: + """Return a snapshot from the microscope.""" + jpeg_data = await self.lores_mjpeg_stream.grab_frame() + return Response(content=jpeg_data, media_type="image/jpeg") + @property def focus_fom(self) -> int: """Return the focus figure of merit. diff --git a/tests/unit_tests/test_simulated_camera.py b/tests/unit_tests/test_simulated_camera.py index 2c7d4a81..8b93152d 100644 --- a/tests/unit_tests/test_simulated_camera.py +++ b/tests/unit_tests/test_simulated_camera.py @@ -2,11 +2,13 @@ import logging import time +from io import BytesIO import numpy as np import pytest from hypothesis import given from hypothesis import strategies as st +from PIL import Image from pydantic import ValidationError import labthings_fastapi as lt @@ -44,6 +46,19 @@ def stage(test_env) -> lt.Thing: return test_env.get_thing_by_type(DummyStage) +def test_snapshot(test_env) -> lt.Thing: + """Check the snapshot GET request returns an image.""" + http_client = test_env.get_thing_client("camera").client + response = http_client.get("camera/snapshot") + response.raise_for_status() + assert response.headers["content-type"] == "image/jpeg" + image = Image.open(BytesIO(response.content)) + + assert image.format == "JPEG" + assert image.width == 320 + assert image.height == 240 + + def test_downsample_shape_2d(): """Test downsampling for 2D array.""" shape_2d = (100, 80)