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
« 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.
3This module provides the public ``BluetoothSIGTranslator`` class, which
4delegates all work to five focused components:
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
12The facade preserves every public method signature, ``@overload``
13decorator, async wrapper, and the singleton pattern from the original
14monolithic implementation.
15"""
17from __future__ import annotations
19from typing import Any, TypeVar, overload
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
39# Re-export for backward compatibility
40__all__ = ["BluetoothSIGTranslator", "BluetoothSIG", "CharacteristicDataDict"]
42T = TypeVar("T")
45class BluetoothSIGTranslator: # pylint: disable=too-many-public-methods
46 """Pure Bluetooth SIG standards translator for characteristic and service interpretation.
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.
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.
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
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 """
69 _instance: BluetoothSIGTranslator | None = None
70 _instance_lock: bool = False # Simple lock to prevent recursion
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
78 @classmethod
79 def get_instance(cls) -> BluetoothSIGTranslator:
80 """Get the singleton instance of BluetoothSIGTranslator.
82 Returns:
83 The singleton BluetoothSIGTranslator instance
85 Example::
87 from bluetooth_sig import BluetoothSIGTranslator
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
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
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()
109 def __str__(self) -> str:
110 """Return string representation of the translator."""
111 return "BluetoothSIGTranslator(pure SIG standards)"
113 # -------------------------------------------------------------------------
114 # Parse
115 # -------------------------------------------------------------------------
117 @overload
118 def parse_characteristic(
119 self,
120 char: type[BaseCharacteristic[T]],
121 raw_data: bytes | bytearray,
122 ctx: CharacteristicContext | None = ...,
123 ) -> T: ...
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
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.
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
146 Returns:
147 Parsed value. Return type is inferred when passing characteristic class.
149 Raises:
150 SpecialValueDetectedError: Special sentinel value detected
151 CharacteristicParseError: Parse/validation failure
153 Example::
155 from bluetooth_sig import BluetoothSIGTranslator
156 from bluetooth_sig.gatt.characteristics import BatteryLevelCharacteristic
158 translator = BluetoothSIGTranslator()
160 # Type-safe: pass characteristic class, return type is inferred
161 level: int = translator.parse_characteristic(BatteryLevelCharacteristic, b"\\x64")
163 # Not type-safe: pass UUID string, returns Any
164 value = translator.parse_characteristic("2A19", b"\\x64")
166 """
167 return self._parser.parse_characteristic(char, raw_data, ctx)
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.
176 Args:
177 char_data: Dictionary mapping UUIDs to raw data bytes
178 ctx: Optional CharacteristicContext used as the starting context
180 Returns:
181 Dictionary mapping UUIDs to parsed values
183 Raises:
184 ValueError: If circular dependencies are detected
185 CharacteristicParseError: If parsing fails for any characteristic
187 Example::
189 from bluetooth_sig import BluetoothSIGTranslator
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}")
201 """
202 return self._parser.parse_characteristics(char_data, ctx)
204 # -------------------------------------------------------------------------
205 # Encode
206 # -------------------------------------------------------------------------
208 @overload
209 def encode_characteristic(
210 self,
211 char: type[BaseCharacteristic[T]],
212 value: T,
213 validate: bool = ...,
214 ) -> bytes: ...
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: ...
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.
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)
237 Returns:
238 Encoded bytes ready to write to the characteristic
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
245 Example::
247 from bluetooth_sig import BluetoothSIGTranslator
248 from bluetooth_sig.gatt.characteristics import AlertLevelCharacteristic
249 from bluetooth_sig.gatt.characteristics.alert_level import AlertLevel
251 translator = BluetoothSIGTranslator()
253 # Type-safe: pass characteristic class and typed value
254 data: bytes = translator.encode_characteristic(AlertLevelCharacteristic, AlertLevel.HIGH)
256 # Not type-safe: pass UUID string
257 data = translator.encode_characteristic("2A06", 2)
259 """
260 return self._encoder.encode_characteristic(char, value, validate)
262 def validate_characteristic_data(self, uuid: str, data: bytes) -> ValidationResult:
263 """Validate characteristic data format against SIG specifications.
265 Args:
266 uuid: The characteristic UUID
267 data: Raw data bytes to validate
269 Returns:
270 ValidationResult with validation details
272 """
273 return self._encoder.validate_characteristic_data(uuid, data)
275 def create_value(self, uuid: str, **kwargs: Any) -> Any: # noqa: ANN401
276 """Create a properly typed value instance for a characteristic.
278 Args:
279 uuid: The characteristic UUID
280 **kwargs: Field values for the characteristic's type
282 Returns:
283 Properly typed value instance
285 Raises:
286 ValueError: If UUID is invalid or characteristic not found
287 TypeError: If kwargs don't match the characteristic's expected fields
289 Example::
291 from bluetooth_sig import BluetoothSIGTranslator
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)
297 """
298 return self._encoder.create_value(uuid, **kwargs)
300 # -------------------------------------------------------------------------
301 # Query / Info
302 # -------------------------------------------------------------------------
304 def get_value_type(self, uuid: str) -> type | str | None:
305 """Get the expected Python type for a characteristic.
307 Args:
308 uuid: The characteristic UUID (16-bit short form or full 128-bit)
310 Returns:
311 Python type if characteristic is found, None otherwise
313 """
314 return self._query.get_value_type(uuid)
316 def supports(self, uuid: str) -> bool:
317 """Check if a characteristic UUID is supported.
319 Args:
320 uuid: The characteristic UUID to check
322 Returns:
323 True if the characteristic has a parser/encoder, False otherwise
325 """
326 return self._query.supports(uuid)
328 def get_characteristic_info_by_uuid(self, uuid: str) -> CharacteristicInfo | None:
329 """Get information about a characteristic by UUID.
331 Args:
332 uuid: The characteristic UUID (16-bit short form or full 128-bit)
334 Returns:
335 CharacteristicInfo with metadata or None if not found
337 """
338 return self._query.get_characteristic_info_by_uuid(uuid)
340 def get_characteristic_uuid_by_name(self, name: CharacteristicName) -> BluetoothUUID | None:
341 """Get the UUID for a characteristic name enum.
343 Args:
344 name: CharacteristicName enum
346 Returns:
347 Characteristic UUID or None if not found
349 """
350 return self._query.get_characteristic_uuid_by_name(name)
352 def get_service_uuid_by_name(self, name: str | ServiceName) -> BluetoothUUID | None:
353 """Get the UUID for a service name or enum.
355 Args:
356 name: Service name or enum
358 Returns:
359 Service UUID or None if not found
361 """
362 return self._query.get_service_uuid_by_name(name)
364 def get_characteristic_info_by_name(self, name: CharacteristicName) -> CharacteristicInfo | None:
365 """Get characteristic info by enum name.
367 Args:
368 name: CharacteristicName enum
370 Returns:
371 CharacteristicInfo if found, None otherwise
373 """
374 return self._query.get_characteristic_info_by_name(name)
376 def get_service_info_by_name(self, name: str | ServiceName) -> ServiceInfo | None:
377 """Get service info by name or enum.
379 Args:
380 name: Service name string or ServiceName enum
382 Returns:
383 ServiceInfo if found, None otherwise
385 """
386 return self._query.get_service_info_by_name(name)
388 def get_service_info_by_uuid(self, uuid: str) -> ServiceInfo | None:
389 """Get information about a service by UUID.
391 Args:
392 uuid: The service UUID
394 Returns:
395 ServiceInfo with metadata or None if not found
397 """
398 return self._query.get_service_info_by_uuid(uuid)
400 def list_supported_characteristics(self) -> dict[str, str]:
401 """List all supported characteristics with their names and UUIDs.
403 Returns:
404 Dictionary mapping characteristic names to UUIDs
406 """
407 return self._query.list_supported_characteristics()
409 def list_supported_services(self) -> dict[str, str]:
410 """List all supported services with their names and UUIDs.
412 Returns:
413 Dictionary mapping service names to UUIDs
415 """
416 return self._query.list_supported_services()
418 def get_characteristics_info_by_uuids(self, uuids: list[str]) -> dict[str, CharacteristicInfo | None]:
419 """Get information about multiple characteristics by UUID.
421 Args:
422 uuids: List of characteristic UUIDs
424 Returns:
425 Dictionary mapping UUIDs to CharacteristicInfo (or None if not found)
427 """
428 return self._query.get_characteristics_info_by_uuids(uuids)
430 def get_service_characteristics(self, service_uuid: str) -> list[BaseCharacteristic[Any]]:
431 """Get the characteristic instances associated with a service.
433 Instantiates each required characteristic class from the service
434 definition and returns the live objects.
436 Args:
437 service_uuid: The service UUID
439 Returns:
440 List of BaseCharacteristic instances for this service's
441 required characteristics.
443 """
444 return self._query.get_service_characteristics(service_uuid)
446 def get_sig_info_by_name(self, name: str) -> SIGInfo | None:
447 """Get Bluetooth SIG information for a characteristic or service by name.
449 Args:
450 name: Characteristic or service name
452 Returns:
453 CharacteristicInfo or ServiceInfo if found, None otherwise
455 """
456 return self._query.get_sig_info_by_name(name)
458 def get_sig_info_by_uuid(self, uuid: str) -> SIGInfo | None:
459 """Get Bluetooth SIG information for a UUID.
461 Args:
462 uuid: UUID string (with or without dashes)
464 Returns:
465 CharacteristicInfo or ServiceInfo if found, None otherwise
467 """
468 return self._query.get_sig_info_by_uuid(uuid)
470 # -------------------------------------------------------------------------
471 # Service lifecycle
472 # -------------------------------------------------------------------------
474 def process_services(self, services: dict[str, dict[str, CharacteristicDataDict]]) -> None:
475 """Process discovered services and their characteristics.
477 Args:
478 services: Dictionary of service UUIDs to their characteristics
480 """
481 self._services.process_services(services)
483 def get_service_by_uuid(self, uuid: str) -> BaseGattService | None:
484 """Get a service instance by UUID.
486 Args:
487 uuid: The service UUID
489 Returns:
490 Service instance if found, None otherwise
492 """
493 return self._services.get_service_by_uuid(uuid)
495 @property
496 def discovered_services(self) -> list[BaseGattService]:
497 """Get list of discovered service instances.
499 Returns:
500 List of discovered service instances
502 """
503 return self._services.discovered_services
505 def clear_services(self) -> None:
506 """Clear all discovered services."""
507 self._services.clear_services()
509 # -------------------------------------------------------------------------
510 # Registration
511 # -------------------------------------------------------------------------
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.
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
528 Raises:
529 TypeError: If cls does not inherit from BaseCharacteristic
530 ValueError: If UUID conflicts with existing registration and override=False
532 Example::
534 from bluetooth_sig import BluetoothSIGTranslator, CharacteristicInfo
535 from bluetooth_sig.types import BluetoothUUID
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)
546 """
547 self._registration.register_custom_characteristic_class(uuid_or_name, cls, info, override)
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.
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
564 Raises:
565 TypeError: If cls does not inherit from BaseGattService
566 ValueError: If UUID conflicts with existing registration and override=False
568 Example::
570 from bluetooth_sig import BluetoothSIGTranslator, ServiceInfo
571 from bluetooth_sig.types import BluetoothUUID
573 translator = BluetoothSIGTranslator()
574 info = ServiceInfo(uuid=BluetoothUUID("12345678-..."), name="Custom Service")
575 translator.register_custom_service_class(str(info.uuid), MyService, info=info)
577 """
578 self._registration.register_custom_service_class(uuid_or_name, cls, info, override)
580 # -------------------------------------------------------------------------
581 # Async wrappers
582 # -------------------------------------------------------------------------
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: ...
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
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.
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
613 Returns:
614 Parsed value. Return type is inferred when passing characteristic class.
616 Raises:
617 SpecialValueDetectedError: Special sentinel value detected
618 CharacteristicParseError: Parse/validation failure
620 """
621 if isinstance(char, type) and issubclass(char, BaseCharacteristic):
622 return self._parser.parse_characteristic(char, raw_data, ctx)
624 uuid_str = str(char) if isinstance(char, BluetoothUUID) else char
625 return self._parser.parse_characteristic(uuid_str, raw_data, ctx)
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.
634 Args:
635 char_data: Dictionary mapping UUIDs to raw data bytes
636 ctx: Optional context
638 Returns:
639 Dictionary mapping UUIDs to parsed values
641 """
642 return self._parser.parse_characteristics(char_data, ctx)
644 @overload
645 async def encode_characteristic_async(
646 self,
647 char: type[BaseCharacteristic[T]],
648 value: T,
649 validate: bool = ...,
650 ) -> bytes: ...
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: ...
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.
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)
673 Returns:
674 Encoded bytes ready to write
676 """
677 if isinstance(char, type) and issubclass(char, BaseCharacteristic):
678 return self._encoder.encode_characteristic(char, value, validate)
680 uuid_str = str(char) if isinstance(char, BluetoothUUID) else char
681 return self._encoder.encode_characteristic(uuid_str, value, validate)
684# Global instance
685BluetoothSIG = BluetoothSIGTranslator()