Add Trigger functionality to Basler backend - #81
Conversation
There was a problem hiding this comment.
Pull request overview
This PR extends the Basler camera backend with trigger configuration support (hardware trigger input + master output + early software-trigger hooks), and updates the trigger configuration UI to be backend-aware (Basler vs GenTL). It also attempts to reduce log spam when follower/external cameras are waiting for trigger pulses.
Changes:
- Added Basler backend trigger configuration parsing and GenICam/pypylon feature configuration (plus timeout handling and best-effort restore on close).
- Updated trigger settings dialog to use backend-specific UI profiles (show/hide relevant fields and provide backend-appropriate suggestions).
- Introduced throttled logging for “waiting for hardware trigger” timeouts (currently implemented incorrectly; see comments).
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
dlclivegui/services/multi_camera_controller.py |
Adds throttled logging for trigger-wait timeouts (but currently breaks at runtime due to method placement/state). |
dlclivegui/gui/main_window.py |
Removes in-file pypylon emulation env var snippet from the main window module. |
dlclivegui/gui/camera_config/trigger_config_dialog.py |
Makes the trigger dialog backend-aware via TriggerUiProfile and hides irrelevant fields for Basler/GenTL. |
dlclivegui/gui/camera_config/camera_config_dialog.py |
Ensures default trigger config is created for Basler as well as GenTL. |
dlclivegui/cameras/backends/basler_backend.py |
Implements Basler trigger settings parsing and configuration, adds trigger capabilities, and adjusts read timeouts for trigger-wait mode. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
b1337ba to
732f5fd
Compare
cc59691 to
ab3d764
Compare
1856e96 to
4b27757
Compare
66fe26b to
2925176
Compare
4b27757 to
783c1f0
Compare
4de155a to
f50bbf4
Compare
| return | ||
|
|
||
| if role == "software": | ||
| # TODO: @C-Achard 2026-07-16 - software mode not implemented yet |
There was a problem hiding this comment.
Is this still a todo? the implementation seems very complete already. Besides adding it as TriggerRole and enabling it in the ui, what needs to be done more?
There was a problem hiding this comment.
It does still need quite a bit of backend worker wiring/waiting for frames in trigger_once, which then would likely best be added as a generic method in the base class.
It's a bit of a can of worms and I thought it was not the most important to have. It also creates extra branching in timeout handling, start priorities, etc.
Overall it's feasible but I'm not sure the extra work is worth it if we are not using it? Do you think it should be added, or can we keep it partially like here and implement later if needed?
| def _ensure_default_trigger_config(self, cam: CameraSettings) -> None: | ||
| backend = (cam.backend or "").lower() | ||
| if backend != "gentl": | ||
| if backend not in {"gentl", "basler"}: |
There was a problem hiding this comment.
CameraSettings.with_save_defaults does now
if backend != "gentl":
return out
should this also be updated to include basler?
There was a problem hiding this comment.
I think I will remove this for now. The idea was to inject defaults so that users can edit them, but with the config dialog this is a bit redundant I think.
Right now missing trigger settings just default to off, which also makes configs more readable.
It may be easier for users to start with the trigger config dialog and then make necessary edits; if we want to keep this default populating step I would then avoid hard-coding backends like I did and instead fetch those which support the trigger settings.
Let me know if you agree!
f50bbf4 to
47c5652
Compare
Implement comprehensive trigger support for the Basler backend: import CameraTriggerSettings, parse trigger config (roles: off, external/follower, software, master), and persist trigger_actual into the namespace. Add trigger configuration helpers (_configure_trigger*, _resolve_trigger_source, _restore_trigger_idle), software trigger execution (trigger_once), and many feature/enum/numeric helper methods with debug logging. Make RetrieveResult use a configurable _retrieve_timeout_ms (derived from trigger.timeout) and limit it for hardware-triggered cameras to allow prompt shutdown; raise a TimeoutError when waiting for hardware triggers. Expose hardware_trigger capability as BEST_EFFORT and add an env var-based pylon emulation toggle for testing. Misc: add debug dumps of trigger-related nodes and best-effort restore of trigger state on close.
Delete commented-out code that forced pypylon to create emulation virtual cameras (PYLON_CAMEMU), which was only intended for testing and should not be enabled for release. Also remove an extraneous blank line to tidy up the file.
Introduce a TriggerUiProfile dataclass and trigger_ui_profile_for_backend() to drive dialog presentation per backend. Replace free-text source field with an editable QComboBox providing backend suggestions and defaults. Add profile-driven visibility/enabling for input, master, software and strobe/line output fields, plus helper methods to manage form rows and combo text. Only include strobe-related payload fields when the backend profile exposes them. Misc: expand info/help text, show backend in group title, increase dialog min-width, refine tooltips, and improve model↔UI mapping and payload construction.
Previously the default trigger configuration was only applied for backend 'gentl' and always stored under the 'gentl' properties key. This change treats the backend name dynamically (accepting both 'gentl' and 'basler'), and stores the default trigger settings under the actual backend key in cam.properties. Also preserves behavior when cam.properties or the backend namespace is not a dict.
Reduce log flooding when waiting for hardware triggers by adding throttled logging. Changes in dlclivegui/services/multi_camera_controller.py: - Import time. - Add a new _log_interval_while_waiting_for_trigger_s attribute to SingleCameraWorker. - Replace the direct LOGGER.debug call for expected trigger wait timeouts with a call to _log_trigger_wait_throttled. - Implement _log_trigger_wait_throttled to suppress repeated timeout messages, emit a consolidated debug message, and report how many repeated logs were suppressed. This prevents high-frequency expected poll-timeout logs (common in trigger-waiting modes) from overwhelming the logs.
Extend the test conftest FakePylon to better emulate pypylon: add FakePylonTimeoutException, richer _Feature (symbolics, min/max/inc, read/write checks, call tracking), _EnumEntry, expanded _DeviceInfo, GrabResult.release tracking, and a more complete InstantCamera (timeouts, software trigger, trigger/line features, buffer and grab controls, test knobs). Reset the fake factory and provide default fake devices and a basler_settings_factory fixture. Patch the basler SDK fixture to use FakePylon. Add new test suite tests/cameras/backends/test_basler_backend.py covering lifecycle (open/read/close, fast-start, idempotent close), discovery/rebind, resolution/exposure/gain/fps handling, and comprehensive trigger behavior (follower/master/software/external) to validate backend logic.
Add throttled logging for hardware-trigger wait timeouts in SingleCameraWorker to avoid noisy repeated timeout messages. Introduce _trigger_wait_log_interval, _last_trigger_wait_log and _trigger_wait_suppressed_count and move _log_trigger_wait_throttled into the worker; remove the duplicate implementation from MultiCameraController. Also mark the Basler software-trigger test as xfail because software trigger support is not implemented yet.
Integrates optional WorkerTimingStats into the Basler camera backend and refactors FPS handling and frame retrieval. Adds a _configure_frame_rate() helper to centralize AcquisitionFrameRate enabling, setting and readbacks (logs many related nodes and records actual_fps). Initializes a WorkerTimingStats instance (controlled by SINGLE_CAMERA_WORKER_DO_LOG_TIMING) and wraps RetrieveResult/convert/get array/release steps with timing measurements, improved error handling, proper grab_result release on exceptions, and frame counting/logging. Overall improves observability and robustness when setting frame rates and reading frames from Basler cameras.
Refactors `MultiCameraController.stop()` to track a dedicated stopping state and finalize shutdown only after all camera threads are actually stopped. It now delays `all_stopped` emission until cleanup is truly complete, keeps references to threads that refuse to terminate, and avoids the previous premature "stopped" state. The main window now explicitly calls `stop(wait=True)` during camera/project shutdown so UI transitions block until camera teardown is done.
Import `genicam` alongside `pylon` and add a helper to detect `genicam.TimeoutException` safely. The hardware-trigger timeout path now converts exceptions to `TimeoutError` only for true SDK timeout exceptions, avoiding incorrect timeout handling for unrelated grab failures.
Add coverage for hardware-triggered Basler reads to ensure SDK timeout errors are re-raised as `TimeoutError` with the expected message and exception cause. Update the Basler SDK test patching to also inject a fake `genicam.TimeoutException`, so timeout handling paths can be exercised reliably in tests.
Tightens controller start/stop state handling to prevent unsafe restarts while shutdown is in progress, and finalizes shutdown only after all camera threads actually exit. Cleanup now resets runtime state consistently, `all_stopped` emission is guarded against duplicates, and stop requests only fan out to workers when actively running. Camera stop handling was also refined so full initialization failure and last-camera runtime shutdown both transition into the same safe finalization path, with clearer logging around incomplete shutdowns.
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Stop clearing `_runtime_info` as it is not currently defined
Update the hardware-trigger timeout test to raise `bb.genicam.TimeoutException` directly instead of a fake timeout class, and assert that this real pylon exception is preserved as the `__cause__` of the mapped `TimeoutError`. This keeps the test aligned with actual backend behavior.
Simplify `CameraSettings.with_save_defaults()` to return only a deep copy, removing the special-case logic that auto-populated `properties.gentl.trigger`. This avoids silently adding GenTL trigger settings during save.
Removes the helper that injected default trigger settings for GenTL/Basler cameras and drops its call sites when adding or loading cameras. This avoids silently mutating camera properties with trigger defaults unless they are explicitly configured.
Updates `tests/test_config.py` to reflect the new trigger configuration semantics. The tests now verify that missing trigger config defaults to `role="off"` and `source="auto"`, explicit trigger settings round-trip through `ApplicationSettings` for both `gentl` and `basler`, and serialization does not inject implicit trigger blocks into camera properties.
de6be91 to
42bbb1f
Compare
Scope
Finished implementing software trigger modeMotivation
Adds the possibility to setup master/follower or software-controlled camera schemes to the Basler backend so as to obtain viable files for DLC3D or other experimental work.