Skip to content

Commit e529643

Browse files
authored
Merge pull request #79 from DeepLabCut/cy/gentl-trigger-ui
Add UI to configure Trigger mode
2 parents 7f8b7e6 + 10957b0 commit e529643

7 files changed

Lines changed: 868 additions & 86 deletions

File tree

‎dlclivegui/cameras/backends/gentl_backend.py‎

Lines changed: 261 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,89 @@ def static_capabilities(cls) -> dict[str, SupportLevel]:
185185
"hardware_trigger": SupportLevel.BEST_EFFORT,
186186
}
187187

188+
def _debug_trigger_nodes(self, node_map, *, context: str = "") -> None:
189+
names = (
190+
"TriggerMode",
191+
"TriggerSelector",
192+
"TriggerSource",
193+
"TriggerActivation",
194+
"AcquisitionMode",
195+
# Generic line nodes, if available.
196+
"LineSelector",
197+
"LineMode",
198+
"LineSource",
199+
# TIS 37U / DMK 37BUX287 strobe/output nodes.
200+
"GPIn",
201+
"GPOut",
202+
"StrobeEnable",
203+
"StrobePolarity",
204+
"StrobeOperation",
205+
"StrobeDuration",
206+
"StrobeDelay",
207+
)
208+
209+
label = f"GenTL trigger debug {context}".strip()
210+
211+
for name in names:
212+
node = self._node(node_map, name)
213+
if node is None:
214+
continue
215+
216+
value = self._node_value(node_map, name, None)
217+
218+
extras = []
219+
220+
symbolics = self._node_symbolics(node)
221+
if symbolics:
222+
extras.append(f"symbolics={symbolics}")
223+
224+
for attr in ("access_mode", "is_writable", "is_readable"):
225+
try:
226+
extras.append(f"{attr}={getattr(node, attr)}")
227+
except Exception:
228+
pass
229+
230+
LOG.debug("%s: %s=%r %s", label, name, value, " ".join(extras))
231+
232+
def _debug_frame_rate_nodes(self, node_map, *, context: str = "") -> None:
233+
names = (
234+
"AcquisitionFrameRateEnable",
235+
"AcquisitionFrameRateControlEnable",
236+
"AcquisitionFrameRate",
237+
"AcquisitionFrameRateAbs",
238+
"AcquisitionResultingFrameRate",
239+
"ResultingFrameRate",
240+
"AcquisitionFrameRateResulting",
241+
"DeviceFrameRate",
242+
"ExposureAuto",
243+
"ExposureTime",
244+
"ExposureTimeAbs",
245+
"DeviceLinkThroughputLimit",
246+
"DeviceLinkThroughputLimitMode",
247+
"PayloadSize",
248+
"Width",
249+
"Height",
250+
"PixelFormat",
251+
)
252+
253+
label = f"GenTL FPS debug {context}".strip()
254+
255+
for name in names:
256+
node = self._node(node_map, name)
257+
if node is None:
258+
continue
259+
260+
value = self._node_value(node_map, name, None)
261+
262+
extras = []
263+
for attr in ("min", "max", "inc"):
264+
try:
265+
extras.append(f"{attr}={getattr(node, attr)}")
266+
except Exception:
267+
pass
268+
269+
LOG.debug("%s: %s=%r %s", label, name, value, " ".join(extras))
270+
188271
# ------------------------------------------------------------------
189272
# Discovery
190273
# ------------------------------------------------------------------
@@ -462,6 +545,7 @@ def open(self) -> None:
462545

463546
self._configure_trigger(node_map)
464547
self._apply_effective_fetch_timeout()
548+
self._debug_trigger_nodes(node_map, context="after configuration before acquisition")
465549

466550
actual_trigger = self._trigger_to_dict(self._trigger)
467551
actual_trigger["fetch_timeout"] = self._timeout
@@ -1015,6 +1099,32 @@ def _node(node_map, name: str):
10151099
except Exception:
10161100
return None
10171101

1102+
@staticmethod
1103+
def _node_value(node_map, name: str, default=None):
1104+
"""Best-effort read of a GenICam node value.
1105+
1106+
Debug helpers must not make open() fail just because a value cannot be read.
1107+
Harvesters-style fake/test nodes usually expose `.value`; some SDK-style
1108+
nodes may expose `GetValue()`.
1109+
"""
1110+
node = GenTLCameraBackend._node(node_map, name)
1111+
if node is None:
1112+
return default
1113+
1114+
try:
1115+
return node.value
1116+
except Exception:
1117+
pass
1118+
1119+
try:
1120+
getter = getattr(node, "GetValue", None)
1121+
if getter is not None:
1122+
return getter()
1123+
except Exception:
1124+
pass
1125+
1126+
return default
1127+
10181128
@staticmethod
10191129
def _node_symbolics(node) -> list[str]:
10201130
try:
@@ -1214,16 +1324,35 @@ def _configure_trigger_off(self, node_map, *, strict: bool = False) -> None:
12141324
def _configure_trigger_input(self, node_map, cfg, *, strict: bool = False) -> None:
12151325
role = str(self._trigger_attr(cfg, "role", "external") or "external").strip().lower()
12161326
selector = str(self._trigger_attr(cfg, "selector", "FrameStart") or "FrameStart")
1217-
source = str(self._trigger_attr(cfg, "source", "Line0") or "Line0")
12181327
activation = str(self._trigger_attr(cfg, "activation", "RisingEdge") or "RisingEdge")
1328+
source = str(self._trigger_attr(cfg, "source", "auto") or "auto").strip()
12191329

12201330
# Disable trigger while changing trigger-related nodes.
12211331
self._set_enum_node(node_map, "TriggerMode", "Off", strict=False)
12221332

12231333
selector_ok = self._set_enum_node(node_map, "TriggerSelector", selector, strict=strict)
1224-
source, source_resolved = self._resolve_trigger_source(node_map, source, strict=strict)
1225-
source_ok = source_resolved and self._set_enum_node(node_map, "TriggerSource", source, strict=strict)
1226-
activation_ok = self._set_enum_node(node_map, "TriggerActivation", activation, strict=strict)
1334+
1335+
resolved_source, source_supported = self._resolve_trigger_source(
1336+
node_map,
1337+
source,
1338+
strict=strict,
1339+
)
1340+
1341+
source_ok = False
1342+
if source_supported:
1343+
source_ok = self._set_enum_node(
1344+
node_map,
1345+
"TriggerSource",
1346+
resolved_source,
1347+
strict=strict,
1348+
)
1349+
1350+
activation_ok = self._set_enum_node(
1351+
node_map,
1352+
"TriggerActivation",
1353+
activation,
1354+
strict=False,
1355+
)
12271356

12281357
# TriggerSelector and TriggerSource are required routing nodes.
12291358
# If either failed in non-strict mode, do not arm TriggerMode=On.
@@ -1232,12 +1361,13 @@ def _configure_trigger_input(self, node_map, cfg, *, strict: bool = False) -> No
12321361
LOG.warning(
12331362
"Could not apply GenTL trigger input routing "
12341363
"(selector_ok=%s, source_ok=%s); disabling trigger. "
1235-
"requested role=%s selector=%s source=%s activation=%s",
1364+
"requested role=%s selector=%s source=%s resolved_source=%s activation=%s",
12361365
selector_ok,
12371366
source_ok,
12381367
role,
12391368
selector,
12401369
source,
1370+
resolved_source,
12411371
activation,
12421372
)
12431373
self._configure_trigger_off(node_map, strict=False)
@@ -1259,55 +1389,154 @@ def _configure_trigger_input(self, node_map, cfg, *, strict: bool = False) -> No
12591389
return
12601390

12611391
LOG.info(
1262-
"GenTL trigger input configured: role=%s selector=%s source=%s activation=%s activation_ok=%s",
1392+
"GenTL trigger input configured: role=%s selector=%s source_requested=%s "
1393+
"source=%s activation=%s selector_ok=%s source_ok=%s activation_ok=%s",
12631394
role,
12641395
selector,
12651396
source,
1397+
resolved_source,
12661398
activation,
1399+
selector_ok,
1400+
source_ok,
12671401
activation_ok,
12681402
)
12691403

12701404
def _configure_trigger_master(self, node_map, cfg, *, strict: bool = False) -> None:
1405+
"""Configure this camera as a free-running master that emits STROBE_OUT pulses.
1406+
1407+
For DMK 37BUX287 / TIS 37U series, the physical output is controlled by
1408+
StrobeEnable/StrobePolarity/StrobeOperation rather than SFNC LineSelector/
1409+
LineMode/LineSource nodes.
1410+
"""
12711411
output_line = str(self._trigger_attr(cfg, "output_line", "Line2") or "Line2")
12721412
output_source = str(self._trigger_attr(cfg, "output_source", "ExposureActive") or "ExposureActive")
12731413

1274-
# Master camera runs freerun and exposes an output signal.
1414+
# Optional extra fields if present in trigger dict/model.
1415+
strobe_polarity = str(self._trigger_attr(cfg, "strobe_polarity", "ActiveHigh") or "ActiveHigh")
1416+
strobe_operation = str(self._trigger_attr(cfg, "strobe_operation", "Exposure") or "Exposure")
1417+
strobe_duration = self._trigger_attr(cfg, "strobe_duration", None)
1418+
strobe_delay = self._trigger_attr(cfg, "strobe_delay", None)
1419+
1420+
# Master camera should be free-running.
12751421
self._configure_trigger_off(node_map, strict=False)
12761422

1277-
line_selected = self._set_enum_node(
1278-
node_map,
1279-
"LineSelector",
1280-
output_line,
1281-
strict=strict,
1282-
)
1423+
# ------------------------------------------------------------------
1424+
# Preferred path for The Imaging Source 37U / DMK 37BUX287:
1425+
# StrobeEnable, StrobePolarity, StrobeOperation, StrobeDuration, StrobeDelay
1426+
# ------------------------------------------------------------------
1427+
strobe_enable_node = self._node(node_map, "StrobeEnable")
1428+
1429+
if strobe_enable_node is not None:
1430+
# Disable first while changing parameters.
1431+
self._set_enum_node(node_map, "StrobeEnable", "Off", strict=False)
1432+
1433+
polarity_ok = self._set_enum_node(
1434+
node_map,
1435+
"StrobePolarity",
1436+
strobe_polarity,
1437+
strict=False,
1438+
)
12831439

1284-
# In non-strict mode, do not continue configuring output behavior if the
1285-
# requested line could not be selected. Otherwise we may accidentally drive
1286-
# whichever GPIO line the camera had selected previously/defaulted to.
1287-
if not line_selected:
1288-
LOG.warning(
1289-
"Could not select GenTL output line '%s'; skipping trigger output configuration.",
1290-
output_line,
1440+
operation_ok = self._set_enum_node(
1441+
node_map,
1442+
"StrobeOperation",
1443+
strobe_operation,
1444+
strict=False,
12911445
)
1292-
return
12931446

1294-
mode_ok = self._set_enum_node(node_map, "LineMode", "Output", strict=strict)
1295-
source_ok = self._set_enum_node(node_map, "LineSource", output_source, strict=strict)
1447+
if strobe_duration is not None:
1448+
try:
1449+
node = self._node(node_map, "StrobeDuration")
1450+
if node is not None:
1451+
node.value = int(strobe_duration)
1452+
LOG.info("Configured GenTL StrobeDuration=%s", int(strobe_duration))
1453+
except Exception as exc:
1454+
if strict:
1455+
raise RuntimeError(f"Failed to set StrobeDuration={strobe_duration}: {exc}") from exc
1456+
LOG.warning("Failed to set StrobeDuration=%s: %s", strobe_duration, exc)
1457+
1458+
if strobe_delay is not None:
1459+
try:
1460+
node = self._node(node_map, "StrobeDelay")
1461+
if node is not None:
1462+
node.value = int(strobe_delay)
1463+
LOG.info("Configured GenTL StrobeDelay=%s", int(strobe_delay))
1464+
except Exception as exc:
1465+
if strict:
1466+
raise RuntimeError(f"Failed to set StrobeDelay={strobe_delay}: {exc}") from exc
1467+
LOG.warning("Failed to set StrobeDelay=%s: %s", strobe_delay, exc)
1468+
1469+
enable_ok = self._set_enum_node(
1470+
node_map,
1471+
"StrobeEnable",
1472+
"On",
1473+
strict=strict,
1474+
)
1475+
1476+
if enable_ok:
1477+
LOG.info(
1478+
"GenTL trigger master configured via Strobe*: "
1479+
"StrobeEnable=On StrobePolarity=%s polarity_ok=%s "
1480+
"StrobeOperation=%s operation_ok=%s",
1481+
strobe_polarity,
1482+
polarity_ok,
1483+
strobe_operation,
1484+
operation_ok,
1485+
)
1486+
return
1487+
1488+
if strict:
1489+
raise RuntimeError("Could not enable GenTL StrobeEnable=On")
12961490

1297-
if not (mode_ok and source_ok):
12981491
LOG.warning(
1299-
"GenTL trigger master output configuration incomplete (LineMode ok=%s, LineSource ok=%s).",
1300-
mode_ok,
1301-
source_ok,
1492+
"StrobeEnable node exists but could not be enabled; falling back to generic Line* output configuration."
13021493
)
1303-
return
13041494

1305-
LOG.info(
1306-
"GenTL trigger master configured: output_line=%s output_source=%s",
1307-
output_line,
1308-
output_source,
1495+
# ------------------------------------------------------------------
1496+
# Generic SFNC fallback for cameras that expose LineSelector/LineMode/LineSource.
1497+
# ------------------------------------------------------------------
1498+
line_selector = self._node(node_map, "LineSelector")
1499+
if line_selector is not None:
1500+
line_selected = self._set_enum_node(
1501+
node_map,
1502+
"LineSelector",
1503+
output_line,
1504+
strict=strict,
1505+
)
1506+
1507+
if not line_selected:
1508+
LOG.warning(
1509+
"Could not select GenTL output line '%s'; skipping Line* output configuration.",
1510+
output_line,
1511+
)
1512+
else:
1513+
mode_ok = self._set_enum_node(node_map, "LineMode", "Output", strict=strict)
1514+
source_ok = self._set_enum_node(node_map, "LineSource", output_source, strict=strict)
1515+
1516+
if mode_ok and source_ok:
1517+
LOG.info(
1518+
"GenTL trigger master configured via Line*: output_line=%s output_source=%s",
1519+
output_line,
1520+
output_source,
1521+
)
1522+
return
1523+
1524+
LOG.warning(
1525+
"GenTL Line* trigger output configuration incomplete (LineMode ok=%s, LineSource ok=%s).",
1526+
mode_ok,
1527+
source_ok,
1528+
)
1529+
1530+
msg = (
1531+
"Could not configure GenTL trigger master output. "
1532+
"No supported Strobe* or Line* output path was successfully configured."
13091533
)
13101534

1535+
if strict:
1536+
raise RuntimeError(msg)
1537+
1538+
LOG.warning(msg)
1539+
13111540
def _restore_trigger_idle(self, node_map) -> None:
13121541
"""Best-effort restore to a safe non-triggering state after acquisition stops.
13131542

0 commit comments

Comments
 (0)