|
3 | 3 | import time |
4 | 4 | import threading |
5 | 5 | import logging |
6 | | -import importlib |
7 | | -import inspect |
8 | | -from typing import Dict, List, Any, Tuple |
| 6 | +from typing import Dict, List, Any |
9 | 7 | import yaml |
10 | 8 | from .logger import setup_logger |
11 | 9 | from .registry import DeviceRegistry |
|
18 | 16 | from .metrics import MetricsRecorder |
19 | 17 |
|
20 | 18 | logger = logging.getLogger(__name__) |
21 | | -IDENTIFY_INTERVAL = 1.0 |
22 | 19 |
|
23 | 20 |
|
24 | 21 | MANIFEST_ALIASES = { |
@@ -48,158 +45,10 @@ def _load_manifest(driver_key: str) -> Dict: |
48 | 45 |
|
49 | 46 |
|
50 | 47 |
|
51 | | -def _get_class_channel_counts(dev: dict) -> Dict[str, int]: |
52 | | - """Return mapping of class -> channel count for a device from manifest.""" |
53 | | - out: Dict[str, int] = {} |
54 | | - try: |
55 | | - driver_key = dev.get("driver") |
56 | | - manifest = _load_manifest(driver_key) if driver_key else None |
57 | | - if not isinstance(manifest, dict): |
58 | | - return out |
59 | | - models = manifest.get("models") or {} |
60 | | - model_key = dev.get("model") |
61 | | - model_cfg = None |
62 | | - if model_key and isinstance(models.get(model_key), dict): |
63 | | - model_cfg = models.get(model_key) |
64 | | - elif isinstance(models, dict) and models: |
65 | | - model_cfg = next(iter(models.values())) |
66 | | - if not isinstance(model_cfg, dict): |
67 | | - return out |
68 | | - inst_class_block = model_cfg.get("instrument_class") or {} |
69 | | - for klass, cfg in (inst_class_block or {}).items(): |
70 | | - features = (cfg or {}).get("features") or {} |
71 | | - try: |
72 | | - ch = int(features.get("channels", 1) or 1) |
73 | | - except Exception: |
74 | | - ch = 1 |
75 | | - out[str(klass)] = max(1, ch) |
76 | | - # Fallback: detect nested class blocks mistakenly placed under another class |
77 | | - for subk, subcfg in (cfg or {}).items(): |
78 | | - if not isinstance(subcfg, dict): |
79 | | - continue |
80 | | - if subk in ("features", "modes", "pooling", "polling"): |
81 | | - continue |
82 | | - sub_features = (subcfg or {}).get("features") or {} |
83 | | - if sub_features: |
84 | | - try: |
85 | | - sch = int(sub_features.get("channels", 1) or 1) |
86 | | - except Exception: |
87 | | - sch = 1 |
88 | | - out[str(subk)] = max(1, sch) |
89 | | - return out |
90 | | - except Exception: |
91 | | - return out |
92 | | - |
93 | | - |
94 | | -def _get_class_poll_intervals(dev: dict) -> Dict[str, float]: |
95 | | - """Return mapping of class -> poll interval (seconds) for classes that declare a polling method. |
96 | | -
|
97 | | - Only classes with a defined pooling/polling entry are included. This prevents polling |
98 | | - classes that have no configured poll method. |
99 | | - """ |
100 | | - out: Dict[str, float] = {} |
101 | | - try: |
102 | | - driver_key = dev.get("driver") |
103 | | - manifest = _load_manifest(driver_key) if driver_key else None |
104 | | - if not isinstance(manifest, dict): |
105 | | - return out |
106 | | - models = manifest.get("models") or {} |
107 | | - model_key = dev.get("model") |
108 | | - model_cfg = None |
109 | | - if model_key and isinstance(models.get(model_key), dict): |
110 | | - model_cfg = models.get(model_key) |
111 | | - elif isinstance(models, dict) and models: |
112 | | - model_cfg = next(iter(models.values())) |
113 | | - if not isinstance(model_cfg, dict): |
114 | | - return out |
115 | | - inst_class_block = model_cfg.get("instrument_class") or {} |
116 | | - for klass, cfg in (inst_class_block or {}).items(): |
117 | | - pooling = (cfg or {}).get("pooling") or (cfg or {}).get("polling") or [] |
118 | | - # Pick the first polling entry we can use for the top-level class |
119 | | - iv = None |
120 | | - for entry in pooling: |
121 | | - try: |
122 | | - mname = entry.get("method") |
123 | | - if mname: |
124 | | - iv = float(entry.get("interval", 2.0)) |
125 | | - break |
126 | | - except Exception: |
127 | | - continue |
128 | | - if iv is not None: |
129 | | - out[str(klass)] = iv |
130 | | - # Also detect nested class blocks and their pooling |
131 | | - for subk, subcfg in (cfg or {}).items(): |
132 | | - if not isinstance(subcfg, dict) or subk in ("features", "modes", "pooling", "polling"): |
133 | | - continue |
134 | | - sub_pool = (subcfg or {}).get("pooling") or (subcfg or {}).get("polling") or [] |
135 | | - sub_iv = None |
136 | | - for entry in sub_pool: |
137 | | - try: |
138 | | - mname = entry.get("method") |
139 | | - if mname: |
140 | | - sub_iv = float(entry.get("interval", 2.0)) |
141 | | - break |
142 | | - except Exception: |
143 | | - continue |
144 | | - if sub_iv is not None: |
145 | | - out[str(subk)] = sub_iv |
146 | | - return out |
147 | | - except Exception: |
148 | | - return out |
149 | | -def _load_driver_class(driver_key: str): |
150 | | - """Load a driver class given its key. |
151 | | -
|
152 | | - Supports both legacy flat modules (benchmesh_service.drivers.<driver_key>) |
153 | | - and new layout with subpackages (benchmesh_service.drivers.<pkg>.<module>). |
154 | | - The class is expected to be reachable from the imported module namespace, |
155 | | - either defined there or re-exported by the package's __init__.py. |
156 | | - """ |
157 | | - tried = [] |
158 | | - folder_key = MANIFEST_ALIASES.get(driver_key, driver_key) |
159 | | - |
160 | | - # Candidate import names in order of preference |
161 | | - candidates = [ |
162 | | - f"benchmesh_service.drivers.{driver_key}", |
163 | | - f"benchmesh_service.drivers.{folder_key}", |
164 | | - f"benchmesh_service.drivers.{folder_key}.driver", |
165 | | - # explicit known class module for tenma alias |
166 | | - f"benchmesh_service.drivers.{folder_key}.driver" if folder_key == 'tenma_72' else None, |
167 | | - ] |
168 | | - candidates = [c for c in candidates if c] |
169 | | - |
170 | | - for mod_name in candidates: |
171 | | - try: |
172 | | - mod = importlib.import_module(mod_name) |
173 | | - # Return first class exposed on the module namespace |
174 | | - for _, obj in inspect.getmembers(mod, inspect.isclass): |
175 | | - # Accept classes defined in the module or its submodules |
176 | | - if getattr(obj, "__module__", "").startswith(mod.__name__): |
177 | | - return obj |
178 | | - # If no classes found yet but module imported, keep trying next candidate |
179 | | - tried.append(mod_name) |
180 | | - except Exception as e: |
181 | | - tried.append(f"{mod_name} ({e.__class__.__name__}: {e})") |
182 | | - continue |
183 | | - |
184 | | - # As a fallback, attempt direct file import for <folder_key>/<driver_key>.py or <folder_key>/<folder_key>.py |
185 | | - base_dir = os.path.join(os.path.dirname(__file__), 'drivers', folder_key) |
186 | | - for file_base in (driver_key, folder_key, 'driver'): |
187 | | - file_path = os.path.join(base_dir, f"{file_base}.py") |
188 | | - if os.path.exists(file_path): |
189 | | - try: |
190 | | - spec = importlib.util.spec_from_file_location(f"benchmesh_service.drivers.{folder_key}.{file_base}", file_path) |
191 | | - if spec and spec.loader: |
192 | | - mod = importlib.util.module_from_spec(spec) |
193 | | - spec.loader.exec_module(mod) |
194 | | - for _, obj in inspect.getmembers(mod, inspect.isclass): |
195 | | - # Only accept classes defined in this module (exclude imported helpers) |
196 | | - if getattr(obj, "__module__", "").startswith(mod.__name__): |
197 | | - return obj |
198 | | - except Exception as e: |
199 | | - tried.append(f"file:{file_path} ({e.__class__.__name__}: {e})") |
200 | | - continue |
201 | 48 |
|
202 | | - raise ImportError(f"No driver class found for key '{driver_key}'. Tried: {tried}") |
| 49 | + |
| 50 | + |
| 51 | +# Note: legacy dynamic driver loader removed. DriverFactory is the single source of truth. |
203 | 52 |
|
204 | 53 |
|
205 | 54 | class SerialManager: |
@@ -325,18 +174,6 @@ def identify(self): |
325 | 174 | self.metrics.inc_identify_fail(dev_id) |
326 | 175 | return conn.driver |
327 | 176 |
|
328 | | - def _try_identify(self, drv): |
329 | | - try: |
330 | | - if hasattr(drv, 'identify'): |
331 | | - return drv.identify() |
332 | | - t = getattr(drv, 't', None) |
333 | | - if t: |
334 | | - t.write_line('*IDN?') |
335 | | - return t.read_until_reol(256) |
336 | | - except Exception as e: |
337 | | - logger.warning("Identify failed: %s", e) |
338 | | - raise |
339 | | - return None |
340 | 177 |
|
341 | 178 | def _update_registry(self, dev_id: str, key: str, value: Any, klass: str | None = None): |
342 | 179 | self.registry_obj.update(dev_id, key, value, klass) |
|
0 commit comments