Skip to content

Commit f7e521f

Browse files
committed
Add per-camera hardware trigger settings
Introduce support for per-camera hardware trigger configuration in the camera config dialog. Imports TriggerConfigDialog and CameraTriggerSettings, wires the trigger settings button to open a modal, and adds _open_trigger_settings_dialog to commit edits, show the dialog, apply updates, and restart the preview if needed. Adds helpers: _ensure_default_trigger_config to initialize gentl.trigger defaults, _trigger_role_for_label to show trigger role in camera list labels, and _trigger_dict_for_cam to compare trigger settings when deciding to restart previews. Also integrates the hardware trigger field into the settings summary and ensures new/loaded cameras get a default trigger config.
1 parent 002c957 commit f7e521f

1 file changed

Lines changed: 80 additions & 2 deletions

File tree

dlclivegui/gui/camera_config/camera_config_dialog.py

Lines changed: 80 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,10 @@
1818
)
1919

2020
from ...cameras.factory import CameraFactory, DetectedCamera, apply_detected_identity, camera_identity_key
21-
from ...config import CameraSettings, MultiCameraSettings
21+
from ...config import CameraSettings, CameraTriggerSettings, MultiCameraSettings
2222
from .loaders import CameraLoadWorker, CameraProbeWorker, CameraScanState, DetectCamerasWorker
2323
from .preview import PreviewSession, PreviewState, apply_crop, apply_rotation, resize_to_fit, to_display_pixmap
24+
from .trigger_config_dialog import TriggerConfigDialog
2425
from .ui_blocks import setup_camera_config_dialog_ui
2526

2627
LOGGER = logging.getLogger(__name__)
@@ -328,6 +329,7 @@ def _connect_signals(self) -> None:
328329
self.active_cameras_list.currentRowChanged.connect(self._on_active_camera_selected)
329330
self.available_cameras_list.currentRowChanged.connect(self._on_available_camera_selected)
330331
self.available_cameras_list.itemDoubleClicked.connect(self._on_available_camera_double_clicked)
332+
self.trigger_settings_btn.clicked.connect(self._open_trigger_settings_dialog)
331333
self.apply_settings_btn.clicked.connect(self._apply_camera_settings)
332334
self.reset_settings_btn.clicked.connect(self._reset_selected_camera)
333335
self.preview_btn.clicked.connect(self._toggle_preview)
@@ -451,11 +453,24 @@ def _refresh_camera_labels(self) -> None:
451453
finally:
452454
cam_list.blockSignals(False)
453455

456+
def _trigger_role_for_label(self, cam: CameraSettings) -> str:
457+
backend = (cam.backend or "").lower()
458+
props = cam.properties if isinstance(cam.properties, dict) else {}
459+
ns = props.get(backend, {}) if isinstance(props.get(backend), dict) else {}
460+
trigger = ns.get("trigger", {})
461+
if not isinstance(trigger, dict):
462+
return "off"
463+
return str(trigger.get("role", "off") or "off").lower()
464+
454465
def _format_camera_label(self, cam: CameraSettings, index: int = -1) -> str:
455466
status = "✓" if cam.enabled else "○"
456467
this_id = f"{(cam.backend or '').lower()}:{cam.index}"
457468
dlc_indicator = " [DLC]" if this_id == self._dlc_camera_id and cam.enabled else ""
458-
return f"{status} {cam.name} [{cam.backend}:{cam.index}]{dlc_indicator}"
469+
470+
trigger_role = self._trigger_role_for_label(cam)
471+
trigger_indicator = "" if trigger_role in {"off", "disabled"} else f" [{trigger_role}]"
472+
473+
return f"{status} {cam.name} [{cam.backend}:{cam.index}]{trigger_indicator}{dlc_indicator}"
459474

460475
def _selected_detected_camera(self) -> DetectedCamera | None:
461476
row = self.available_cameras_list.currentRow()
@@ -514,6 +529,9 @@ def apply(widget, feature: str, label: str, *, allow_best_effort: bool = True):
514529
apply(self.cam_exposure, "set_exposure", "Exposure")
515530
apply(self.cam_gain, "set_gain", "Gain")
516531

532+
# Hardware trigger / sync
533+
apply(self.trigger_settings_btn, "hardware_trigger", "Hardware trigger")
534+
517535
def _set_preview_button_loading(self, loading: bool) -> None:
518536
if loading:
519537
self.preview_btn.setText("Cancel Loading")
@@ -800,6 +818,21 @@ def _on_active_camera_selected(self, row: int) -> None:
800818
self._load_camera_to_form(cam)
801819
self._start_probe_for_camera(cam, apply_to_requested=False)
802820

821+
def _ensure_default_trigger_config(self, cam: CameraSettings) -> None:
822+
backend = (cam.backend or "").lower()
823+
if backend != "gentl":
824+
return
825+
826+
if not isinstance(cam.properties, dict):
827+
cam.properties = {}
828+
829+
ns = cam.properties.setdefault("gentl", {})
830+
if not isinstance(ns, dict):
831+
ns = {}
832+
cam.properties["gentl"] = ns
833+
834+
ns.setdefault("trigger", CameraTriggerSettings().model_dump(exclude_none=True))
835+
803836
def _add_selected_camera(self) -> None:
804837
if not self._commit_pending_edits(reason="before adding a new camera"):
805838
return
@@ -850,6 +883,7 @@ def _add_selected_camera(self) -> None:
850883
properties={},
851884
)
852885
apply_detected_identity(new_cam, detected, backend)
886+
self._ensure_default_trigger_config(new_cam)
853887
self._working_settings.cameras.append(new_cam)
854888
new_index = len(self._working_settings.cameras) - 1
855889
new_item = QListWidgetItem(self._format_camera_label(new_cam, new_index))
@@ -969,6 +1003,7 @@ def _load_camera_to_form(self, cam: CameraSettings) -> None:
9691003
self.cam_crop_y0.setValue(cam.crop_y0)
9701004
self.cam_crop_x1.setValue(cam.crop_x1)
9711005
self.cam_crop_y1.setValue(cam.crop_y1)
1006+
self._ensure_default_trigger_config(cam)
9721007
self.apply_settings_btn.setEnabled(True)
9731008
self._set_detected_labels(cam)
9741009
finally:
@@ -1029,6 +1064,39 @@ def _enabled_count_with(self, row: int, new_enabled: bool) -> int:
10291064
count += 1
10301065
return count
10311066

1067+
def _open_trigger_settings_dialog(self) -> None:
1068+
"""Open per-camera hardware trigger settings dialog."""
1069+
if self._current_edit_index is None:
1070+
return
1071+
1072+
row = self._current_edit_index
1073+
if row < 0 or row >= len(self._working_settings.cameras):
1074+
return
1075+
1076+
# Commit normal camera edits first so we do not lose pending UI changes.
1077+
if not self._commit_pending_edits(reason="before opening trigger settings"):
1078+
return
1079+
1080+
cam = self._working_settings.cameras[row]
1081+
1082+
dlg = TriggerConfigDialog(cam, self)
1083+
if dlg.exec() != QDialog.Accepted:
1084+
return
1085+
1086+
updated = dlg.camera_settings
1087+
1088+
self._working_settings.cameras[row] = updated
1089+
self._update_active_list_item(row, updated)
1090+
self._load_camera_to_form(updated)
1091+
1092+
# Trigger changes require reopening the camera preview/backend.
1093+
if self._preview.state == PreviewState.ACTIVE:
1094+
self._append_status("[Trigger] Restarting preview to apply trigger settings.")
1095+
self._request_preview_restart(updated, reason="trigger-settings")
1096+
1097+
self.apply_settings_btn.setEnabled(False)
1098+
self._set_apply_dirty(False)
1099+
10321100
def _apply_camera_settings(self) -> bool:
10331101
try:
10341102
for sb in (
@@ -1597,6 +1665,13 @@ def _bump_epoch(self) -> int:
15971665
self._preview.epoch += 1
15981666
return self._preview.epoch
15991667

1668+
def _trigger_dict_for_cam(self, cam: CameraSettings) -> dict:
1669+
backend = (cam.backend or "").lower()
1670+
props = cam.properties if isinstance(cam.properties, dict) else {}
1671+
ns = props.get(backend, {}) if isinstance(props.get(backend), dict) else {}
1672+
trigger = ns.get("trigger", {})
1673+
return trigger if isinstance(trigger, dict) else {}
1674+
16001675
def _should_restart_preview(self, old: CameraSettings, new: CameraSettings) -> bool:
16011676
"""
16021677
Fast UX policy:
@@ -1612,6 +1687,9 @@ def _should_restart_preview(self, old: CameraSettings, new: CameraSettings) -> b
16121687
except Exception:
16131688
return True # safest: restart
16141689

1690+
if self._trigger_dict_for_cam(old) != self._trigger_dict_for_cam(new):
1691+
return True
1692+
16151693
# No restart needed if only rotation/crop/enabled changed
16161694
return False
16171695

0 commit comments

Comments
 (0)