Thing Actions

Introduction

As well as properties, the OpenFlexure Microscope Server also supports Thing Actions. Thing Actions “invoke a function of the Thing, which manipulates state (e.g., toggling a lamp on or off) or triggers a process on the Thing (e.g., dim a lamp over time).” For the microscope, this would include moving the stage or taking a capture. Both of these require internal logic, and cannot be performed by changing a simple property.

Actions should be triggered with POST requests only. Ideally, a view corresponding to an action should only support POST requests.

Like properties, we use a special view class to identify a view as an action: ActionView. For example, a view to perform a “quick-capture” action may look like:

class QuickCaptureAPI(ActionView):
    """
    Take an image capture and return it without saving
    """
    # Expect a "use_video_port" boolean, which defaults to True if none is given
    args = {"use_video_port": fields.Boolean(load_default=True)}

    # Our success response (200) returns an image (image/jpeg mimetype)
    responses = {
        200: {
            "content": { "image/jpeg": {} },
        }
    }

    def post(self, args):
        """
        Take a non-persistant image capture.
        """
        # Find our microscope component
        microscope = find_component("org.openflexure.microscope")

        # Open a BytesIO stream to be destroyed once request has returned
        with io.BytesIO() as stream:

            # Capture to our stream object
            microscope.camera.capture(stream, use_video_port=args.get("use_video_port"))

            # Rewind the stream
            stream.seek(0)

            # Return our image data using Flasks send_file function
            return send_file(io.BytesIO(stream.read()), mimetype="image/jpeg")

In this example, we are also making use of the responses attribute, to document that our successful response (HTTP code 200) will return data with a mimetype image/jpeg, as well as args to accept optional parameters with POST requests.

Complete example

Adding this new view into our example extension, we now have:

import io  # Used in our capture action

from flask import send_file  # Used to send images from our server
from labthings import Schema, fields, find_component
from labthings.extensions import BaseExtension
from labthings.views import ActionView, PropertyView


# Create the extension class
class MyExtension(BaseExtension):
    def __init__(self):
        # Superclass init function
        super().__init__("com.myname.myextension", version="0.0.0")

        # Add our API Views (defined below MyExtension)
        self.add_view(ExampleIdentifyView, "/identify")
        self.add_view(ExampleRenameView, "/rename")

    def rename(self, microscope, new_name):
        """
        Rename the microscope
        """
        microscope.name = new_name
        microscope.save_settings()


# Define which properties of a Microscope object we care about,
# and what types they should be converted to
class MicroscopeIdentifySchema(Schema):
    name = fields.String()  # Microscopes name
    id = fields.UUID()  # Microscopes unique ID
    state = fields.Dict()  # Status dictionary
    camera = fields.String()  # Camera object (represented as a string)
    stage = fields.String()  # Stage object (represented as a string)


## Extension views

# Since we only have a GET method here, it'll register as a read-only property
class ExampleIdentifyView(PropertyView):
    # Format our returned object using MicroscopeIdentifySchema
    schema = MicroscopeIdentifySchema()

    def get(self):
        """
        Show identifying information about the current microscope object
        """
        # Find our microscope component
        microscope = find_component("org.openflexure.microscope")

        # Return our microscope object,
        # let schema handle formatting the output
        return microscope


# We can use a single schema as the input and output will be formatted identically
# Eg. We always expect a "name" string argument, and always return a "name" string attribute
class ExampleRenameView(PropertyView):
    schema = {
        "name": fields.String(
            required=True, metadata={"example": "My Example Microscope"}
        )
    }

    def get(self):
        """
        Show the current microscope name
        """
        # Find our microscope component
        microscope = find_component("org.openflexure.microscope")

        return microscope

    def post(self, args):
        """
        Change the current microscope name
        """
        # Look for our "name" parameter in the request arguments
        new_name = args.get("name")

        # Find our microscope component
        microscope = find_component("org.openflexure.microscope")

        # Pass microscope and new name to our rename function
        self.extension.rename(microscope, new_name)

        # Return our microscope object,
        # let schema handle formatting the output
        return microscope


class QuickCaptureAPI(ActionView):
    """
    Take an image capture and return it without saving
    """

    # Expect a "use_video_port" boolean, which defaults to True if none is given
    args = {"use_video_port": fields.Boolean(load_default=True)}

    # Our success response (200) returns an image (image/jpeg mimetype)
    responses = {200: {"content": {"image/jpeg": {}}}}

    def post(self, args):
        """
        Take a non-persistant image capture.
        """
        # Find our microscope component
        microscope = find_component("org.openflexure.microscope")

        # Open a BytesIO stream to be destroyed once request has returned
        with io.BytesIO() as stream:

            # Capture to our stream object
            microscope.camera.capture(stream, use_video_port=args.get("use_video_port"))

            # Rewind the stream
            stream.seek(0)

            # Return our image data using Flasks send_file function
            return send_file(io.BytesIO(stream.read()), mimetype="image/jpeg")


LABTHINGS_EXTENSIONS = (MyExtension,)