Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
60 commits
Select commit Hold shift + click to select a range
f360b73
Add shared Harvester pool and path normalization
C-Achard May 11, 2026
971f629
Use shared Harvester pool for GenTL producers
C-Achard May 11, 2026
60b7607
WIP Working multi CTI implementation
C-Achard May 26, 2026
4032402
Clean up file
C-Achard May 26, 2026
b2e53dc
Handle GenTL open errors & improve device matching
C-Achard May 26, 2026
ab2598e
Improve GenTL pixel format selection and conversion
C-Achard May 26, 2026
95c8647
Expose shared GenTL harvester diagnostics
C-Achard May 26, 2026
f2d0cc7
Handle CTI load failures and track failed files
C-Achard May 26, 2026
02bcbb9
Improve duplicate camera detection and reporting
C-Achard May 26, 2026
92dd624
Sort file paths in FakeSharedHarvesterPool._key
C-Achard May 26, 2026
5f56a5f
Merge failed CTI files instead of overwrite
C-Achard May 26, 2026
9b77c42
Avoid overwriting failed CTIs
C-Achard May 26, 2026
90241f6
pre-commit
C-Achard May 26, 2026
760825f
Revert display name change
C-Achard May 29, 2026
2e54469
Use get_display_id in multicam test
C-Achard May 29, 2026
ece0005
Add GenTL hardware trigger support
C-Achard May 28, 2026
de99825
Handle camera trigger defaults & trigger-aware startup
C-Achard May 28, 2026
a27a1f2
Improve signal handling and graceful shutdown
C-Achard May 28, 2026
4e6dac0
Add GenTL trigger tests and fake node map
C-Achard May 28, 2026
3e40d94
Respect user camera order for display/tiling
C-Achard May 28, 2026
d602c1e
tests: preserve display order and add MultiCamera tests
C-Achard May 28, 2026
cf7a237
Mock _maybe_allow_keyboard_interrupt in GUI tests
C-Achard May 28, 2026
f567103
Support strict GenTL trigger and defaults
C-Achard May 28, 2026
ea49e5a
Apply gentl trigger defaults when saving
C-Achard May 28, 2026
b073ce3
Don't sort available camera IDs
C-Achard May 28, 2026
cd562d5
Preserve DLC config using model_copy
C-Achard May 28, 2026
0cc318b
Warn when GenTL TriggerMode fails to enable
C-Achard May 28, 2026
973d69f
Improve GenTL trigger routing safety and tests
C-Achard May 28, 2026
7942066
Cap hardware-trigger fetch timeout and update tests
C-Achard May 28, 2026
93c2a73
Interruptible camera waits; simplify config save
C-Achard May 29, 2026
96354ec
Potential fix for pull request finding
C-Achard May 29, 2026
fe826a1
Fix broken suggestion
C-Achard May 29, 2026
d44bf18
Resolve 'auto' trigger source in GenTL backend
C-Achard May 29, 2026
662313a
Fix display tests
C-Achard May 29, 2026
3edb48c
Update test_multicam_controller.py
C-Achard May 29, 2026
0af5615
Propagate display IDs for camera labeling
C-Achard Jun 1, 2026
ed1a9f5
Support fp: targets and downgrade CTI error log
C-Achard Jun 1, 2026
0d5e1dc
Add tests for stable camera ID usage
C-Achard Jun 1, 2026
ce587cb
Merge branch 'master' into cy/gentl-cti-lock-fix
C-Achard Jun 19, 2026
dbaccbb
Merge branch 'cy/gentl-cti-lock-fix' into cy/gentl-trigger-config
C-Achard Jun 19, 2026
f6894a0
Update test_multicam_controller.py
C-Achard Jun 19, 2026
3675751
tests: preserve display order and add MultiCamera tests
C-Achard May 28, 2026
ded0b55
Add trigger settings dialog and UI button
C-Achard May 28, 2026
bec049d
Add per-camera hardware trigger settings
C-Achard May 28, 2026
8981788
Update trigger_config_dialog.py
C-Achard May 28, 2026
3cea2a3
Use _is_preview_live and clear trigger timeout
C-Achard May 29, 2026
d12c098
Restart active previews; ignore bad trigger
C-Achard May 29, 2026
2c387b5
Improve trigger dialog defaults and error handling
C-Achard May 29, 2026
d377d19
Add worker timing logs to camera worker
C-Achard May 29, 2026
4003e91
Add per-camera timing to MultiCameraController
C-Achard May 29, 2026
5cac636
Add pretty/str/repr to CameraSettings
C-Achard May 29, 2026
ff81a9e
Reduce lock scope and refactor frame handling
C-Achard May 29, 2026
52feea9
Add GenTL strobe support and trigger debugging
C-Achard Jun 2, 2026
658cea6
Add _node_value helper for safe node reads
C-Achard Jun 19, 2026
732f5fd
Refactor GenTL trigger source handling and logging
C-Achard Jun 19, 2026
0d13a4c
Split multi-camera frame signals; throttle display
C-Achard May 29, 2026
126c1c3
Add GenTL telemetry and frame-rate debugging
C-Achard May 29, 2026
0e7710f
Remove temp debug block
C-Achard May 29, 2026
4f09b21
Fix missing signal wiring
C-Achard May 29, 2026
fb1b2ed
Allow zero node values and reduce log level
C-Achard Jun 1, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2,487 changes: 1,491 additions & 996 deletions dlclivegui/cameras/backends/gentl_backend.py

Large diffs are not rendered by default.

137 changes: 129 additions & 8 deletions dlclivegui/cameras/backends/utils/gentl_discovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
from __future__ import annotations

import glob
import logging
import os
import threading
from collections.abc import Iterable, Sequence
from dataclasses import dataclass, field
from enum import Enum, auto
Expand All @@ -18,6 +20,125 @@ class GenTLDiscoveryPolicy(Enum):
RAISE_IF_MULTIPLE = auto() # if > N candidates, raise an error to avoid ambiguity (forces explicit config)


try: # pragma: no cover - optional dependency
from harvesters.core import Harvester # type: ignore
except Exception: # pragma: no cover - optional dependency
Harvester = None # type: ignore

logger = logging.getLogger(__name__)


class SharedHarvesterEntry:
"""
A shared Harvester instance keyed by a canonical tuple of CTI files.
"""

def __init__(self, cti_files: list[str]):
if Harvester is None: # pragma: no cover
raise RuntimeError(
"The 'harvesters' package is required for the GenTL backend. Install it via 'pip install harvesters'."
)

self.lock = threading.RLock()
self.key = tuple(sorted(_normalize_path(p, casefold_windows=True) for p in cti_files))
self.refcount = 0
self.harvester = Harvester()
self.loaded_files: list[str] = []
self.failed_files: dict[str, str] = {}

for cti in self.key:
try:
self.harvester.add_file(cti)
self.loaded_files.append(cti)
except Exception as e:
logger.warning(f"Failed to load CTI file: {cti}. Skipping.")
self.failed_files[cti] = str(e)

if not self.loaded_files:
e = RuntimeError("No GenTL producer (.cti) could be loaded by shared Harvester.")
self._raise_and_reset_harvester(e)

# Initial device enumeration.
try:
self.harvester.update()
except Exception as e:
self._raise_and_reset_harvester(e)

def _raise_and_reset_harvester(self, exc: Exception) -> None:
exc.loaded_files = self.loaded_files[:]
exc.failed_files = dict(self.failed_files)
try:
self.harvester.reset()
except Exception:
pass
raise exc


class SharedHarvesterPool:
"""
Process-local pool of shared Harvester instances.

Keyed by the canonicalized CTI file set.
"""

_lock = threading.RLock()
_entries: dict[tuple[str, ...], SharedHarvesterEntry] = {}

@classmethod
def acquire(cls, cti_files: list[str]) -> SharedHarvesterEntry:
key = tuple(sorted(_normalize_path(p, casefold_windows=True) for p in cti_files))
with cls._lock:
entry = cls._entries.get(key)
if entry is None:
entry = SharedHarvesterEntry(list(key))
cls._entries[key] = entry
entry.refcount += 1
return entry

@classmethod
def release(cls, entry: SharedHarvesterEntry | None) -> None:
if entry is None:
return

with cls._lock:
current = cls._entries.get(entry.key)
if current is None:
# Already released/reset.
return

current.refcount -= 1
if current.refcount > 0:
return

try:
with current.lock:
try:
current.harvester.reset()
except Exception:
pass
finally:
cls._entries.pop(entry.key, None)

@classmethod
def refresh(cls, entry: SharedHarvesterEntry | None) -> None:
"""
Optional helper when callers want to re-enumerate the device list
on an already-shared Harvester instance.
"""
if entry is None:
return
with entry.lock:
entry.harvester.update()

@classmethod
def get_refcount(cls, entry: SharedHarvesterEntry | None) -> int:
if entry is None:
return 0
with cls._lock:
current = cls._entries.get(entry.key)
return int(current.refcount) if current is not None else 0


@dataclass
class CTIDiscoveryDiagnostics:
explicit_files: list[str] = field(default_factory=list)
Expand Down Expand Up @@ -81,18 +202,18 @@ def _expand_user_and_env(value: str) -> str:
return s


def _normalize_path(p: str) -> str:
"""
Normalize a filesystem path in a cross-platform way:
- expands ~ and environment variables
- resolves to absolute where possible (without requiring existence)
"""
def _normalize_path(p: str, *, casefold_windows: bool = False) -> str:
expanded = _expand_user_and_env(p)
pp = Path(expanded)
try:
return str(pp.resolve(strict=False))
out = str(pp.resolve(strict=False))
except Exception:
return str(pp.absolute())
out = str(pp.absolute())

if casefold_windows:
out = os.path.normcase(out)

return out


def _iter_cti_files_in_dir(directory: str, recursive: bool = False) -> Iterable[str]:
Expand Down
1 change: 1 addition & 0 deletions dlclivegui/cameras/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ class SupportLevel(str, Enum):
"set_gain": SupportLevel.UNSUPPORTED,
"device_discovery": SupportLevel.UNSUPPORTED,
"stable_identity": SupportLevel.UNSUPPORTED,
"hardware_trigger": SupportLevel.UNSUPPORTED,
}


Expand Down
Loading
Loading