Skip to content

Commit 873ad07

Browse files
committed
Add cti_files_source marker and fallback logic
Introduce a cti_files_source marker (auto/user) and wiring to track whether CTI file lists were user-specified or auto-discovered. Treat legacy top-level properties.cti_file(s) as strict user overrides; treat properties.gentl.cti_file(s) as either an auto-cache (falls back to discovery if stale) or a user override depending on the marker. Persist the resolved source back into the namespace when resolving CTIs, and update harvester-selection/rebind logic to fall back to discovery when auto-cached CTIs are stale while still raising for strict user overrides. Also add a small import and internal field (_cti_files_source_used) to track the chosen source, plus logging when falling back from a stale auto-cache.
1 parent a42d496 commit 873ad07

1 file changed

Lines changed: 106 additions & 40 deletions

File tree

dlclivegui/cameras/backends/gentl_backend.py

Lines changed: 106 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,11 @@ class GenTLCameraBackend(CameraBackend):
3939
r"C:\\Program Files\\The Imaging Source Europe GmbH\\TIS Camera SDK\\bin\\win64_x64\\*.cti",
4040
r"C:\\Program Files (x86)\\The Imaging Source Europe GmbH\\TIS Grabber\\bin\\win64_x64\\*.cti",
4141
)
42+
# Source marker stored in properties["gentl"]["cti_files_source"]
43+
# auto : persisted by auto-discovery (env vars, patterns, etc.). Cache, may be stale, re-discover if missing.
44+
# user : explicitly set by user via properties.gentl.cti_file(s). Cache, strict raise if missing.
45+
_CTI_FILES_SOURCE_AUTO: ClassVar[str] = "auto"
46+
_CTI_FILES_SOURCE_USER: ClassVar[str] = "user"
4247

4348
def __init__(self, settings):
4449
super().__init__(settings)
@@ -129,6 +134,8 @@ def __init__(self, settings):
129134
self._acquirer = None
130135
self._device_label: str | None = None
131136

137+
self._cti_files_source_used: str | None = None
138+
132139
@property
133140
def actual_resolution(self) -> tuple[int, int] | None:
134141
if self._actual_width and self._actual_height:
@@ -191,54 +198,95 @@ def _resolve_cti_files_for_settings(self) -> list[str]:
191198
"""
192199
Resolve CTI files to load.
193200
194-
Policy:
195-
- If the user explicitly provides ctis (cti_files / cti_file), use only those.
196-
- Otherwise, discover all CTIs (env vars + configured patterns/dirs) and return all.
197-
- Never raise just because multiple CTIs exist.
198-
- Raise only when none are found.
201+
Option B policy (source marker + fallback):
202+
- User override (properties.gentl.cti_file/cti_files OR legacy properties.cti_file/cti_files):
203+
* strict: must exist, otherwise raise
204+
* source = "user"
205+
- Auto-persisted cache (properties.gentl.cti_files_source == "auto"):
206+
* try persisted ctis first
207+
* if stale/missing, fall back to discovery
208+
* source = "auto"
209+
- Default: discovery (env + configured patterns/dirs) => source = "auto"
210+
211+
Never raise just because multiple CTIs exist.
212+
Raise only when none are found (after allowed fallback).
199213
"""
200214
props = self.settings.properties if isinstance(self.settings.properties, dict) else {}
201215
ns = props.get(self.OPTIONS_KEY, {})
202216
if not isinstance(ns, dict):
203217
ns = {}
204218

219+
# Read source marker
220+
source = ns.get("cti_files_source")
221+
source = str(source).strip().lower() if source is not None else None
222+
205223
# Explicit CTIs (namespace first, then legacy top-level)
206224
ns_cti_files = ns.get("cti_files")
207225
ns_cti_file = ns.get("cti_file")
208226
legacy_cti_files = props.get("cti_files")
209227
legacy_cti_file = props.get("cti_file")
210228

211-
# 1) If user provided explicit list/file in namespace, use that only
212-
if ns_cti_files or ns_cti_file:
229+
# ------------------------------------------------------------
230+
# 1) Legacy explicit CTIs: always treat as user override (strict)
231+
# ------------------------------------------------------------
232+
if legacy_cti_files or legacy_cti_file:
233+
self._cti_files_source_used = self._CTI_FILES_SOURCE_USER
234+
213235
candidates, diag = cti_finder.discover_cti_files(
214-
cti_file=str(ns_cti_file) if ns_cti_file else None,
215-
cti_files=cti_finder.cti_files_as_list(ns_cti_files) if ns_cti_files else None,
236+
cti_file=str(legacy_cti_file) if legacy_cti_file else None,
237+
cti_files=cti_finder.cti_files_as_list(legacy_cti_files) if legacy_cti_files else None,
216238
include_env=False,
217239
must_exist=True,
218240
)
219241
if not candidates:
220242
raise RuntimeError(
221-
"No valid GenTL producer (.cti) found from properties.gentl.cti_file/cti_files.\n\n"
243+
"No valid GenTL producer (.cti) found from properties.cti_file/cti_files.\n\n"
222244
f"Discovery details:\n{diag.summarize()}"
223245
)
224246
return list(candidates)
225247

226-
# 2) If user provided explicit list/file in legacy top-level, use that only
227-
if legacy_cti_files or legacy_cti_file:
248+
# ------------------------------------------------------------------------
249+
# 2) Namespace explicit CTIs: behavior depends on cti_files_source marker
250+
# - source=="auto": treat as cache, stale => fallback to discovery
251+
# - otherwise: strict user override
252+
# ------------------------------------------------------------------------
253+
if ns_cti_files or ns_cti_file:
254+
is_auto_cache = source == self._CTI_FILES_SOURCE_AUTO
255+
256+
# Default to "user" if the marker is missing/unknown.
257+
self._cti_files_source_used = self._CTI_FILES_SOURCE_AUTO if is_auto_cache else self._CTI_FILES_SOURCE_USER
258+
228259
candidates, diag = cti_finder.discover_cti_files(
229-
cti_file=str(legacy_cti_file) if legacy_cti_file else None,
230-
cti_files=cti_finder.cti_files_as_list(legacy_cti_files) if legacy_cti_files else None,
260+
cti_file=str(ns_cti_file) if ns_cti_file else None,
261+
cti_files=cti_finder.cti_files_as_list(ns_cti_files) if ns_cti_files else None,
231262
include_env=False,
232263
must_exist=True,
233264
)
234-
if not candidates:
265+
266+
if candidates:
267+
return list(candidates)
268+
269+
# If auto cache is stale, fall back to discovery
270+
if is_auto_cache:
271+
LOG.info(
272+
"Auto-persisted GenTL CTIs appear stale/missing; falling back to discovery. "
273+
"Persisted cti_file=%s cti_files=%s",
274+
ns_cti_file,
275+
ns_cti_files,
276+
)
277+
# Fall through to discovery (below)
278+
else:
279+
# User override: strict failure
235280
raise RuntimeError(
236-
"No valid GenTL producer (.cti) found from properties.cti_file/cti_files.\n\n"
281+
"No valid GenTL producer (.cti) found from properties.gentl.cti_file/cti_files.\n\n"
237282
f"Discovery details:\n{diag.summarize()}"
238283
)
239-
return list(candidates)
240284

241-
# 3) Discovery path: find all CTIs from env vars + configured patterns/dirs
285+
# ------------------------------------------------------------
286+
# 3) Discovery path: env vars + patterns/dirs (source = "auto")
287+
# ------------------------------------------------------------
288+
self._cti_files_source_used = self._CTI_FILES_SOURCE_AUTO
289+
242290
search_paths = ns.get("cti_search_paths", props.get("cti_search_paths"))
243291
extra_dirs = ns.get("cti_dirs", props.get("cti_dirs"))
244292

@@ -261,7 +309,6 @@ def _resolve_cti_files_for_settings(self) -> list[str]:
261309
f"Discovery details:\n{diag.summarize()}"
262310
)
263311

264-
# Default: try to load ALL discovered producers
265312
return list(candidates)
266313

267314
@classmethod
@@ -346,6 +393,9 @@ def open(self) -> None:
346393

347394
# Resolve CTIs (may return many). This no longer raises just because there are multiple.
348395
cti_files = self._resolve_cti_files_for_settings()
396+
ns["cti_files_source"] = (
397+
self._cti_files_source_used or ns.get("cti_files_source") or self._CTI_FILES_SOURCE_AUTO
398+
)
349399

350400
self._harvester = Harvester()
351401

@@ -377,7 +427,8 @@ def open(self) -> None:
377427
"No GenTL producer (.cti) could be loaded.\n\n"
378428
f"Resolved CTIs: {cti_files}\n"
379429
f"Failures: {failed}\n"
380-
"Fix: remove/repair incompatible producers or set properties.gentl.cti_file to a known working producer."
430+
"Fix: remove/repair incompatible producers or "
431+
"set properties.gentl.cti_file to a known working producer."
381432
)
382433

383434
# Update device list after loading producers
@@ -755,7 +806,9 @@ def rebind_settings(cls, settings):
755806
correct current index (and serial_number if available).
756807
757808
Strategy:
758-
- If ctis were previously persisted (cti_files/cti_file), prefer those.
809+
- If CTIs were persisted:
810+
* if source == "auto" and they are stale -> fall back to discovery
811+
* otherwise use them (best stability)
759812
- Otherwise, fall back to env-var + pattern discovery (best-effort).
760813
"""
761814
if Harvester is None:
@@ -770,38 +823,52 @@ def rebind_settings(cls, settings):
770823
if not target_id:
771824
return settings
772825

826+
source = ns.get("cti_files_source")
827+
source = str(source).strip().lower() if source is not None else None
828+
is_auto_cache = source == cls._CTI_FILES_SOURCE_AUTO
829+
773830
harvester = None
774831
try:
775-
# Prefer persisted CTIs for stability if present
776832
explicit_files = ns.get("cti_files") or props.get("cti_files")
777833
explicit_file = ns.get("cti_file") or props.get("cti_file")
778834

779835
if explicit_files or explicit_file:
780-
candidates, diag = cti_finder.discover_cti_files(
836+
candidates, _diag = cti_finder.discover_cti_files(
781837
cti_file=explicit_file,
782838
cti_files=cti_finder.cti_files_as_list(explicit_files),
783839
include_env=False,
784840
must_exist=True,
785841
)
786-
if not candidates:
787-
return settings
788842

789-
harvester = Harvester()
790-
loaded: list[str] = []
791-
for cti in candidates:
792-
try:
793-
harvester.add_file(cti)
794-
loaded.append(cti)
795-
except Exception:
796-
continue
797-
798-
if not loaded:
799-
cls._reset_select_harvester(harvester)
843+
if not candidates and is_auto_cache:
844+
# Auto cache stale -> fallback to discovery
845+
harvester, _loaded, _diag2 = cls._build_harvester_for_discovery(strict_single=False)
846+
if harvester is None:
847+
return settings
848+
elif not candidates:
849+
# User override stale or unknown -> no rebind
800850
return settings
801-
802-
harvester.update()
851+
else:
852+
harvester = Harvester()
853+
loaded: list[str] = []
854+
for cti in candidates:
855+
try:
856+
harvester.add_file(cti)
857+
loaded.append(cti)
858+
except Exception:
859+
continue
860+
if not loaded:
861+
cls._reset_select_harvester(harvester)
862+
if is_auto_cache:
863+
harvester, _loaded, _diag2 = cls._build_harvester_for_discovery(strict_single=False)
864+
if harvester is None:
865+
return settings
866+
else:
867+
return settings
868+
else:
869+
harvester.update()
803870
else:
804-
harvester, loaded, diag = cls._build_harvester_for_discovery(strict_single=False)
871+
harvester, _loaded, _diag = cls._build_harvester_for_discovery(strict_single=False)
805872
if harvester is None:
806873
return settings
807874

@@ -810,7 +877,6 @@ def rebind_settings(cls, settings):
810877
return settings
811878

812879
target_id_str = str(target_id).strip()
813-
814880
match_index = None
815881
match_serial = None
816882

0 commit comments

Comments
 (0)