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

101 statements  

« prev     ^ index     » next       coverage.py v7.14.3, created at 2026-06-28 01:26 +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.base import BaseGattService 

23from ..types import ( 

24 CharacteristicContext, 

25 CharacteristicInfo, 

26 ServiceInfo, 

27 SIGInfo, 

28 ValidationResult, 

29) 

30from ..types.gatt_enums import CharacteristicName, ServiceName 

31from ..types.uuid import BluetoothUUID 

32from .encoder import CharacteristicEncoder 

33from .parser import CharacteristicParser 

34from .query import CharacteristicQueryEngine 

35from .registration import RegistrationManager 

36from .service_manager import CharacteristicDataDict, ServiceManager 

37 

38# Re-export for backward compatibility 

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

40 

41T = TypeVar("T") 

42 

43 

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

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

46 

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

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

49 management. 

50 

51 Singleton Pattern: 

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

53 custom characteristics and services. Access the singleton instance using 

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

55 

56 Key features: 

57 - Parse raw BLE characteristic data using Bluetooth SIG specifications 

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

59 and [ServiceInfo][bluetooth_sig.types.ServiceInfo] 

60 - Create BaseGattService instances from service UUIDs 

61 - Access comprehensive registry of supported characteristics and services 

62 

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

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

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

66 """ 

67 

68 _instance: BluetoothSIGTranslator | None = None 

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

70 

71 def __new__(cls) -> BluetoothSIGTranslator: 

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

73 if cls._instance is None: 

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

75 return cls._instance 

76 

77 @classmethod 

78 def get_instance(cls) -> BluetoothSIGTranslator: 

79 """Get the singleton instance of BluetoothSIGTranslator. 

80 

81 Returns: 

82 The singleton BluetoothSIGTranslator instance 

83 

84 Example:: 

85 

86 from bluetooth_sig import BluetoothSIGTranslator 

87 

88 # Get the singleton instance 

89 translator = BluetoothSIGTranslator.get_instance() 

90 """ 

91 if cls._instance is None: 

92 cls._instance = cls() 

93 return cls._instance 

94 

95 def __init__(self) -> None: 

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

97 if self.__class__._instance_lock: 

98 return 

99 self.__class__._instance_lock = True 

100 

101 # Compose delegates 

102 self._query = CharacteristicQueryEngine() 

103 self._parser = CharacteristicParser() 

104 self._encoder = CharacteristicEncoder(self._parser) 

105 self._registration = RegistrationManager() 

106 self._services = ServiceManager() 

107 

108 def __str__(self) -> str: 

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

110 return "BluetoothSIGTranslator(pure SIG standards)" 

111 

112 # ------------------------------------------------------------------------- 

113 # Parse 

114 # ------------------------------------------------------------------------- 

115 

116 @overload 

117 def parse_characteristic( 

118 self, 

119 char: type[BaseCharacteristic[T]], 

120 raw_data: bytes | bytearray, 

121 ctx: CharacteristicContext | None = ..., 

122 ) -> T: ... 

123 

124 @overload 

125 def parse_characteristic( 

126 self, 

127 char: str, 

128 raw_data: bytes | bytearray, 

129 ctx: CharacteristicContext | None = ..., 

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

131 

132 def parse_characteristic( 

133 self, 

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

135 raw_data: bytes | bytearray, 

136 ctx: CharacteristicContext | None = None, 

137 ) -> T | Any: 

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

139 

140 Args: 

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

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

143 ctx: Optional CharacteristicContext providing device-level info 

144 

145 Returns: 

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

147 

148 Raises: 

149 SpecialValueDetectedError: Special sentinel value detected 

150 CharacteristicParseError: Parse/validation failure 

151 

152 Example:: 

153 

154 from bluetooth_sig import BluetoothSIGTranslator 

155 from bluetooth_sig.gatt.characteristics import BatteryLevelCharacteristic 

156 

157 translator = BluetoothSIGTranslator() 

158 

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

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

161 

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

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

164 

165 """ 

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

167 

168 def parse_characteristics( 

169 self, 

170 char_data: dict[str, bytes], 

171 ctx: CharacteristicContext | None = None, 

172 ) -> dict[str, Any]: 

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

174 

175 Args: 

176 char_data: Dictionary mapping UUIDs to raw data bytes 

177 ctx: Optional CharacteristicContext used as the starting context 

178 

179 Returns: 

180 Dictionary mapping UUIDs to parsed values 

181 

182 Raises: 

183 ValueError: If circular dependencies are detected 

184 CharacteristicParseError: If parsing fails for any characteristic 

185 

186 Example:: 

187 

188 from bluetooth_sig import BluetoothSIGTranslator 

189 

190 translator = BluetoothSIGTranslator() 

191 data = { 

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

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

194 } 

195 try: 

196 results = translator.parse_characteristics(data) 

197 except CharacteristicParseError as e: 

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

199 

200 """ 

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

202 

203 # ------------------------------------------------------------------------- 

204 # Encode 

205 # ------------------------------------------------------------------------- 

206 

207 @overload 

208 def encode_characteristic( 

209 self, 

210 char: type[BaseCharacteristic[T]], 

211 value: T, 

212 validate: bool = ..., 

213 ) -> bytes: ... 

214 

215 @overload 

216 def encode_characteristic( 

217 self, 

218 char: str, 

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

220 validate: bool = ..., 

221 ) -> bytes: ... 

222 

223 def encode_characteristic( 

224 self, 

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

226 value: T | Any, 

227 validate: bool = True, 

228 ) -> bytes: 

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

230 

231 Args: 

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

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

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

235 

236 Returns: 

237 Encoded bytes ready to write to the characteristic 

238 

239 Raises: 

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

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

242 CharacteristicEncodeError: If encoding fails 

243 

244 Example:: 

245 

246 from bluetooth_sig import BluetoothSIGTranslator 

247 from bluetooth_sig.gatt.characteristics import AlertLevelCharacteristic 

248 from bluetooth_sig.gatt.characteristics.alert_level import AlertLevel 

249 

250 translator = BluetoothSIGTranslator() 

251 

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

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

254 

255 # Not type-safe: pass UUID string 

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

257 

258 """ 

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

260 

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

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

263 

264 Args: 

265 uuid: The characteristic UUID 

266 data: Raw data bytes to validate 

267 

268 Returns: 

269 ValidationResult with validation details 

270 

271 """ 

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

273 

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

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

276 

277 Args: 

278 uuid: The characteristic UUID 

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

280 

281 Returns: 

282 Properly typed value instance 

283 

284 Raises: 

285 ValueError: If UUID is invalid or characteristic not found 

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

287 

288 Example:: 

289 

290 from bluetooth_sig import BluetoothSIGTranslator 

291 

292 translator = BluetoothSIGTranslator() 

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

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

295 

296 """ 

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

298 

299 # ------------------------------------------------------------------------- 

300 # Query / Info 

301 # ------------------------------------------------------------------------- 

302 

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

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

305 

306 Args: 

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

308 

309 Returns: 

310 Python type if characteristic is found, None otherwise 

311 

312 """ 

313 return self._query.get_value_type(uuid) 

314 

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

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

317 

318 Args: 

319 uuid: The characteristic UUID to check 

320 

321 Returns: 

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

323 

324 """ 

325 return self._query.supports(uuid) 

326 

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

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

329 

330 Args: 

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

332 

333 Returns: 

334 CharacteristicInfo with metadata or None if not found 

335 

336 """ 

337 return self._query.get_characteristic_info_by_uuid(uuid) 

338 

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

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

341 

342 Args: 

343 name: CharacteristicName enum 

344 

345 Returns: 

346 Characteristic UUID or None if not found 

347 

348 """ 

349 return self._query.get_characteristic_uuid_by_name(name) 

350 

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

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

353 

354 Args: 

355 name: Service name or enum 

356 

357 Returns: 

358 Service UUID or None if not found 

359 

360 """ 

361 return self._query.get_service_uuid_by_name(name) 

362 

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

364 """Get characteristic info by enum name. 

365 

366 Args: 

367 name: CharacteristicName enum 

368 

369 Returns: 

370 CharacteristicInfo if found, None otherwise 

371 

372 """ 

373 return self._query.get_characteristic_info_by_name(name) 

374 

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

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

377 

378 Args: 

379 name: Service name string or ServiceName enum 

380 

381 Returns: 

382 ServiceInfo if found, None otherwise 

383 

384 """ 

385 return self._query.get_service_info_by_name(name) 

386 

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

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

389 

390 Args: 

391 uuid: The service UUID 

392 

393 Returns: 

394 ServiceInfo with metadata or None if not found 

395 

396 """ 

397 return self._query.get_service_info_by_uuid(uuid) 

398 

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

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

401 

402 Returns: 

403 Dictionary mapping characteristic names to UUIDs 

404 

405 """ 

406 return self._query.list_supported_characteristics() 

407 

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

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

410 

411 Returns: 

412 Dictionary mapping service names to UUIDs 

413 

414 """ 

415 return self._query.list_supported_services() 

416 

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

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

419 

420 Args: 

421 uuids: List of characteristic UUIDs 

422 

423 Returns: 

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

425 

426 """ 

427 return self._query.get_characteristics_info_by_uuids(uuids) 

428 

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

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

431 

432 Instantiates each required characteristic class from the service 

433 definition and returns the live objects. 

434 

435 Args: 

436 service_uuid: The service UUID 

437 

438 Returns: 

439 List of BaseCharacteristic instances for this service's 

440 required characteristics. 

441 

442 """ 

443 return self._query.get_service_characteristics(service_uuid) 

444 

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

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

447 

448 Args: 

449 name: Characteristic or service name 

450 

451 Returns: 

452 CharacteristicInfo or ServiceInfo if found, None otherwise 

453 

454 """ 

455 return self._query.get_sig_info_by_name(name) 

456 

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

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

459 

460 Args: 

461 uuid: UUID string (with or without dashes) 

462 

463 Returns: 

464 CharacteristicInfo or ServiceInfo if found, None otherwise 

465 

466 """ 

467 return self._query.get_sig_info_by_uuid(uuid) 

468 

469 # ------------------------------------------------------------------------- 

470 # Service lifecycle 

471 # ------------------------------------------------------------------------- 

472 

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

474 """Process discovered services and their characteristics. 

475 

476 Args: 

477 services: Dictionary of service UUIDs to their characteristics 

478 

479 """ 

480 self._services.process_services(services) 

481 

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

483 """Get a service instance by UUID. 

484 

485 Args: 

486 uuid: The service UUID 

487 

488 Returns: 

489 Service instance if found, None otherwise 

490 

491 """ 

492 return self._services.get_service_by_uuid(uuid) 

493 

494 @property 

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

496 """Get list of discovered service instances. 

497 

498 Returns: 

499 List of discovered service instances 

500 

501 """ 

502 return self._services.discovered_services 

503 

504 def clear_services(self) -> None: 

505 """Clear all discovered services.""" 

506 self._services.clear_services() 

507 

508 # ------------------------------------------------------------------------- 

509 # Registration 

510 # ------------------------------------------------------------------------- 

511 

512 def register_custom_characteristic_class( 

513 self, 

514 uuid_or_name: str, 

515 cls: type[BaseCharacteristic[Any]], 

516 info: CharacteristicInfo | None = None, 

517 override: bool = False, 

518 ) -> None: 

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

520 

521 Args: 

522 uuid_or_name: The characteristic UUID or name 

523 cls: The characteristic class to register 

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

525 override: Whether to override existing registrations 

526 

527 Raises: 

528 TypeError: If cls does not inherit from BaseCharacteristic 

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

530 

531 Example:: 

532 

533 from bluetooth_sig import BluetoothSIGTranslator, CharacteristicInfo 

534 from bluetooth_sig.types import BluetoothUUID 

535 

536 translator = BluetoothSIGTranslator() 

537 info = CharacteristicInfo( 

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

539 name="Custom Temperature", 

540 unit="°C", 

541 python_type=float, 

542 ) 

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

544 

545 """ 

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

547 

548 def register_custom_service_class( 

549 self, 

550 uuid_or_name: str, 

551 cls: type[BaseGattService], 

552 info: ServiceInfo | None = None, 

553 override: bool = False, 

554 ) -> None: 

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

556 

557 Args: 

558 uuid_or_name: The service UUID or name 

559 cls: The service class to register 

560 info: Optional ServiceInfo with metadata (name) 

561 override: Whether to override existing registrations 

562 

563 Raises: 

564 TypeError: If cls does not inherit from BaseGattService 

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

566 

567 Example:: 

568 

569 from bluetooth_sig import BluetoothSIGTranslator, ServiceInfo 

570 from bluetooth_sig.types import BluetoothUUID 

571 

572 translator = BluetoothSIGTranslator() 

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

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

575 

576 """ 

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

578 

579 # ------------------------------------------------------------------------- 

580 # Async wrappers 

581 # ------------------------------------------------------------------------- 

582 

583 @overload 

584 async def parse_characteristic_async( 

585 self, 

586 char: type[BaseCharacteristic[T]], 

587 raw_data: bytes, 

588 ctx: CharacteristicContext | None = ..., 

589 ) -> T: ... 

590 

591 @overload 

592 async def parse_characteristic_async( 

593 self, 

594 char: str | BluetoothUUID, 

595 raw_data: bytes, 

596 ctx: CharacteristicContext | None = ..., 

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

598 

599 async def parse_characteristic_async( 

600 self, 

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

602 raw_data: bytes, 

603 ctx: CharacteristicContext | None = None, 

604 ) -> T | Any: 

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

606 

607 Args: 

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

609 raw_data: Raw bytes from the characteristic 

610 ctx: Optional context providing device-level info 

611 

612 Returns: 

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

614 

615 Raises: 

616 SpecialValueDetectedError: Special sentinel value detected 

617 CharacteristicParseError: Parse/validation failure 

618 

619 """ 

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

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

622 

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

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

625 

626 async def parse_characteristics_async( 

627 self, 

628 char_data: dict[str, bytes], 

629 ctx: CharacteristicContext | None = None, 

630 ) -> dict[str, Any]: 

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

632 

633 Args: 

634 char_data: Dictionary mapping UUIDs to raw data bytes 

635 ctx: Optional context 

636 

637 Returns: 

638 Dictionary mapping UUIDs to parsed values 

639 

640 """ 

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

642 

643 @overload 

644 async def encode_characteristic_async( 

645 self, 

646 char: type[BaseCharacteristic[T]], 

647 value: T, 

648 validate: bool = ..., 

649 ) -> bytes: ... 

650 

651 @overload 

652 async def encode_characteristic_async( 

653 self, 

654 char: str | BluetoothUUID, 

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

656 validate: bool = ..., 

657 ) -> bytes: ... 

658 

659 async def encode_characteristic_async( 

660 self, 

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

662 value: T | Any, 

663 validate: bool = True, 

664 ) -> bytes: 

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

666 

667 Args: 

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

669 value: The value to encode. 

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

671 

672 Returns: 

673 Encoded bytes ready to write 

674 

675 """ 

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

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

678 

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

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

681 

682 

683# Global instance 

684BluetoothSIG = BluetoothSIGTranslator()