Skip to content

Commit 52feea9

Browse files
committed
Add GenTL strobe support and trigger debugging
Add debugging helpers for GenTL trigger and frame-rate nodes and log their values during camera configuration. Improve GenTL trigger input handling by treating TriggerSource as best-effort (supporting read-only/auto cases) and emitting clearer warnings. Implement a dedicated master/output path for TIS/DMK 37U cameras using Strobe* nodes (StrobeEnable/Polarity/Operation/Duration/Delay) with a fallback to generic Line* configuration; respect strict mode and surface informative errors. Extend CameraTriggerSettings with strobe fields (polarity, operation, duration, delay) and validation/coercion. Update the trigger configuration dialog to expose strobe controls, tooltips, UI sync logic, and include strobe values in the saved payload.
1 parent ff81a9e commit 52feea9

3 files changed

Lines changed: 350 additions & 49 deletions

File tree

dlclivegui/cameras/backends/gentl_backend.py

Lines changed: 236 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,89 @@ def static_capabilities(cls) -> dict[str, SupportLevel]:
192192
"hardware_trigger": SupportLevel.BEST_EFFORT,
193193
}
194194

195+
def _debug_trigger_nodes(self, node_map, *, context: str = "") -> None:
196+
names = (
197+
"TriggerMode",
198+
"TriggerSelector",
199+
"TriggerSource",
200+
"TriggerActivation",
201+
"AcquisitionMode",
202+
# Generic line nodes, if available.
203+
"LineSelector",
204+
"LineMode",
205+
"LineSource",
206+
# TIS 37U / DMK 37BUX287 strobe/output nodes.
207+
"GPIn",
208+
"GPOut",
209+
"StrobeEnable",
210+
"StrobePolarity",
211+
"StrobeOperation",
212+
"StrobeDuration",
213+
"StrobeDelay",
214+
)
215+
216+
label = f"GenTL trigger debug {context}".strip()
217+
218+
for name in names:
219+
node = self._node(node_map, name)
220+
if node is None:
221+
continue
222+
223+
value = self._node_value(node_map, name, None)
224+
225+
extras = []
226+
227+
symbolics = self._node_symbolics(node)
228+
if symbolics:
229+
extras.append(f"symbolics={symbolics}")
230+
231+
for attr in ("access_mode", "is_writable", "is_readable"):
232+
try:
233+
extras.append(f"{attr}={getattr(node, attr)}")
234+
except Exception:
235+
pass
236+
237+
LOG.debug("%s: %s=%r %s", label, name, value, " ".join(extras))
238+
239+
def _debug_frame_rate_nodes(self, node_map, *, context: str = "") -> None:
240+
names = (
241+
"AcquisitionFrameRateEnable",
242+
"AcquisitionFrameRateControlEnable",
243+
"AcquisitionFrameRate",
244+
"AcquisitionFrameRateAbs",
245+
"AcquisitionResultingFrameRate",
246+
"ResultingFrameRate",
247+
"AcquisitionFrameRateResulting",
248+
"DeviceFrameRate",
249+
"ExposureAuto",
250+
"ExposureTime",
251+
"ExposureTimeAbs",
252+
"DeviceLinkThroughputLimit",
253+
"DeviceLinkThroughputLimitMode",
254+
"PayloadSize",
255+
"Width",
256+
"Height",
257+
"PixelFormat",
258+
)
259+
260+
label = f"GenTL FPS debug {context}".strip()
261+
262+
for name in names:
263+
node = self._node(node_map, name)
264+
if node is None:
265+
continue
266+
267+
value = self._node_value(node_map, name, None)
268+
269+
extras = []
270+
for attr in ("min", "max", "inc"):
271+
try:
272+
extras.append(f"{attr}={getattr(node, attr)}")
273+
except Exception:
274+
pass
275+
276+
LOG.debug("%s: %s=%r %s", label, name, value, " ".join(extras))
277+
195278
# ------------------------------------------------------------------
196279
# Discovery
197280
# ------------------------------------------------------------------
@@ -464,6 +547,7 @@ def open(self) -> None:
464547
self._configure_gain(node_map)
465548
self._configure_frame_rate(node_map)
466549
self._configure_trigger(node_map) # keep low in the list
550+
self._debug_trigger_nodes(node_map, context="after configuration before acquisition")
467551
self._ensure_settings_ns()["trigger_actual"] = self._trigger_to_dict(self._trigger)
468552
self._read_telemetry(node_map)
469553
self._persist_device_metadata(selected_info, selected_serial)
@@ -1186,31 +1270,42 @@ def _configure_trigger_off(self, node_map, *, strict: bool = False) -> None:
11861270
def _configure_trigger_input(self, node_map, cfg, *, strict: bool = False) -> None:
11871271
role = str(self._trigger_attr(cfg, "role", "external") or "external").strip().lower()
11881272
selector = str(self._trigger_attr(cfg, "selector", "FrameStart") or "FrameStart")
1189-
source = str(self._trigger_attr(cfg, "source", "Line0") or "Line0")
11901273
activation = str(self._trigger_attr(cfg, "activation", "RisingEdge") or "RisingEdge")
1274+
source = str(self._trigger_attr(cfg, "source", "auto") or "auto").strip()
11911275

11921276
# Disable trigger while changing trigger-related nodes.
11931277
self._set_enum_node(node_map, "TriggerMode", "Off", strict=False)
11941278

11951279
selector_ok = self._set_enum_node(node_map, "TriggerSelector", selector, strict=strict)
1196-
source, source_resolved = self._resolve_trigger_source(node_map, source, strict=strict)
1197-
source_ok = source_resolved and self._set_enum_node(node_map, "TriggerSource", source, strict=strict)
11981280
activation_ok = self._set_enum_node(node_map, "TriggerActivation", activation, strict=strict)
11991281

1200-
# TriggerSelector and TriggerSource are required routing nodes.
1201-
# If either failed in non-strict mode, do not arm TriggerMode=On.
1202-
# Otherwise the camera may wait on a previous/default input line.
1203-
if not (selector_ok and source_ok):
1282+
source_ok = False
1283+
if source and source.lower() not in {"", "auto", "none"}:
1284+
source_node = self._node(node_map, "TriggerSource")
1285+
source_symbolics = self._node_symbolics(source_node)
1286+
1287+
if source_node is not None:
1288+
if source in source_symbolics:
1289+
source_ok = self._set_enum_node(node_map, "TriggerSource", source, strict=False)
1290+
if not source_ok:
1291+
LOG.warning(
1292+
"GenTL TriggerSource=%s is supported but not writable; "
1293+
"continuing without changing TriggerSource. Available: %s",
1294+
source,
1295+
source_symbolics,
1296+
)
1297+
else:
1298+
LOG.warning(
1299+
"Requested GenTL TriggerSource=%s not in available sources %s; "
1300+
"continuing without changing TriggerSource.",
1301+
source,
1302+
source_symbolics,
1303+
)
1304+
1305+
if not selector_ok:
12041306
LOG.warning(
1205-
"Could not apply GenTL trigger input routing "
1206-
"(selector_ok=%s, source_ok=%s); disabling trigger. "
1207-
"requested role=%s selector=%s source=%s activation=%s",
1208-
selector_ok,
1209-
source_ok,
1210-
role,
1307+
"Could not apply GenTL TriggerSelector=%s; disabling trigger.",
12111308
selector,
1212-
source,
1213-
activation,
12141309
)
12151310
self._configure_trigger_off(node_map, strict=False)
12161311
self._trigger = CameraTriggerSettings()
@@ -1231,55 +1326,153 @@ def _configure_trigger_input(self, node_map, cfg, *, strict: bool = False) -> No
12311326
return
12321327

12331328
LOG.info(
1234-
"GenTL trigger input configured: role=%s selector=%s source=%s activation=%s activation_ok=%s",
1329+
"GenTL trigger input configured: role=%s selector=%s activation=%s "
1330+
"selector_ok=%s activation_ok=%s source_requested=%s source_ok=%s",
12351331
role,
12361332
selector,
1237-
source,
12381333
activation,
1334+
selector_ok,
12391335
activation_ok,
1336+
source,
1337+
source_ok,
12401338
)
12411339

12421340
def _configure_trigger_master(self, node_map, cfg, *, strict: bool = False) -> None:
1341+
"""Configure this camera as a free-running master that emits STROBE_OUT pulses.
1342+
1343+
For DMK 37BUX287 / TIS 37U series, the physical output is controlled by
1344+
StrobeEnable/StrobePolarity/StrobeOperation rather than SFNC LineSelector/
1345+
LineMode/LineSource nodes.
1346+
"""
12431347
output_line = str(self._trigger_attr(cfg, "output_line", "Line2") or "Line2")
12441348
output_source = str(self._trigger_attr(cfg, "output_source", "ExposureActive") or "ExposureActive")
12451349

1246-
# Master camera runs freerun and exposes an output signal.
1350+
# Optional extra fields if present in trigger dict/model.
1351+
strobe_polarity = str(self._trigger_attr(cfg, "strobe_polarity", "ActiveHigh") or "ActiveHigh")
1352+
strobe_operation = str(self._trigger_attr(cfg, "strobe_operation", "Exposure") or "Exposure")
1353+
strobe_duration = self._trigger_attr(cfg, "strobe_duration", None)
1354+
strobe_delay = self._trigger_attr(cfg, "strobe_delay", None)
1355+
1356+
# Master camera should be free-running.
12471357
self._configure_trigger_off(node_map, strict=False)
12481358

1249-
line_selected = self._set_enum_node(
1250-
node_map,
1251-
"LineSelector",
1252-
output_line,
1253-
strict=strict,
1254-
)
1359+
# ------------------------------------------------------------------
1360+
# Preferred path for The Imaging Source 37U / DMK 37BUX287:
1361+
# StrobeEnable, StrobePolarity, StrobeOperation, StrobeDuration, StrobeDelay
1362+
# ------------------------------------------------------------------
1363+
strobe_enable_node = self._node(node_map, "StrobeEnable")
1364+
1365+
if strobe_enable_node is not None:
1366+
# Disable first while changing parameters.
1367+
self._set_enum_node(node_map, "StrobeEnable", "Off", strict=False)
1368+
1369+
polarity_ok = self._set_enum_node(
1370+
node_map,
1371+
"StrobePolarity",
1372+
strobe_polarity,
1373+
strict=False,
1374+
)
12551375

1256-
# In non-strict mode, do not continue configuring output behavior if the
1257-
# requested line could not be selected. Otherwise we may accidentally drive
1258-
# whichever GPIO line the camera had selected previously/defaulted to.
1259-
if not line_selected:
1260-
LOG.warning(
1261-
"Could not select GenTL output line '%s'; skipping trigger output configuration.",
1262-
output_line,
1376+
operation_ok = self._set_enum_node(
1377+
node_map,
1378+
"StrobeOperation",
1379+
strobe_operation,
1380+
strict=False,
12631381
)
1264-
return
12651382

1266-
mode_ok = self._set_enum_node(node_map, "LineMode", "Output", strict=strict)
1267-
source_ok = self._set_enum_node(node_map, "LineSource", output_source, strict=strict)
1383+
if strobe_duration is not None:
1384+
try:
1385+
node = self._node(node_map, "StrobeDuration")
1386+
if node is not None:
1387+
node.value = int(strobe_duration)
1388+
LOG.info("Configured GenTL StrobeDuration=%s", int(strobe_duration))
1389+
except Exception as exc:
1390+
if strict:
1391+
raise RuntimeError(f"Failed to set StrobeDuration={strobe_duration}: {exc}") from exc
1392+
LOG.warning("Failed to set StrobeDuration=%s: %s", strobe_duration, exc)
1393+
1394+
if strobe_delay is not None:
1395+
try:
1396+
node = self._node(node_map, "StrobeDelay")
1397+
if node is not None:
1398+
node.value = int(strobe_delay)
1399+
LOG.info("Configured GenTL StrobeDelay=%s", int(strobe_delay))
1400+
except Exception as exc:
1401+
if strict:
1402+
raise RuntimeError(f"Failed to set StrobeDelay={strobe_delay}: {exc}") from exc
1403+
LOG.warning("Failed to set StrobeDelay=%s: %s", strobe_delay, exc)
1404+
1405+
enable_ok = self._set_enum_node(
1406+
node_map,
1407+
"StrobeEnable",
1408+
"On",
1409+
strict=strict,
1410+
)
1411+
1412+
if enable_ok:
1413+
LOG.info(
1414+
"GenTL trigger master configured via Strobe*: "
1415+
"StrobeEnable=On StrobePolarity=%s polarity_ok=%s "
1416+
"StrobeOperation=%s operation_ok=%s",
1417+
strobe_polarity,
1418+
polarity_ok,
1419+
strobe_operation,
1420+
operation_ok,
1421+
)
1422+
return
1423+
1424+
if strict:
1425+
raise RuntimeError("Could not enable GenTL StrobeEnable=On")
12681426

1269-
if not (mode_ok and source_ok):
12701427
LOG.warning(
1271-
"GenTL trigger master output configuration incomplete (LineMode ok=%s, LineSource ok=%s).",
1272-
mode_ok,
1273-
source_ok,
1428+
"StrobeEnable node exists but could not be enabled; falling back to generic Line* output configuration."
12741429
)
1275-
return
12761430

1277-
LOG.info(
1278-
"GenTL trigger master configured: output_line=%s output_source=%s",
1279-
output_line,
1280-
output_source,
1431+
# ------------------------------------------------------------------
1432+
# Generic SFNC fallback for cameras that expose LineSelector/LineMode/LineSource.
1433+
# ------------------------------------------------------------------
1434+
line_selector = self._node(node_map, "LineSelector")
1435+
if line_selector is not None:
1436+
line_selected = self._set_enum_node(
1437+
node_map,
1438+
"LineSelector",
1439+
output_line,
1440+
strict=strict,
1441+
)
1442+
1443+
if not line_selected:
1444+
LOG.warning(
1445+
"Could not select GenTL output line '%s'; skipping Line* output configuration.",
1446+
output_line,
1447+
)
1448+
else:
1449+
mode_ok = self._set_enum_node(node_map, "LineMode", "Output", strict=strict)
1450+
source_ok = self._set_enum_node(node_map, "LineSource", output_source, strict=strict)
1451+
1452+
if mode_ok and source_ok:
1453+
LOG.info(
1454+
"GenTL trigger master configured via Line*: output_line=%s output_source=%s",
1455+
output_line,
1456+
output_source,
1457+
)
1458+
return
1459+
1460+
LOG.warning(
1461+
"GenTL Line* trigger output configuration incomplete (LineMode ok=%s, LineSource ok=%s).",
1462+
mode_ok,
1463+
source_ok,
1464+
)
1465+
1466+
msg = (
1467+
"Could not configure GenTL trigger master output. "
1468+
"No supported Strobe* or Line* output path was successfully configured."
12811469
)
12821470

1471+
if strict:
1472+
raise RuntimeError(msg)
1473+
1474+
LOG.warning(msg)
1475+
12831476
def _restore_trigger_idle(self, node_map) -> None:
12841477
"""Best-effort restore to a safe non-triggering state after acquisition stops.
12851478

dlclivegui/config.py

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
ModelType = Literal["pytorch", "tensorflow"]
1515
TriggerRole = Literal["off", "external", "master", "follower"]
1616
TriggerActivation = Literal["RisingEdge", "FallingEdge", "AnyEdge", "LevelHigh", "LevelLow"]
17+
TriggerStrobePolarity = Literal["ActiveHigh", "ActiveLow"]
18+
TriggerStrobeOperation = Literal["Exposure", "FixedDuration"]
1719

1820
SINGLE_CAMERA_WORKER_DO_LOG_TIMING = False
1921
MULTI_CAMERA_WORKER_DO_LOG_TIMING = True
@@ -239,9 +241,13 @@ class CameraTriggerSettings(BaseModel):
239241
Generic hardware-trigger settings.
240242
241243
Backend-specific code may ignore fields that are unsupported by a given
242-
camera/SDK. For GenTL, these map to common GenICam nodes such as:
243-
TriggerMode, TriggerSelector, TriggerSource, TriggerActivation,
244-
LineSelector, LineMode, and LineSource.
244+
camera/SDK.
245+
246+
For GenTL/TIS DMK 37BUX287:
247+
- follower/external maps mainly to TriggerMode, TriggerSelector,
248+
TriggerActivation. TriggerSource may be read-only and is best-effort.
249+
- master output maps primarily to StrobeEnable, StrobePolarity,
250+
StrobeOperation, StrobeDuration, and StrobeDelay.
245251
"""
246252

247253
role: TriggerRole = "off"
@@ -251,10 +257,16 @@ class CameraTriggerSettings(BaseModel):
251257
source: str = "auto"
252258
activation: TriggerActivation | str = "RisingEdge"
253259

254-
# Output config: master
260+
# Generic/SFNC output config: master fallback for cameras exposing Line* nodes.
255261
output_line: str = "Line2"
256262
output_source: str = "ExposureActive"
257263

264+
# Strobe output config: master path for TIS/DMK 37U cameras.
265+
strobe_polarity: TriggerStrobePolarity | str = "ActiveHigh"
266+
strobe_operation: TriggerStrobeOperation | str = "Exposure"
267+
strobe_duration: int | None = None # µs, used when strobe_operation=FixedDuration
268+
strobe_delay: int | None = None # µs
269+
258270
# Runtime behavior
259271
timeout: float | None = None
260272
strict: bool = False
@@ -296,6 +308,17 @@ def _coerce_timeout(cls, v):
296308
return None
297309
return fv if fv > 0 else None
298310

311+
@field_validator("strobe_duration", "strobe_delay", mode="before")
312+
@classmethod
313+
def _coerce_optional_nonnegative_int(cls, v):
314+
if v in (None, ""):
315+
return None
316+
try:
317+
iv = int(float(v))
318+
except Exception:
319+
return None
320+
return iv if iv >= 0 else None
321+
299322
@field_validator("source", mode="before")
300323
@classmethod
301324
def _coerce_source(cls, v):

0 commit comments

Comments
 (0)