Coverage for src / bluetooth_sig / registry / core / namespace_description.py: 80%
64 statements
« prev ^ index » next coverage.py v7.13.1, created at 2026-01-11 20:14 +0000
« prev ^ index » next coverage.py v7.13.1, created at 2026-01-11 20:14 +0000
1"""Namespace Description registry for CPF descriptor description field resolution.
3The CPF (Characteristic Presentation Format) descriptor has a description field
4that, when namespace=0x01 (Bluetooth SIG), can be resolved to human-readable
5names like "first", "left", "front", etc.
7Used during CPF descriptor parsing to provide description_name resolution.
8"""
10from __future__ import annotations
12import logging
14import msgspec
16from bluetooth_sig.registry.base import BaseGenericRegistry
17from bluetooth_sig.registry.utils import find_bluetooth_sig_path
18from bluetooth_sig.types.registry.namespace import NamespaceDescriptionInfo
20logger = logging.getLogger(__name__)
23class NamespaceDescriptionRegistry(BaseGenericRegistry[NamespaceDescriptionInfo]):
24 """Registry for Bluetooth SIG namespace description values with lazy loading.
26 This registry loads namespace description definitions from the official
27 Bluetooth SIG assigned_numbers YAML file (namespace.yaml), enabling
28 resolution of CPF description field values to human-readable names.
30 The description field in CPF is a 16-bit value that, when namespace=0x01
31 (Bluetooth SIG Assigned Numbers), can be resolved to names like:
32 - 0x0001 → "first"
33 - 0x010D → "left"
34 - 0x010E → "right"
35 - 0x0102 → "top"
37 Examples:
38 >>> from bluetooth_sig.registry.core.namespace_description import namespace_description_registry
39 >>> info = namespace_description_registry.get_description_info(0x010D)
40 >>> info.name
41 'left'
42 """
44 def __init__(self) -> None:
45 """Initialize the namespace description registry."""
46 super().__init__()
47 self._descriptions: dict[int, NamespaceDescriptionInfo] = {}
48 self._descriptions_by_name: dict[str, NamespaceDescriptionInfo] = {}
50 def _load(self) -> None:
51 """Perform the actual loading of namespace description data."""
52 base_path = find_bluetooth_sig_path()
53 if not base_path:
54 logger.warning("Bluetooth SIG path not found. Namespace description registry will be empty.")
55 self._loaded = True
56 return
58 yaml_path = base_path.parent / "core" / "namespace.yaml"
59 if not yaml_path.exists():
60 logger.warning(
61 "Namespace YAML file not found at %s. Registry will be empty.",
62 yaml_path,
63 )
64 self._loaded = True
65 return
67 try:
68 with yaml_path.open("r", encoding="utf-8") as f:
69 data = msgspec.yaml.decode(f.read())
71 if not data or "namespace" not in data:
72 logger.warning("Invalid namespace YAML format. Registry will be empty.")
73 self._loaded = True
74 return
76 for item in data["namespace"]:
77 value = item.get("value")
78 name = item.get("name")
80 if value is None or not name:
81 continue
83 # Handle hex values in YAML (e.g., 0x010D)
84 if isinstance(value, str):
85 value = int(value, 16)
87 description_info = NamespaceDescriptionInfo(
88 value=value,
89 name=name,
90 )
92 self._descriptions[value] = description_info
93 self._descriptions_by_name[name.lower()] = description_info
95 logger.info("Loaded %d namespace descriptions from specification", len(self._descriptions))
96 except (FileNotFoundError, OSError, msgspec.DecodeError, KeyError) as e:
97 logger.warning(
98 "Failed to load namespace descriptions from YAML: %s. Registry will be empty.",
99 e,
100 )
102 self._loaded = True
104 def get_description_info(self, value: int) -> NamespaceDescriptionInfo | None:
105 """Get description info by value (lazy loads on first call).
107 Args:
108 value: The description value (e.g., 0x010D for "left")
110 Returns:
111 NamespaceDescriptionInfo object, or None if not found
112 """
113 self._ensure_loaded()
114 with self._lock:
115 return self._descriptions.get(value)
117 def get_description_by_name(self, name: str) -> NamespaceDescriptionInfo | None:
118 """Get description info by name (lazy loads on first call).
120 Args:
121 name: Description name (case-insensitive, e.g., "left", "first")
123 Returns:
124 NamespaceDescriptionInfo object, or None if not found
125 """
126 self._ensure_loaded()
127 with self._lock:
128 return self._descriptions_by_name.get(name.lower())
130 def is_known_description(self, value: int) -> bool:
131 """Check if description value is known (lazy loads on first call).
133 Args:
134 value: The description value to check
136 Returns:
137 True if the description is registered, False otherwise
138 """
139 self._ensure_loaded()
140 with self._lock:
141 return value in self._descriptions
143 def get_all_descriptions(self) -> dict[int, NamespaceDescriptionInfo]:
144 """Get all registered namespace descriptions (lazy loads on first call).
146 Returns:
147 Dictionary mapping description values to NamespaceDescriptionInfo objects
148 """
149 self._ensure_loaded()
150 with self._lock:
151 return self._descriptions.copy()
153 def resolve_description_name(self, value: int) -> str | None:
154 """Resolve a description value to its string name.
156 Convenience method for CPF parsing that returns the description
157 name directly, or None if unknown.
159 Args:
160 value: The description value to resolve
162 Returns:
163 Description name string (e.g., "left", "first"), or None if unknown
164 """
165 info = self.get_description_info(value)
166 return info.name if info else None
169# Global singleton instance
170namespace_description_registry = NamespaceDescriptionRegistry()