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
« 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.
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.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
38# Re-export for backward compatibility
39__all__ = ["BluetoothSIGTranslator", "BluetoothSIG", "CharacteristicDataDict"]
41T = TypeVar("T")
44class BluetoothSIGTranslator: # pylint: disable=too-many-public-methods
45 """Pure Bluetooth SIG standards translator for characteristic and service interpretation.
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.
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.
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
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 """
68 _instance: BluetoothSIGTranslator | None = None
69 _instance_lock: bool = False # Simple lock to prevent recursion
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
77 @classmethod
78 def get_instance(cls) -> BluetoothSIGTranslator:
79 """Get the singleton instance of BluetoothSIGTranslator.
81 Returns:
82 The singleton BluetoothSIGTranslator instance
84 Example::
86 from bluetooth_sig import BluetoothSIGTranslator
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
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
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()
108 def __str__(self) -> str:
109 """Return string representation of the translator."""
110 return "BluetoothSIGTranslator(pure SIG standards)"
112 # -------------------------------------------------------------------------
113 # Parse
114 # -------------------------------------------------------------------------
116 @overload
117 def parse_characteristic(
118 self,
119 char: type[BaseCharacteristic[T]],
120 raw_data: bytes | bytearray,
121 ctx: CharacteristicContext | None = ...,
122 ) -> T: ...
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
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.
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
145 Returns:
146 Parsed value. Return type is inferred when passing characteristic class.
148 Raises:
149 SpecialValueDetectedError: Special sentinel value detected
150 CharacteristicParseError: Parse/validation failure
152 Example::
154 from bluetooth_sig import BluetoothSIGTranslator
155 from bluetooth_sig.gatt.characteristics import BatteryLevelCharacteristic
157 translator = BluetoothSIGTranslator()
159 # Type-safe: pass characteristic class, return type is inferred
160 level: int = translator.parse_characteristic(BatteryLevelCharacteristic, b"\\x64")
162 # Not type-safe: pass UUID string, returns Any
163 value = translator.parse_characteristic("2A19", b"\\x64")
165 """
166 return self._parser.parse_characteristic(char, raw_data, ctx)
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.
175 Args:
176 char_data: Dictionary mapping UUIDs to raw data bytes
177 ctx: Optional CharacteristicContext used as the starting context
179 Returns:
180 Dictionary mapping UUIDs to parsed values
182 Raises:
183 ValueError: If circular dependencies are detected
184 CharacteristicParseError: If parsing fails for any characteristic
186 Example::
188 from bluetooth_sig import BluetoothSIGTranslator
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}")
200 """
201 return self._parser.parse_characteristics(char_data, ctx)
203 # -------------------------------------------------------------------------
204 # Encode
205 # -------------------------------------------------------------------------
207 @overload
208 def encode_characteristic(
209 self,
210 char: type[BaseCharacteristic[T]],
211 value: T,
212 validate: bool = ...,
213 ) -> bytes: ...
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: ...
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.
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)
236 Returns:
237 Encoded bytes ready to write to the characteristic
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
244 Example::
246 from bluetooth_sig import BluetoothSIGTranslator
247 from bluetooth_sig.gatt.characteristics import AlertLevelCharacteristic
248 from bluetooth_sig.gatt.characteristics.alert_level import AlertLevel
250 translator = BluetoothSIGTranslator()
252 # Type-safe: pass characteristic class and typed value
253 data: bytes = translator.encode_characteristic(AlertLevelCharacteristic, AlertLevel.HIGH)
255 # Not type-safe: pass UUID string
256 data = translator.encode_characteristic("2A06", 2)
258 """
259 return self._encoder.encode_characteristic(char, value, validate)
261 def validate_characteristic_data(self, uuid: str, data: bytes) -> ValidationResult:
262 """Validate characteristic data format against SIG specifications.
264 Args:
265 uuid: The characteristic UUID
266 data: Raw data bytes to validate
268 Returns:
269 ValidationResult with validation details
271 """
272 return self._encoder.validate_characteristic_data(uuid, data)
274 def create_value(self, uuid: str, **kwargs: Any) -> Any: # noqa: ANN401
275 """Create a properly typed value instance for a characteristic.
277 Args:
278 uuid: The characteristic UUID
279 **kwargs: Field values for the characteristic's type
281 Returns:
282 Properly typed value instance
284 Raises:
285 ValueError: If UUID is invalid or characteristic not found
286 TypeError: If kwargs don't match the characteristic's expected fields
288 Example::
290 from bluetooth_sig import BluetoothSIGTranslator
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)
296 """
297 return self._encoder.create_value(uuid, **kwargs)
299 # -------------------------------------------------------------------------
300 # Query / Info
301 # -------------------------------------------------------------------------
303 def get_value_type(self, uuid: str) -> type | str | None:
304 """Get the expected Python type for a characteristic.
306 Args:
307 uuid: The characteristic UUID (16-bit short form or full 128-bit)
309 Returns:
310 Python type if characteristic is found, None otherwise
312 """
313 return self._query.get_value_type(uuid)
315 def supports(self, uuid: str) -> bool:
316 """Check if a characteristic UUID is supported.
318 Args:
319 uuid: The characteristic UUID to check
321 Returns:
322 True if the characteristic has a parser/encoder, False otherwise
324 """
325 return self._query.supports(uuid)
327 def get_characteristic_info_by_uuid(self, uuid: str) -> CharacteristicInfo | None:
328 """Get information about a characteristic by UUID.
330 Args:
331 uuid: The characteristic UUID (16-bit short form or full 128-bit)
333 Returns:
334 CharacteristicInfo with metadata or None if not found
336 """
337 return self._query.get_characteristic_info_by_uuid(uuid)
339 def get_characteristic_uuid_by_name(self, name: CharacteristicName) -> BluetoothUUID | None:
340 """Get the UUID for a characteristic name enum.
342 Args:
343 name: CharacteristicName enum
345 Returns:
346 Characteristic UUID or None if not found
348 """
349 return self._query.get_characteristic_uuid_by_name(name)
351 def get_service_uuid_by_name(self, name: str | ServiceName) -> BluetoothUUID | None:
352 """Get the UUID for a service name or enum.
354 Args:
355 name: Service name or enum
357 Returns:
358 Service UUID or None if not found
360 """
361 return self._query.get_service_uuid_by_name(name)
363 def get_characteristic_info_by_name(self, name: CharacteristicName) -> CharacteristicInfo | None:
364 """Get characteristic info by enum name.
366 Args:
367 name: CharacteristicName enum
369 Returns:
370 CharacteristicInfo if found, None otherwise
372 """
373 return self._query.get_characteristic_info_by_name(name)
375 def get_service_info_by_name(self, name: str | ServiceName) -> ServiceInfo | None:
376 """Get service info by name or enum.
378 Args:
379 name: Service name string or ServiceName enum
381 Returns:
382 ServiceInfo if found, None otherwise
384 """
385 return self._query.get_service_info_by_name(name)
387 def get_service_info_by_uuid(self, uuid: str) -> ServiceInfo | None:
388 """Get information about a service by UUID.
390 Args:
391 uuid: The service UUID
393 Returns:
394 ServiceInfo with metadata or None if not found
396 """
397 return self._query.get_service_info_by_uuid(uuid)
399 def list_supported_characteristics(self) -> dict[str, str]:
400 """List all supported characteristics with their names and UUIDs.
402 Returns:
403 Dictionary mapping characteristic names to UUIDs
405 """
406 return self._query.list_supported_characteristics()
408 def list_supported_services(self) -> dict[str, str]:
409 """List all supported services with their names and UUIDs.
411 Returns:
412 Dictionary mapping service names to UUIDs
414 """
415 return self._query.list_supported_services()
417 def get_characteristics_info_by_uuids(self, uuids: list[str]) -> dict[str, CharacteristicInfo | None]:
418 """Get information about multiple characteristics by UUID.
420 Args:
421 uuids: List of characteristic UUIDs
423 Returns:
424 Dictionary mapping UUIDs to CharacteristicInfo (or None if not found)
426 """
427 return self._query.get_characteristics_info_by_uuids(uuids)
429 def get_service_characteristics(self, service_uuid: str) -> list[BaseCharacteristic[Any]]:
430 """Get the characteristic instances associated with a service.
432 Instantiates each required characteristic class from the service
433 definition and returns the live objects.
435 Args:
436 service_uuid: The service UUID
438 Returns:
439 List of BaseCharacteristic instances for this service's
440 required characteristics.
442 """
443 return self._query.get_service_characteristics(service_uuid)
445 def get_sig_info_by_name(self, name: str) -> SIGInfo | None:
446 """Get Bluetooth SIG information for a characteristic or service by name.
448 Args:
449 name: Characteristic or service name
451 Returns:
452 CharacteristicInfo or ServiceInfo if found, None otherwise
454 """
455 return self._query.get_sig_info_by_name(name)
457 def get_sig_info_by_uuid(self, uuid: str) -> SIGInfo | None:
458 """Get Bluetooth SIG information for a UUID.
460 Args:
461 uuid: UUID string (with or without dashes)
463 Returns:
464 CharacteristicInfo or ServiceInfo if found, None otherwise
466 """
467 return self._query.get_sig_info_by_uuid(uuid)
469 # -------------------------------------------------------------------------
470 # Service lifecycle
471 # -------------------------------------------------------------------------
473 def process_services(self, services: dict[str, dict[str, CharacteristicDataDict]]) -> None:
474 """Process discovered services and their characteristics.
476 Args:
477 services: Dictionary of service UUIDs to their characteristics
479 """
480 self._services.process_services(services)
482 def get_service_by_uuid(self, uuid: str) -> BaseGattService | None:
483 """Get a service instance by UUID.
485 Args:
486 uuid: The service UUID
488 Returns:
489 Service instance if found, None otherwise
491 """
492 return self._services.get_service_by_uuid(uuid)
494 @property
495 def discovered_services(self) -> list[BaseGattService]:
496 """Get list of discovered service instances.
498 Returns:
499 List of discovered service instances
501 """
502 return self._services.discovered_services
504 def clear_services(self) -> None:
505 """Clear all discovered services."""
506 self._services.clear_services()
508 # -------------------------------------------------------------------------
509 # Registration
510 # -------------------------------------------------------------------------
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.
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
527 Raises:
528 TypeError: If cls does not inherit from BaseCharacteristic
529 ValueError: If UUID conflicts with existing registration and override=False
531 Example::
533 from bluetooth_sig import BluetoothSIGTranslator, CharacteristicInfo
534 from bluetooth_sig.types import BluetoothUUID
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)
545 """
546 self._registration.register_custom_characteristic_class(uuid_or_name, cls, info, override)
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.
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
563 Raises:
564 TypeError: If cls does not inherit from BaseGattService
565 ValueError: If UUID conflicts with existing registration and override=False
567 Example::
569 from bluetooth_sig import BluetoothSIGTranslator, ServiceInfo
570 from bluetooth_sig.types import BluetoothUUID
572 translator = BluetoothSIGTranslator()
573 info = ServiceInfo(uuid=BluetoothUUID("12345678-..."), name="Custom Service")
574 translator.register_custom_service_class(str(info.uuid), MyService, info=info)
576 """
577 self._registration.register_custom_service_class(uuid_or_name, cls, info, override)
579 # -------------------------------------------------------------------------
580 # Async wrappers
581 # -------------------------------------------------------------------------
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: ...
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
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.
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
612 Returns:
613 Parsed value. Return type is inferred when passing characteristic class.
615 Raises:
616 SpecialValueDetectedError: Special sentinel value detected
617 CharacteristicParseError: Parse/validation failure
619 """
620 if isinstance(char, type) and issubclass(char, BaseCharacteristic):
621 return self._parser.parse_characteristic(char, raw_data, ctx)
623 uuid_str = str(char) if isinstance(char, BluetoothUUID) else char
624 return self._parser.parse_characteristic(uuid_str, raw_data, ctx)
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.
633 Args:
634 char_data: Dictionary mapping UUIDs to raw data bytes
635 ctx: Optional context
637 Returns:
638 Dictionary mapping UUIDs to parsed values
640 """
641 return self._parser.parse_characteristics(char_data, ctx)
643 @overload
644 async def encode_characteristic_async(
645 self,
646 char: type[BaseCharacteristic[T]],
647 value: T,
648 validate: bool = ...,
649 ) -> bytes: ...
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: ...
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.
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)
672 Returns:
673 Encoded bytes ready to write
675 """
676 if isinstance(char, type) and issubclass(char, BaseCharacteristic):
677 return self._encoder.encode_characteristic(char, value, validate)
679 uuid_str = str(char) if isinstance(char, BluetoothUUID) else char
680 return self._encoder.encode_characteristic(uuid_str, value, validate)
683# Global instance
684BluetoothSIG = BluetoothSIGTranslator()