Skip to content

Commit d44bf18

Browse files
committed
Resolve 'auto' trigger source in GenTL backend
Add resolution logic for trigger source: implement _resolve_trigger_source to map the model-level default 'auto' to the first supported camera line (Line0/Line1/Line2/Any), return whether the resolved value is supported, and log/warn or raise depending on strict mode. Integrate this into _configure_pixel_format/_configure trigger flow so TriggerSource is only set if resolution succeeded. Change CameraTriggerSettings default source to 'auto' and add a field validator to coerce common aliases (e.g. 'default', 'automatic', 'device', 'camera') to 'auto'. Update tests and fixtures: adjust fake node default TriggerSource to Line1, update expectations, and add unit tests for auto-selection behavior and strict-mode error. This makes trigger configuration more robust across cameras with differing GenICam enum symbolics.
1 parent fe826a1 commit d44bf18

5 files changed

Lines changed: 112 additions & 5 deletions

File tree

‎dlclivegui/cameras/backends/gentl_backend.py‎

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1056,6 +1056,51 @@ def _trigger_to_dict(trigger) -> dict[str, Any]:
10561056
pass
10571057
return {}
10581058

1059+
def _resolve_trigger_source(self, node_map, requested: str, *, strict: bool) -> tuple[str, bool]:
1060+
"""Resolve TriggerSource against the camera-supported GenICam enum values.
1061+
1062+
Model-level default is "auto"; this backend maps it to the first preferred
1063+
source supported by the actual camera.
1064+
"""
1065+
requested = str(requested or "auto").strip()
1066+
node = self._node(node_map, "TriggerSource")
1067+
available = self._node_symbolics(node)
1068+
1069+
if not available:
1070+
if strict:
1071+
raise RuntimeError("GenICam node 'TriggerSource' is not available or has no symbolics")
1072+
LOG.warning("GenICam node 'TriggerSource' is not available; disabling trigger input.")
1073+
return requested, False
1074+
1075+
if requested in available:
1076+
return requested, True
1077+
1078+
if requested.lower() == "auto":
1079+
for candidate in ("Line0", "Line1", "Line2", "Any"):
1080+
if candidate in available:
1081+
LOG.info(
1082+
"GenTL TriggerSource auto-selected '%s'. Available: %s",
1083+
candidate,
1084+
available,
1085+
)
1086+
return candidate, True
1087+
1088+
LOG.warning(
1089+
"Could not auto-select a GenTL TriggerSource. Available: %s",
1090+
available,
1091+
)
1092+
return requested, False
1093+
1094+
if strict:
1095+
raise RuntimeError(f"GenICam node 'TriggerSource' does not support '{requested}'. Available: {available}")
1096+
1097+
LOG.warning(
1098+
"GenTL TriggerSource '%s' is not available. Available: %s",
1099+
requested,
1100+
available,
1101+
)
1102+
return requested, False
1103+
10591104
def _configure_pixel_format(self, node_map) -> None:
10601105
try:
10611106
pixel_format_node = getattr(node_map, "PixelFormat", None)
@@ -1145,7 +1190,8 @@ def _configure_trigger_input(self, node_map, cfg, *, strict: bool = False) -> No
11451190
self._set_enum_node(node_map, "TriggerMode", "Off", strict=False)
11461191

11471192
selector_ok = self._set_enum_node(node_map, "TriggerSelector", selector, strict=strict)
1148-
source_ok = self._set_enum_node(node_map, "TriggerSource", source, strict=strict)
1193+
source, source_resolved = self._resolve_trigger_source(node_map, source, strict=strict)
1194+
source_ok = source_resolved and self._set_enum_node(node_map, "TriggerSource", source, strict=strict)
11491195
activation_ok = self._set_enum_node(node_map, "TriggerActivation", activation, strict=strict)
11501196

11511197
# TriggerSelector and TriggerSource are required routing nodes.

‎dlclivegui/config.py‎

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -224,7 +224,7 @@ class CameraTriggerSettings(BaseModel):
224224

225225
# Input trigger config: external/follower
226226
selector: str = "FrameStart"
227-
source: str = "Line0"
227+
source: str = "auto"
228228
activation: TriggerActivation | str = "RisingEdge"
229229

230230
# Output config: master
@@ -272,6 +272,24 @@ def _coerce_timeout(cls, v):
272272
return None
273273
return fv if fv > 0 else None
274274

275+
@field_validator("source", mode="before")
276+
@classmethod
277+
def _coerce_source(cls, v):
278+
if v is None:
279+
return "auto"
280+
281+
s = str(v).strip()
282+
if not s:
283+
return "auto"
284+
285+
aliases = {
286+
"default": "auto",
287+
"automatic": "auto",
288+
"device": "auto",
289+
"camera": "auto",
290+
}
291+
return aliases.get(s.lower(), s)
292+
275293
@classmethod
276294
def from_any(cls, value) -> CameraTriggerSettings:
277295
if isinstance(value, cls):

‎tests/cameras/backends/conftest.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -603,7 +603,7 @@ def __init__(
603603
self.AcquisitionMode = _FakeNode("Continuous", symbolics=["Continuous", "SingleFrame"])
604604
self.TriggerSelector = _FakeNode("FrameStart", symbolics=["FrameStart"])
605605
self.TriggerMode = _FakeNode("Off", symbolics=["Off", "On"])
606-
self.TriggerSource = _FakeNode("Line0", symbolics=["Line0", "Line1", "Software"])
606+
self.TriggerSource = _FakeNode("Line1", symbolics=["Line0", "Line1", "Software"])
607607
self.TriggerActivation = _FakeNode("RisingEdge", symbolics=["RisingEdge", "FallingEdge"])
608608

609609
# GPIO output nodes for master/follower setups

‎tests/cameras/backends/test_gentl_trigger.py‎

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ def test_trigger_invalid_source_non_strict_disables_trigger(patch_gentl_sdk, gen
158158
nm = be._acquirer.remote_device.node_map
159159

160160
# Source was unsupported, so the fake node should retain its default.
161-
assert nm.TriggerSource.value == "Line0"
161+
assert nm.TriggerSource.value == "Line1"
162162

163163
# Safety behavior: do not arm TriggerMode on the previous/default source.
164164
assert nm.TriggerMode.value == "Off"
@@ -422,3 +422,39 @@ def test_trigger_timeout_not_capped_for_master_mode(patch_gentl_sdk, gentl_setti
422422

423423
finally:
424424
be.close()
425+
426+
427+
def test_resolve_trigger_source_auto_selects_supported_line(
428+
patch_gentl_sdk,
429+
gentl_settings_factory,
430+
):
431+
gb = patch_gentl_sdk
432+
be = gb.GenTLCameraBackend(gentl_settings_factory())
433+
434+
class Node:
435+
symbolics = ["Line1", "Software", "Any"]
436+
437+
class NodeMap:
438+
TriggerSource = Node()
439+
440+
source, ok = be._resolve_trigger_source(NodeMap(), "auto", strict=False)
441+
442+
assert ok is True
443+
assert source == "Line1"
444+
445+
446+
def test_resolve_trigger_source_strict_raises_for_unsupported_explicit_line(
447+
patch_gentl_sdk,
448+
gentl_settings_factory,
449+
):
450+
gb = patch_gentl_sdk
451+
be = gb.GenTLCameraBackend(gentl_settings_factory())
452+
453+
class Node:
454+
symbolics = ["Line1", "Software", "Any"]
455+
456+
class NodeMap:
457+
TriggerSource = Node()
458+
459+
with pytest.raises(RuntimeError, match="TriggerSource.*Line0"):
460+
be._resolve_trigger_source(NodeMap(), "Line0", strict=True)

‎tests/test_config.py‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import pytest
22

3-
from dlclivegui.config import ApplicationSettings, CameraSettings, MultiCameraSettings
3+
from dlclivegui.config import ApplicationSettings, CameraSettings, CameraTriggerSettings, MultiCameraSettings
44

55

66
@pytest.mark.unit
@@ -34,3 +34,10 @@ def test_save_applies_gentl_trigger_defaults_to_multi_camera():
3434
data = settings.to_dict()
3535

3636
assert "trigger" in data["multi_camera"]["cameras"][0]["properties"]["gentl"]
37+
38+
39+
@pytest.mark.unit
40+
def test_trigger_source_defaults_to_auto():
41+
trigger = CameraTriggerSettings()
42+
43+
assert trigger.source == "auto"

0 commit comments

Comments
 (0)