diff --git a/openflexure_microscope/api/v2/views/actions/stage.py b/openflexure_microscope/api/v2/views/actions/stage.py index a87e100c..206d9623 100644 --- a/openflexure_microscope/api/v2/views/actions/stage.py +++ b/openflexure_microscope/api/v2/views/actions/stage.py @@ -1,50 +1,47 @@ import logging -from typing import List, Tuple from labthings import fields, find_component from labthings.views import ActionView -from openflexure_microscope.utilities import axes_to_array - class MoveStageAPI(ActionView): args = { "absolute": fields.Boolean( missing=False, example=False, description="Move to an absolute position" ), - "x": fields.Int(missing=0, example=100), - "y": fields.Int(missing=0, example=100), - "z": fields.Int(missing=0, example=20), + "x": fields.Int(missing=None, example=100), + "y": fields.Int(missing=None, example=100), + "z": fields.Int(missing=None, example=20), } def post(self, args): """ Move the microscope stage in x, y, z + + Any axes that are not specifed will not move. """ microscope = find_component("org.openflexure.microscope") - # Handle absolute positioning (calculate a relative move from current position and target) - if (args.get("absolute")) and (microscope.stage): # Only if stage exists - target_position: List[int] = axes_to_array(args, ["x", "y", "z"]) - logging.debug("TARGET: %s", (target_position)) - position: Tuple[int, int, int] = ( - target_position[i] - microscope.stage.position[i] for i in range(3) - ) - logging.debug("DELTA: %s", (position)) - - else: - # Get coordinates from payload - position = axes_to_array(args, ["x", "y", "z"], [0, 0, 0]) - - logging.debug(position) - - # Move if stage exists - if microscope.stage: - # Explicitally acquire lock with 1s timeout - with microscope.stage.lock(timeout=1): - microscope.stage.move_rel(position) - else: + if not microscope.stage: logging.warning("Unable to move. No stage found.") + return microscope.state["stage"]["position"] + + absolute_move = args.get("absolute") + move = [0, 0, 0] # Default to no motion + for i, axis in enumerate(["x", "y", "z"]): + if axis in args and args[axis] is not None: + if absolute_move: + # We emulate absolute moves by calculating a relative move that + # will take us to the right position. + move[i] = args[axis] - microscope.stage.position[i] + else: + move[i] = args[axis] + + logging.debug(f"Moving stage by {move}, request was {args}") + + # Explicitly acquire lock with 1s timeout + with microscope.stage.lock(timeout=1): + microscope.stage.move_rel(move) return microscope.state["stage"]["position"] diff --git a/openflexure_microscope/utilities.py b/openflexure_microscope/utilities.py index b0b22e28..1a36f86b 100644 --- a/openflexure_microscope/utilities.py +++ b/openflexure_microscope/utilities.py @@ -4,7 +4,7 @@ import logging import sys import time from contextlib import contextmanager -from typing import Dict, List, Optional, Tuple, Type, Union +from typing import Dict, List, Optional, Sequence, Tuple, Type, Union import numpy as np @@ -102,12 +102,29 @@ def set_properties(obj, **kwargs): def axes_to_array( - coordinate_dictionary: Dict[str, int], - axis_keys=("x", "y", "z"), + coordinate_dictionary: Dict[str, Optional[int]], + axis_keys: Sequence[str] = ("x", "y", "z"), base_array: Optional[List[int]] = None, asint: bool = True, ) -> List[int]: - """Takes key-value pairs of a JSON value, and maps onto an array""" + """Takes key-value pairs of a JSON value, and maps onto an array + + This is designed to take a dictionary like `{"x": 1, "y":2, "z":3}` + and return a list like `[1, 2, 3]` to convert between the argument + format expected by most of our stages, and the usual argument + format in JSON. + + `axis_keys` is an ordered sequence of key names to extract from + the input dictionary. + + `base_array` specifies a default value for each axis. It must + have the same length as `axis_keys`. + + `asint` casts values to integers if it is `True` (default). + + Missing keys, or keys that have a `None` value will be left + at the specified default value, or zero if none is specified. + """ # If no base array is given if not base_array: # Create an array of zeros @@ -119,8 +136,14 @@ def axes_to_array( # Do the mapping for axis, key in enumerate(axis_keys): if key in coordinate_dictionary: - base_array[axis] = ( - int(coordinate_dictionary[key]) if asint else coordinate_dictionary[key] - ) + value = coordinate_dictionary[key] + if value is None: + # Values set to None should be treated as if they + # are missing + # i.e. we leave the default value in place. + break + if asint: + value = int(value) + base_array[axis] = value return base_array diff --git a/pyproject.toml b/pyproject.toml index d90d950c..1465fb29 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,7 +25,13 @@ ensure_newline_before_comments = true line_length = 88 [tool.pylint.'MESSAGES CONTROL'] -disable = "fixme,C,R" +# W1203 warns about using f strings in logging statements. +# I'm really not concerned about the performance implications of this, +# particularly as we often have the log level set quite high. I think +# for our current purposes, using f strings in logging statements is +# more readable and thus a good idea in many places - I've disabled +# this warning code globally for that reason. +disable = "fixme,C,R,W1203" max-line-length = 88 [tool.poe.executor]