diff --git a/software/control/camera_toupcam.py b/software/control/camera_toupcam.py index 8b1c085ce..0e54eb45e 100644 --- a/software/control/camera_toupcam.py +++ b/software/control/camera_toupcam.py @@ -1128,6 +1128,15 @@ def _set_acquisition_mode_imp(self, acquisition_mode: CameraAcquisitionMode): raise ValueError(f"Do not know how to handle {acquisition_mode=}") self._camera.put_Option(toupcam.TOUPCAM_OPTION_TRIGGER, trigger_option_value) + if acquisition_mode == CameraAcquisitionMode.SOFTWARE_TRIGGER: + # The SDK moves the trigger source to software along with the option only while the stream runs. + # Switched while stopped, the source stays on the hardware input (GPIO0) and every software + # trigger is ignored, so set it explicitly, as the hardware branch below does for its source. + try: + self._camera.IoControl(1, toupcam.TOUPCAM_IOCONTROLTYPE_SET_TRIGGERSOURCE, 5) # 5 => software + except toupcam.HRESULTException as ex: + self._log.warning("Unable to set trigger source to software: " + hresult_checker(ex)) + if acquisition_mode == CameraAcquisitionMode.HARDWARE_TRIGGER: if HARDWARE_TRIGGER_MODE == HardwareTriggerMode.LEVEL: try: diff --git a/software/tests/control/test_camera_toupcam_trigger_source.py b/software/tests/control/test_camera_toupcam_trigger_source.py new file mode 100644 index 000000000..85544767d --- /dev/null +++ b/software/tests/control/test_camera_toupcam_trigger_source.py @@ -0,0 +1,61 @@ +"""Regression test: switching the ToupTek camera to software trigger while its stream is stopped. + +Measured on an ITR3CMOS26000KMA: the SDK moves the trigger source (IOCONTROLTYPE TRIGGERSOURCE: 1 = GPIO0, +5 = software) along with OPTION_TRIGGER only while the stream runs. Switched from hardware to software +trigger while stopped, the source stays on GPIO0 and every software trigger is ignored, which aborted +Record + Z-Stack z-stacks started from hardware-trigger live view. The fake SDK below models that. +""" + +import squid.logging +from squid.abc import CameraAcquisitionMode +import control.camera_toupcam as camera_toupcam +import control.toupcam as toupcam + +_GPIO0, _SOFTWARE = 1, 5 + + +class _FakeSDK: + def __init__(self): + self.streaming = False + self.option_trigger = 1 + self.source = _SOFTWARE + + def put_Option(self, option, value): + if option == toupcam.TOUPCAM_OPTION_TRIGGER: + self.option_trigger = value + if self.streaming and value == 1: + self.source = _SOFTWARE # the SDK follows the option only while streaming + + def IoControl(self, line, kind, value): + if kind == toupcam.TOUPCAM_IOCONTROLTYPE_SET_TRIGGERSOURCE: + self.source = value + return 0 + + +def _make_camera(): + cam = object.__new__(camera_toupcam.ToupcamCamera) + cam._camera = _FakeSDK() + cam._log = squid.logging.get_logger("test_toupcam_trigger_source") + cam.get_exposure_time = lambda: 20.0 + cam.set_exposure_time = lambda exposure_ms: None + return cam + + +def test_software_trigger_set_while_stopped_moves_the_source_off_the_hardware_input(): + cam = _make_camera() + cam._set_acquisition_mode_imp(CameraAcquisitionMode.HARDWARE_TRIGGER) + assert cam._camera.source == _GPIO0 + + cam._camera.streaming = False + cam._set_acquisition_mode_imp(CameraAcquisitionMode.SOFTWARE_TRIGGER) + + assert cam._camera.option_trigger == 1 + assert cam._camera.source == _SOFTWARE + + +def test_hardware_trigger_still_selects_the_hardware_input(): + cam = _make_camera() + cam._set_acquisition_mode_imp(CameraAcquisitionMode.SOFTWARE_TRIGGER) + cam._set_acquisition_mode_imp(CameraAcquisitionMode.HARDWARE_TRIGGER) + assert cam._camera.option_trigger == 2 + assert cam._camera.source == _GPIO0