Coverage for src/bluetooth_sig/core/translator.py: 77%

295 statements  

« prev     ^ index     » next       coverage.py v7.11.0, created at 2025-10-30 00:10 +0000

1"""Core Bluetooth SIG standards translator functionality.""" 

2 

3from __future__ import annotations 

4 

5import logging 

6from collections.abc import Mapping 

7from graphlib import TopologicalSorter 

8from typing import Any, cast 

9 

10from ..gatt.characteristics.base import BaseCharacteristic 

11from ..gatt.characteristics.registry import CharacteristicRegistry 

12from ..gatt.descriptors import DescriptorRegistry 

13from ..gatt.exceptions import MissingDependencyError 

14from ..gatt.services import ServiceName 

15from ..gatt.services.base import BaseGattService 

16from ..gatt.services.registry import GattServiceRegistry 

17from ..gatt.uuid_registry import CustomUuidEntry, uuid_registry 

18from ..types import ( 

19 CharacteristicContext, 

20 CharacteristicData, 

21 CharacteristicDataProtocol, 

22 CharacteristicInfo, 

23 CharacteristicRegistration, 

24 ServiceInfo, 

25 ServiceRegistration, 

26 SIGInfo, 

27 ValidationResult, 

28) 

29from ..types.descriptor_types import DescriptorData, DescriptorInfo 

30from ..types.gatt_enums import CharacteristicName, ValueType 

31from ..types.uuid import BluetoothUUID 

32 

33# Type alias for characteristic data in process_services 

34CharacteristicDataDict = dict[str, Any] 

35 

36logger = logging.getLogger(__name__) 

37 

38 

39class BluetoothSIGTranslator: # pylint: disable=too-many-public-methods 

40 """Pure Bluetooth SIG standards translator for characteristic and service interpretation. 

41 

42 This class provides the primary API surface for Bluetooth SIG standards translation, 

43 covering characteristic parsing, service discovery, UUID resolution, and registry 

44 management. 

45 

46 Key features: 

47 - Parse raw BLE characteristic data using Bluetooth SIG specifications 

48 - Resolve UUIDs to [CharacteristicInfo][bluetooth_sig.types.CharacteristicInfo] 

49 and [ServiceInfo][bluetooth_sig.types.ServiceInfo] 

50 - Create BaseGattService instances from service UUIDs 

51 - Access comprehensive registry of supported characteristics and services 

52 

53 Note: This class intentionally has >20 public methods as it serves as the 

54 primary API surface for Bluetooth SIG standards translation. The methods are 

55 organized by functionality and reducing them would harm API clarity. 

56 """ 

57 

58 def __init__(self) -> None: 

59 """Initialize the SIG translator.""" 

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

61 

62 def __str__(self) -> str: 

63 """Return string representation of the translator.""" 

64 return "BluetoothSIGTranslator(pure SIG standards)" 

65 

66 def parse_characteristic( 

67 self, 

68 uuid: str, 

69 raw_data: bytes, 

70 ctx: CharacteristicContext | None = None, 

71 descriptor_data: dict[str, bytes] | None = None, 

72 ) -> CharacteristicData: 

73 r"""Parse a characteristic's raw data using Bluetooth SIG standards. 

74 

75 This method takes raw BLE characteristic data and converts it to structured, 

76 type-safe Python objects using official Bluetooth SIG specifications. 

77 

78 Args: 

79 uuid: The characteristic UUID (with or without dashes) 

80 raw_data: Raw bytes from the characteristic 

81 ctx: Optional CharacteristicContext providing device-level info 

82 and previously-parsed characteristics to the parser. 

83 properties: Optional set of characteristic properties (unused, kept for protocol compatibility) 

84 descriptor_data: Optional dictionary mapping descriptor UUIDs to their raw data 

85 

86 Returns: 

87 [CharacteristicData][bluetooth_sig.types.CharacteristicData] with parsed value and metadata 

88 

89 Example: 

90 Parse battery level data: 

91 

92 ```python 

93 from bluetooth_sig import BluetoothSIGTranslator 

94 

95 translator = BluetoothSIGTranslator() 

96 result = translator.parse_characteristic("2A19", b"\\x64") 

97 print(f"Battery: {result.value}%") # Battery: 100% 

98 ``` 

99 

100 """ 

101 logger.debug("Parsing characteristic UUID=%s, data_len=%d", uuid, len(raw_data)) 

102 

103 # Create characteristic instance for parsing 

104 characteristic = CharacteristicRegistry.create_characteristic(uuid) 

105 

106 if characteristic: 

107 logger.debug("Found parser for UUID=%s: %s", uuid, type(characteristic).__name__) 

108 # Use the parse_value method; pass context when provided. 

109 result = characteristic.parse_value(raw_data, ctx) 

110 

111 # Attach context if available and result doesn't already have it 

112 if ctx is not None: 

113 result.source_context = ctx 

114 

115 if result.parse_success: 

116 logger.debug("Successfully parsed %s: %s", result.name, result.value) 

117 else: 

118 logger.warning("Parse failed for %s: %s", result.name, result.error_message) 

119 

120 else: 

121 # No parser found, return fallback result 

122 logger.info("No parser available for UUID=%s", uuid) 

123 fallback_info = CharacteristicInfo( 

124 uuid=BluetoothUUID(uuid), 

125 name="Unknown", 

126 description="", 

127 value_type=ValueType.UNKNOWN, 

128 unit="", 

129 properties=[], 

130 ) 

131 result = CharacteristicData( 

132 info=fallback_info, 

133 value=raw_data, 

134 raw_data=raw_data, 

135 parse_success=False, 

136 error_message="No parser available for this characteristic UUID", 

137 descriptors={}, # No descriptors for unknown characteristics 

138 ) 

139 

140 # Handle descriptors if provided 

141 if descriptor_data: 

142 parsed_descriptors: dict[str, DescriptorData] = {} 

143 for desc_uuid, desc_raw_data in descriptor_data.items(): 

144 logger.debug("Parsing descriptor %s for characteristic %s", desc_uuid, uuid) 

145 descriptor = DescriptorRegistry.create_descriptor(desc_uuid) 

146 if descriptor: 

147 desc_result = descriptor.parse_value(desc_raw_data) 

148 if desc_result.parse_success: 

149 logger.debug("Successfully parsed descriptor %s: %s", desc_uuid, desc_result.value) 

150 else: 

151 logger.warning("Descriptor parse failed for %s: %s", desc_uuid, desc_result.error_message) 

152 parsed_descriptors[desc_uuid] = desc_result 

153 else: 

154 logger.info("No parser available for descriptor UUID=%s", desc_uuid) 

155 # Create fallback descriptor data 

156 desc_fallback_info = DescriptorInfo( 

157 uuid=BluetoothUUID(desc_uuid), 

158 name="Unknown Descriptor", 

159 description="", 

160 has_structured_data=False, 

161 data_format="bytes", 

162 ) 

163 parsed_descriptors[desc_uuid] = DescriptorData( 

164 info=desc_fallback_info, 

165 value=desc_raw_data, 

166 raw_data=desc_raw_data, 

167 parse_success=False, 

168 error_message="No parser available for this descriptor UUID", 

169 ) 

170 

171 # Update result with parsed descriptors 

172 result.descriptors = parsed_descriptors 

173 

174 return result 

175 

176 def get_characteristic_info_by_uuid(self, uuid: str) -> CharacteristicInfo | None: 

177 """Get information about a characteristic by UUID. 

178 

179 Retrieve metadata for a Bluetooth characteristic using its UUID. This includes 

180 the characteristic's name, description, value type, unit, and properties. 

181 

182 Args: 

183 uuid: The characteristic UUID (16-bit short form or full 128-bit) 

184 

185 Returns: 

186 [CharacteristicInfo][bluetooth_sig.CharacteristicInfo] with metadata or None if not found 

187 

188 Example: 

189 Get battery level characteristic info: 

190 

191 ```python 

192 from bluetooth_sig import BluetoothSIGTranslator 

193 

194 translator = BluetoothSIGTranslator() 

195 info = translator.get_characteristic_info_by_uuid("2A19") 

196 if info: 

197 print(f"Name: {info.name}") # Name: Battery Level 

198 ``` 

199 

200 """ 

201 try: 

202 bt_uuid = BluetoothUUID(uuid) 

203 except ValueError: 

204 return None 

205 

206 char_class = CharacteristicRegistry.get_characteristic_class_by_uuid(bt_uuid) 

207 if not char_class: 

208 return None 

209 

210 # Create temporary instance to get metadata (no parameters needed for auto-resolution) 

211 try: 

212 temp_char = char_class() 

213 return temp_char.info 

214 except Exception: # pylint: disable=broad-exception-caught 

215 return None 

216 

217 def get_characteristic_uuid_by_name(self, name: CharacteristicName) -> BluetoothUUID | None: 

218 """Get the UUID for a characteristic name enum. 

219 

220 Args: 

221 name: CharacteristicName enum 

222 

223 Returns: 

224 Characteristic UUID or None if not found 

225 

226 """ 

227 info = self.get_characteristic_info_by_name(name) 

228 return info.uuid if info else None 

229 

230 def get_service_uuid_by_name(self, name: str | ServiceName) -> BluetoothUUID | None: 

231 """Get the UUID for a service name or enum. 

232 

233 Args: 

234 name: Service name or enum 

235 

236 Returns: 

237 Service UUID or None if not found 

238 

239 """ 

240 info = self.get_service_info_by_name(str(name)) 

241 return info.uuid if info else None 

242 

243 def get_characteristic_info_by_name(self, name: CharacteristicName) -> CharacteristicInfo | None: 

244 """Get characteristic info by enum name. 

245 

246 Args: 

247 name: CharacteristicName enum 

248 

249 Returns: 

250 CharacteristicInfo if found, None otherwise 

251 

252 """ 

253 char_class = CharacteristicRegistry.get_characteristic_class(name) 

254 if char_class: 

255 return char_class.get_configured_info() 

256 return None 

257 

258 def get_service_info_by_name(self, name: str) -> ServiceInfo | None: 

259 """Get service info by name instead of UUID. 

260 

261 Args: 

262 name: Service name 

263 

264 Returns: 

265 ServiceInfo if found, None otherwise 

266 

267 """ 

268 # Use UUID registry for name-based lookup 

269 try: 

270 uuid_info = uuid_registry.get_service_info(name) 

271 if uuid_info: 

272 return ServiceInfo(uuid=uuid_info.uuid, name=uuid_info.name, characteristics=[]) 

273 except Exception: # pylint: disable=broad-exception-caught 

274 pass 

275 

276 return None 

277 

278 def get_service_info_by_uuid(self, uuid: str) -> ServiceInfo | None: 

279 """Get information about a service by UUID. 

280 

281 Args: 

282 uuid: The service UUID 

283 

284 Returns: 

285 ServiceInfo with metadata or None if not found 

286 

287 """ 

288 service_class = GattServiceRegistry.get_service_class_by_uuid(BluetoothUUID(uuid)) 

289 if not service_class: 

290 return None 

291 

292 try: 

293 temp_service = service_class() 

294 # Convert characteristics dict to list of CharacteristicInfo 

295 char_infos: list[CharacteristicInfo] = [] 

296 for _, char_instance in temp_service.characteristics.items(): 

297 # Use public info property 

298 char_infos.append(char_instance.info) 

299 return ServiceInfo( 

300 uuid=temp_service.uuid, 

301 name=temp_service.name, 

302 characteristics=char_infos, 

303 ) 

304 except Exception: # pylint: disable=broad-exception-caught 

305 return None 

306 

307 def list_supported_characteristics(self) -> dict[str, str]: 

308 """List all supported characteristics with their names and UUIDs. 

309 

310 Returns: 

311 Dictionary mapping characteristic names to UUIDs 

312 

313 """ 

314 result: dict[str, str] = {} 

315 for name, char_class in CharacteristicRegistry.get_all_characteristics().items(): 

316 # Try to get configured_info from class using public accessor 

317 configured_info = char_class.get_configured_info() 

318 if configured_info: 

319 # Convert CharacteristicName enum to string for dict key 

320 name_str = name.value if hasattr(name, "value") else str(name) 

321 result[name_str] = str(configured_info.uuid) 

322 return result 

323 

324 def list_supported_services(self) -> dict[str, str]: 

325 """List all supported services with their names and UUIDs. 

326 

327 Returns: 

328 Dictionary mapping service names to UUIDs 

329 

330 """ 

331 result: dict[str, str] = {} 

332 for service_class in GattServiceRegistry.get_all_services(): 

333 try: 

334 temp_service = service_class() 

335 service_name = getattr(temp_service, "_service_name", service_class.__name__) 

336 result[service_name] = str(temp_service.uuid) 

337 except Exception: # pylint: disable=broad-exception-caught 

338 continue 

339 return result 

340 

341 def process_services(self, services: dict[str, dict[str, CharacteristicDataDict]]) -> None: 

342 """Process discovered services and their characteristics. 

343 

344 Args: 

345 services: Dictionary of service UUIDs to their characteristics 

346 

347 """ 

348 for uuid_str, service_data in services.items(): 

349 uuid = BluetoothUUID(uuid_str) 

350 # Convert dict[str, dict] to ServiceDiscoveryData 

351 characteristics: dict[BluetoothUUID, CharacteristicInfo] = {} 

352 for char_uuid_str, char_data in service_data.get("characteristics", {}).items(): 

353 char_uuid = BluetoothUUID(char_uuid_str) 

354 # Create CharacteristicInfo from dict 

355 vtype_raw = char_data.get("value_type", "bytes") 

356 if isinstance(vtype_raw, str): 

357 value_type = ValueType(vtype_raw) 

358 elif isinstance(vtype_raw, ValueType): 

359 value_type = vtype_raw 

360 else: 

361 value_type = ValueType.BYTES 

362 characteristics[char_uuid] = CharacteristicInfo( 

363 uuid=char_uuid, 

364 name=char_data.get("name", ""), 

365 unit=char_data.get("unit", ""), 

366 value_type=value_type, 

367 properties=char_data.get("properties", []), 

368 ) 

369 service = GattServiceRegistry.create_service(uuid, characteristics) 

370 if service: 

371 self._services[str(uuid)] = service 

372 

373 def get_service_by_uuid(self, uuid: str) -> BaseGattService | None: 

374 """Get a service instance by UUID. 

375 

376 Args: 

377 uuid: The service UUID 

378 

379 Returns: 

380 Service instance if found, None otherwise 

381 

382 """ 

383 return self._services.get(uuid) 

384 

385 @property 

386 def discovered_services(self) -> list[BaseGattService]: 

387 """Get list of discovered service instances. 

388 

389 Returns: 

390 List of discovered service instances 

391 

392 """ 

393 return list(self._services.values()) 

394 

395 def clear_services(self) -> None: 

396 """Clear all discovered services.""" 

397 self._services.clear() 

398 

399 def get_sig_info_by_name(self, name: str) -> SIGInfo | None: 

400 """Get Bluetooth SIG information for a characteristic or service by name. 

401 

402 Args: 

403 name: Characteristic or service name 

404 

405 Returns: 

406 CharacteristicInfo or ServiceInfo if found, None otherwise 

407 

408 """ 

409 # Use the UUID registry for name-based lookups (string inputs). 

410 try: 

411 char_info = uuid_registry.get_characteristic_info(name) 

412 if char_info: 

413 # Build CharacteristicInfo from registry data 

414 value_type = ValueType.UNKNOWN 

415 if char_info.value_type: 

416 try: 

417 value_type = ValueType(char_info.value_type) 

418 except (ValueError, KeyError): 

419 value_type = ValueType.UNKNOWN 

420 return CharacteristicInfo( 

421 uuid=char_info.uuid, 

422 name=char_info.name, 

423 value_type=value_type, 

424 unit=char_info.unit or "", 

425 ) 

426 except Exception: # pylint: disable=broad-exception-caught 

427 pass 

428 

429 # Try service 

430 service_info = self.get_service_info_by_name(name) 

431 if service_info: 

432 return service_info 

433 

434 return None 

435 

436 def get_sig_info_by_uuid(self, uuid: str) -> SIGInfo | None: 

437 """Get Bluetooth SIG information for a UUID. 

438 

439 Args: 

440 uuid: UUID string (with or without dashes) 

441 

442 Returns: 

443 CharacteristicInfo or ServiceInfo if found, None otherwise 

444 

445 """ 

446 # Try characteristic first 

447 char_info = self.get_characteristic_info_by_uuid(uuid) 

448 if char_info: 

449 return char_info 

450 

451 # Try service 

452 service_info = self.get_service_info_by_uuid(uuid) 

453 if service_info: 

454 return service_info 

455 

456 return None 

457 

458 def parse_characteristics( 

459 self, 

460 char_data: dict[str, bytes], 

461 descriptor_data: dict[str, dict[str, bytes]] | None = None, 

462 ctx: CharacteristicContext | None = None, 

463 ) -> dict[str, CharacteristicData]: 

464 r"""Parse multiple characteristics at once with dependency-aware ordering. 

465 

466 This method automatically handles multi-characteristic dependencies by parsing 

467 independent characteristics first, then parsing characteristics that depend on them. 

468 The parsing order is determined by the `required_dependencies` and `optional_dependencies` 

469 attributes declared on characteristic classes. 

470 

471 Required dependencies MUST be present and successfully parsed; missing required 

472 dependencies result in parse failure with MissingDependencyError. Optional dependencies 

473 enrich parsing when available but are not mandatory. 

474 

475 Args: 

476 char_data: Dictionary mapping UUIDs to raw data bytes 

477 descriptor_data: Optional nested dictionary mapping characteristic UUIDs to 

478 dictionaries of descriptor UUIDs to raw descriptor data 

479 ctx: Optional CharacteristicContext used as the starting 

480 device-level context for each parsed characteristic. 

481 

482 Returns: 

483 Dictionary mapping UUIDs to [CharacteristicData][bluetooth_sig.types.CharacteristicData] results 

484 with parsed descriptors included when descriptor_data is provided 

485 

486 Raises: 

487 ValueError: If circular dependencies are detected 

488 

489 Example: 

490 Parse multiple environmental characteristics: 

491 

492 ```python 

493 from bluetooth_sig import BluetoothSIGTranslator 

494 

495 translator = BluetoothSIGTranslator() 

496 data = { 

497 "2A6E": b"\\x0A\\x00", # Temperature 

498 "2A6F": b"\\x32\\x00", # Humidity 

499 } 

500 results = translator.parse_characteristics(data) 

501 for uuid, result in results.items(): 

502 print(f"{uuid}: {result.value}") 

503 ``` 

504 

505 """ 

506 return self._parse_characteristics_batch(char_data, descriptor_data, ctx) 

507 

508 def _parse_characteristics_batch( 

509 self, 

510 char_data: dict[str, bytes], 

511 descriptor_data: dict[str, dict[str, bytes]] | None, 

512 ctx: CharacteristicContext | None, 

513 ) -> dict[str, CharacteristicData]: 

514 """Parse multiple characteristics with optional descriptors using dependency-aware ordering.""" 

515 logger.debug( 

516 "Batch parsing %d characteristics%s", len(char_data), " with descriptors" if descriptor_data else "" 

517 ) 

518 

519 # Prepare characteristics and dependencies 

520 uuid_to_characteristic, uuid_to_required_deps, uuid_to_optional_deps = ( 

521 self._prepare_characteristic_dependencies(char_data) 

522 ) 

523 

524 # Resolve dependency order 

525 sorted_uuids = self._resolve_dependency_order(char_data, uuid_to_required_deps, uuid_to_optional_deps) 

526 

527 # Build base context 

528 base_context = ctx 

529 

530 results: dict[str, CharacteristicData] = {} 

531 for uuid_str in sorted_uuids: 

532 raw_data = char_data[uuid_str] 

533 characteristic = uuid_to_characteristic.get(uuid_str) 

534 

535 missing_required = self._find_missing_required_dependencies( 

536 uuid_str, 

537 uuid_to_required_deps.get(uuid_str, []), 

538 results, 

539 base_context, 

540 ) 

541 

542 if missing_required: 

543 results[uuid_str] = self._build_missing_dependency_failure( 

544 uuid_str, 

545 raw_data, 

546 characteristic, 

547 missing_required, 

548 ) 

549 continue 

550 

551 self._log_optional_dependency_gaps( 

552 uuid_str, 

553 uuid_to_optional_deps.get(uuid_str, []), 

554 results, 

555 base_context, 

556 ) 

557 

558 parse_context = self._build_parse_context(base_context, results) 

559 

560 # Choose parsing method based on whether descriptors are provided 

561 if descriptor_data is None: 

562 results[uuid_str] = self.parse_characteristic(uuid_str, raw_data, ctx=parse_context) 

563 else: 

564 results[uuid_str] = self.parse_characteristic( 

565 uuid_str, raw_data, ctx=parse_context, descriptor_data=descriptor_data.get(uuid_str, {}) 

566 ) 

567 

568 logger.debug("Batch parsing complete: %d results", len(results)) 

569 return results 

570 

571 def _prepare_characteristic_dependencies( 

572 self, characteristic_data: Mapping[str, bytes] 

573 ) -> tuple[dict[str, BaseCharacteristic], dict[str, list[str]], dict[str, list[str]]]: 

574 """Instantiate characteristics once and collect declared dependencies.""" 

575 uuid_to_characteristic: dict[str, BaseCharacteristic] = {} 

576 uuid_to_required_deps: dict[str, list[str]] = {} 

577 uuid_to_optional_deps: dict[str, list[str]] = {} 

578 

579 for uuid in characteristic_data: 

580 characteristic = CharacteristicRegistry.create_characteristic(uuid) 

581 if characteristic is None: 

582 continue 

583 

584 uuid_to_characteristic[uuid] = characteristic 

585 

586 required = characteristic.required_dependencies 

587 optional = characteristic.optional_dependencies 

588 

589 if required: 

590 uuid_to_required_deps[uuid] = required 

591 logger.debug("Characteristic %s has required dependencies: %s", uuid, required) 

592 if optional: 

593 uuid_to_optional_deps[uuid] = optional 

594 logger.debug("Characteristic %s has optional dependencies: %s", uuid, optional) 

595 

596 return uuid_to_characteristic, uuid_to_required_deps, uuid_to_optional_deps 

597 

598 def _resolve_dependency_order( 

599 self, 

600 characteristic_data: Mapping[str, bytes], 

601 uuid_to_required_deps: Mapping[str, list[str]], 

602 uuid_to_optional_deps: Mapping[str, list[str]], 

603 ) -> list[str]: 

604 """Topologically sort characteristics based on declared dependencies.""" 

605 try: 

606 sorter: TopologicalSorter[str] = TopologicalSorter() 

607 for uuid in characteristic_data: 

608 all_deps = uuid_to_required_deps.get(uuid, []) + uuid_to_optional_deps.get(uuid, []) 

609 batch_deps = [dep for dep in all_deps if dep in characteristic_data] 

610 sorter.add(uuid, *batch_deps) 

611 

612 sorted_sequence = sorter.static_order() 

613 sorted_uuids = list(sorted_sequence) 

614 logger.debug("Dependency-sorted parsing order: %s", sorted_uuids) 

615 return sorted_uuids 

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

617 logger.warning("Dependency sorting failed: %s. Using original order.", exc) 

618 return list(characteristic_data.keys()) 

619 

620 def _find_missing_required_dependencies( 

621 self, 

622 uuid: str, 

623 required_deps: list[str], 

624 results: Mapping[str, CharacteristicData], 

625 base_context: CharacteristicContext | None, 

626 ) -> list[str]: 

627 """Determine which required dependencies are unavailable for a characteristic.""" 

628 if not required_deps: 

629 return [] 

630 

631 missing: list[str] = [] 

632 other_characteristics = ( 

633 base_context.other_characteristics if base_context and base_context.other_characteristics else None 

634 ) 

635 

636 for dep_uuid in required_deps: 

637 if dep_uuid in results: 

638 if not results[dep_uuid].parse_success: 

639 missing.append(dep_uuid) 

640 continue 

641 

642 if other_characteristics and dep_uuid in other_characteristics: 

643 if not other_characteristics[dep_uuid].parse_success: 

644 missing.append(dep_uuid) 

645 continue 

646 

647 missing.append(dep_uuid) 

648 

649 if missing: 

650 logger.debug("Characteristic %s missing required dependencies: %s", uuid, missing) 

651 

652 return missing 

653 

654 def _build_missing_dependency_failure( 

655 self, 

656 uuid: str, 

657 raw_data: bytes, 

658 characteristic: BaseCharacteristic | None, 

659 missing_required: list[str], 

660 ) -> CharacteristicData: 

661 """Create a failure result when required dependencies are absent.""" 

662 char_name = characteristic.name if characteristic else "Unknown" 

663 error = MissingDependencyError(char_name, missing_required) 

664 logger.warning("Skipping %s due to missing required dependencies: %s", uuid, missing_required) 

665 

666 if characteristic is not None: 

667 failure_info = characteristic.info 

668 else: 

669 fallback_info = self.get_characteristic_info_by_uuid(uuid) 

670 if fallback_info is not None: 

671 failure_info = fallback_info 

672 else: 

673 failure_info = CharacteristicInfo( 

674 uuid=BluetoothUUID(uuid), 

675 name=char_name, 

676 description="", 

677 value_type=ValueType.UNKNOWN, 

678 unit="", 

679 properties=[], 

680 ) 

681 

682 return CharacteristicData( 

683 info=failure_info, 

684 value=None, 

685 raw_data=raw_data, 

686 parse_success=False, 

687 error_message=str(error), 

688 descriptors={}, # No descriptors available for failed parsing 

689 ) 

690 

691 def _log_optional_dependency_gaps( 

692 self, 

693 uuid: str, 

694 optional_deps: list[str], 

695 results: Mapping[str, CharacteristicData], 

696 base_context: CharacteristicContext | None, 

697 ) -> None: 

698 """Emit debug logs when optional dependencies are unavailable.""" 

699 if not optional_deps: 

700 return 

701 

702 other_characteristics = ( 

703 base_context.other_characteristics if base_context and base_context.other_characteristics else None 

704 ) 

705 

706 for dep_uuid in optional_deps: 

707 if dep_uuid in results: 

708 continue 

709 if other_characteristics and dep_uuid in other_characteristics: 

710 continue 

711 logger.debug("Optional dependency %s not available for %s", dep_uuid, uuid) 

712 

713 def _build_parse_context( 

714 self, 

715 base_context: CharacteristicContext | None, 

716 results: Mapping[str, CharacteristicData], 

717 ) -> CharacteristicContext: 

718 """Construct the context passed to per-characteristic parsers.""" 

719 results_mapping = cast(Mapping[str, CharacteristicDataProtocol], results) 

720 

721 if base_context is not None: 

722 return CharacteristicContext( 

723 device_info=base_context.device_info, 

724 advertisement=base_context.advertisement, 

725 other_characteristics=results_mapping, 

726 raw_service=base_context.raw_service, 

727 ) 

728 

729 return CharacteristicContext(other_characteristics=results_mapping) 

730 

731 def get_characteristics_info_by_uuids(self, uuids: list[str]) -> dict[str, CharacteristicInfo | None]: 

732 """Get information about multiple characteristics by UUID. 

733 

734 Args: 

735 uuids: List of characteristic UUIDs 

736 

737 Returns: 

738 Dictionary mapping UUIDs to CharacteristicInfo 

739 (or None if not found) 

740 

741 """ 

742 results: dict[str, CharacteristicInfo | None] = {} 

743 for uuid in uuids: 

744 results[uuid] = self.get_characteristic_info_by_uuid(uuid) 

745 return results 

746 

747 def validate_characteristic_data(self, uuid: str, data: bytes) -> ValidationResult: 

748 """Validate characteristic data format against SIG specifications. 

749 

750 Args: 

751 uuid: The characteristic UUID 

752 data: Raw data bytes to validate 

753 

754 Returns: 

755 ValidationResult with validation details 

756 

757 """ 

758 try: 

759 # Attempt to parse the data - if it succeeds, format is valid 

760 parsed = self.parse_characteristic(uuid, data) 

761 return ValidationResult( 

762 uuid=BluetoothUUID(uuid), 

763 name=parsed.name, 

764 is_valid=parsed.parse_success, 

765 actual_length=len(data), 

766 error_message=parsed.error_message, 

767 ) 

768 except Exception as e: # pylint: disable=broad-exception-caught 

769 # If parsing failed, data format is invalid 

770 return ValidationResult( 

771 uuid=BluetoothUUID(uuid), 

772 name="Unknown", 

773 is_valid=False, 

774 actual_length=len(data), 

775 error_message=str(e), 

776 ) 

777 

778 def get_service_characteristics(self, service_uuid: str) -> list[str]: # pylint: disable=too-many-return-statements 

779 """Get the characteristic UUIDs associated with a service. 

780 

781 Args: 

782 service_uuid: The service UUID 

783 

784 Returns: 

785 List of characteristic UUIDs for this service 

786 

787 """ 

788 service_class = GattServiceRegistry.get_service_class_by_uuid(BluetoothUUID(service_uuid)) 

789 if not service_class: 

790 return [] 

791 

792 try: 

793 temp_service = service_class() 

794 required_chars = temp_service.get_required_characteristics() 

795 return [str(k) for k in required_chars] 

796 except Exception: # pylint: disable=broad-exception-caught 

797 return [] 

798 

799 def register_custom_characteristic_class( 

800 self, 

801 uuid_or_name: str, 

802 cls: type[BaseCharacteristic], 

803 metadata: CharacteristicRegistration | None = None, 

804 override: bool = False, 

805 ) -> None: 

806 """Register a custom characteristic class at runtime. 

807 

808 Args: 

809 uuid_or_name: The characteristic UUID or name 

810 cls: The characteristic class to register 

811 metadata: Optional metadata dataclass with name, unit, value_type, summary 

812 override: Whether to override existing registrations 

813 

814 Raises: 

815 TypeError: If cls does not inherit from BaseCharacteristic 

816 ValueError: If UUID conflicts with existing registration and override=False 

817 

818 """ 

819 # Register the class 

820 CharacteristicRegistry.register_characteristic_class(uuid_or_name, cls, override) 

821 

822 # Register metadata if provided 

823 if metadata: 

824 # Convert ValueType enum to string for registry storage 

825 vtype_str = metadata.value_type.value 

826 entry = CustomUuidEntry( 

827 uuid=metadata.uuid, 

828 name=metadata.name or cls.__name__, 

829 id=metadata.id, 

830 summary=metadata.summary, 

831 unit=metadata.unit, 

832 value_type=vtype_str, 

833 ) 

834 uuid_registry.register_characteristic(entry, override) 

835 

836 def register_custom_service_class( 

837 self, 

838 uuid_or_name: str, 

839 cls: type[BaseGattService], 

840 metadata: ServiceRegistration | None = None, 

841 override: bool = False, 

842 ) -> None: 

843 """Register a custom service class at runtime. 

844 

845 Args: 

846 uuid_or_name: The service UUID or name 

847 cls: The service class to register 

848 metadata: Optional metadata dataclass with name, summary 

849 override: Whether to override existing registrations 

850 

851 Raises: 

852 TypeError: If cls does not inherit from BaseGattService 

853 ValueError: If UUID conflicts with existing registration and override=False 

854 

855 """ 

856 # Register the class 

857 GattServiceRegistry.register_service_class(uuid_or_name, cls, override) 

858 

859 # Register metadata if provided 

860 if metadata: 

861 entry = CustomUuidEntry( 

862 uuid=metadata.uuid, 

863 name=metadata.name or cls.__name__, 

864 id=metadata.id, 

865 summary=metadata.summary, 

866 ) 

867 uuid_registry.register_service(entry, override) 

868 

869 

870# Global instance 

871BluetoothSIG = BluetoothSIGTranslator()