Merge branch 'fix-movements' into 'v3'

Fix movements axes

Closes #498

See merge request openflexure/openflexure-microscope-server!338
This commit is contained in:
Julian Stirling 2025-08-06 15:50:08 +00:00
commit 73beb2b09d
8 changed files with 562 additions and 57 deletions

View file

@ -11,33 +11,71 @@ As the object will be used as a context manager create the hardware connection i
from __future__ import annotations
from collections.abc import Sequence, Mapping
from typing import Literal
import labthings_fastapi as lt
class RedefinedBaseMovementError(RuntimeError):
"""The subclass of BaseStage has overridden ``move_relative`` or ``move_absolute``.
Overriding ``move_relative`` or ``move_absolute`` can be problematic as these use the
external position not the hardware position. It is recommended to override
``_hardware_move_relative`` and ``_hardware_move_absolute`` instead.
The BaseStage will raise this on ``__init__``, it is the last thing ``__init__``
does. As such, this exception can be captured by ``try`` if a stage needs to
override these for a specific reason.
"""
class BaseStage(lt.Thing):
"""A base stage class for OpenFlexure translation stages.
This can't be used directly but should reduce boilerplate code when
implementing new stages. A minimal working stage must implement
``move_relative`` and ``move_absolute`` actions, which update the
``position`` property on completion, and provide ``set_zero_position``.
implementing new stages.
Note that the coordinate system used for the microscope may need to have different
axis direction as those used by the underlying stage controller.
A minimal working stage must implement ``_hardware_move_relative``
and ``_hardware_move_absolute`` actions, which update the ``_hardware_position``
attribute on completion, and also should implement ``set_zero_position``.
"""
_axis_names = ("x", "y", "z")
def __init__(self):
"""Initialise the stage.
:raises RedefinedBaseMovementError: if ``move_relative`` and/or
``move_absolute`` are overridden. It is recommended to override
``_hardware_move_relative`` and/or ``_hardware_move_absolute`` instead so
that all code in the child class uses the hardware reference frame.
"""
self._hardware_position = dict.fromkeys(self._axis_names, 0)
# This must be the last thing the function does in case it is caught in a try.
if (
self.__class__.move_relative.func is not BaseStage.move_relative.func
or self.__class__.move_absolute.func is not BaseStage.move_absolute.func
):
raise RedefinedBaseMovementError(
"move_relative and/or move_absolute has been overridden. This may "
"cause issues as the base methods implement converting from program "
"coordinates to hardware coordinates. Consider overriding "
"_hardware_move_relative and/or _hardware_move_absolute instead."
)
@lt.thing_property
def axis_names(self) -> Sequence[str]:
"""The names of the stage's axes, in order."""
return self._axis_names
position = lt.ThingProperty(
Mapping[str, int],
dict.fromkeys(_axis_names, 0),
readonly=True,
observable=True,
)
"""Current position of the stage."""
@lt.thing_property
def position(self) -> Mapping[str, int]:
"""Current position of the stage."""
return self._apply_axis_direction(self._hardware_position)
moving = lt.ThingProperty(
bool,
@ -47,11 +85,56 @@ class BaseStage(lt.Thing):
)
"""Whether the stage is in motion."""
axis_inverted = lt.ThingSetting(
initial_value={"x": False, "y": False, "z": False},
model=Mapping[str, bool],
readonly=True,
)
"""Used to convert coordinates between the program frame and the hardware frame."""
def _apply_axis_direction(
self, position: list[int] | tuple[int] | Mapping[str, int]
) -> list[int] | Mapping[str, int]:
if isinstance(position, (list, tuple)):
return [
-int(pos) if inverted else int(pos)
for pos, inverted in zip(position, self.axis_inverted.values())
]
if isinstance(position, Mapping):
try:
return {
ax: -int(position[ax])
if self.axis_inverted[ax]
else int(position[ax])
for ax in position
}
except KeyError as e:
raise KeyError(
f"One or more axis in {position.keys()} is not defined."
) from e
raise TypeError(
"Position must be a sequence of positions or a mapping from axis to position."
)
@property
def thing_state(self):
"""Summary metadata describing the current state of the stage."""
return {"position": self.position}
@lt.thing_action
def invert_axis_direction(self, axis: Literal["x", "y", "z"]):
"""Invert the direction setting of the given axis.
:param axis: The axis name (x, y or z) to invert.
"""
# Not mutating in place so that setting is saved on change.
direction = self.axis_inverted
try:
direction[axis] = not direction[axis]
except KeyError as e:
raise KeyError(f"The axis {axis} is not defined.") from e
self.axis_inverted = direction
@lt.thing_action
def move_relative(
self,
@ -60,8 +143,24 @@ class BaseStage(lt.Thing):
**kwargs: Mapping[str, int],
):
"""Make a relative move. Keyword arguments should be axis names."""
self._hardware_move_relative(
cancel=cancel,
block_cancellation=block_cancellation,
**self._apply_axis_direction(kwargs),
)
def _hardware_move_relative(
self,
cancel: lt.deps.CancelHook,
block_cancellation: bool = False,
**kwargs: Mapping[str, int],
):
"""Make a relative move in the coordinate system used by the physical hardware.
Make sure to use and update ``self._hardware_position`` not ``self.position``.
"""
raise NotImplementedError(
"StageThings must define their own move_relative method"
"StageThings must define their own _hardware_move_relative method"
)
@lt.thing_action
@ -72,6 +171,22 @@ class BaseStage(lt.Thing):
**kwargs: Mapping[str, int],
):
"""Make an absolute move. Keyword arguments should be axis names."""
self._hardware_move_absolute(
cancel=cancel,
block_cancellation=block_cancellation,
**self._apply_axis_direction(kwargs),
)
def _hardware_move_absolute(
self,
cancel: lt.deps.CancelHook,
block_cancellation: bool = False,
**kwargs: Mapping[str, int],
):
"""Make a absolute move in the coordinate system used by the physical hardware.
Make sure to use and update ``self._hardware_position`` not ``self.position``.
"""
raise NotImplementedError(
"StageThings must define their own move_absolute method"
)

View file

@ -27,16 +27,23 @@ class DummyStage(BaseStage):
"""
super().__init__(**kwargs)
self.step_time = step_time
self.instantaneous_position = self._hardware_position
def __enter__(self):
"""Register the stage position when the Thing context manager is opened."""
self.instantaneous_position = self.position
self.instantaneous_position = self._hardware_position
def __exit__(self, _exc_type, _exc_value, _traceback):
"""Nothing to do when the Thing context manager is closed."""
@lt.thing_action
def move_relative(
axis_inverted = lt.ThingSetting(
initial_value={"x": True, "y": False, "z": False},
model=Mapping[str, bool],
readonly=True,
)
"""Used to convert coordinates between the program frame and the hardware frame."""
def _hardware_move_relative(
self,
cancel: lt.deps.CancelHook,
block_cancellation: bool = False,
@ -57,8 +64,8 @@ class DummyStage(BaseStage):
cancel.sleep(dt)
fraction_complete = (time.time() - start_time) / (dt * max_displacement)
self.instantaneous_position = {
k: self.position[k] + int(fraction_complete * v)
for k, v in zip(self.axis_names, displacement)
ax: self._hardware_position[ax] + int(fraction_complete * disp)
for ax, disp in zip(self.axis_names, displacement)
}
fraction_complete = 1.0
except lt.exceptions.InvocationCancelledError as e:
@ -68,14 +75,13 @@ class DummyStage(BaseStage):
raise e
finally:
self.moving = False
self.position = {
k: self.position[k] + int(fraction_complete * v)
for k, v in zip(self.axis_names, displacement)
self._hardware_position = {
ax: self._hardware_position[ax] + int(fraction_complete * disp)
for ax, disp in zip(self.axis_names, displacement)
}
self.instantaneous_position = self.position
self.instantaneous_position = self._hardware_position
@lt.thing_action
def move_absolute(
def _hardware_move_absolute(
self,
cancel: lt.deps.CancelHook,
block_cancellation: bool = False,
@ -83,11 +89,11 @@ class DummyStage(BaseStage):
):
"""Make an absolute move. Keyword arguments should be axis names."""
displacement = {
axis: int(pos) - self.position[axis]
axis: int(pos) - self._hardware_position[axis]
for axis, pos in kwargs.items()
if axis in self.axis_names
}
self.move_relative(
self._hardware_move_relative(
cancel, block_cancellation=block_cancellation, **displacement
)
@ -99,5 +105,5 @@ class DummyStage(BaseStage):
It is intended for use after manually or automatically recentring the
stage.
"""
self.position = dict.fromkeys(self.axis_names, 0)
self.instantaneous_position = self.position
self._hardware_position = dict.fromkeys(self.axis_names, 0)
self.instantaneous_position = self._hardware_position

View file

@ -4,6 +4,7 @@ from __future__ import annotations
import logging
import threading
import time
from copy import copy
from typing import Iterator, Literal
from contextlib import contextmanager
from collections.abc import Mapping
@ -36,8 +37,9 @@ class SangaboardThing(BaseStage):
Sangaboard class
"""
self.sangaboard_kwargs = kwargs
self.sangaboard_kwargs = copy(kwargs)
self.sangaboard_kwargs["port"] = port
super().__init__(**kwargs)
def __enter__(self):
"""Connect to the sangaboard when the Thing context manager is opened."""
@ -65,19 +67,25 @@ class SangaboardThing(BaseStage):
with self._sangaboard_lock:
yield self._sangaboard
axis_inverted = lt.ThingSetting(
initial_value={"x": True, "y": False, "z": True},
model=Mapping[str, bool],
readonly=True,
)
"""Used to convert coordinates between the program frame and the hardware frame."""
def update_position(self) -> None:
"""Read position from the stage and set the corresponding property."""
with self.sangaboard() as sb:
self.position = dict(zip(self.axis_names, sb.position))
self._hardware_position = dict(zip(self.axis_names, sb.position))
@lt.thing_action
def move_relative(
def _hardware_move_relative(
self,
cancel: lt.deps.CancelHook,
block_cancellation: bool = False,
**kwargs: Mapping[str, int],
) -> None:
"""Make a relative move. Keyword arguments should be axis names."""
"""Make a relative move in the coordinate system used by the sangaboard."""
displacement = [kwargs.get(axis, 0) for axis in self.axis_names]
with self.sangaboard() as sb:
self.moving = True
@ -98,22 +106,21 @@ class SangaboardThing(BaseStage):
self.moving = False
self.update_position()
@lt.thing_action
def move_absolute(
def _hardware_move_absolute(
self,
cancel: lt.deps.CancelHook,
block_cancellation: bool = False,
**kwargs: Mapping[str, int],
) -> None:
"""Make an absolute move. Keyword arguments should be axis names."""
"""Make a absolute move in the coordinate system used by the sangaboard."""
with self.sangaboard():
self.update_position()
displacement = {
axis: int(pos) - self.position[axis]
axis: int(pos) - self._hardware_position[axis]
for axis, pos in kwargs.items()
if axis in self.axis_names
}
self.move_relative(
self._hardware_move_relative(
cancel, block_cancellation=block_cancellation, **displacement
)

View file

@ -1,7 +1,7 @@
"""Test the server without creating a full HTTP server and socket connection.
Rather than spinning up a full uvicorn webserver for each test these tests use
the FastAPI ``TestClient`` or to directly communicate with the underlying
the FastAPI ``TestClient`` or directly communicate with the underlying
LabThings-FastAPI code. This increases speed of testing significantly.
For tests that require a full running server see the ``integration-tests``

335
tests/test_stage.py Normal file
View file

@ -0,0 +1,335 @@
"""Test the stage without creating a full HTTP server and socket connection."""
from collections.abc import Mapping
import tempfile
import itertools
from fastapi.testclient import TestClient
import pytest
from httpx import HTTPStatusError
from hypothesis import given, settings, HealthCheck, strategies as st
import labthings_fastapi as lt
from labthings_fastapi.exceptions import NotConnectedToServerError
from openflexure_microscope_server.things.stage import (
BaseStage,
RedefinedBaseMovementError,
)
from openflexure_microscope_server.things.stage.dummy import DummyStage
# Keep the size and number of moves fairly small or the tests can take forever
point3d = st.tuples(
st.integers(min_value=-100, max_value=100),
st.integers(min_value=-100, max_value=100),
st.integers(min_value=-100, max_value=100),
)
path3d = st.lists(point3d, min_size=5, max_size=10)
@pytest.fixture
def dummy_stage():
"""Return a dummy stage with a very low step time."""
return DummyStage(step_time=0.000001)
@pytest.fixture
def thing_server(dummy_stage):
"""Yield a server with a very basic configuration."""
with tempfile.TemporaryDirectory() as tmpdir:
server = lt.ThingServer(settings_folder=tmpdir)
server.add_thing(dummy_stage, "/stage/")
yield server
@pytest.fixture
def stage_client(thing_server):
"""Yield a labthings ThingClient for the stage."""
with TestClient(thing_server.app) as test_client:
yield lt.ThingClient.from_url("/stage/", client=test_client)
def test_override_base_movement():
"""Child classes of stage should implement functions in the hardware reference frame.
``move_absolute`` and ``move_relative`` are in the program reference frame and
convert to the hardware frame. It is recommended to override
``_hardware_move_relative`` and ``_hardware_move_absolute`` instead.
Check that the expected error is raised if the methods are overridden.
"""
class BadStage1(BaseStage):
@lt.thing_action
def move_relative(
self,
cancel: lt.deps.CancelHook,
block_cancellation: bool = False,
**kwargs: Mapping[str, int],
):
pass
with pytest.raises(RedefinedBaseMovementError):
BadStage1()
class BadStage2(BaseStage):
@lt.thing_action
def move_absolute(
self,
cancel: lt.deps.CancelHook,
block_cancellation: bool = False,
**kwargs: Mapping[str, int],
):
pass
with pytest.raises(RedefinedBaseMovementError):
BadStage2()
def _set_axis_direction(dummy_stage, direction):
"""Set the axis direction directly even if not connected to server."""
try:
dummy_stage.axis_inverted = direction
except NotConnectedToServerError:
pass
def test_apply_axis_direction_all_pos(dummy_stage):
"""Test the apply axis direction function behaves as expected when axis +v3."""
# Directly create a stage not through a ThingServer to access private methods
_set_axis_direction(dummy_stage, {"x": False, "y": False, "z": False})
# A list of positions to try
positions = [
[1, 2, 3], # list
{"x": 1, "y": 2, "z": 3}, # mapping
{"x": 1, "z": 2, "y": 3}, # mapping out of order
{"x": 3}, # Mapping with only 1 value
{"x": 1, "z": 2}, # Mapping with only 2 values
]
for pos in positions:
assert dummy_stage._apply_axis_direction(pos) == pos
# Check tuple separately as it gets converted to list
assert dummy_stage._apply_axis_direction((1, 2, 0)) == [1, 2, 0]
def test_apply_axis_direction_mixed(dummy_stage):
"""Test the apply axis direction function behaves as expected when axis dirs are mixed."""
# Make x and z negative
_set_axis_direction(dummy_stage, {"x": True, "y": False, "z": True})
# A list of (input position, output position) to try
position_pairs = [
([1, 2, 3], [-1, 2, -3]), # list
([1, "2", "3"], [-1, 2, -3]), # list with strings
((1, 2, 0), [-1, 2, 0]), # tuple (gets converted to list)
({"x": 1, "y": 2, "z": 3}, {"x": -1, "y": 2, "z": -3}), # mapping
({"x": 1, "z": 2, "y": 3}, {"x": -1, "z": -2, "y": 3}), # mapping out of order
({"x": 1, "y": "2", "z": "3"}, {"x": -1, "y": 2, "z": -3}), # mapping w strings
({"x": 3}, {"x": -3}), # Mapping with only 1 value
({"x": 1, "z": 2}, {"x": -1, "z": -2}), # Mapping with only 2 values
]
for pos, expected_pos in position_pairs:
assert dummy_stage._apply_axis_direction(pos) == expected_pos
def test_apply_axis_errors(dummy_stage):
"""Test the apply axis direction returns appropriate errors."""
with pytest.raises(TypeError):
dummy_stage._apply_axis_direction(None)
with pytest.raises(TypeError):
dummy_stage._apply_axis_direction("Onwards!")
with pytest.raises(KeyError):
dummy_stage._apply_axis_direction({"x": -1, "y": 2, "z": 4, "up": -3})
with pytest.raises(KeyError):
dummy_stage._apply_axis_direction({"x": -1, "y": 2, "up": -3})
def test_default_values(stage_client, dummy_stage):
"""Check the default values for the dummy stage."""
# axes are x, y, z (note that going through the thing, client the tuple is
# converted to a list.
assert stage_client.axis_names == ["x", "y", "z"]
# position starts at 0, 0, 0
assert stage_client.position == {"x": 0, "y": 0, "z": 0}
# axis direction starts is -1, 1, 1 for the dummy stage
assert stage_client.axis_inverted == {"x": True, "y": False, "z": False}
# And check the thing state
assert dummy_stage.thing_state == {"position": {"x": 0, "y": 0, "z": 0}}
def test_direction_inversion(stage_client, dummy_stage):
"""Check axes invert as expected when called."""
# Can't set an arbitrary value via a client as read only:
with pytest.raises(HTTPStatusError) as excinfo:
stage_client.axis_inverted = {"x": 2, "y": 1, "z": 1}
# Read only should set a 405 error code
assert excinfo.value.response.status_code == 405
# And not modify th initial value
assert stage_client.axis_inverted == {"x": True, "y": False, "z": False}
stage_client.invert_axis_direction(axis="x")
assert stage_client.axis_inverted == {"x": False, "y": False, "z": False}
stage_client.invert_axis_direction(axis="x")
assert stage_client.axis_inverted == {"x": True, "y": False, "z": False}
stage_client.invert_axis_direction(axis="y")
assert stage_client.axis_inverted == {"x": True, "y": True, "z": False}
stage_client.invert_axis_direction(axis="y")
assert stage_client.axis_inverted == {"x": True, "y": False, "z": False}
stage_client.invert_axis_direction(axis="z")
assert stage_client.axis_inverted == {"x": True, "y": False, "z": True}
stage_client.invert_axis_direction(axis="z")
assert stage_client.axis_inverted == {"x": True, "y": False, "z": False}
# Should error if axis doesn't exist, this is a KeyError in the server
with pytest.raises(KeyError):
dummy_stage.invert_axis_direction(axis="theta")
# But a 422 over HTTP
with pytest.raises(HTTPStatusError) as excinfo:
stage_client.invert_axis_direction(axis="theta")
assert excinfo.value.response.status_code == 422
def _test_move_relative(stage_client, dummy_stage, axis_inverted, path):
"""Test moving relative, ensuring position and hardware position behave as expected.
:param axis_inverted: Is used to set the inversion.
:param path: The 3d path to move over, generated by hypothesis.
"""
_set_axis_direction(dummy_stage, axis_inverted)
# Explicitly do axes calculation here to check logic in main code.
x_dir = -1 if axis_inverted["x"] else 1
y_dir = -1 if axis_inverted["y"] else 1
z_dir = -1 if axis_inverted["z"] else 1
position = list(stage_client.position.values())
for movement in path:
stage_client.move_relative(x=movement[0], y=movement[1], z=movement[2])
position = [pos + move for pos, move in zip(position, movement)]
stage_pos = stage_client.get_xyz_position()
hw_pos = dummy_stage._hardware_position
assert position[0] == stage_pos[0] == hw_pos["x"] * x_dir
assert position[1] == stage_pos[1] == hw_pos["y"] * y_dir
assert position[2] == stage_pos[2] == hw_pos["z"] * z_dir
@given(path=path3d)
@settings(
max_examples=3,
suppress_health_check=[HealthCheck.function_scoped_fixture],
deadline=10000,
)
def test_move_relative(stage_client, dummy_stage, path):
"""Loop over different inversion options and check that the stage moves as expected.
3 paths are tried for each case of axis inversion. This checks both that the
reported position changes as is input in path, and that the hardware position is
inverted when appropriate.
"""
# Note that the fixture is not reset. This is fine, because the stage_should work
# no matter the starting position.
axis_names = stage_client.axis_names
# Create every combination of True/False for x,y,z.
inversion_combinations = [
dict(zip(axis_names, inverted))
for inverted in itertools.product([True, False], repeat=len(axis_names))
]
print(path)
for axis_inverted in inversion_combinations:
_test_move_relative(
stage_client=stage_client,
dummy_stage=dummy_stage,
axis_inverted=axis_inverted,
path=path,
)
def _test_move_absolute(stage_client, dummy_stage, axis_inverted, path):
"""Test moving relative, ensuring position and hardware position behave as expected.
:param axis_inverted: Is used to set the inversion.
:param path: The 3d path to move over, generated by hypothesis.
"""
_set_axis_direction(dummy_stage, axis_inverted)
# Explicitly do axes calculation here to check logic in main code.
x_dir = -1 if axis_inverted["x"] else 1
y_dir = -1 if axis_inverted["y"] else 1
z_dir = -1 if axis_inverted["z"] else 1
position = list(stage_client.position.values())
for move_to in path:
stage_client.move_absolute(x=move_to[0], y=move_to[1], z=move_to[2])
position = move_to
stage_pos = stage_client.get_xyz_position()
hw_pos = dummy_stage._hardware_position
assert position[0] == stage_pos[0] == hw_pos["x"] * x_dir
assert position[1] == stage_pos[1] == hw_pos["y"] * y_dir
assert position[2] == stage_pos[2] == hw_pos["z"] * z_dir
@given(path=path3d)
@settings(
max_examples=3,
suppress_health_check=[HealthCheck.function_scoped_fixture],
deadline=10000,
)
def test_move_absolute(stage_client, dummy_stage, path):
"""Loop over different inversion options and check that the stage moves as expected.
3 paths are tried for each case of axis inversion. This checks both that the
reported position changes as is input in path, and that the hardware position is
inverted when appropriate.
"""
# Note that the fixture is not reset. This is fine, because the stage_should work
# no matter the starting position.
axis_names = stage_client.axis_names
# Create every combination of True/False for x,y,z.
inversion_combinations = [
dict(zip(axis_names, inverted))
for inverted in itertools.product([True, False], repeat=len(axis_names))
]
print(path)
for axis_inverted in inversion_combinations:
_test_move_absolute(
stage_client=stage_client,
dummy_stage=dummy_stage,
axis_inverted=axis_inverted,
path=path,
)
def test_thing_description_equivalence(dummy_stage, mocker):
"""Stop extra actions getting added to child classes without explicit approval.
To add an extra action to a stage this test needs to be updated, highlighting it at
review. This tests should explain why the action isn't on the general base class.
"""
mock_sangaboard = mocker.Mock()
mocker.patch.dict("sys.modules", {"sangaboard": mock_sangaboard})
from openflexure_microscope_server.things.stage.sangaboard import SangaboardThing
# Flash LED isn't a standard stage action, most stages do not control illumination.
extra_sanga_actions = ["flash_led"]
base_td = BaseStage().thing_description()
base_actions = set(base_td.actions.keys())
base_properties = set(base_td.properties.keys())
dummy_td = dummy_stage.thing_description()
dummy_actions = set(dummy_td.actions.keys())
dummy_properties = set(dummy_td.properties.keys())
sanga_td = SangaboardThing().thing_description()
# Remove known extra actions
sanga_actions = list(sanga_td.actions.keys())
for action in extra_sanga_actions:
index = sanga_actions.index(action)
sanga_actions.pop(index)
sanga_actions = set(sanga_actions)
sanga_properties = set(sanga_td.properties.keys())
assert sanga_actions == dummy_actions == base_actions
assert sanga_properties == dummy_properties == base_properties

View file

@ -272,15 +272,19 @@ export default {
var x_rel = 0;
var y_rel = 0;
var z_rel = 0;
// 37 corresponds to the left key
if (37 in this.arrowKeysDown) {
x_rel = x_rel + 1;
}
if (39 in this.arrowKeysDown) {
x_rel = x_rel - 1;
}
// 39 corresponds to the right key
if (39 in this.arrowKeysDown) {
x_rel = x_rel + 1;
}
// 38 corresponds to the up key
if (38 in this.arrowKeysDown) {
y_rel = y_rel + 1;
}
// 40 corresponds to the down key
if (40 in this.arrowKeysDown) {
y_rel = y_rel - 1;
}

View file

@ -5,7 +5,7 @@
<li>
<a class="uk-accordion-title" href="#">Configure</a>
<div class="uk-accordion-content">
<b>Step Size</b>
<b>Keyboard Step Size</b>
<div class="uk-grid-small uk-child-width-1-3" uk-grid>
<div>
<label class="uk-form-label" for="form-stacked-text">x</label>
@ -57,17 +57,9 @@
name="inputStepZz"
/>
</div>
<label
><input
v-model="invert.z"
class="uk-checkbox"
type="checkbox"
/>
Invert z</label
>
</div>
</div>
<br>
<action-button
thing="stage"
action="set_zero_position"

View file

@ -1,20 +1,66 @@
<template>
<div id="stageSettings" class="uk-width-large">
The microscope stage is a <b>{{ stageType }}</b
>. There are no settings to adjust here: to set the geometry of your stage
(i.e. switch between delta and cartesian), you will need to adjust the
configuration of your microscope, which cannot be done while it is running.
<div id="stageSettings" class="uk-width-large" v-observe-visibility="visibilityChanged">
The microscope stage is a <b>{{ stageType }}</b>
<div>
<div class="uk-margin">
<p>
<br>
Your z motor is currently {{ this.z_inverted }} inverted.
<br>
<br>
We expect that moving in +z:
<ul>
<li> Moves your objective up, towards the sample and illumination</li>
<li> Turns the exposed z gear anti-clockwise (when viewed from above)</li>
</ul>
If this is not the case, click the button below to switch.
</p>
<div class="uk-margin">
<div class="uk-margin">
<action-button
class="uk-width-1-2"
thing="stage"
action="invert_axis_direction"
:submit-data="{ axis: 'z' }"
submit-label="Invert z"
@response=readAxis()
/>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import ActionButton from "../../labThingsComponents/actionButton.vue";
export default {
name: "StageSettings",
components: {},
components: {
ActionButton,
},
data: function() {
return {};
return {
z_inverted: "",
};
},
methods:{
visibilityChanged(isVisible) {
if (isVisible) {
this.readAxis();
}
},
async readAxis() {
let axes_inverted = await this.readThingProperty(
"stage",
"axis_inverted"
);
this.z_inverted = axes_inverted['z'] ? "" : "not ";
}
},
computed: {