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
« prev ^ index » next coverage.py v7.11.0, created at 2025-10-30 00:10 +0000
1"""Core Bluetooth SIG standards translator functionality."""
3from __future__ import annotations
5import logging
6from collections.abc import Mapping
7from graphlib import TopologicalSorter
8from typing import Any, cast
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
33# Type alias for characteristic data in process_services
34CharacteristicDataDict = dict[str, Any]
36logger = logging.getLogger(__name__)
39class BluetoothSIGTranslator: # pylint: disable=too-many-public-methods
40 """Pure Bluetooth SIG standards translator for characteristic and service interpretation.
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.
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
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 """
58 def __init__(self) -> None:
59 """Initialize the SIG translator."""
60 self._services: dict[str, BaseGattService] = {}
62 def __str__(self) -> str:
63 """Return string representation of the translator."""
64 return "BluetoothSIGTranslator(pure SIG standards)"
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.
75 This method takes raw BLE characteristic data and converts it to structured,
76 type-safe Python objects using official Bluetooth SIG specifications.
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
86 Returns:
87 [CharacteristicData][bluetooth_sig.types.CharacteristicData] with parsed value and metadata
89 Example:
90 Parse battery level data:
92 ```python
93 from bluetooth_sig import BluetoothSIGTranslator
95 translator = BluetoothSIGTranslator()
96 result = translator.parse_characteristic("2A19", b"\\x64")
97 print(f"Battery: {result.value}%") # Battery: 100%
98 ```
100 """
101 logger.debug("Parsing characteristic UUID=%s, data_len=%d", uuid, len(raw_data))
103 # Create characteristic instance for parsing
104 characteristic = CharacteristicRegistry.create_characteristic(uuid)
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)
111 # Attach context if available and result doesn't already have it
112 if ctx is not None:
113 result.source_context = ctx
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)
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 )
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 )
171 # Update result with parsed descriptors
172 result.descriptors = parsed_descriptors
174 return result
176 def get_characteristic_info_by_uuid(self, uuid: str) -> CharacteristicInfo | None:
177 """Get information about a characteristic by UUID.
179 Retrieve metadata for a Bluetooth characteristic using its UUID. This includes
180 the characteristic's name, description, value type, unit, and properties.
182 Args:
183 uuid: The characteristic UUID (16-bit short form or full 128-bit)
185 Returns:
186 [CharacteristicInfo][bluetooth_sig.CharacteristicInfo] with metadata or None if not found
188 Example:
189 Get battery level characteristic info:
191 ```python
192 from bluetooth_sig import BluetoothSIGTranslator
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 ```
200 """
201 try:
202 bt_uuid = BluetoothUUID(uuid)
203 except ValueError:
204 return None
206 char_class = CharacteristicRegistry.get_characteristic_class_by_uuid(bt_uuid)
207 if not char_class:
208 return None
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
217 def get_characteristic_uuid_by_name(self, name: CharacteristicName) -> BluetoothUUID | None:
218 """Get the UUID for a characteristic name enum.
220 Args:
221 name: CharacteristicName enum
223 Returns:
224 Characteristic UUID or None if not found
226 """
227 info = self.get_characteristic_info_by_name(name)
228 return info.uuid if info else None
230 def get_service_uuid_by_name(self, name: str | ServiceName) -> BluetoothUUID | None:
231 """Get the UUID for a service name or enum.
233 Args:
234 name: Service name or enum
236 Returns:
237 Service UUID or None if not found
239 """
240 info = self.get_service_info_by_name(str(name))
241 return info.uuid if info else None
243 def get_characteristic_info_by_name(self, name: CharacteristicName) -> CharacteristicInfo | None:
244 """Get characteristic info by enum name.
246 Args:
247 name: CharacteristicName enum
249 Returns:
250 CharacteristicInfo if found, None otherwise
252 """
253 char_class = CharacteristicRegistry.get_characteristic_class(name)
254 if char_class:
255 return char_class.get_configured_info()
256 return None
258 def get_service_info_by_name(self, name: str) -> ServiceInfo | None:
259 """Get service info by name instead of UUID.
261 Args:
262 name: Service name
264 Returns:
265 ServiceInfo if found, None otherwise
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
276 return None
278 def get_service_info_by_uuid(self, uuid: str) -> ServiceInfo | None:
279 """Get information about a service by UUID.
281 Args:
282 uuid: The service UUID
284 Returns:
285 ServiceInfo with metadata or None if not found
287 """
288 service_class = GattServiceRegistry.get_service_class_by_uuid(BluetoothUUID(uuid))
289 if not service_class:
290 return None
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
307 def list_supported_characteristics(self) -> dict[str, str]:
308 """List all supported characteristics with their names and UUIDs.
310 Returns:
311 Dictionary mapping characteristic names to UUIDs
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
324 def list_supported_services(self) -> dict[str, str]:
325 """List all supported services with their names and UUIDs.
327 Returns:
328 Dictionary mapping service names to UUIDs
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
341 def process_services(self, services: dict[str, dict[str, CharacteristicDataDict]]) -> None:
342 """Process discovered services and their characteristics.
344 Args:
345 services: Dictionary of service UUIDs to their characteristics
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
373 def get_service_by_uuid(self, uuid: str) -> BaseGattService | None:
374 """Get a service instance by UUID.
376 Args:
377 uuid: The service UUID
379 Returns:
380 Service instance if found, None otherwise
382 """
383 return self._services.get(uuid)
385 @property
386 def discovered_services(self) -> list[BaseGattService]:
387 """Get list of discovered service instances.
389 Returns:
390 List of discovered service instances
392 """
393 return list(self._services.values())
395 def clear_services(self) -> None:
396 """Clear all discovered services."""
397 self._services.clear()
399 def get_sig_info_by_name(self, name: str) -> SIGInfo | None:
400 """Get Bluetooth SIG information for a characteristic or service by name.
402 Args:
403 name: Characteristic or service name
405 Returns:
406 CharacteristicInfo or ServiceInfo if found, None otherwise
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
429 # Try service
430 service_info = self.get_service_info_by_name(name)
431 if service_info:
432 return service_info
434 return None
436 def get_sig_info_by_uuid(self, uuid: str) -> SIGInfo | None:
437 """Get Bluetooth SIG information for a UUID.
439 Args:
440 uuid: UUID string (with or without dashes)
442 Returns:
443 CharacteristicInfo or ServiceInfo if found, None otherwise
445 """
446 # Try characteristic first
447 char_info = self.get_characteristic_info_by_uuid(uuid)
448 if char_info:
449 return char_info
451 # Try service
452 service_info = self.get_service_info_by_uuid(uuid)
453 if service_info:
454 return service_info
456 return None
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.
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.
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.
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.
482 Returns:
483 Dictionary mapping UUIDs to [CharacteristicData][bluetooth_sig.types.CharacteristicData] results
484 with parsed descriptors included when descriptor_data is provided
486 Raises:
487 ValueError: If circular dependencies are detected
489 Example:
490 Parse multiple environmental characteristics:
492 ```python
493 from bluetooth_sig import BluetoothSIGTranslator
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 ```
505 """
506 return self._parse_characteristics_batch(char_data, descriptor_data, ctx)
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 )
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 )
524 # Resolve dependency order
525 sorted_uuids = self._resolve_dependency_order(char_data, uuid_to_required_deps, uuid_to_optional_deps)
527 # Build base context
528 base_context = ctx
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)
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 )
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
551 self._log_optional_dependency_gaps(
552 uuid_str,
553 uuid_to_optional_deps.get(uuid_str, []),
554 results,
555 base_context,
556 )
558 parse_context = self._build_parse_context(base_context, results)
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 )
568 logger.debug("Batch parsing complete: %d results", len(results))
569 return results
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]] = {}
579 for uuid in characteristic_data:
580 characteristic = CharacteristicRegistry.create_characteristic(uuid)
581 if characteristic is None:
582 continue
584 uuid_to_characteristic[uuid] = characteristic
586 required = characteristic.required_dependencies
587 optional = characteristic.optional_dependencies
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)
596 return uuid_to_characteristic, uuid_to_required_deps, uuid_to_optional_deps
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)
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())
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 []
631 missing: list[str] = []
632 other_characteristics = (
633 base_context.other_characteristics if base_context and base_context.other_characteristics else None
634 )
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
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
647 missing.append(dep_uuid)
649 if missing:
650 logger.debug("Characteristic %s missing required dependencies: %s", uuid, missing)
652 return missing
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)
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 )
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 )
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
702 other_characteristics = (
703 base_context.other_characteristics if base_context and base_context.other_characteristics else None
704 )
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)
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)
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 )
729 return CharacteristicContext(other_characteristics=results_mapping)
731 def get_characteristics_info_by_uuids(self, uuids: list[str]) -> dict[str, CharacteristicInfo | None]:
732 """Get information about multiple characteristics by UUID.
734 Args:
735 uuids: List of characteristic UUIDs
737 Returns:
738 Dictionary mapping UUIDs to CharacteristicInfo
739 (or None if not found)
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
747 def validate_characteristic_data(self, uuid: str, data: bytes) -> ValidationResult:
748 """Validate characteristic data format against SIG specifications.
750 Args:
751 uuid: The characteristic UUID
752 data: Raw data bytes to validate
754 Returns:
755 ValidationResult with validation details
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 )
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.
781 Args:
782 service_uuid: The service UUID
784 Returns:
785 List of characteristic UUIDs for this service
787 """
788 service_class = GattServiceRegistry.get_service_class_by_uuid(BluetoothUUID(service_uuid))
789 if not service_class:
790 return []
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 []
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.
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
814 Raises:
815 TypeError: If cls does not inherit from BaseCharacteristic
816 ValueError: If UUID conflicts with existing registration and override=False
818 """
819 # Register the class
820 CharacteristicRegistry.register_characteristic_class(uuid_or_name, cls, override)
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)
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.
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
851 Raises:
852 TypeError: If cls does not inherit from BaseGattService
853 ValueError: If UUID conflicts with existing registration and override=False
855 """
856 # Register the class
857 GattServiceRegistry.register_service_class(uuid_or_name, cls, override)
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)
870# Global instance
871BluetoothSIG = BluetoothSIGTranslator()