-
Notifications
You must be signed in to change notification settings - Fork 28
Add UI to configure Trigger mode #79
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
C-Achard
wants to merge
14
commits into
cy/gentl-trigger-config
Choose a base branch
from
cy/gentl-trigger-ui
base: cy/gentl-trigger-config
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
14 commits
Select commit
Hold shift + click to select a range
65712ca
Add trigger settings dialog and UI button
C-Achard d51c8f7
Add per-camera hardware trigger settings
C-Achard 9739824
Update trigger_config_dialog.py
C-Achard 358ebcf
Use _is_preview_live and clear trigger timeout
C-Achard a1a139b
Restart active previews; ignore bad trigger
C-Achard 2043e57
Improve trigger dialog defaults and error handling
C-Achard 18446c2
Add worker timing logs to camera worker
C-Achard b5c586f
Add per-camera timing to MultiCameraController
C-Achard 60077f2
Add pretty/str/repr to CameraSettings
C-Achard e605c80
Reduce lock scope and refactor frame handling
C-Achard fa31121
Add GenTL strobe support and trigger debugging
C-Achard c0b6e58
Add _node_value helper for safe node reads
C-Achard 715f270
Refactor GenTL trigger source handling and logging
C-Achard 2925176
Update multi_camera_controller.py
C-Achard File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -185,6 +185,89 @@ def static_capabilities(cls) -> dict[str, SupportLevel]: | |
| "hardware_trigger": SupportLevel.BEST_EFFORT, | ||
| } | ||
|
|
||
| def _debug_trigger_nodes(self, node_map, *, context: str = "") -> None: | ||
| names = ( | ||
| "TriggerMode", | ||
| "TriggerSelector", | ||
| "TriggerSource", | ||
| "TriggerActivation", | ||
| "AcquisitionMode", | ||
| # Generic line nodes, if available. | ||
| "LineSelector", | ||
| "LineMode", | ||
| "LineSource", | ||
| # TIS 37U / DMK 37BUX287 strobe/output nodes. | ||
| "GPIn", | ||
| "GPOut", | ||
| "StrobeEnable", | ||
| "StrobePolarity", | ||
| "StrobeOperation", | ||
| "StrobeDuration", | ||
| "StrobeDelay", | ||
| ) | ||
|
|
||
| label = f"GenTL trigger debug {context}".strip() | ||
|
|
||
| for name in names: | ||
| node = self._node(node_map, name) | ||
| if node is None: | ||
| continue | ||
|
|
||
| value = self._node_value(node_map, name, None) | ||
|
|
||
| extras = [] | ||
|
|
||
| symbolics = self._node_symbolics(node) | ||
| if symbolics: | ||
| extras.append(f"symbolics={symbolics}") | ||
|
|
||
| for attr in ("access_mode", "is_writable", "is_readable"): | ||
| try: | ||
| extras.append(f"{attr}={getattr(node, attr)}") | ||
| except Exception: | ||
| pass | ||
|
|
||
| LOG.debug("%s: %s=%r %s", label, name, value, " ".join(extras)) | ||
|
|
||
| def _debug_frame_rate_nodes(self, node_map, *, context: str = "") -> None: | ||
| names = ( | ||
| "AcquisitionFrameRateEnable", | ||
| "AcquisitionFrameRateControlEnable", | ||
| "AcquisitionFrameRate", | ||
| "AcquisitionFrameRateAbs", | ||
| "AcquisitionResultingFrameRate", | ||
| "ResultingFrameRate", | ||
| "AcquisitionFrameRateResulting", | ||
| "DeviceFrameRate", | ||
| "ExposureAuto", | ||
| "ExposureTime", | ||
| "ExposureTimeAbs", | ||
| "DeviceLinkThroughputLimit", | ||
| "DeviceLinkThroughputLimitMode", | ||
| "PayloadSize", | ||
| "Width", | ||
| "Height", | ||
| "PixelFormat", | ||
| ) | ||
|
|
||
| label = f"GenTL FPS debug {context}".strip() | ||
|
|
||
| for name in names: | ||
| node = self._node(node_map, name) | ||
| if node is None: | ||
| continue | ||
|
|
||
| value = self._node_value(node_map, name, None) | ||
|
|
||
| extras = [] | ||
| for attr in ("min", "max", "inc"): | ||
| try: | ||
| extras.append(f"{attr}={getattr(node, attr)}") | ||
| except Exception: | ||
| pass | ||
|
|
||
| LOG.debug("%s: %s=%r %s", label, name, value, " ".join(extras)) | ||
|
|
||
| # ------------------------------------------------------------------ | ||
| # Discovery | ||
| # ------------------------------------------------------------------ | ||
|
|
@@ -462,6 +545,7 @@ def open(self) -> None: | |
|
|
||
| self._configure_trigger(node_map) | ||
| self._apply_effective_fetch_timeout() | ||
| self._debug_trigger_nodes(node_map, context="after configuration before acquisition") | ||
|
|
||
| actual_trigger = self._trigger_to_dict(self._trigger) | ||
| actual_trigger["fetch_timeout"] = self._timeout | ||
|
|
@@ -1015,6 +1099,32 @@ def _node(node_map, name: str): | |
| except Exception: | ||
| return None | ||
|
|
||
| @staticmethod | ||
| def _node_value(node_map, name: str, default=None): | ||
| """Best-effort read of a GenICam node value. | ||
|
|
||
| Debug helpers must not make open() fail just because a value cannot be read. | ||
| Harvesters-style fake/test nodes usually expose `.value`; some SDK-style | ||
| nodes may expose `GetValue()`. | ||
| """ | ||
| node = GenTLCameraBackend._node(node_map, name) | ||
| if node is None: | ||
| return default | ||
|
|
||
| try: | ||
| return node.value | ||
| except Exception: | ||
| pass | ||
|
|
||
| try: | ||
| getter = getattr(node, "GetValue", None) | ||
| if getter is not None: | ||
| return getter() | ||
| except Exception: | ||
| pass | ||
|
|
||
| return default | ||
|
|
||
| @staticmethod | ||
| def _node_symbolics(node) -> list[str]: | ||
| try: | ||
|
|
@@ -1214,16 +1324,35 @@ def _configure_trigger_off(self, node_map, *, strict: bool = False) -> None: | |
| def _configure_trigger_input(self, node_map, cfg, *, strict: bool = False) -> None: | ||
| role = str(self._trigger_attr(cfg, "role", "external") or "external").strip().lower() | ||
| selector = str(self._trigger_attr(cfg, "selector", "FrameStart") or "FrameStart") | ||
| source = str(self._trigger_attr(cfg, "source", "Line0") or "Line0") | ||
| activation = str(self._trigger_attr(cfg, "activation", "RisingEdge") or "RisingEdge") | ||
| source = str(self._trigger_attr(cfg, "source", "auto") or "auto").strip() | ||
|
|
||
| # Disable trigger while changing trigger-related nodes. | ||
| self._set_enum_node(node_map, "TriggerMode", "Off", strict=False) | ||
|
|
||
| selector_ok = self._set_enum_node(node_map, "TriggerSelector", selector, strict=strict) | ||
| source, source_resolved = self._resolve_trigger_source(node_map, source, strict=strict) | ||
| source_ok = source_resolved and self._set_enum_node(node_map, "TriggerSource", source, strict=strict) | ||
| activation_ok = self._set_enum_node(node_map, "TriggerActivation", activation, strict=strict) | ||
|
|
||
| resolved_source, source_supported = self._resolve_trigger_source( | ||
| node_map, | ||
| source, | ||
| strict=strict, | ||
| ) | ||
|
|
||
| source_ok = False | ||
| if source_supported: | ||
| source_ok = self._set_enum_node( | ||
| node_map, | ||
| "TriggerSource", | ||
| resolved_source, | ||
| strict=strict, | ||
| ) | ||
|
|
||
| activation_ok = self._set_enum_node( | ||
| node_map, | ||
| "TriggerActivation", | ||
| activation, | ||
| strict=False, | ||
| ) | ||
|
|
||
| # TriggerSelector and TriggerSource are required routing nodes. | ||
| # 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 | |
| LOG.warning( | ||
| "Could not apply GenTL trigger input routing " | ||
| "(selector_ok=%s, source_ok=%s); disabling trigger. " | ||
| "requested role=%s selector=%s source=%s activation=%s", | ||
| "requested role=%s selector=%s source=%s resolved_source=%s activation=%s", | ||
| selector_ok, | ||
| source_ok, | ||
| role, | ||
| selector, | ||
| source, | ||
| resolved_source, | ||
| activation, | ||
| ) | ||
| self._configure_trigger_off(node_map, strict=False) | ||
|
|
@@ -1259,55 +1389,154 @@ def _configure_trigger_input(self, node_map, cfg, *, strict: bool = False) -> No | |
| return | ||
|
|
||
| LOG.info( | ||
| "GenTL trigger input configured: role=%s selector=%s source=%s activation=%s activation_ok=%s", | ||
| "GenTL trigger input configured: role=%s selector=%s source_requested=%s " | ||
| "source=%s activation=%s selector_ok=%s source_ok=%s activation_ok=%s", | ||
| role, | ||
| selector, | ||
| source, | ||
| resolved_source, | ||
| activation, | ||
| selector_ok, | ||
| source_ok, | ||
| activation_ok, | ||
| ) | ||
|
|
||
| def _configure_trigger_master(self, node_map, cfg, *, strict: bool = False) -> None: | ||
| """Configure this camera as a free-running master that emits STROBE_OUT pulses. | ||
|
|
||
| For DMK 37BUX287 / TIS 37U series, the physical output is controlled by | ||
| StrobeEnable/StrobePolarity/StrobeOperation rather than SFNC LineSelector/ | ||
| LineMode/LineSource nodes. | ||
| """ | ||
| output_line = str(self._trigger_attr(cfg, "output_line", "Line2") or "Line2") | ||
| output_source = str(self._trigger_attr(cfg, "output_source", "ExposureActive") or "ExposureActive") | ||
|
|
||
| # Master camera runs freerun and exposes an output signal. | ||
| # Optional extra fields if present in trigger dict/model. | ||
| strobe_polarity = str(self._trigger_attr(cfg, "strobe_polarity", "ActiveHigh") or "ActiveHigh") | ||
| strobe_operation = str(self._trigger_attr(cfg, "strobe_operation", "Exposure") or "Exposure") | ||
| strobe_duration = self._trigger_attr(cfg, "strobe_duration", None) | ||
| strobe_delay = self._trigger_attr(cfg, "strobe_delay", None) | ||
|
|
||
| # Master camera should be free-running. | ||
| self._configure_trigger_off(node_map, strict=False) | ||
|
|
||
| line_selected = self._set_enum_node( | ||
| node_map, | ||
| "LineSelector", | ||
| output_line, | ||
| strict=strict, | ||
| ) | ||
| # ------------------------------------------------------------------ | ||
| # Preferred path for The Imaging Source 37U / DMK 37BUX287: | ||
| # StrobeEnable, StrobePolarity, StrobeOperation, StrobeDuration, StrobeDelay | ||
| # ------------------------------------------------------------------ | ||
| strobe_enable_node = self._node(node_map, "StrobeEnable") | ||
|
|
||
| if strobe_enable_node is not None: | ||
| # Disable first while changing parameters. | ||
| self._set_enum_node(node_map, "StrobeEnable", "Off", strict=False) | ||
|
|
||
| polarity_ok = self._set_enum_node( | ||
| node_map, | ||
| "StrobePolarity", | ||
| strobe_polarity, | ||
| strict=False, | ||
| ) | ||
|
|
||
| # In non-strict mode, do not continue configuring output behavior if the | ||
| # requested line could not be selected. Otherwise we may accidentally drive | ||
| # whichever GPIO line the camera had selected previously/defaulted to. | ||
| if not line_selected: | ||
| LOG.warning( | ||
| "Could not select GenTL output line '%s'; skipping trigger output configuration.", | ||
| output_line, | ||
| operation_ok = self._set_enum_node( | ||
| node_map, | ||
| "StrobeOperation", | ||
| strobe_operation, | ||
| strict=False, | ||
| ) | ||
| return | ||
|
|
||
| mode_ok = self._set_enum_node(node_map, "LineMode", "Output", strict=strict) | ||
| source_ok = self._set_enum_node(node_map, "LineSource", output_source, strict=strict) | ||
| if strobe_duration is not None: | ||
| try: | ||
| node = self._node(node_map, "StrobeDuration") | ||
| if node is not None: | ||
| node.value = int(strobe_duration) | ||
| LOG.info("Configured GenTL StrobeDuration=%s", int(strobe_duration)) | ||
| except Exception as exc: | ||
| if strict: | ||
| raise RuntimeError(f"Failed to set StrobeDuration={strobe_duration}: {exc}") from exc | ||
| LOG.warning("Failed to set StrobeDuration=%s: %s", strobe_duration, exc) | ||
|
|
||
| if strobe_delay is not None: | ||
| try: | ||
| node = self._node(node_map, "StrobeDelay") | ||
| if node is not None: | ||
| node.value = int(strobe_delay) | ||
| LOG.info("Configured GenTL StrobeDelay=%s", int(strobe_delay)) | ||
| except Exception as exc: | ||
| if strict: | ||
| raise RuntimeError(f"Failed to set StrobeDelay={strobe_delay}: {exc}") from exc | ||
| LOG.warning("Failed to set StrobeDelay=%s: %s", strobe_delay, exc) | ||
|
|
||
| enable_ok = self._set_enum_node( | ||
| node_map, | ||
| "StrobeEnable", | ||
| "On", | ||
| strict=strict, | ||
| ) | ||
|
|
||
| if enable_ok: | ||
| LOG.info( | ||
| "GenTL trigger master configured via Strobe*: " | ||
| "StrobeEnable=On StrobePolarity=%s polarity_ok=%s " | ||
| "StrobeOperation=%s operation_ok=%s", | ||
| strobe_polarity, | ||
| polarity_ok, | ||
| strobe_operation, | ||
| operation_ok, | ||
| ) | ||
| return | ||
|
|
||
| if strict: | ||
| raise RuntimeError("Could not enable GenTL StrobeEnable=On") | ||
|
|
||
| if not (mode_ok and source_ok): | ||
| LOG.warning( | ||
| "GenTL trigger master output configuration incomplete (LineMode ok=%s, LineSource ok=%s).", | ||
| mode_ok, | ||
| source_ok, | ||
| "StrobeEnable node exists but could not be enabled; falling back to generic Line* output configuration." | ||
| ) | ||
| return | ||
|
|
||
| LOG.info( | ||
| "GenTL trigger master configured: output_line=%s output_source=%s", | ||
| output_line, | ||
| output_source, | ||
| # ------------------------------------------------------------------ | ||
| # Generic SFNC fallback for cameras that expose LineSelector/LineMode/LineSource. | ||
| # ------------------------------------------------------------------ | ||
| line_selector = self._node(node_map, "LineSelector") | ||
| if line_selector is not None: | ||
| line_selected = self._set_enum_node( | ||
| node_map, | ||
| "LineSelector", | ||
| output_line, | ||
| strict=strict, | ||
| ) | ||
|
|
||
| if not line_selected: | ||
| LOG.warning( | ||
| "Could not select GenTL output line '%s'; skipping Line* output configuration.", | ||
| output_line, | ||
| ) | ||
| else: | ||
| mode_ok = self._set_enum_node(node_map, "LineMode", "Output", strict=strict) | ||
| source_ok = self._set_enum_node(node_map, "LineSource", output_source, strict=strict) | ||
|
|
||
| if mode_ok and source_ok: | ||
| LOG.info( | ||
| "GenTL trigger master configured via Line*: output_line=%s output_source=%s", | ||
| output_line, | ||
| output_source, | ||
| ) | ||
| return | ||
|
|
||
| LOG.warning( | ||
| "GenTL Line* trigger output configuration incomplete (LineMode ok=%s, LineSource ok=%s).", | ||
| mode_ok, | ||
| source_ok, | ||
| ) | ||
|
|
||
| msg = ( | ||
| "Could not configure GenTL trigger master output. " | ||
| "No supported Strobe* or Line* output path was successfully configured." | ||
| ) | ||
|
|
||
| if strict: | ||
| raise RuntimeError(msg) | ||
|
|
||
| LOG.warning(msg) | ||
|
|
||
| def _restore_trigger_idle(self, node_map) -> None: | ||
| """Best-effort restore to a safe non-triggering state after acquisition stops. | ||
|
|
||
|
Comment on lines
1540
to
1542
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should this also restore |
||
|
|
||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hypothetical case:
source_supportedis not True becauseTriggerSourceis read-only on some camera. This is currently treated as couldn't writeTriggerSource-> abort trigger setup.Would it be better to accept the camera default as fallback (best-effort) and just emit a warning for that, instead of aborting the setup. If you are also unsure, maybe good to think about it together.