From f77542110ed91aca8df1036f0f36eafa43f5a9eb Mon Sep 17 00:00:00 2001 From: Joe Knapper Date: Tue, 30 Jun 2026 14:20:41 +0100 Subject: [PATCH 01/12] Calibration wizard asks about motor direction --- .../modalComponents/calibrationWizard.vue | 10 ++ .../calibrationWizardTask.vue | 1 + .../zmotordirectionstep.vue | 99 +++++++++++++++++++ 3 files changed, 110 insertions(+) create mode 100644 webapp/src/components/modalComponents/calibrationWizardComponents/zmotordirectionstep.vue diff --git a/webapp/src/components/modalComponents/calibrationWizard.vue b/webapp/src/components/modalComponents/calibrationWizard.vue index 1e54ec6b..55618348 100644 --- a/webapp/src/components/modalComponents/calibrationWizard.vue +++ b/webapp/src/components/modalComponents/calibrationWizard.vue @@ -28,6 +28,7 @@ + + \ No newline at end of file From fb96568df4728bbcc8d68c5d7679a2bf4f9e8715 Mon Sep 17 00:00:00 2001 From: Joe Knapper Date: Tue, 30 Jun 2026 14:23:28 +0100 Subject: [PATCH 02/12] Linting --- .../zmotordirectionstep.vue | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/webapp/src/components/modalComponents/calibrationWizardComponents/zmotordirectionstep.vue b/webapp/src/components/modalComponents/calibrationWizardComponents/zmotordirectionstep.vue index 454fff7d..3b122294 100644 --- a/webapp/src/components/modalComponents/calibrationWizardComponents/zmotordirectionstep.vue +++ b/webapp/src/components/modalComponents/calibrationWizardComponents/zmotordirectionstep.vue @@ -1,7 +1,10 @@ diff --git a/webapp/src/components/modalComponents/calibrationWizardComponents/zMotorDirectionStep.vue b/webapp/src/components/modalComponents/calibrationWizardComponents/zMotorDirectionStep.vue index f7f3505b..23d73660 100644 --- a/webapp/src/components/modalComponents/calibrationWizardComponents/zMotorDirectionStep.vue +++ b/webapp/src/components/modalComponents/calibrationWizardComponents/zMotorDirectionStep.vue @@ -69,7 +69,7 @@ export default { }, mounted() { - this.$emit("awaiting-user", true); + this.checkCalibrationState(); }, methods: { @@ -94,7 +94,9 @@ export default { }, // Once a direction is selected, advance selectDir() { - this.$emit("advance"); + this.invokeAction("stage", "mark_calibration_complete").then(() => { + this.$emit("advance"); + }); }, // If motor moves clockwise, invert z direction, then progress to next step triggerClockwise() { @@ -102,6 +104,10 @@ export default { this.selectDir(); }); }, + async checkCalibrationState() { + const needsCal = await this.readThingProperty("stage", "calibration_required"); + this.$emit("awaiting-user", needsCal); + }, }, }; From 274f698fab6571527f5c99efc51c1e288bed4e36 Mon Sep 17 00:00:00 2001 From: Julian Stirling Date: Tue, 7 Jul 2026 15:11:30 +0100 Subject: [PATCH 11/12] Add z-direction calibration action to sangaboard --- .../things/stage/__init__.py | 15 +++---- .../things/stage/sangaboard.py | 34 ++++++++++++++-- .../zMotorDirectionStep.vue | 39 +++++++++++-------- webapp/src/tests/unit/overrides.js | 13 +++++++ 4 files changed, 74 insertions(+), 27 deletions(-) diff --git a/src/openflexure_microscope_server/things/stage/__init__.py b/src/openflexure_microscope_server/things/stage/__init__.py index 11c22097..f70eabfe 100644 --- a/src/openflexure_microscope_server/things/stage/__init__.py +++ b/src/openflexure_microscope_server/things/stage/__init__.py @@ -166,8 +166,14 @@ class BaseStage(lt.Thing): ) """Used to convert coordinates between the program frame and the hardware frame.""" - calibration_required: bool = lt.setting(default=False) - """Whether calibration of the stage z axis is required.""" + @lt.property + def calibration_required(self) -> bool: + """Whether the stage requires calibration. + + Currently the only calibration method for the stage is the z-gear rotation + direction in the sangaboard stage. + """ + return False def update_position(self) -> None: """Update the position property from the stage.""" @@ -538,8 +544,3 @@ class BaseStage(lt.Thing): This method provides the interface expected by the camera_stage_mapping. """ self.move_absolute(x=xyz_pos[0], y=xyz_pos[1], z=xyz_pos[2]) - - @lt.action - def mark_calibration_complete(self) -> None: - """Mark stage calibration as complete.""" - self.calibration_required = False diff --git a/src/openflexure_microscope_server/things/stage/sangaboard.py b/src/openflexure_microscope_server/things/stage/sangaboard.py index ed34b41d..7ab3103f 100644 --- a/src/openflexure_microscope_server/things/stage/sangaboard.py +++ b/src/openflexure_microscope_server/things/stage/sangaboard.py @@ -80,6 +80,37 @@ class SangaboardThing(BaseStage): ) """Used to convert coordinates between the program frame and the hardware frame.""" + z_axis_calibrated: bool = lt.setting(default=False, readonly=True) + + @lt.property + def calibration_required(self) -> bool: + """Whether the stage requires calibration. + + Return False only if z-direction has been calibrated. + """ + return not self.z_axis_calibrated + + @lt.action + def calibrate_z_direction( + self, positive_motion: Literal["clockwise", "anti-clockwise"] + ) -> None: + """Calibrate the z motor direction based on the gear rotation direction. + + Positive z motion should turn the exposed gear anti-clockwise. Calling this + action marks the z motor as calibrated. + + :param positive_motion: The direction the gear moved during pisitive z motion. + """ + if positive_motion == "clockwise": + # Wrong direction, mark as such. + self.invert_axis_direction(axis="z") + elif positive_motion != "anti-clockwise": + raise ValueError( + "Motion should be reported as either 'clockwise' or 'anti-clockwise'." + ) + # Set z_axis_calibrated to true once this function is called. + self.z_axis_calibrated = True + def _hardware_update_position(self) -> None: """Read position from the stage and set internal attribute _hardware_position. @@ -236,6 +267,3 @@ class SangaboardThing(BaseStage): self._sangaboard.query(f"{led_command} {on_brightness}") else: self._sangaboard.query(f"{led_command} 0") - - calibration_required: bool = lt.setting(default=True) - """By default, Sangaboard stage will require calibration of z direction.""" diff --git a/webapp/src/components/modalComponents/calibrationWizardComponents/zMotorDirectionStep.vue b/webapp/src/components/modalComponents/calibrationWizardComponents/zMotorDirectionStep.vue index 23d73660..b0d97e4f 100644 --- a/webapp/src/components/modalComponents/calibrationWizardComponents/zMotorDirectionStep.vue +++ b/webapp/src/components/modalComponents/calibrationWizardComponents/zMotorDirectionStep.vue @@ -21,13 +21,13 @@ src="/direction_AC.png" alt="Anti-clockwise" class="dir-image clickable" - @click="selectDir" + @click="reportDirection('anti-clockwise')" /> @@ -39,13 +39,13 @@ src="/direction_CW.png" alt="Clockwise" class="dir-image clickable" - @click="triggerClockwise" + @click="reportDirection('clockwise')" /> @@ -68,7 +68,7 @@ export default { }; }, - mounted() { + async mounted() { this.checkCalibrationState(); }, @@ -92,20 +92,25 @@ export default { } this.invokeAction("stage", "jog", { stop: true }); }, - // Once a direction is selected, advance - selectDir() { - this.invokeAction("stage", "mark_calibration_complete").then(() => { - this.$emit("advance"); - }); - }, - // If motor moves clockwise, invert z direction, then progress to next step - triggerClockwise() { - this.invokeAction("stage", "invert_axis_direction", { axis: "z" }).then(() => { - this.selectDir(); - }); + /** + * Report the direction to the microscope then advance to next pane in wizard. + */ + async reportDirection(direction) { + await this.invokeAction("stage", "calibrate_z_direction", { positive_motion: direction }); + this.$emit("advance"); }, async checkCalibrationState() { - const needsCal = await this.readThingProperty("stage", "calibration_required"); + let needsCal = await this.readThingProperty("stage", "calibration_required"); + if (needsCal) { + // If it needs calibration check the relevant calibration action exists. + const actions = this.thingDescription("stage").actions; + if (!("calibrate_z_direction" in actions)) { + console.error( + "Stage is requesting calibration but has no 'calibrate_z_direction' action", + ); + needsCal = false; + } + } this.$emit("awaiting-user", needsCal); }, }, diff --git a/webapp/src/tests/unit/overrides.js b/webapp/src/tests/unit/overrides.js index 53797e78..90ebdcaa 100644 --- a/webapp/src/tests/unit/overrides.js +++ b/webapp/src/tests/unit/overrides.js @@ -133,4 +133,17 @@ export const componentOverrides = { "cameraCalibrationSettings.vue": { props: { cameraUri: "" }, }, + "zMotorDirectionStep.vue": { + mocks: { + thingDescription: () => ({ + properties: { + test_property_1: {}, + test_property_2: {}, + }, + actions: { + calibrate_z_direction: {}, + }, + }), + }, + }, }; From 304fd6dfe38700a697de78cb5e7f924139bcdc49 Mon Sep 17 00:00:00 2001 From: Julian Stirling Date: Tue, 7 Jul 2026 16:07:30 +0100 Subject: [PATCH 12/12] Fix python unit tests for extra sangaboard action and setting --- tests/unit_tests/test_stage.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/tests/unit_tests/test_stage.py b/tests/unit_tests/test_stage.py index f922ef65..84c82305 100644 --- a/tests/unit_tests/test_stage.py +++ b/tests/unit_tests/test_stage.py @@ -603,10 +603,13 @@ def test_thing_description_equivalence(dummy_stage, mocker): dummy_properties = set(dummy_td.properties.keys()) sanga_td = create_thing_without_server(SangaboardThing).thing_description() - # Remove known extra actions - sanga_actions = list(sanga_td.actions.keys()) - sanga_actions = set(sanga_actions) - sanga_properties = set(sanga_td.properties.keys()) + + # Sangaboard has one extra action for calibrating z-motor direction. This is + # handled in the webapp calibration wizard, and an extra setting for persisting it + sanga_extra_actions = {"calibrate_z_direction"} + sanga_extra_properties = {"z_axis_calibrated"} + sanga_actions = set(sanga_td.actions.keys()) - sanga_extra_actions + sanga_properties = set(sanga_td.properties.keys()) - sanga_extra_properties assert sanga_actions == dummy_actions == base_actions assert sanga_properties == dummy_properties == base_properties