@@ -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