@@ -65,13 +65,30 @@ def _get_class_channel_counts(dev: dict) -> Dict[str, int]:
6565 except Exception :
6666 ch = 1
6767 out [str (klass )] = max (1 , ch )
68+ # Fallback: detect nested class blocks mistakenly placed under another class
69+ for subk , subcfg in (cfg or {}).items ():
70+ if not isinstance (subcfg , dict ):
71+ continue
72+ if subk in ("features" , "modes" , "pooling" , "polling" ):
73+ continue
74+ sub_features = (subcfg or {}).get ("features" ) or {}
75+ if sub_features :
76+ try :
77+ sch = int (sub_features .get ("channels" , 1 ) or 1 )
78+ except Exception :
79+ sch = 1
80+ out [str (subk )] = max (1 , sch )
6881 return out
6982 except Exception :
7083 return out
7184
7285
7386def _get_class_poll_intervals (dev : dict ) -> Dict [str , float ]:
74- """Return mapping of class -> poll interval (seconds), defaults to 2.0 if not specified."""
87+ """Return mapping of class -> poll interval (seconds) for classes that declare a polling method.
88+
89+ Only classes with a defined pooling/polling entry are included. This prevents polling
90+ classes that have no configured poll method.
91+ """
7592 out : Dict [str , float ] = {}
7693 try :
7794 driver_key = dev .get ("driver" )
@@ -90,16 +107,34 @@ def _get_class_poll_intervals(dev: dict) -> Dict[str, float]:
90107 inst_class_block = model_cfg .get ("instrument_class" ) or {}
91108 for klass , cfg in (inst_class_block or {}).items ():
92109 pooling = (cfg or {}).get ("pooling" ) or (cfg or {}).get ("polling" ) or []
93- # pooling can be a list of entries, pick poll_status if present
94- iv = 2.0
110+ # Pick the first polling entry we can use for the top-level class
111+ iv = None
95112 for entry in pooling :
96113 try :
97- if entry .get ("method" ) == "poll_status" :
98- iv = float (entry .get ("interval" , iv ))
114+ mname = entry .get ("method" )
115+ if mname :
116+ iv = float (entry .get ("interval" , 2.0 ))
99117 break
100118 except Exception :
101119 continue
102- out [str (klass )] = iv
120+ if iv is not None :
121+ out [str (klass )] = iv
122+ # Also detect nested class blocks and their pooling
123+ for subk , subcfg in (cfg or {}).items ():
124+ if not isinstance (subcfg , dict ) or subk in ("features" , "modes" , "pooling" , "polling" ):
125+ continue
126+ sub_pool = (subcfg or {}).get ("pooling" ) or (subcfg or {}).get ("polling" ) or []
127+ sub_iv = None
128+ for entry in sub_pool :
129+ try :
130+ mname = entry .get ("method" )
131+ if mname :
132+ sub_iv = float (entry .get ("interval" , 2.0 ))
133+ break
134+ except Exception :
135+ continue
136+ if sub_iv is not None :
137+ out [str (subk )] = sub_iv
103138 return out
104139 except Exception :
105140 return out
@@ -288,10 +323,42 @@ def monitor_connections(self):
288323 continue
289324 try :
290325 polled_any = False
326+ # Resolve poll method name from manifest config
327+ poll_method = None
328+ try :
329+ driver_key = dev .get ('driver' )
330+ manifest = _load_manifest (driver_key )
331+ models = manifest .get ('models' , {}) or {}
332+ model_cfg = models .get (dev .get ('model' )) if dev .get ('model' ) in models else (next (iter (models .values ())) if models else {})
333+ iclasses = (model_cfg or {}).get ('instrument_class' , {}) or {}
334+ icfg = iclasses .get (klass , {})
335+ pooling = (icfg or {}).get ('pooling' ) or (icfg or {}).get ('polling' ) or []
336+ if not pooling :
337+ for topk , topcfg in (iclasses or {}).items ():
338+ if isinstance (topcfg , dict ) and isinstance (topcfg .get (klass ), dict ):
339+ alt = topcfg .get (klass ) or {}
340+ pooling = (alt .get ('pooling' ) or alt .get ('polling' ) or [])
341+ if pooling :
342+ break
343+ for entry in pooling :
344+ name = entry .get ('method' )
345+ if name :
346+ poll_method = name
347+ break
348+ except Exception :
349+ pass
350+ # Default fallback
351+ if not poll_method :
352+ poll_method = 'poll_status'
353+ meth = getattr (drv , poll_method , None )
354+ if not callable (meth ):
355+ logger .warning ("Poll method %s not implemented on driver %s; skipping class %s" , poll_method , type (drv ).__name__ , klass )
356+ continue
291357 for ch in range (1 , max (1 , ch_count ) + 1 ):
292358 try :
293- status = drv .poll_status (ch ) if hasattr (drv , 'poll_status' ) else {}
294- except Exception :
359+ status = meth (ch )
360+ except Exception as e :
361+ logger .warning ("Polling %s[%s] failed: %s" , device_id , klass , e )
295362 status = {}
296363 if not status :
297364 self ._clear_disconnected_registry (device_id )
@@ -377,10 +444,43 @@ def _device_worker(self, dev_id: str):
377444 continue
378445 try :
379446 polled_any = False
447+ # Resolve poll method name from manifest config
448+ poll_method = None
449+ try :
450+ dev_cfg = next ((d for d in self .devices if d .get ('id' ) == dev_id ), None )
451+ if dev_cfg :
452+ driver_key = dev_cfg .get ('driver' )
453+ manifest = _load_manifest (driver_key )
454+ models = manifest .get ('models' , {}) or {}
455+ model_cfg = models .get (dev_cfg .get ('model' )) if dev_cfg .get ('model' ) in models else (next (iter (models .values ())) if models else {})
456+ iclasses = (model_cfg or {}).get ('instrument_class' , {}) or {}
457+ icfg = iclasses .get (klass , {})
458+ pooling = (icfg or {}).get ('pooling' ) or (icfg or {}).get ('polling' ) or []
459+ if not pooling :
460+ for topk , topcfg in (iclasses or {}).items ():
461+ if isinstance (topcfg , dict ) and isinstance (topcfg .get (klass ), dict ):
462+ alt = topcfg .get (klass ) or {}
463+ pooling = (alt .get ('pooling' ) or alt .get ('polling' ) or [])
464+ if pooling :
465+ break
466+ for entry in pooling :
467+ name = entry .get ('method' )
468+ if name :
469+ poll_method = name
470+ break
471+ except Exception :
472+ pass
473+ if not poll_method :
474+ poll_method = 'poll_status'
475+ meth = getattr (drv , poll_method , None )
476+ if not callable (meth ):
477+ logger .warning ("Poll method %s not implemented on driver %s; skipping class %s" , poll_method , type (drv ).__name__ , klass )
478+ continue
380479 for ch in range (1 , max (1 , ch_count )+ 1 ):
381480 try :
382- st = drv .poll_status (ch ) if hasattr (drv , 'poll_status' ) else {}
383- except Exception :
481+ st = meth (ch )
482+ except Exception as e :
483+ logger .warning ("Polling %s[%s] failed: %s" , dev_id , klass , e )
384484 st = {}
385485 if not st :
386486 self ._clear_disconnected_registry (dev_id )
0 commit comments