From fd2decf58e0e489f916c4d767fd13c10bab07029 Mon Sep 17 00:00:00 2001 From: Hongquan Li Date: Sat, 28 Feb 2026 18:34:37 -0800 Subject: [PATCH 1/4] feat: Add hardware trigger mode setting to Settings > Advanced Add Edge/Level trigger mode dropdown in Hardware Configuration section. The setting takes effect on next acquisition without restart by reading HARDWARE_TRIGGER_MODE via control._def module reference instead of stale local bindings from wildcard import. Only visible when camera type is Toupcam or Tucsen (cameras that support level trigger). Co-Authored-By: Claude Opus 4.6 --- software/control/camera_toupcam.py | 3 ++- software/control/core/live_controller.py | 3 ++- software/control/gui_hcs.py | 2 +- software/control/widgets.py | 24 ++++++++++++++++++++++++ 4 files changed, 29 insertions(+), 3 deletions(-) diff --git a/software/control/camera_toupcam.py b/software/control/camera_toupcam.py index d82b0b6bd..3470b2c04 100644 --- a/software/control/camera_toupcam.py +++ b/software/control/camera_toupcam.py @@ -16,6 +16,7 @@ CameraFrame, ) from squid.config import CameraConfig, ToupcamCameraModel +import control._def from control._def import * import threading @@ -833,7 +834,7 @@ def _set_acquisition_mode_imp(self, acquisition_mode: CameraAcquisitionMode): self._camera.put_Option(toupcam.TOUPCAM_OPTION_TRIGGER, trigger_option_value) if acquisition_mode == CameraAcquisitionMode.HARDWARE_TRIGGER: - if HARDWARE_TRIGGER_MODE == HardwareTriggerMode.LEVEL: + if control._def.HARDWARE_TRIGGER_MODE == HardwareTriggerMode.LEVEL: try: self._camera.put_Option(toupcam.TOUPCAM_OPTION_TRIGGER, 2) except toupcam.HRESULTException as ex: diff --git a/software/control/core/live_controller.py b/software/control/core/live_controller.py index 7d69c174c..c5915d43b 100644 --- a/software/control/core/live_controller.py +++ b/software/control/core/live_controller.py @@ -8,6 +8,7 @@ from control.microcontroller import Microcontroller from squid.abc import CameraAcquisitionMode, AbstractCamera +import control._def from control._def import * from control.core.config.utils import apply_confocal_override from control.models import merge_channel_configs @@ -428,7 +429,7 @@ def set_trigger_mode(self, mode): if self.is_live and self.use_internal_timer_for_hardware_trigger: self._start_triggerred_acquisition() - self.microscope.low_level_drivers.microcontroller.set_trigger_mode(HARDWARE_TRIGGER_MODE) + self.microscope.low_level_drivers.microcontroller.set_trigger_mode(control._def.HARDWARE_TRIGGER_MODE) if mode == TriggerMode.CONTINUOUS: if (self.trigger_mode == TriggerMode.SOFTWARE) or ( diff --git a/software/control/gui_hcs.py b/software/control/gui_hcs.py index cc69e8ccd..ceb780798 100644 --- a/software/control/gui_hcs.py +++ b/software/control/gui_hcs.py @@ -845,7 +845,7 @@ def setup_hardware(self, skip_init: bool = False): if DEFAULT_TRIGGER_MODE == TriggerMode.HARDWARE: print("Setting acquisition mode to HARDWARE_TRIGGER") self.camera.set_acquisition_mode(squid.abc.CameraAcquisitionMode.HARDWARE_TRIGGER) - self.microcontroller.set_trigger_mode(HARDWARE_TRIGGER_MODE) + self.microcontroller.set_trigger_mode(control._def.HARDWARE_TRIGGER_MODE) else: self.camera.set_acquisition_mode(squid.abc.CameraAcquisitionMode.SOFTWARE_TRIGGER) self.camera.add_frame_callback(self.streamHandler.get_frame_callback()) diff --git a/software/control/widgets.py b/software/control/widgets.py index 4dee98ac5..2891ae8ff 100644 --- a/software/control/widgets.py +++ b/software/control/widgets.py @@ -1346,6 +1346,22 @@ def _create_advanced_tab(self): self.illumination_factor.setValue(self._get_config_float("GENERAL", "illumination_intensity_factor", 0.6)) hw_layout.addRow("Illumination Intensity Factor:", self.illumination_factor) + self.hw_trigger_mode_combo = QComboBox() + self.hw_trigger_mode_combo.addItems(["Edge", "Level"]) + self.hw_trigger_mode_combo.setToolTip( + "Edge: Fixed pulse width (TRIGGER_PULSE_LENGTH_us)\n" "Level: Variable pulse width (illumination_on_time)" + ) + hw_trigger_value = self._get_config_int("GENERAL", "hardware_trigger_mode", 0) + self.hw_trigger_mode_combo.setCurrentIndex(hw_trigger_value) + self.hw_trigger_mode_label = QLabel("Hardware Trigger Mode:") + hw_layout.addRow(self.hw_trigger_mode_label, self.hw_trigger_mode_combo) + + # Only show for cameras that support level trigger + camera_type = self._get_config_value("GENERAL", "camera_type", "Default") + supports_level_trigger = camera_type in ("Toupcam", "Tucsen") + self.hw_trigger_mode_label.setVisible(supports_level_trigger) + self.hw_trigger_mode_combo.setVisible(supports_level_trigger) + hw_group.content.addLayout(hw_layout) layout.addWidget(hw_group) @@ -1837,6 +1853,8 @@ def _apply_settings(self) -> bool: self.config.set("GENERAL", "led_matrix_g_factor", str(self.led_g_factor.value())) self.config.set("GENERAL", "led_matrix_b_factor", str(self.led_b_factor.value())) self.config.set("GENERAL", "illumination_intensity_factor", str(self.illumination_factor.value())) + self.config.set("GENERAL", "hardware_trigger_mode", str(self.hw_trigger_mode_combo.currentIndex())) + control._def.HARDWARE_TRIGGER_MODE = self.hw_trigger_mode_combo.currentIndex() # Advanced - Development Settings self.config.set( @@ -2202,6 +2220,12 @@ def _get_changes(self): if not self._floats_equal(old_val, new_val): changes.append(("Illumination Intensity Factor", str(old_val), str(new_val), False)) + old_val = self._get_config_int("GENERAL", "hardware_trigger_mode", 0) + new_val = self.hw_trigger_mode_combo.currentIndex() + if old_val != new_val: + mode_names = ["Edge", "Level"] + changes.append(("Hardware Trigger Mode", mode_names[old_val], mode_names[new_val], False)) + # Advanced - Development Settings # Enable/disable requires restart (for warning banner/dialog), but speed/compression # take effect on next acquisition since each acquisition starts a fresh subprocess From 6d0ab9da5d26c1fd0e1caaf03835e2946587c69c Mon Sep 17 00:00:00 2001 From: Hongquan Li Date: Sat, 28 Feb 2026 18:48:57 -0800 Subject: [PATCH 2/4] fix: Clamp INI trigger mode value and use HardwareTriggerMode constants - Clamp hardware_trigger_mode from INI to valid range [0,1] to prevent IndexError in change detection if INI is manually corrupted - Use HardwareTriggerMode.EDGE/LEVEL constants when setting runtime value instead of raw int, for type safety Co-Authored-By: Claude Opus 4.6 --- software/control/widgets.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/software/control/widgets.py b/software/control/widgets.py index 2891ae8ff..6964872e5 100644 --- a/software/control/widgets.py +++ b/software/control/widgets.py @@ -1352,6 +1352,7 @@ def _create_advanced_tab(self): "Edge: Fixed pulse width (TRIGGER_PULSE_LENGTH_us)\n" "Level: Variable pulse width (illumination_on_time)" ) hw_trigger_value = self._get_config_int("GENERAL", "hardware_trigger_mode", 0) + hw_trigger_value = max(0, min(hw_trigger_value, 1)) self.hw_trigger_mode_combo.setCurrentIndex(hw_trigger_value) self.hw_trigger_mode_label = QLabel("Hardware Trigger Mode:") hw_layout.addRow(self.hw_trigger_mode_label, self.hw_trigger_mode_combo) @@ -1854,7 +1855,8 @@ def _apply_settings(self) -> bool: self.config.set("GENERAL", "led_matrix_b_factor", str(self.led_b_factor.value())) self.config.set("GENERAL", "illumination_intensity_factor", str(self.illumination_factor.value())) self.config.set("GENERAL", "hardware_trigger_mode", str(self.hw_trigger_mode_combo.currentIndex())) - control._def.HARDWARE_TRIGGER_MODE = self.hw_trigger_mode_combo.currentIndex() + trigger_modes = [HardwareTriggerMode.EDGE, HardwareTriggerMode.LEVEL] + control._def.HARDWARE_TRIGGER_MODE = trigger_modes[self.hw_trigger_mode_combo.currentIndex()] # Advanced - Development Settings self.config.set( From 8ec8c6016cf5b6c68030392edd2c529293a9687e Mon Sep 17 00:00:00 2001 From: Hongquan Li Date: Sat, 28 Feb 2026 19:08:12 -0800 Subject: [PATCH 3/4] refactor: Convert HardwareTriggerMode to IntEnum for proper validation Change HardwareTriggerMode from plain class to IntEnum, enabling HardwareTriggerMode(value) for validation instead of manual clamping with magic numbers. Simplifies widgets.py code that reads/writes the setting. Co-Authored-By: Claude Opus 4.6 --- software/control/_def.py | 4 ++-- software/control/widgets.py | 21 ++++++++++++--------- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/software/control/_def.py b/software/control/_def.py index 73099f023..ee4d8cb6c 100644 --- a/software/control/_def.py +++ b/software/control/_def.py @@ -7,7 +7,7 @@ import json import csv import squid.logging -from enum import Enum, auto +from enum import Enum, IntEnum, auto log = squid.logging.get_logger(__name__) @@ -1107,7 +1107,7 @@ class SOFTWARE_POS_LIMIT: DISPLAY_TOUPCAMER_BLACKLEVEL_SETTINGS = False -class HardwareTriggerMode: +class HardwareTriggerMode(IntEnum): EDGE = 0 # Fixed pulse width (TRIGGER_PULSE_LENGTH_us) LEVEL = 1 # Variable pulse width (illumination_on_time) diff --git a/software/control/widgets.py b/software/control/widgets.py index 6964872e5..4555c4ebf 100644 --- a/software/control/widgets.py +++ b/software/control/widgets.py @@ -1351,8 +1351,10 @@ def _create_advanced_tab(self): self.hw_trigger_mode_combo.setToolTip( "Edge: Fixed pulse width (TRIGGER_PULSE_LENGTH_us)\n" "Level: Variable pulse width (illumination_on_time)" ) - hw_trigger_value = self._get_config_int("GENERAL", "hardware_trigger_mode", 0) - hw_trigger_value = max(0, min(hw_trigger_value, 1)) + try: + hw_trigger_value = HardwareTriggerMode(self._get_config_int("GENERAL", "hardware_trigger_mode", 0)) + except ValueError: + hw_trigger_value = HardwareTriggerMode.EDGE self.hw_trigger_mode_combo.setCurrentIndex(hw_trigger_value) self.hw_trigger_mode_label = QLabel("Hardware Trigger Mode:") hw_layout.addRow(self.hw_trigger_mode_label, self.hw_trigger_mode_combo) @@ -1855,8 +1857,7 @@ def _apply_settings(self) -> bool: self.config.set("GENERAL", "led_matrix_b_factor", str(self.led_b_factor.value())) self.config.set("GENERAL", "illumination_intensity_factor", str(self.illumination_factor.value())) self.config.set("GENERAL", "hardware_trigger_mode", str(self.hw_trigger_mode_combo.currentIndex())) - trigger_modes = [HardwareTriggerMode.EDGE, HardwareTriggerMode.LEVEL] - control._def.HARDWARE_TRIGGER_MODE = trigger_modes[self.hw_trigger_mode_combo.currentIndex()] + control._def.HARDWARE_TRIGGER_MODE = HardwareTriggerMode(self.hw_trigger_mode_combo.currentIndex()) # Advanced - Development Settings self.config.set( @@ -2222,11 +2223,13 @@ def _get_changes(self): if not self._floats_equal(old_val, new_val): changes.append(("Illumination Intensity Factor", str(old_val), str(new_val), False)) - old_val = self._get_config_int("GENERAL", "hardware_trigger_mode", 0) - new_val = self.hw_trigger_mode_combo.currentIndex() - if old_val != new_val: - mode_names = ["Edge", "Level"] - changes.append(("Hardware Trigger Mode", mode_names[old_val], mode_names[new_val], False)) + try: + old_mode = HardwareTriggerMode(self._get_config_int("GENERAL", "hardware_trigger_mode", 0)) + except ValueError: + old_mode = HardwareTriggerMode.EDGE + new_mode = HardwareTriggerMode(self.hw_trigger_mode_combo.currentIndex()) + if old_mode != new_mode: + changes.append(("Hardware Trigger Mode", old_mode.name.title(), new_mode.name.title(), False)) # Advanced - Development Settings # Enable/disable requires restart (for warning banner/dialog), but speed/compression From 3e14022f20914733feed9363d35913d1ab2559a3 Mon Sep 17 00:00:00 2001 From: Hongquan Li Date: Sat, 28 Feb 2026 19:55:42 -0800 Subject: [PATCH 4/4] fix: Add logging for invalid INI values and build combo from enum - Log warning when hardware_trigger_mode INI value is invalid instead of silently falling back to EDGE - Build combo box items from HardwareTriggerMode enum members instead of hardcoding ["Edge", "Level"], eliminating duplication Co-Authored-By: Claude Opus 4.6 --- software/control/widgets.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/software/control/widgets.py b/software/control/widgets.py index 4555c4ebf..2a7ccd31b 100644 --- a/software/control/widgets.py +++ b/software/control/widgets.py @@ -1347,13 +1347,16 @@ def _create_advanced_tab(self): hw_layout.addRow("Illumination Intensity Factor:", self.illumination_factor) self.hw_trigger_mode_combo = QComboBox() - self.hw_trigger_mode_combo.addItems(["Edge", "Level"]) + for member in HardwareTriggerMode: + self.hw_trigger_mode_combo.addItem(member.name.title(), member.value) self.hw_trigger_mode_combo.setToolTip( "Edge: Fixed pulse width (TRIGGER_PULSE_LENGTH_us)\n" "Level: Variable pulse width (illumination_on_time)" ) try: hw_trigger_value = HardwareTriggerMode(self._get_config_int("GENERAL", "hardware_trigger_mode", 0)) except ValueError: + raw = self._get_config_int("GENERAL", "hardware_trigger_mode", 0) + logger.warning("Invalid hardware_trigger_mode=%d in INI, defaulting to EDGE", raw) hw_trigger_value = HardwareTriggerMode.EDGE self.hw_trigger_mode_combo.setCurrentIndex(hw_trigger_value) self.hw_trigger_mode_label = QLabel("Hardware Trigger Mode:") @@ -2226,6 +2229,8 @@ def _get_changes(self): try: old_mode = HardwareTriggerMode(self._get_config_int("GENERAL", "hardware_trigger_mode", 0)) except ValueError: + raw = self._get_config_int("GENERAL", "hardware_trigger_mode", 0) + logger.warning("Invalid hardware_trigger_mode=%d in INI, defaulting to EDGE", raw) old_mode = HardwareTriggerMode.EDGE new_mode = HardwareTriggerMode(self.hw_trigger_mode_combo.currentIndex()) if old_mode != new_mode: