Coverage for src/bluetooth_sig/gatt/uuid_registry.py: 90%

341 statements  

« prev     ^ index     » next       coverage.py v7.14.3, created at 2026-06-28 01:26 +0000

1"""UUID registry loading from Bluetooth SIG YAML files.""" 

2 

3from __future__ import annotations 

4 

5import logging 

6import threading 

7 

8from bluetooth_sig.registry.gss import GssRegistry 

9from bluetooth_sig.registry.uuids.units import UnitsRegistry 

10from bluetooth_sig.types import CharacteristicInfo, ServiceInfo 

11from bluetooth_sig.types.base_types import SIGInfo 

12from bluetooth_sig.types.registry.descriptor_types import DescriptorInfo 

13from bluetooth_sig.types.registry.gss_characteristic import GssCharacteristicSpec 

14from bluetooth_sig.types.uuid import BluetoothUUID 

15 

16from ..registry.utils import find_bluetooth_sig_path, load_yaml_uuids, normalize_uuid_string 

17from ..types.registry import CharacteristicSpec, FieldInfo, UnitMetadata 

18 

19__all__ = [ 

20 "UuidRegistry", 

21 "get_uuid_registry", 

22] 

23 

24logger = logging.getLogger(__name__) 

25 

26 

27class UuidRegistry: # pylint: disable=too-many-instance-attributes 

28 """Registry for Bluetooth SIG UUIDs with canonical storage + alias indices. 

29 

30 This registry stores a number of internal caches and mappings which 

31 legitimately exceed the default pylint instance attribute limit. The 

32 complexity is intentional and centralised; an inline disable is used to 

33 avoid noisy global configuration changes. 

34 """ 

35 

36 _instance: UuidRegistry | None = None 

37 _class_lock = threading.RLock() 

38 

39 @classmethod 

40 def get_instance(cls) -> UuidRegistry: 

41 """Return the process-wide UuidRegistry singleton instance.""" 

42 if cls._instance is None: 

43 with cls._class_lock: 

44 if cls._instance is None: 

45 cls._instance = cls() 

46 return cls._instance 

47 

48 def __init__(self) -> None: 

49 """Initialize the UUID registry.""" 

50 self._lock = threading.RLock() 

51 self._loaded = False 

52 self._load_error: Exception | None = None 

53 

54 # Canonical storage: normalized_uuid -> domain types (single source of truth) 

55 self._services: dict[str, ServiceInfo] = {} 

56 self._characteristics: dict[str, CharacteristicInfo] = {} 

57 self._descriptors: dict[str, DescriptorInfo] = {} 

58 

59 # Lightweight alias indices: alias -> normalized_uuid 

60 self._service_aliases: dict[str, str] = {} 

61 self._characteristic_aliases: dict[str, str] = {} 

62 self._descriptor_aliases: dict[str, str] = {} 

63 

64 # Preserve SIG entries overridden at runtime so we can restore them 

65 self._service_overrides: dict[str, ServiceInfo] = {} 

66 self._characteristic_overrides: dict[str, CharacteristicInfo] = {} 

67 self._descriptor_overrides: dict[str, DescriptorInfo] = {} 

68 

69 # Track runtime-registered UUIDs (replaces origin field checks) 

70 self._runtime_uuids: set[str] = set() 

71 

72 self._gss_registry: GssRegistry | None = None 

73 

74 def _ensure_loaded(self) -> None: 

75 """Ensure the registry has loaded its YAML data exactly once.""" 

76 with self._lock: 

77 if self._loaded: 

78 return 

79 if self._load_error is not None: 

80 raise RuntimeError("UUID registry failed to load SIG data") from self._load_error 

81 try: 

82 self._load_uuids() 

83 except Exception as exc: # pylint: disable=broad-exception-caught 

84 self._load_error = exc 

85 raise RuntimeError("UUID registry failed to load SIG data") from exc 

86 self._loaded = True 

87 

88 def ensure_loaded(self) -> None: 

89 """Public API to eagerly load UUID registry data.""" 

90 self._ensure_loaded() 

91 

92 def _store_service(self, info: ServiceInfo) -> None: 

93 """Store service info with canonical storage + aliases.""" 

94 canonical_key = info.uuid.normalized 

95 

96 # Store once in canonical location 

97 self._services[canonical_key] = info 

98 

99 # Create lightweight alias mappings (normalized to lowercase) 

100 aliases = self._generate_aliases(info) 

101 for alias in aliases: 

102 self._service_aliases[alias.lower()] = canonical_key 

103 

104 def _store_characteristic(self, info: CharacteristicInfo) -> None: 

105 """Store characteristic info with canonical storage + aliases.""" 

106 canonical_key = info.uuid.normalized 

107 

108 # Store once in canonical location 

109 self._characteristics[canonical_key] = info 

110 

111 # Create lightweight alias mappings (normalized to lowercase) 

112 aliases = self._generate_aliases(info) 

113 for alias in aliases: 

114 self._characteristic_aliases[alias.lower()] = canonical_key 

115 

116 def _store_descriptor(self, info: DescriptorInfo) -> None: 

117 """Store descriptor info with canonical storage + aliases.""" 

118 canonical_key = info.uuid.normalized 

119 

120 # Store once in canonical location 

121 self._descriptors[canonical_key] = info 

122 

123 # Create lightweight alias mappings (normalized to lowercase) 

124 aliases = self._generate_aliases(info) 

125 for alias in aliases: 

126 self._descriptor_aliases[alias.lower()] = canonical_key 

127 

128 def _generate_aliases(self, info: SIGInfo) -> set[str]: 

129 """Generate name/ID-based alias keys for domain info types (UUID variations handled by BluetoothUUID).""" 

130 aliases: set[str] = { 

131 info.name.lower(), 

132 } 

133 

134 if info.id: 

135 aliases.add(info.id) 

136 

137 if info.id and "service" in info.id: 

138 service_name = info.id.replace("org.bluetooth.service.", "") 

139 if service_name.endswith("_service"): 

140 service_name = service_name[:-8] # Remove _service 

141 service_name = service_name.replace("_", " ").title() 

142 aliases.add(service_name) 

143 # Also add "Service" suffix if not present 

144 if not service_name.endswith(" Service"): 

145 aliases.add(service_name + " Service") 

146 elif info.id and "characteristic" in info.id: 

147 char_name = info.id.replace("org.bluetooth.characteristic.", "") 

148 char_name = char_name.replace("_", " ").title() 

149 aliases.add(char_name) 

150 

151 # Add space-separated words from name 

152 name_words = info.name.replace("_", " ").replace("-", " ") 

153 if " " in name_words: 

154 aliases.add(name_words.title()) 

155 aliases.add(name_words.lower()) 

156 

157 # Remove empty strings, None values, and the canonical key itself 

158 canonical_key = info.uuid.normalized 

159 return {alias for alias in aliases if alias and alias.strip() and alias != canonical_key} 

160 

161 def _load_uuids(self) -> None: # pylint: disable=too-many-branches 

162 """Load all UUIDs from YAML files.""" 

163 base_path = find_bluetooth_sig_path() 

164 if not base_path: 

165 return 

166 

167 # Load service UUIDs 

168 service_yaml = base_path / "service_uuids.yaml" 

169 if service_yaml.exists(): 

170 for uuid_info in load_yaml_uuids(service_yaml): 

171 uuid = normalize_uuid_string(uuid_info["uuid"]) 

172 

173 bt_uuid = BluetoothUUID(uuid) 

174 info = ServiceInfo( 

175 uuid=bt_uuid, 

176 name=uuid_info["name"], 

177 id=uuid_info.get("id", ""), 

178 ) 

179 self._store_service(info) 

180 

181 # Load characteristic UUIDs 

182 characteristic_yaml = base_path / "characteristic_uuids.yaml" 

183 if characteristic_yaml.exists(): 

184 for uuid_info in load_yaml_uuids(characteristic_yaml): 

185 uuid = normalize_uuid_string(uuid_info["uuid"]) 

186 

187 bt_uuid = BluetoothUUID(uuid) 

188 char_info = CharacteristicInfo( 

189 uuid=bt_uuid, 

190 name=uuid_info["name"], 

191 id=uuid_info.get("id", ""), 

192 unit="", # Will be set from unit mappings if available 

193 ) 

194 self._store_characteristic(char_info) 

195 

196 # Load descriptor UUIDs 

197 descriptor_yaml = base_path / "descriptors.yaml" 

198 if descriptor_yaml.exists(): 

199 for uuid_info in load_yaml_uuids(descriptor_yaml): 

200 uuid = normalize_uuid_string(uuid_info["uuid"]) 

201 

202 bt_uuid = BluetoothUUID(uuid) 

203 desc_info = DescriptorInfo( 

204 uuid=bt_uuid, 

205 name=uuid_info["name"], 

206 id=uuid_info.get("id", ""), 

207 ) 

208 self._store_descriptor(desc_info) 

209 

210 # Load GSS specifications 

211 self._gss_registry = GssRegistry.get_instance() 

212 self._load_gss_characteristic_info() 

213 

214 # TODO: Remove when bluetooth_sig submodule includes these UUIDs. 

215 # Analog (0x2A58) and Digital (0x2A56) are present in service specs 

216 # (Automation IO) but absent from the assigned-number YAML files. 

217 _yaml_absent: list[tuple[BluetoothUUID, str, str]] = [ 

218 (BluetoothUUID(0x2A56), "Digital", "org.bluetooth.characteristic.digital"), 

219 (BluetoothUUID(0x2A58), "Analog", "org.bluetooth.characteristic.analog"), 

220 ] 

221 for _bt_uuid, _name, _identifier in _yaml_absent: 

222 if _bt_uuid.normalized not in self._characteristics: 

223 self._store_characteristic( 

224 CharacteristicInfo( 

225 uuid=_bt_uuid, 

226 name=_name, 

227 id=_identifier, 

228 unit="", 

229 python_type=None, 

230 ) 

231 ) 

232 

233 def _load_gss_characteristic_info(self) -> None: 

234 """Load GSS specs and update characteristics with extracted info.""" 

235 if self._gss_registry is None: 

236 return 

237 

238 all_specs = self._gss_registry.get_all_specs() 

239 

240 # Group by identifier to avoid duplicate processing 

241 processed_ids: set[str] = set() 

242 for spec in all_specs.values(): 

243 if spec.identifier in processed_ids: 

244 continue 

245 processed_ids.add(spec.identifier) 

246 

247 # Extract unit and value_type from structure 

248 char_data = { 

249 "structure": [ 

250 { 

251 "field": f.field, 

252 "type": f.type, 

253 "size": f.size, 

254 "description": f.description, 

255 } 

256 for f in spec.structure 

257 ] 

258 } 

259 unit, value_type = self._gss_registry.extract_info_from_gss(char_data) 

260 

261 # Multi-field structs have per-field units; no single representative 

262 # unit, and the first field's scalar wire type (e.g. int) is not 

263 # representative of the struct-valued characteristic. 

264 if len(spec.structure) > 1: 

265 unit = None 

266 value_type = None 

267 

268 if unit or value_type: 

269 self._update_characteristic_with_gss_info(spec.name, spec.identifier, unit, value_type) 

270 

271 def _update_characteristic_with_gss_info( 

272 self, char_name: str, char_id: str, unit: str | None, python_type: type | None 

273 ) -> None: 

274 """Update existing characteristic with GSS info.""" 

275 with self._lock: 

276 # Find the canonical entry by checking aliases (normalized to lowercase) 

277 canonical_uuid = None 

278 for search_key in [char_name, char_id]: 

279 canonical_uuid = self._characteristic_aliases.get(search_key.lower()) 

280 if canonical_uuid: 

281 break 

282 

283 if not canonical_uuid or canonical_uuid not in self._characteristics: 

284 return 

285 

286 # Get existing info and create updated version 

287 existing_info = self._characteristics[canonical_uuid] 

288 

289 # Use provided python_type or keep existing 

290 new_python_type = python_type if python_type is not None else existing_info.python_type 

291 

292 # Create updated CharacteristicInfo (immutable, so create new instance) 

293 updated_info = CharacteristicInfo( 

294 uuid=existing_info.uuid, 

295 name=existing_info.name, 

296 id=existing_info.id, 

297 unit=unit or existing_info.unit, 

298 python_type=new_python_type, 

299 ) 

300 

301 # Update canonical store (aliases remain the same since UUID/name/id unchanged) 

302 self._characteristics[canonical_uuid] = updated_info 

303 

304 def _convert_bluetooth_unit_to_readable(self, unit_spec: str) -> str: 

305 """Convert Bluetooth SIG unit specification to human-readable symbol. 

306 

307 Args: 

308 unit_spec: Unit specification (e.g., "thermodynamic_temperature.degree_celsius") 

309 

310 Returns: 

311 Human-readable symbol (e.g., "°C"), or unit_spec if no mapping found 

312 """ 

313 unit_spec = unit_spec.rstrip(".").lower() 

314 unit_id = f"org.bluetooth.unit.{unit_spec}" 

315 

316 units_registry = UnitsRegistry.get_instance() 

317 unit_info = units_registry.get_info(unit_id) 

318 if unit_info and unit_info.symbol: 

319 return unit_info.symbol 

320 

321 return unit_spec 

322 

323 def register_characteristic( # pylint: disable=too-many-arguments,too-many-positional-arguments 

324 self, 

325 uuid: BluetoothUUID, 

326 name: str, 

327 identifier: str | None = None, 

328 unit: str | None = None, 

329 python_type: type | str | None = None, 

330 override: bool = False, 

331 ) -> None: 

332 """Register a custom characteristic at runtime. 

333 

334 Args: 

335 uuid: The Bluetooth UUID for the characteristic 

336 name: Human-readable name 

337 identifier: Optional identifier (auto-generated if not provided) 

338 unit: Optional unit of measurement 

339 python_type: Optional Python type for the value 

340 override: If True, allow overriding existing entries 

341 """ 

342 self._ensure_loaded() 

343 with self._lock: 

344 canonical_key = uuid.normalized 

345 

346 # Check for conflicts with existing entries 

347 if canonical_key in self._characteristics: 

348 # Check if it's a SIG characteristic (not in runtime set) 

349 if canonical_key not in self._runtime_uuids: 

350 if not override: 

351 raise ValueError( 

352 f"UUID {uuid} conflicts with existing SIG " 

353 "characteristic entry. Use override=True to replace." 

354 ) 

355 # Preserve original SIG entry for restoration 

356 self._characteristic_overrides.setdefault(canonical_key, self._characteristics[canonical_key]) 

357 elif not override: 

358 # Runtime entry already exists 

359 raise ValueError( 

360 f"UUID {uuid} already registered as runtime characteristic. Use override=True to replace." 

361 ) 

362 

363 info = CharacteristicInfo( 

364 uuid=uuid, 

365 name=name, 

366 id=identifier or f"runtime.characteristic.{name.lower().replace(' ', '_')}", 

367 unit=unit or "", 

368 python_type=python_type, 

369 ) 

370 

371 # Track as runtime-registered UUID 

372 self._runtime_uuids.add(canonical_key) 

373 

374 self._store_characteristic(info) 

375 

376 def register_service( 

377 self, 

378 uuid: BluetoothUUID, 

379 name: str, 

380 identifier: str | None = None, 

381 override: bool = False, 

382 ) -> None: 

383 """Register a custom service at runtime. 

384 

385 Args: 

386 uuid: The Bluetooth UUID for the service 

387 name: Human-readable name 

388 identifier: Optional identifier (auto-generated if not provided) 

389 override: If True, allow overriding existing entries 

390 """ 

391 self._ensure_loaded() 

392 with self._lock: 

393 canonical_key = uuid.normalized 

394 

395 # Check for conflicts with existing entries 

396 if canonical_key in self._services: 

397 # Check if it's a SIG service (not in runtime set) 

398 if canonical_key not in self._runtime_uuids: 

399 if not override: 

400 raise ValueError( 

401 f"UUID {uuid} conflicts with existing SIG service entry. Use override=True to replace." 

402 ) 

403 # Preserve original SIG entry for restoration 

404 self._service_overrides.setdefault(canonical_key, self._services[canonical_key]) 

405 elif not override: 

406 # Runtime entry already exists 

407 raise ValueError( 

408 f"UUID {uuid} already registered as runtime service. Use override=True to replace." 

409 ) 

410 

411 info = ServiceInfo( 

412 uuid=uuid, 

413 name=name, 

414 id=identifier or f"runtime.service.{name.lower().replace(' ', '_')}", 

415 ) 

416 

417 # Track as runtime-registered UUID 

418 self._runtime_uuids.add(canonical_key) 

419 

420 self._store_service(info) 

421 

422 def get_service_info(self, key: str | BluetoothUUID) -> ServiceInfo | None: 

423 """Get information about a service by UUID, name, or ID.""" 

424 self._ensure_loaded() 

425 with self._lock: 

426 # Convert BluetoothUUID to canonical key 

427 if isinstance(key, BluetoothUUID): 

428 canonical_key = key.normalized 

429 # Direct canonical lookup 

430 if canonical_key in self._services: 

431 return self._services[canonical_key] 

432 else: 

433 search_key = str(key).strip() 

434 

435 # Try UUID normalization first 

436 try: 

437 bt_uuid = BluetoothUUID(search_key) 

438 canonical_key = bt_uuid.normalized 

439 if canonical_key in self._services: 

440 return self._services[canonical_key] 

441 except ValueError: 

442 pass # UUID normalization failed, continue to alias lookup 

443 

444 # Check alias index (normalized to lowercase) 

445 alias_key = self._service_aliases.get(search_key.lower()) 

446 if alias_key and alias_key in self._services: 

447 return self._services[alias_key] 

448 

449 return None 

450 

451 def get_characteristic_info(self, identifier: str | BluetoothUUID) -> CharacteristicInfo | None: 

452 """Get information about a characteristic by UUID, name, or ID.""" 

453 self._ensure_loaded() 

454 with self._lock: 

455 # Convert BluetoothUUID to canonical key 

456 if isinstance(identifier, BluetoothUUID): 

457 canonical_key = identifier.normalized 

458 # Direct canonical lookup 

459 if canonical_key in self._characteristics: 

460 return self._characteristics[canonical_key] 

461 else: 

462 search_key = str(identifier).strip() 

463 

464 # Try UUID normalization first 

465 try: 

466 bt_uuid = BluetoothUUID(search_key) 

467 canonical_key = bt_uuid.normalized 

468 if canonical_key in self._characteristics: 

469 return self._characteristics[canonical_key] 

470 except ValueError: 

471 pass # UUID normalization failed, continue to alias lookup 

472 

473 # Check alias index (normalized to lowercase) 

474 alias_key = self._characteristic_aliases.get(search_key.lower()) 

475 if alias_key and alias_key in self._characteristics: 

476 return self._characteristics[alias_key] 

477 

478 return None 

479 

480 def get_descriptor_info(self, identifier: str | BluetoothUUID) -> DescriptorInfo | None: 

481 """Get information about a descriptor by UUID, name, or ID.""" 

482 self._ensure_loaded() 

483 with self._lock: 

484 # Convert BluetoothUUID to canonical key 

485 if isinstance(identifier, BluetoothUUID): 

486 canonical_key = identifier.normalized 

487 # Direct canonical lookup 

488 if canonical_key in self._descriptors: 

489 return self._descriptors[canonical_key] 

490 else: 

491 search_key = str(identifier).strip() 

492 

493 # Try UUID normalization first 

494 try: 

495 bt_uuid = BluetoothUUID(search_key) 

496 canonical_key = bt_uuid.normalized 

497 if canonical_key in self._descriptors: 

498 return self._descriptors[canonical_key] 

499 except ValueError: 

500 pass # UUID normalization failed, continue to alias lookup 

501 

502 # Check alias index (normalized to lowercase) 

503 alias_key = self._descriptor_aliases.get(search_key.lower()) 

504 if alias_key and alias_key in self._descriptors: 

505 return self._descriptors[alias_key] 

506 

507 return None 

508 

509 def get_gss_spec(self, identifier: str | BluetoothUUID) -> GssCharacteristicSpec | None: 

510 """Get the full GSS characteristic specification with all field metadata. 

511 

512 This provides access to the complete YAML structure including all fields, 

513 their units, resolutions, ranges, and presence conditions. 

514 

515 Args: 

516 identifier: Characteristic name, ID, or UUID 

517 

518 Returns: 

519 GssCharacteristicSpec with full field structure, or None if not found 

520 

521 Example:: 

522 gss = get_uuid_registry().get_gss_spec("Location and Speed") 

523 if gss: 

524 for field in gss.structure: 

525 print(f"{field.python_name}: unit={field.unit_id}, resolution={field.resolution}") 

526 

527 """ 

528 self._ensure_loaded() 

529 if self._gss_registry is None: 

530 return None 

531 

532 with self._lock: 

533 # Try direct lookup by name or ID 

534 if isinstance(identifier, str): 

535 spec = self._gss_registry.get_spec(identifier) 

536 if spec: 

537 return spec 

538 

539 # Try to get CharacteristicInfo to find the ID 

540 char_info = self.get_characteristic_info(identifier) 

541 if char_info: 

542 spec = self._gss_registry.get_spec(char_info.id) 

543 if spec: 

544 return spec 

545 else: 

546 # Look up by UUID 

547 char_info = self.get_characteristic_info(identifier) 

548 if char_info: 

549 spec = self._gss_registry.get_spec(char_info.name) 

550 if spec: 

551 return spec 

552 spec = self._gss_registry.get_spec(char_info.id) 

553 if spec: 

554 return spec 

555 

556 return None 

557 

558 def resolve_characteristic_spec(self, characteristic_name: str) -> CharacteristicSpec | None: # pylint: disable=too-many-locals 

559 """Resolve characteristic specification with rich YAML metadata. 

560 

561 This method provides detailed characteristic information including data types, 

562 field sizes, units, and descriptions by cross-referencing multiple YAML sources. 

563 

564 Args: 

565 characteristic_name: Name of the characteristic (e.g., "Temperature", "Battery Level") 

566 

567 Returns: 

568 CharacteristicSpec with full metadata, or None if not found 

569 

570 Example:: 

571 spec = get_uuid_registry().resolve_characteristic_spec("Temperature") 

572 if spec: 

573 print(f"UUID: {spec.uuid}, Unit: {spec.unit_symbol}, Type: {spec.data_type}") 

574 

575 """ 

576 self._ensure_loaded() 

577 with self._lock: 

578 # 1. Get UUID from characteristic registry 

579 char_info = self.get_characteristic_info(characteristic_name) 

580 if not char_info: 

581 return None 

582 

583 # 2. Get typed GSS specification if available 

584 gss_spec = self.get_gss_spec(characteristic_name) 

585 

586 # 3. Extract metadata from GSS specification 

587 data_type = None 

588 field_size = None 

589 unit_id = None 

590 unit_symbol = None 

591 unit_readable_name = None 

592 base_unit = None 

593 resolution_text = None 

594 description = None 

595 

596 if gss_spec: 

597 description = gss_spec.description 

598 

599 # Only set data_type for single-field characteristics 

600 # Multi-field characteristics have complex structures and no single data type 

601 if len(gss_spec.structure) == 1: 

602 # Use primary field for metadata extraction 

603 primary = gss_spec.primary_field 

604 if primary: 

605 data_type = primary.type 

606 field_size = str(primary.fixed_size) if primary.fixed_size else primary.size 

607 

608 # Use FieldSpec's unit_id property (auto-parsed from description) 

609 if primary.unit_id: 

610 unit_id = f"org.bluetooth.unit.{primary.unit_id}" 

611 unit_symbol = self._convert_bluetooth_unit_to_readable(primary.unit_id) 

612 # Preserve the human-readable long-form name 

613 unit_info_obj = UnitsRegistry.get_instance().get_info(unit_id) 

614 if unit_info_obj: 

615 unit_readable_name = unit_info_obj.readable_name 

616 base_unit = unit_id 

617 

618 # Get resolution from FieldSpec 

619 if primary.resolution is not None: 

620 resolution_text = f"Resolution: {primary.resolution}" 

621 

622 # 4. Use existing unit from CharacteristicInfo if GSS didn't provide one. 

623 # Multi-field structs have per-field units; don't promote one to top-level. 

624 is_multi_field = gss_spec is not None and len(gss_spec.structure) > 1 

625 if not unit_symbol and char_info.unit and not is_multi_field: 

626 unit_symbol = char_info.unit 

627 

628 return CharacteristicSpec( 

629 uuid=char_info.uuid, 

630 name=char_info.name, 

631 field_info=FieldInfo(data_type=data_type, field_size=field_size), 

632 unit_info=UnitMetadata( 

633 unit_id=unit_id, 

634 unit_symbol=unit_symbol, 

635 unit_name=unit_readable_name, 

636 base_unit=base_unit, 

637 resolution_text=resolution_text, 

638 ), 

639 description=description, 

640 structure=gss_spec.structure if gss_spec else [], 

641 ) 

642 

643 def get_signed_from_data_type(self, data_type: str | None) -> bool: 

644 """Determine if data type is signed from GSS data type. 

645 

646 Args: 

647 data_type: GSS data type string (e.g., "sint16", "float32", "uint8") 

648 

649 Returns: 

650 True if the type represents signed values, False otherwise 

651 

652 """ 

653 if not data_type: 

654 return False 

655 # Comprehensive signed type detection 

656 signed_types = {"float32", "float64", "medfloat16", "medfloat32"} 

657 return data_type.startswith("sint") or data_type in signed_types 

658 

659 @staticmethod 

660 def get_byte_order_hint() -> str: 

661 """Get byte order hint for Bluetooth SIG specifications. 

662 

663 Returns: 

664 "little" - Bluetooth SIG uses little-endian by convention 

665 

666 """ 

667 return "little" 

668 

669 def clear_custom_registrations(self) -> None: 

670 """Clear all custom registrations (for testing).""" 

671 with self._lock: 

672 # Use runtime_uuids set to identify what to remove 

673 runtime_keys = list(self._runtime_uuids) 

674 

675 # Remove runtime entries from canonical stores 

676 for key in runtime_keys: 

677 self._services.pop(key, None) 

678 self._characteristics.pop(key, None) 

679 self._descriptors.pop(key, None) 

680 

681 # Remove corresponding aliases (alias -> canonical_key where canonical_key is runtime) 

682 runtime_service_aliases = [ 

683 alias for alias, canonical in self._service_aliases.items() if canonical in runtime_keys 

684 ] 

685 runtime_char_aliases = [ 

686 alias for alias, canonical in self._characteristic_aliases.items() if canonical in runtime_keys 

687 ] 

688 runtime_desc_aliases = [ 

689 alias for alias, canonical in self._descriptor_aliases.items() if canonical in runtime_keys 

690 ] 

691 

692 for alias in runtime_service_aliases: 

693 del self._service_aliases[alias] 

694 for alias in runtime_char_aliases: 

695 del self._characteristic_aliases[alias] 

696 for alias in runtime_desc_aliases: 

697 del self._descriptor_aliases[alias] 

698 

699 # Restore any preserved SIG entries that were overridden 

700 for key in runtime_keys: 

701 original = self._service_overrides.pop(key, None) 

702 if original is not None: 

703 self._store_service(original) 

704 original = self._characteristic_overrides.pop(key, None) 

705 if original is not None: 

706 self._store_characteristic(original) 

707 original = self._descriptor_overrides.pop(key, None) 

708 if original is not None: 

709 self._store_descriptor(original) 

710 

711 # Clear the runtime tracking set 

712 self._runtime_uuids.clear() 

713 

714 

715def get_uuid_registry() -> UuidRegistry: 

716 """Return the process-wide UUID registry singleton instance.""" 

717 return UuidRegistry.get_instance()