Skip to content

Commit a2eca8e

Browse files
committed
Add GenTL trigger tests and fake node map
Add a comprehensive test suite for GenTL hardware trigger handling (tests/cameras/backends/test_gentl_trigger.py). Tests cover trigger roles (off/external/follower/master), selector/source/activation settings, strict vs non-strict behavior for invalid sources, master output configuration, alias mapping, timeout handling and error messaging, and persistence of trigger_actual for debugging. Update the test conftest fake node map (tests/cameras/backends/conftest.py) to include Trigger* and Line* nodes (AcquisitionMode, TriggerSelector, TriggerMode, TriggerSource, TriggerActivation, LineSelector, LineMode, LineSource) so the tests can exercise trigger and GPIO-related configuration.
1 parent 5040aab commit a2eca8e

2 files changed

Lines changed: 345 additions & 0 deletions

File tree

tests/cameras/backends/conftest.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -599,6 +599,18 @@ def __init__(
599599
self.GainAuto = _FakeNode("Off")
600600
self.Gain = _FakeNode(float(gain))
601601

602+
# Trigger input nodes
603+
self.AcquisitionMode = _FakeNode("Continuous", symbolics=["Continuous", "SingleFrame"])
604+
self.TriggerSelector = _FakeNode("FrameStart", symbolics=["FrameStart"])
605+
self.TriggerMode = _FakeNode("Off", symbolics=["Off", "On"])
606+
self.TriggerSource = _FakeNode("Line0", symbolics=["Line0", "Line1", "Software"])
607+
self.TriggerActivation = _FakeNode("RisingEdge", symbolics=["RisingEdge", "FallingEdge"])
608+
609+
# GPIO output nodes for master/follower setups
610+
self.LineSelector = _FakeNode("Line0", symbolics=["Line0", "Line1", "Line2"])
611+
self.LineMode = _FakeNode("Input", symbolics=["Input", "Output"])
612+
self.LineSource = _FakeNode("Off", symbolics=["Off", "ExposureActive", "AcquisitionActive"])
613+
602614

603615
class _FakeRemoteDevice:
604616
def __init__(self, node_map: _FakeNodeMap):
Lines changed: 333 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,333 @@
1+
# tests/cameras/backends/test_gentl_trigger.py
2+
from __future__ import annotations
3+
4+
import pytest
5+
6+
# ---------------------------------------------------------------------
7+
# GenTL hardware trigger configuration
8+
# ---------------------------------------------------------------------
9+
10+
11+
def _gentl_trigger_settings(gentl_settings_factory, trigger: dict, **kwargs):
12+
"""Build CameraSettings with a GenTL trigger block."""
13+
return gentl_settings_factory(properties={"gentl": {"trigger": trigger}}, **kwargs)
14+
15+
16+
def test_gentl_capabilities_advertise_hardware_trigger_best_effort(patch_gentl_sdk):
17+
gb = patch_gentl_sdk
18+
19+
caps = gb.GenTLCameraBackend.static_capabilities()
20+
21+
assert caps.get("hardware_trigger") == gb.SupportLevel.BEST_EFFORT
22+
23+
24+
def test_trigger_default_off_configures_trigger_mode_off(patch_gentl_sdk, gentl_settings_factory):
25+
gb = patch_gentl_sdk
26+
27+
settings = gentl_settings_factory()
28+
be = gb.GenTLCameraBackend(settings)
29+
30+
be.open()
31+
nm = be._acquirer.remote_device.node_map
32+
33+
assert nm.TriggerMode.value == "Off"
34+
35+
ns = settings.properties.get("gentl", {})
36+
assert ns.get("trigger_actual", {}).get("role") == "off"
37+
38+
be.close()
39+
40+
41+
def test_trigger_explicit_off_configures_trigger_mode_off(patch_gentl_sdk, gentl_settings_factory):
42+
gb = patch_gentl_sdk
43+
44+
settings = _gentl_trigger_settings(gentl_settings_factory, {"role": "off"})
45+
be = gb.GenTLCameraBackend(settings)
46+
47+
be.open()
48+
nm = be._acquirer.remote_device.node_map
49+
50+
assert nm.TriggerMode.value == "Off"
51+
assert settings.properties["gentl"]["trigger_actual"]["role"] == "off"
52+
53+
be.close()
54+
55+
56+
def test_trigger_external_configures_input_line_and_timeout(patch_gentl_sdk, gentl_settings_factory):
57+
gb = patch_gentl_sdk
58+
59+
settings = _gentl_trigger_settings(
60+
gentl_settings_factory,
61+
{
62+
"role": "external",
63+
"selector": "FrameStart",
64+
"source": "Line0",
65+
"activation": "RisingEdge",
66+
"timeout": 10.0,
67+
},
68+
)
69+
be = gb.GenTLCameraBackend(settings)
70+
71+
be.open()
72+
nm = be._acquirer.remote_device.node_map
73+
74+
assert nm.TriggerSelector.value == "FrameStart"
75+
assert nm.TriggerSource.value == "Line0"
76+
assert nm.TriggerActivation.value == "RisingEdge"
77+
assert nm.TriggerMode.value == "On"
78+
assert be._timeout == pytest.approx(10.0)
79+
80+
ns = settings.properties["gentl"]
81+
assert ns["trigger_actual"]["role"] == "external"
82+
assert ns["trigger_actual"]["source"] == "Line0"
83+
84+
be.close()
85+
86+
87+
def test_trigger_follower_configures_input_line(patch_gentl_sdk, gentl_settings_factory):
88+
gb = patch_gentl_sdk
89+
90+
settings = _gentl_trigger_settings(
91+
gentl_settings_factory,
92+
{
93+
"role": "follower",
94+
"selector": "FrameStart",
95+
"source": "Line1",
96+
"activation": "FallingEdge",
97+
},
98+
)
99+
be = gb.GenTLCameraBackend(settings)
100+
101+
be.open()
102+
nm = be._acquirer.remote_device.node_map
103+
104+
assert nm.TriggerSelector.value == "FrameStart"
105+
assert nm.TriggerSource.value == "Line1"
106+
assert nm.TriggerActivation.value == "FallingEdge"
107+
assert nm.TriggerMode.value == "On"
108+
109+
ns = settings.properties["gentl"]
110+
assert ns["trigger_actual"]["role"] == "follower"
111+
112+
be.close()
113+
114+
115+
def test_trigger_master_configures_output_line_and_keeps_trigger_off(patch_gentl_sdk, gentl_settings_factory):
116+
gb = patch_gentl_sdk
117+
118+
settings = _gentl_trigger_settings(
119+
gentl_settings_factory,
120+
{
121+
"role": "master",
122+
"output_line": "Line2",
123+
"output_source": "ExposureActive",
124+
},
125+
)
126+
be = gb.GenTLCameraBackend(settings)
127+
128+
be.open()
129+
nm = be._acquirer.remote_device.node_map
130+
131+
assert nm.TriggerMode.value == "Off"
132+
assert nm.LineSelector.value == "Line2"
133+
assert nm.LineMode.value == "Output"
134+
assert nm.LineSource.value == "ExposureActive"
135+
136+
ns = settings.properties["gentl"]
137+
assert ns["trigger_actual"]["role"] == "master"
138+
assert ns["trigger_actual"]["output_line"] == "Line2"
139+
140+
be.close()
141+
142+
143+
def test_trigger_invalid_source_non_strict_does_not_crash(patch_gentl_sdk, gentl_settings_factory):
144+
gb = patch_gentl_sdk
145+
146+
settings = _gentl_trigger_settings(
147+
gentl_settings_factory,
148+
{
149+
"role": "external",
150+
"source": "LineDoesNotExist",
151+
"strict": False,
152+
},
153+
)
154+
be = gb.GenTLCameraBackend(settings)
155+
156+
be.open()
157+
nm = be._acquirer.remote_device.node_map
158+
159+
# Source was unsupported, so the fake node should retain its default.
160+
assert nm.TriggerSource.value == "Line0"
161+
# Non-strict mode should still allow opening; TriggerMode may be enabled
162+
# because TriggerSource failure is best-effort in this mode.
163+
assert be._acquirer is not None
164+
165+
be.close()
166+
167+
168+
def test_trigger_invalid_source_strict_raises_and_cleans_up(patch_gentl_sdk, gentl_settings_factory):
169+
gb = patch_gentl_sdk
170+
171+
settings = _gentl_trigger_settings(
172+
gentl_settings_factory,
173+
{
174+
"role": "external",
175+
"source": "LineDoesNotExist",
176+
"strict": True,
177+
},
178+
)
179+
be = gb.GenTLCameraBackend(settings)
180+
181+
with pytest.raises(RuntimeError):
182+
be.open()
183+
184+
assert be._harvester is None
185+
assert be._shared_entry is None
186+
assert be._acquirer is None
187+
188+
189+
def test_trigger_invalid_master_output_source_non_strict_does_not_crash(patch_gentl_sdk, gentl_settings_factory):
190+
gb = patch_gentl_sdk
191+
192+
settings = _gentl_trigger_settings(
193+
gentl_settings_factory,
194+
{
195+
"role": "master",
196+
"output_line": "Line2",
197+
"output_source": "NotARealLineSource",
198+
"strict": False,
199+
},
200+
)
201+
be = gb.GenTLCameraBackend(settings)
202+
203+
be.open()
204+
nm = be._acquirer.remote_device.node_map
205+
206+
assert nm.TriggerMode.value == "Off"
207+
assert nm.LineSelector.value == "Line2"
208+
assert nm.LineMode.value == "Output"
209+
# Unsupported source should not be applied in non-strict mode.
210+
assert nm.LineSource.value == "Off"
211+
212+
be.close()
213+
214+
215+
def test_trigger_invalid_master_output_source_strict_raises_and_cleans_up(patch_gentl_sdk, gentl_settings_factory):
216+
gb = patch_gentl_sdk
217+
218+
settings = _gentl_trigger_settings(
219+
gentl_settings_factory,
220+
{
221+
"role": "master",
222+
"output_line": "Line2",
223+
"output_source": "NotARealLineSource",
224+
"strict": True,
225+
},
226+
)
227+
be = gb.GenTLCameraBackend(settings)
228+
229+
with pytest.raises(RuntimeError):
230+
be.open()
231+
232+
assert be._harvester is None
233+
assert be._shared_entry is None
234+
assert be._acquirer is None
235+
236+
237+
def test_trigger_alias_on_maps_to_external(patch_gentl_sdk, gentl_settings_factory):
238+
gb = patch_gentl_sdk
239+
240+
settings = _gentl_trigger_settings(
241+
gentl_settings_factory,
242+
{
243+
"role": "on",
244+
"source": "Line1",
245+
},
246+
)
247+
be = gb.GenTLCameraBackend(settings)
248+
249+
be.open()
250+
nm = be._acquirer.remote_device.node_map
251+
252+
assert nm.TriggerMode.value == "On"
253+
assert nm.TriggerSource.value == "Line1"
254+
assert settings.properties["gentl"]["trigger_actual"]["role"] == "external"
255+
256+
be.close()
257+
258+
259+
def test_trigger_timeout_overrides_default_fetch_timeout(patch_gentl_sdk, gentl_settings_factory):
260+
gb = patch_gentl_sdk
261+
262+
settings = _gentl_trigger_settings(
263+
gentl_settings_factory,
264+
{
265+
"role": "external",
266+
"timeout": 7.5,
267+
},
268+
)
269+
be = gb.GenTLCameraBackend(settings)
270+
271+
be.open()
272+
assert be._timeout == pytest.approx(7.5)
273+
274+
# Fake acquisition is started, so read should pass and record the timeout.
275+
frame, _ = be.read()
276+
assert frame is not None
277+
assert be._acquirer.fetch_calls[-1] == pytest.approx(7.5)
278+
279+
be.close()
280+
281+
282+
def test_trigger_timeout_error_mentions_hardware_trigger_when_waiting(patch_gentl_sdk, gentl_settings_factory):
283+
gb = patch_gentl_sdk
284+
285+
settings = _gentl_trigger_settings(
286+
gentl_settings_factory,
287+
{
288+
"role": "external",
289+
"timeout": 3.0,
290+
},
291+
# fast_start keeps acquisition stopped; fake fetch then raises timeout.
292+
# This lets us assert the backend timeout message without hardware.
293+
)
294+
settings.properties["gentl"]["fast_start"] = True
295+
be = gb.GenTLCameraBackend(settings)
296+
297+
be.open()
298+
299+
with pytest.raises(TimeoutError) as ei:
300+
be.read()
301+
302+
msg = str(ei.value).lower()
303+
assert "gentl timeout" in msg
304+
assert "hardware trigger" in msg or "trigger" in msg
305+
306+
be.close()
307+
308+
309+
def test_trigger_actual_is_persisted_for_debugging(patch_gentl_sdk, gentl_settings_factory):
310+
gb = patch_gentl_sdk
311+
312+
settings = _gentl_trigger_settings(
313+
gentl_settings_factory,
314+
{
315+
"role": "follower",
316+
"source": "Line1",
317+
"activation": "FallingEdge",
318+
"timeout": 9.0,
319+
"strict": False,
320+
},
321+
)
322+
be = gb.GenTLCameraBackend(settings)
323+
324+
be.open()
325+
326+
actual = settings.properties["gentl"].get("trigger_actual")
327+
assert isinstance(actual, dict)
328+
assert actual["role"] == "follower"
329+
assert actual["source"] == "Line1"
330+
assert actual["activation"] == "FallingEdge"
331+
assert actual["timeout"] == pytest.approx(9.0)
332+
333+
be.close()

0 commit comments

Comments
 (0)