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

102 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-03-18 11:17 +0000

1"""Core Bluetooth SIG standards translator — thin composition facade. 

2 

3This module provides the public ``BluetoothSIGTranslator`` class, which 

4delegates all work to five focused components: 

5 

6* :class:`~.query.CharacteristicQueryEngine` — read-only metadata lookups 

7* :class:`~.parser.CharacteristicParser` — single + batch parse 

8* :class:`~.encoder.CharacteristicEncoder` — encode, validate, create_value 

9* :class:`~.registration.RegistrationManager` — custom class registration 

10* :class:`~.service_manager.ServiceManager` — discovered-service lifecycle 

11 

12The facade preserves every public method signature, ``@overload`` 

13decorator, async wrapper, and the singleton pattern from the original 

14monolithic implementation. 

15""" 

16 

17from __future__ import annotations 

18 

19from typing import Any, TypeVar, overload 

20 

21from ..gatt.characteristics.base import BaseCharacteristic 

22from ..gatt.services import ServiceName 

23from ..gatt.services.base import BaseGattService 

24from ..types import ( 

25 CharacteristicContext, 

26 CharacteristicInfo, 

27 ServiceInfo, 

28 SIGInfo, 

29 ValidationResult, 

30) 

31from ..types.gatt_enums import CharacteristicName 

32from ..types.uuid import BluetoothUUID 

33from .encoder import CharacteristicEncoder 

34from .parser import CharacteristicParser 

35from .query import CharacteristicQueryEngine 

36from .registration import RegistrationManager 

37from .service_manager import CharacteristicDataDict, ServiceManager 

38 

39# Re-export for backward compatibility 

40__all__ = ["BluetoothSIGTranslator", "BluetoothSIG", "CharacteristicDataDict"] 

41 

42T = TypeVar("T") 

43 

44 

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

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

47 

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

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

50 management. 

51 

52 Singleton Pattern: 

53 This class is implemented as a singleton to provide a global registry for 

54 custom characteristics and services. Access the singleton instance using 

55 ``BluetoothSIGTranslator.get_instance()`` or the module-level ``translator`` variable. 

56 

57 Key features: 

58 - Parse raw BLE characteristic data using Bluetooth SIG specifications 

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

60 and [ServiceInfo][bluetooth_sig.types.ServiceInfo] 

61 - Create BaseGattService instances from service UUIDs 

62 - Access comprehensive registry of supported characteristics and services 

63 

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

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

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

67 """ 

68 

69 _instance: BluetoothSIGTranslator | None = None 

70 _instance_lock: bool = False # Simple lock to prevent recursion 

71 

72 def __new__(cls) -> BluetoothSIGTranslator: 

73 """Create or return the singleton instance.""" 

74 if cls._instance is None: 

75 cls._instance = super().__new__(cls) 

76 return cls._instance 

77 

78 @classmethod 

79 def get_instance(cls) -> BluetoothSIGTranslator: 

80 """Get the singleton instance of BluetoothSIGTranslator. 

81 

82 Returns: 

83 The singleton BluetoothSIGTranslator instance 

84 

85 Example:: 

86 

87 from bluetooth_sig import BluetoothSIGTranslator 

88 

89 # Get the singleton instance 

90 translator = BluetoothSIGTranslator.get_instance() 

91 """ 

92 if cls._instance is None: 

93 cls._instance = cls() 

94 return cls._instance 

95 

96 def __init__(self) -> None: 

97 """Initialize the SIG translator (singleton pattern).""" 

98 if self.__class__._instance_lock: 

99 return 

100 self.__class__._instance_lock = True 

101 

102 # Compose delegates 

103 self._query = CharacteristicQueryEngine() 

104 self._parser = CharacteristicParser() 

105 self._encoder = CharacteristicEncoder(self._parser) 

106 self._registration = RegistrationManager() 

107 self._services = ServiceManager() 

108 

109 def __str__(self) -> str: 

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

111 return "BluetoothSIGTranslator(pure SIG standards)" 

112 

113 # ------------------------------------------------------------------------- 

114 # Parse 

115 # ------------------------------------------------------------------------- 

116 

117 @overload 

118 def parse_characteristic( 

119 self, 

120 char: type[BaseCharacteristic[T]], 

121 raw_data: bytes | bytearray, 

122 ctx: CharacteristicContext | None = ..., 

123 ) -> T: ... 

124 

125 @overload 

126 def parse_characteristic( 

127 self, 

128 char: str, 

129 raw_data: bytes | bytearray, 

130 ctx: CharacteristicContext | None = ..., 

131 ) -> Any: ... # noqa: ANN401 # Runtime UUID dispatch cannot be type-safe 

132 

133 def parse_characteristic( 

134 self, 

135 char: str | type[BaseCharacteristic[T]], 

136 raw_data: bytes | bytearray, 

137 ctx: CharacteristicContext | None = None, 

138 ) -> T | Any: 

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

140 

141 Args: 

142 char: Characteristic class (type-safe) or UUID string (not type-safe). 

143 raw_data: Raw bytes from the characteristic (bytes or bytearray) 

144 ctx: Optional CharacteristicContext providing device-level info 

145 

146 Returns: 

147 Parsed value. Return type is inferred when passing characteristic class. 

148 

149 Raises: 

150 SpecialValueDetectedError: Special sentinel value detected 

151 CharacteristicParseError: Parse/validation failure 

152 

153 Example:: 

154 

155 from bluetooth_sig import BluetoothSIGTranslator 

156 from bluetooth_sig.gatt.characteristics import BatteryLevelCharacteristic 

157 

158 translator = BluetoothSIGTranslator() 

159 

160 # Type-safe: pass characteristic class, return type is inferred 

161 level: int = translator.parse_characteristic(BatteryLevelCharacteristic, b"\\x64") 

162 

163 # Not type-safe: pass UUID string, returns Any 

164 value = translator.parse_characteristic("2A19", b"\\x64") 

165 

166 """ 

167 return self._parser.parse_characteristic(char, raw_data, ctx) 

168 

169 def parse_characteristics( 

170 self, 

171 char_data: dict[str, bytes], 

172 ctx: CharacteristicContext | None = None, 

173 ) -> dict[str, Any]: 

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

175 

176 Args: 

177 char_data: Dictionary mapping UUIDs to raw data bytes 

178 ctx: Optional CharacteristicContext used as the starting context 

179 

180 Returns: 

181 Dictionary mapping UUIDs to parsed values 

182 

183 Raises: 

184 ValueError: If circular dependencies are detected 

185 CharacteristicParseError: If parsing fails for any characteristic 

186 

187 Example:: 

188 

189 from bluetooth_sig import BluetoothSIGTranslator 

190 

191 translator = BluetoothSIGTranslator() 

192 data = { 

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

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

195 } 

196 try: 

197 results = translator.parse_characteristics(data) 

198 except CharacteristicParseError as e: 

199 print(f"Parse failed: {e}") 

200 

201 """ 

202 return self._parser.parse_characteristics(char_data, ctx) 

203 

204 # ------------------------------------------------------------------------- 

205 # Encode 

206 # ------------------------------------------------------------------------- 

207 

208 @overload 

209 def encode_characteristic( 

210 self, 

211 char: type[BaseCharacteristic[T]], 

212 value: T, 

213 validate: bool = ..., 

214 ) -> bytes: ... 

215 

216 @overload 

217 def encode_characteristic( 

218 self, 

219 char: str, 

220 value: Any, # noqa: ANN401 # Runtime UUID dispatch cannot be type-safe 

221 validate: bool = ..., 

222 ) -> bytes: ... 

223 

224 def encode_characteristic( 

225 self, 

226 char: str | type[BaseCharacteristic[T]], 

227 value: T | Any, 

228 validate: bool = True, 

229 ) -> bytes: 

230 r"""Encode a value for writing to a characteristic. 

231 

232 Args: 

233 char: Characteristic class (type-safe) or UUID string (not type-safe). 

234 value: The value to encode. Type is checked when using characteristic class. 

235 validate: If True, validates the value before encoding (default: True) 

236 

237 Returns: 

238 Encoded bytes ready to write to the characteristic 

239 

240 Raises: 

241 ValueError: If UUID is invalid, characteristic not found, or value is invalid 

242 TypeError: If value type doesn't match characteristic's expected type 

243 CharacteristicEncodeError: If encoding fails 

244 

245 Example:: 

246 

247 from bluetooth_sig import BluetoothSIGTranslator 

248 from bluetooth_sig.gatt.characteristics import AlertLevelCharacteristic 

249 from bluetooth_sig.gatt.characteristics.alert_level import AlertLevel 

250 

251 translator = BluetoothSIGTranslator() 

252 

253 # Type-safe: pass characteristic class and typed value 

254 data: bytes = translator.encode_characteristic(AlertLevelCharacteristic, AlertLevel.HIGH) 

255 

256 # Not type-safe: pass UUID string 

257 data = translator.encode_characteristic("2A06", 2) 

258 

259 """ 

260 return self._encoder.encode_characteristic(char, value, validate) 

261 

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

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

264 

265 Args: 

266 uuid: The characteristic UUID 

267 data: Raw data bytes to validate 

268 

269 Returns: 

270 ValidationResult with validation details 

271 

272 """ 

273 return self._encoder.validate_characteristic_data(uuid, data) 

274 

275 def create_value(self, uuid: str, **kwargs: Any) -> Any: # noqa: ANN401 

276 """Create a properly typed value instance for a characteristic. 

277 

278 Args: 

279 uuid: The characteristic UUID 

280 **kwargs: Field values for the characteristic's type 

281 

282 Returns: 

283 Properly typed value instance 

284 

285 Raises: 

286 ValueError: If UUID is invalid or characteristic not found 

287 TypeError: If kwargs don't match the characteristic's expected fields 

288 

289 Example:: 

290 

291 from bluetooth_sig import BluetoothSIGTranslator 

292 

293 translator = BluetoothSIGTranslator() 

294 accel = translator.create_value("2C1D", x_axis=1.5, y_axis=0.5, z_axis=9.8) 

295 data = translator.encode_characteristic("2C1D", accel) 

296 

297 """ 

298 return self._encoder.create_value(uuid, **kwargs) 

299 

300 # ------------------------------------------------------------------------- 

301 # Query / Info 

302 # ------------------------------------------------------------------------- 

303 

304 def get_value_type(self, uuid: str) -> type | str | None: 

305 """Get the expected Python type for a characteristic. 

306 

307 Args: 

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

309 

310 Returns: 

311 Python type if characteristic is found, None otherwise 

312 

313 """ 

314 return self._query.get_value_type(uuid) 

315 

316 def supports(self, uuid: str) -> bool: 

317 """Check if a characteristic UUID is supported. 

318 

319 Args: 

320 uuid: The characteristic UUID to check 

321 

322 Returns: 

323 True if the characteristic has a parser/encoder, False otherwise 

324 

325 """ 

326 return self._query.supports(uuid) 

327 

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

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

330 

331 Args: 

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

333 

334 Returns: 

335 CharacteristicInfo with metadata or None if not found 

336 

337 """ 

338 return self._query.get_characteristic_info_by_uuid(uuid) 

339 

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

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

342 

343 Args: 

344 name: CharacteristicName enum 

345 

346 Returns: 

347 Characteristic UUID or None if not found 

348 

349 """ 

350 return self._query.get_characteristic_uuid_by_name(name) 

351 

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

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

354 

355 Args: 

356 name: Service name or enum 

357 

358 Returns: 

359 Service UUID or None if not found 

360 

361 """ 

362 return self._query.get_service_uuid_by_name(name) 

363 

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

365 """Get characteristic info by enum name. 

366 

367 Args: 

368 name: CharacteristicName enum 

369 

370 Returns: 

371 CharacteristicInfo if found, None otherwise 

372 

373 """ 

374 return self._query.get_characteristic_info_by_name(name) 

375 

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

377 """Get service info by name or enum. 

378 

379 Args: 

380 name: Service name string or ServiceName enum 

381 

382 Returns: 

383 ServiceInfo if found, None otherwise 

384 

385 """ 

386 return self._query.get_service_info_by_name(name) 

387 

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

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

390 

391 Args: 

392 uuid: The service UUID 

393 

394 Returns: 

395 ServiceInfo with metadata or None if not found 

396 

397 """ 

398 return self._query.get_service_info_by_uuid(uuid) 

399 

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

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

402 

403 Returns: 

404 Dictionary mapping characteristic names to UUIDs 

405 

406 """ 

407 return self._query.list_supported_characteristics() 

408 

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

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

411 

412 Returns: 

413 Dictionary mapping service names to UUIDs 

414 

415 """ 

416 return self._query.list_supported_services() 

417 

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

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

420 

421 Args: 

422 uuids: List of characteristic UUIDs 

423 

424 Returns: 

425 Dictionary mapping UUIDs to CharacteristicInfo (or None if not found) 

426 

427 """ 

428 return self._query.get_characteristics_info_by_uuids(uuids) 

429 

430 def get_service_characteristics(self, service_uuid: str) -> list[BaseCharacteristic[Any]]: 

431 """Get the characteristic instances associated with a service. 

432 

433 Instantiates each required characteristic class from the service 

434 definition and returns the live objects. 

435 

436 Args: 

437 service_uuid: The service UUID 

438 

439 Returns: 

440 List of BaseCharacteristic instances for this service's 

441 required characteristics. 

442 

443 """ 

444 return self._query.get_service_characteristics(service_uuid) 

445 

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

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

448 

449 Args: 

450 name: Characteristic or service name 

451 

452 Returns: 

453 CharacteristicInfo or ServiceInfo if found, None otherwise 

454 

455 """ 

456 return self._query.get_sig_info_by_name(name) 

457 

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

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

460 

461 Args: 

462 uuid: UUID string (with or without dashes) 

463 

464 Returns: 

465 CharacteristicInfo or ServiceInfo if found, None otherwise 

466 

467 """ 

468 return self._query.get_sig_info_by_uuid(uuid) 

469 

470 # ------------------------------------------------------------------------- 

471 # Service lifecycle 

472 # ------------------------------------------------------------------------- 

473 

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

475 """Process discovered services and their characteristics. 

476 

477 Args: 

478 services: Dictionary of service UUIDs to their characteristics 

479 

480 """ 

481 self._services.process_services(services) 

482 

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

484 """Get a service instance by UUID. 

485 

486 Args: 

487 uuid: The service UUID 

488 

489 Returns: 

490 Service instance if found, None otherwise 

491 

492 """ 

493 return self._services.get_service_by_uuid(uuid) 

494 

495 @property 

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

497 """Get list of discovered service instances. 

498 

499 Returns: 

500 List of discovered service instances 

501 

502 """ 

503 return self._services.discovered_services 

504 

505 def clear_services(self) -> None: 

506 """Clear all discovered services.""" 

507 self._services.clear_services() 

508 

509 # ------------------------------------------------------------------------- 

510 # Registration 

511 # ------------------------------------------------------------------------- 

512 

513 def register_custom_characteristic_class( 

514 self, 

515 uuid_or_name: str, 

516 cls: type[BaseCharacteristic[Any]], 

517 info: CharacteristicInfo | None = None, 

518 override: bool = False, 

519 ) -> None: 

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

521 

522 Args: 

523 uuid_or_name: The characteristic UUID or name 

524 cls: The characteristic class to register 

525 info: Optional CharacteristicInfo with metadata (name, unit, python_type) 

526 override: Whether to override existing registrations 

527 

528 Raises: 

529 TypeError: If cls does not inherit from BaseCharacteristic 

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

531 

532 Example:: 

533 

534 from bluetooth_sig import BluetoothSIGTranslator, CharacteristicInfo 

535 from bluetooth_sig.types import BluetoothUUID 

536 

537 translator = BluetoothSIGTranslator() 

538 info = CharacteristicInfo( 

539 uuid=BluetoothUUID("12345678-1234-1234-1234-123456789abc"), 

540 name="Custom Temperature", 

541 unit="°C", 

542 python_type=float, 

543 ) 

544 translator.register_custom_characteristic_class(str(info.uuid), MyCustomChar, info=info) 

545 

546 """ 

547 self._registration.register_custom_characteristic_class(uuid_or_name, cls, info, override) 

548 

549 def register_custom_service_class( 

550 self, 

551 uuid_or_name: str, 

552 cls: type[BaseGattService], 

553 info: ServiceInfo | None = None, 

554 override: bool = False, 

555 ) -> None: 

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

557 

558 Args: 

559 uuid_or_name: The service UUID or name 

560 cls: The service class to register 

561 info: Optional ServiceInfo with metadata (name) 

562 override: Whether to override existing registrations 

563 

564 Raises: 

565 TypeError: If cls does not inherit from BaseGattService 

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

567 

568 Example:: 

569 

570 from bluetooth_sig import BluetoothSIGTranslator, ServiceInfo 

571 from bluetooth_sig.types import BluetoothUUID 

572 

573 translator = BluetoothSIGTranslator() 

574 info = ServiceInfo(uuid=BluetoothUUID("12345678-..."), name="Custom Service") 

575 translator.register_custom_service_class(str(info.uuid), MyService, info=info) 

576 

577 """ 

578 self._registration.register_custom_service_class(uuid_or_name, cls, info, override) 

579 

580 # ------------------------------------------------------------------------- 

581 # Async wrappers 

582 # ------------------------------------------------------------------------- 

583 

584 @overload 

585 async def parse_characteristic_async( 

586 self, 

587 char: type[BaseCharacteristic[T]], 

588 raw_data: bytes, 

589 ctx: CharacteristicContext | None = ..., 

590 ) -> T: ... 

591 

592 @overload 

593 async def parse_characteristic_async( 

594 self, 

595 char: str | BluetoothUUID, 

596 raw_data: bytes, 

597 ctx: CharacteristicContext | None = ..., 

598 ) -> Any: ... # noqa: ANN401 # Runtime UUID dispatch cannot be type-safe 

599 

600 async def parse_characteristic_async( 

601 self, 

602 char: str | BluetoothUUID | type[BaseCharacteristic[T]], 

603 raw_data: bytes, 

604 ctx: CharacteristicContext | None = None, 

605 ) -> T | Any: 

606 """Parse characteristic data in an async-compatible manner. 

607 

608 Args: 

609 char: Characteristic class (type-safe) or UUID string/BluetoothUUID. 

610 raw_data: Raw bytes from the characteristic 

611 ctx: Optional context providing device-level info 

612 

613 Returns: 

614 Parsed value. Return type is inferred when passing characteristic class. 

615 

616 Raises: 

617 SpecialValueDetectedError: Special sentinel value detected 

618 CharacteristicParseError: Parse/validation failure 

619 

620 """ 

621 if isinstance(char, type) and issubclass(char, BaseCharacteristic): 

622 return self._parser.parse_characteristic(char, raw_data, ctx) 

623 

624 uuid_str = str(char) if isinstance(char, BluetoothUUID) else char 

625 return self._parser.parse_characteristic(uuid_str, raw_data, ctx) 

626 

627 async def parse_characteristics_async( 

628 self, 

629 char_data: dict[str, bytes], 

630 ctx: CharacteristicContext | None = None, 

631 ) -> dict[str, Any]: 

632 """Parse multiple characteristics in an async-compatible manner. 

633 

634 Args: 

635 char_data: Dictionary mapping UUIDs to raw data bytes 

636 ctx: Optional context 

637 

638 Returns: 

639 Dictionary mapping UUIDs to parsed values 

640 

641 """ 

642 return self._parser.parse_characteristics(char_data, ctx) 

643 

644 @overload 

645 async def encode_characteristic_async( 

646 self, 

647 char: type[BaseCharacteristic[T]], 

648 value: T, 

649 validate: bool = ..., 

650 ) -> bytes: ... 

651 

652 @overload 

653 async def encode_characteristic_async( 

654 self, 

655 char: str | BluetoothUUID, 

656 value: Any, # noqa: ANN401 # Runtime UUID dispatch cannot be type-safe 

657 validate: bool = ..., 

658 ) -> bytes: ... 

659 

660 async def encode_characteristic_async( 

661 self, 

662 char: str | BluetoothUUID | type[BaseCharacteristic[T]], 

663 value: T | Any, 

664 validate: bool = True, 

665 ) -> bytes: 

666 """Encode characteristic value in an async-compatible manner. 

667 

668 Args: 

669 char: Characteristic class (type-safe) or UUID string/BluetoothUUID. 

670 value: The value to encode. 

671 validate: If True, validates before encoding (default: True) 

672 

673 Returns: 

674 Encoded bytes ready to write 

675 

676 """ 

677 if isinstance(char, type) and issubclass(char, BaseCharacteristic): 

678 return self._encoder.encode_characteristic(char, value, validate) 

679 

680 uuid_str = str(char) if isinstance(char, BluetoothUUID) else char 

681 return self._encoder.encode_characteristic(uuid_str, value, validate) 

682 

683 

684# Global instance 

685BluetoothSIG = BluetoothSIGTranslator()