1010import cv2
1111import numpy as np
1212
13+ from ...config import CameraSettings
1314from ..base import CameraBackend , SupportLevel , register_backend
15+ from ..factory import DetectedCamera
1416
1517LOG = logging .getLogger (__name__ )
1618
@@ -40,7 +42,7 @@ def __init__(self, settings):
4042 if not isinstance (ns , dict ):
4143 ns = {}
4244
43- self ._camera_id : str | None = ns .get ("camera_id " ) or props .get ("camera_id " )
45+ self ._camera_id : str | None = ns .get ("device_id " ) or props .get ("device_id " )
4446 self ._pixel_format : str = ns .get ("pixel_format" ) or props .get ("pixel_format" , "Mono8" )
4547 self ._timeout : int = int (ns .get ("timeout" , props .get ("timeout" , 2_000_000 )))
4648 self ._n_buffers : int = int (ns .get ("n_buffers" , props .get ("n_buffers" , 10 )))
@@ -103,6 +105,153 @@ def get_device_count(cls) -> int:
103105 except Exception :
104106 return - 1
105107
108+ @classmethod
109+ def quick_ping (cls , index : int , * _args , ** _kwargs ) -> bool :
110+ """
111+ Cheap presence test for CameraFactory probing.
112+ Uses update_device_list() then bounds-check.
113+ """
114+ if not ARAVIS_AVAILABLE :
115+ return False
116+ try :
117+ Aravis .update_device_list ()
118+ n = int (Aravis .get_n_devices () or 0 )
119+ return 0 <= int (index ) < n
120+ except Exception :
121+ return False
122+
123+ @classmethod
124+ def discover_devices (cls , max_devices : int = 10 , should_cancel = None , progress_cb = None ):
125+ if not ARAVIS_AVAILABLE :
126+ return []
127+
128+ # Refresh list once; indices may change after update_device_list()
129+ Aravis .update_device_list ()
130+
131+ snap = cls ._arv_snapshot_devices (limit = max_devices )
132+
133+ cams : list [DetectedCamera ] = []
134+ for d in snap :
135+ if should_cancel and should_cancel ():
136+ break
137+ if progress_cb :
138+ progress_cb (f"Found { d ['label' ]} " )
139+
140+ path = d .get ("physical_id" ) or d .get ("address" )
141+
142+ cams .append (
143+ DetectedCamera (
144+ index = int (d ["index" ]),
145+ label = str (d ["label" ]),
146+ device_id = d .get ("device_id" ),
147+ path = path ,
148+ )
149+ )
150+ return cams
151+
152+ @classmethod
153+ def rebind_settings (cls , settings : CameraSettings ) -> CameraSettings :
154+ """
155+ Best-effort quick rebind using only Aravis enumeration APIs (no camera open).
156+ Indices may change after Aravis.update_device_list().
157+ """
158+ if not ARAVIS_AVAILABLE :
159+ return settings
160+
161+ props = settings .properties if isinstance (settings .properties , dict ) else {}
162+ ns = props .get (cls .OPTIONS_KEY , {}) if isinstance (props .get (cls .OPTIONS_KEY ), dict ) else {}
163+
164+ # Stored identifiers (some may be missing)
165+ stored_device_id = cls ._safe_str (
166+ ns .get ("device_id" ) or props .get ("device_id" ) or ns .get ("camera_id" ) or props .get ("camera_id" )
167+ )
168+ stored_physical = cls ._safe_str (
169+ ns .get ("device_physical_id" ) or ns .get ("device_path" ) or props .get ("device_path" )
170+ )
171+ stored_vendor = cls ._safe_str (ns .get ("device_vendor" ))
172+ stored_model = cls ._safe_str (ns .get ("device_model" ))
173+ stored_serial = cls ._safe_str (ns .get ("device_serial_nbr" ) or ns .get ("device_serial" ))
174+ stored_name = cls ._safe_str (ns .get ("device_name" ))
175+
176+ # Nothing to rebind with
177+ if not any (
178+ [stored_device_id , stored_physical , (stored_vendor and stored_model and stored_serial ), stored_name ]
179+ ):
180+ return settings
181+
182+ try :
183+ Aravis .update_device_list () # must be called before get_device_*
184+ snap = cls ._arv_snapshot_devices (limit = None )
185+
186+ # 1) device_id exact match (fast)
187+ chosen = None
188+ if stored_device_id :
189+ for d in snap :
190+ if d .get ("device_id" ) == stored_device_id :
191+ chosen = d
192+ break
193+
194+ # 2) physical_id exact match
195+ if chosen is None and stored_physical :
196+ for d in snap :
197+ if d .get ("physical_id" ) == stored_physical or d .get ("address" ) == stored_physical :
198+ chosen = d
199+ break
200+
201+ # 3) vendor/model/serial exact triple match
202+ if chosen is None and stored_vendor and stored_model and stored_serial :
203+ for d in snap :
204+ if (d .get ("vendor" ), d .get ("model" ), d .get ("serial" )) == (
205+ stored_vendor ,
206+ stored_model ,
207+ stored_serial ,
208+ ):
209+ chosen = d
210+ break
211+
212+ # 4) name substring match against computed label
213+ if chosen is None and stored_name :
214+ needle = stored_name .lower ()
215+ for d in snap :
216+ label = (d .get ("label" ) or "" ).lower ()
217+ if needle and needle in label :
218+ chosen = d
219+ break
220+
221+ # 5) fallback to current index if still plausible
222+ if chosen is None :
223+ idx = int (getattr (settings , "index" , 0 ) or 0 )
224+ if 0 <= idx < len (snap ):
225+ chosen = snap [idx ]
226+ else :
227+ return settings
228+
229+ # Apply new index
230+ settings .index = int (chosen ["index" ])
231+
232+ # Refresh namespace fields (keeps GUI stable identity fresh)
233+ if isinstance (settings .properties , dict ):
234+ out = settings .properties .setdefault (cls .OPTIONS_KEY , {})
235+ if isinstance (out , dict ):
236+ out ["device_id" ] = chosen .get ("device_id" )
237+ out ["device_physical_id" ] = chosen .get ("physical_id" )
238+ out ["device_vendor" ] = chosen .get ("vendor" )
239+ out ["device_model" ] = chosen .get ("model" )
240+ out ["device_serial_nbr" ] = chosen .get ("serial" )
241+ out ["device_protocol" ] = chosen .get ("protocol" )
242+ out ["device_address" ] = chosen .get ("address" )
243+ out ["device_name" ] = chosen .get ("label" ) # computed label (no open)
244+
245+ # also keep 'device_path' aligned with physical id for GUI fallback
246+ if chosen .get ("physical_id" ):
247+ out ["device_path" ] = chosen .get ("physical_id" )
248+
249+ return settings
250+
251+ except Exception :
252+ # Never hard-fail creation just because rebinding couldn't happen
253+ return settings
254+
106255 def open (self ) -> None :
107256 if not ARAVIS_AVAILABLE :
108257 raise RuntimeError ("Aravis library not available" )
@@ -120,11 +269,68 @@ def open(self) -> None:
120269 raise RuntimeError (f"Camera index { index } out of range for { n_devices } Aravis device(s)" )
121270 camera_id = Aravis .get_device_id (index )
122271 self ._camera = Aravis .Camera .new (camera_id )
272+ self ._camera_id = self ._safe_str (camera_id )
123273
124274 if self ._camera is None :
125275 raise RuntimeError ("Failed to open Aravis camera" )
126276
277+ # --- Refresh identity and align index (best-effort, no heavy open needed) ---
278+ try :
279+ snap = self ._arv_snapshot_devices (limit = None )
280+
281+ opened_id = self ._camera_id
282+ if opened_id is None :
283+ # Opened by index
284+ try :
285+ opened_id = self ._safe_str (Aravis .get_device_id (int (self .settings .index )))
286+ except Exception :
287+ opened_id = None
288+
289+ chosen = None
290+ if opened_id :
291+ for d in snap :
292+ if d .get ("device_id" ) == opened_id :
293+ chosen = d
294+ break
295+
296+ # If we found it, align settings.index and refresh identity cache
297+ if chosen :
298+ self .settings .index = int (chosen ["index" ])
299+ if isinstance (self .settings .properties , dict ):
300+ ns = self .settings .properties .setdefault (self .OPTIONS_KEY , {})
301+ if isinstance (ns , dict ):
302+ ns ["device_id" ] = chosen .get ("device_id" )
303+ ns ["device_physical_id" ] = chosen .get ("physical_id" )
304+ ns ["device_vendor" ] = chosen .get ("vendor" )
305+ ns ["device_model" ] = chosen .get ("model" )
306+ ns ["device_serial_nbr" ] = chosen .get ("serial" )
307+ ns ["device_protocol" ] = chosen .get ("protocol" )
308+ ns ["device_address" ] = chosen .get ("address" )
309+ ns ["device_path" ] = chosen .get ("physical_id" ) or chosen .get ("address" )
310+ else :
311+ if isinstance (self .settings .properties , dict ):
312+ ns = self .settings .properties .setdefault (self .OPTIONS_KEY , {})
313+ if isinstance (ns , dict ):
314+ ns ["device_id" ] = opened_id
315+ except Exception :
316+ pass
317+
318+ # Compute higher-quality label from the opened camera object
127319 self ._device_label = self ._resolve_device_label ()
320+ # Always populate minimal identity into backend namespace for GUI
321+ if isinstance (self .settings .properties , dict ):
322+ ns = self .settings .properties .setdefault (self .OPTIONS_KEY , {})
323+ if isinstance (ns , dict ):
324+ # Always write a device_id after a successful open
325+ try :
326+ if self ._camera_id :
327+ ns ["device_id" ] = self ._camera_id
328+ else :
329+ ns ["device_id" ] = self ._safe_str (Aravis .get_device_id (int (self .settings .index )))
330+ except Exception :
331+ pass
332+ if self ._device_label :
333+ ns ["device_name" ] = self ._device_label
128334
129335 self ._configure_pixel_format ()
130336 self ._configure_resolution ()
@@ -261,6 +467,81 @@ def device_name(self) -> str:
261467 # ------------------------------------------------------------------
262468 # Configuration helpers
263469 # ------------------------------------------------------------------
470+ @staticmethod
471+ def _safe_str (x ) -> str | None :
472+ try :
473+ if x is None :
474+ return None
475+ s = str (x ).strip ()
476+ return s if s else None
477+ except Exception :
478+ return None
479+
480+ @classmethod
481+ def _arv_snapshot_devices (cls , limit : int | None = None ) -> list [dict ]:
482+ """
483+ Fast snapshot of the current Aravis device list without opening cameras.
484+ Requires Aravis.update_device_list() before calling.
485+ """
486+ n = int (Aravis .get_n_devices () or 0 ) # valid until next update_device_list()
487+ if limit is not None :
488+ n = min (n , int (limit ))
489+
490+ devices : list [dict ] = []
491+ for i in range (n ):
492+ try :
493+ dev_id = cls ._safe_str (Aravis .get_device_id (i ))
494+ except Exception :
495+ dev_id = None
496+
497+ try :
498+ physical = cls ._safe_str (Aravis .get_device_physical_id (i ))
499+ except Exception :
500+ physical = None
501+ try :
502+ vendor = cls ._safe_str (Aravis .get_device_vendor (i ))
503+ except Exception :
504+ vendor = None
505+ try :
506+ model = cls ._safe_str (Aravis .get_device_model (i ))
507+ except Exception :
508+ model = None
509+ try :
510+ serial = cls ._safe_str (Aravis .get_device_serial_nbr (i ))
511+ except Exception :
512+ serial = None
513+ try :
514+ protocol = cls ._safe_str (Aravis .get_device_protocol (i ))
515+ except Exception :
516+ protocol = None
517+ try :
518+ address = cls ._safe_str (Aravis .get_device_address (i ))
519+ except Exception :
520+ address = None
521+
522+ # Construct a stable-ish human label without opening the camera
523+ label_parts = [p for p in (vendor , model ) if p ]
524+ label = " " .join (label_parts ) if label_parts else None
525+ if serial :
526+ label = f"{ label } ({ serial } )" if label else f"({ serial } )"
527+ if not label :
528+ label = dev_id or f"Aravis #{ i } "
529+
530+ devices .append (
531+ {
532+ "index" : int (i ),
533+ "device_id" : dev_id ,
534+ "physical_id" : physical ,
535+ "vendor" : vendor ,
536+ "model" : model ,
537+ "serial" : serial ,
538+ "protocol" : protocol ,
539+ "address" : address ,
540+ "label" : label ,
541+ }
542+ )
543+ return devices
544+
264545 def _get_requested_resolution_or_none (self ) -> tuple [int , int ] | None :
265546 """
266547 Return (w, h) if user explicitly requested a resolution.
0 commit comments