Skip to content

Commit 2235399

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 2332c3f commit 2235399

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
# ------------------------------------------------------------------
@@ -461,6 +544,7 @@ def open(self) -> None:
461544
self._configure_gain(node_map)
462545
self._configure_frame_rate(node_map)
463546
self._configure_trigger(node_map) # keep low in the list
547+
self._debug_trigger_nodes(node_map, context="after configuration before acquisition")
464548
self._ensure_settings_ns()["trigger_actual"] = self._trigger_to_dict(self._trigger)
465549
self._read_telemetry(node_map)
466550
self._persist_device_metadata(selected_info, selected_serial)
@@ -1183,31 +1267,42 @@ def _configure_trigger_off(self, node_map, *, strict: bool = False) -> None:
11831267
def _configure_trigger_input(self, node_map, cfg, *, strict: bool = False) -> None:
11841268
role = str(self._trigger_attr(cfg, "role", "external") or "external").strip().lower()
11851269
selector = str(self._trigger_attr(cfg, "selector", "FrameStart") or "FrameStart")
1186-
source = str(self._trigger_attr(cfg, "source", "Line0") or "Line0")
11871270
activation = str(self._trigger_attr(cfg, "activation", "RisingEdge") or "RisingEdge")
1271+
source = str(self._trigger_attr(cfg, "source", "auto") or "auto").strip()
11881272

11891273
# Disable trigger while changing trigger-related nodes.
11901274
self._set_enum_node(node_map, "TriggerMode", "Off", strict=False)
11911275

11921276
selector_ok = self._set_enum_node(node_map, "TriggerSelector", selector, 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)
11951277
activation_ok = self._set_enum_node(node_map, "TriggerActivation", activation, strict=strict)
11961278

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

12301325
LOG.info(
1231-
"GenTL trigger input configured: role=%s selector=%s source=%s activation=%s activation_ok=%s",
1326+
"GenTL trigger input configured: role=%s selector=%s activation=%s "
1327+
"selector_ok=%s activation_ok=%s source_requested=%s source_ok=%s",
12321328
role,
12331329
selector,
1234-
source,
12351330
activation,
1331+
selector_ok,
12361332
activation_ok,
1333+
source,
1334+
source_ok,
12371335
)
12381336

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

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

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

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

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

1266-
if not (mode_ok and source_ok):
12671424
LOG.warning(
1268-
"GenTL trigger master output configuration incomplete (LineMode ok=%s, LineSource ok=%s).",
1269-
mode_ok,
1270-
source_ok,
1425+
"StrobeEnable node exists but could not be enabled; falling back to generic Line* output configuration."
12711426
)
1272-
return
12731427

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

1468+
if strict:
1469+
raise RuntimeError(msg)
1470+
1471+
LOG.warning(msg)
1472+
12801473
def _restore_trigger_idle(self, node_map) -> None:
12811474
"""Best-effort restore to a safe non-triggering state after acquisition stops.
12821475

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)