Coverage for src / bluetooth_sig / registry / core / class_of_device.py: 93%
96 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"""Registry for Class of Device decoding.
3This module provides a registry for decoding 24-bit Class of Device (CoD)
4values from Classic Bluetooth into human-readable device classifications
5including major/minor device classes and service classes.
6"""
8from __future__ import annotations
10from enum import IntEnum, IntFlag
11from pathlib import Path
12from typing import Any
14import msgspec
16from bluetooth_sig.registry.base import BaseGenericRegistry
17from bluetooth_sig.registry.utils import find_bluetooth_sig_path
18from bluetooth_sig.types.registry.class_of_device import (
19 ClassOfDeviceInfo,
20 CodServiceClassInfo,
21 MajorDeviceClassInfo,
22 MinorDeviceClassInfo,
23)
26class CoDBitMask(IntFlag):
27 """Bit masks for extracting Class of Device fields.
29 CoD Structure (24 bits):
30 Bits 23-13: Service Class (11 bits, bit mask)
31 Bits 12-8: Major Device Class (5 bits)
32 Bits 7-2: Minor Device Class (6 bits)
33 Bits 1-0: Format Type (always 0b00)
34 """
36 SERVICE_CLASS = 0x7FF << 13 # Bits 23-13: 11-bit mask
37 MAJOR_CLASS = 0x1F << 8 # Bits 12-8: 5-bit mask
38 MINOR_CLASS = 0x3F << 2 # Bits 7-2: 6-bit mask
39 FORMAT_TYPE = 0x03 # Bits 1-0: format type (always 00)
42class CoDBitShift(IntEnum):
43 """Bit shift values for Class of Device fields."""
45 SERVICE_CLASS = 13
46 MAJOR_CLASS = 8
47 MINOR_CLASS = 2
50class ClassOfDeviceRegistry(BaseGenericRegistry[ClassOfDeviceInfo]):
51 """Registry for Class of Device decoding with lazy loading.
53 This registry loads Class of Device mappings from the Bluetooth SIG
54 assigned_numbers YAML file and provides methods to decode 24-bit CoD
55 values into human-readable device classification information.
57 The registry uses lazy loading - the YAML file is only parsed on the first
58 decode call. This improves startup time and reduces memory usage when the
59 registry is not needed.
61 CoD Structure (24 bits):
62 Bits 23-13: Service Class (11 bits, bit mask)
63 Bits 12-8: Major Device Class (5 bits)
64 Bits 7-2: Minor Device Class (6 bits)
65 Bits 1-0: Format Type (always 0b00)
67 Thread Safety:
68 This registry is thread-safe. Multiple threads can safely call
69 decode_class_of_device() concurrently.
71 Example:
72 >>> registry = ClassOfDeviceRegistry()
73 >>> info = registry.decode_class_of_device(0x02010C)
74 >>> print(info.full_description) # "Computer: Laptop (Networking)"
75 >>> print(info.major_class) # "Computer"
76 >>> print(info.minor_class) # "Laptop"
77 >>> print(info.service_classes) # ["Networking"]
78 """
80 def __init__(self) -> None:
81 """Initialize the registry with lazy loading."""
82 super().__init__()
83 self._service_classes: dict[int, CodServiceClassInfo] = {}
84 self._major_classes: dict[int, MajorDeviceClassInfo] = {}
85 self._minor_classes: dict[tuple[int, int], MinorDeviceClassInfo] = {}
87 def _load(self) -> None:
88 """Perform the actual loading of Class of Device data."""
89 # Get path to uuids/ directory
90 uuids_path = find_bluetooth_sig_path()
91 if not uuids_path:
92 self._loaded = True
93 return
95 # CoD values are in core/ directory (sibling of uuids/)
96 assigned_numbers_path = uuids_path.parent
97 yaml_path = assigned_numbers_path / "core" / "class_of_device.yaml"
98 if not yaml_path.exists():
99 self._loaded = True
100 return
102 self._load_yaml(yaml_path)
103 self._loaded = True
105 def _load_yaml(self, yaml_path: Path) -> None:
106 """Load and parse the class_of_device.yaml file.
108 Args:
109 yaml_path: Path to the class_of_device.yaml file
110 """
111 data: dict[str, Any] = {}
112 with yaml_path.open("r", encoding="utf-8") as f:
113 data = msgspec.yaml.decode(f.read())
115 if not data:
116 return
118 self._load_service_classes(data)
119 self._load_device_classes(data)
121 def _load_service_classes(self, data: dict[str, Any]) -> None:
122 """Load service classes from YAML data.
124 Args:
125 data: Parsed YAML data dictionary
126 """
127 cod_services: list[dict[str, Any]] | None = data.get("cod_services")
128 if not isinstance(cod_services, list):
129 return
131 for item in cod_services:
132 bit_pos = item.get("bit")
133 name = item.get("name")
134 if bit_pos is not None and name:
135 self._service_classes[bit_pos] = CodServiceClassInfo(
136 bit_position=bit_pos,
137 name=name,
138 )
140 def _load_device_classes(self, data: dict[str, Any]) -> None:
141 """Load major and minor device classes from YAML data.
143 Args:
144 data: Parsed YAML data dictionary
145 """
146 cod_device_class: list[dict[str, Any]] | None = data.get("cod_device_class")
147 if not isinstance(cod_device_class, list):
148 return
150 for item in cod_device_class:
151 major_val = item.get("major")
152 major_name = item.get("name")
153 if major_val is not None and major_name:
154 self._major_classes[major_val] = MajorDeviceClassInfo(
155 value=major_val,
156 name=major_name,
157 )
158 self._load_minor_classes(major_val, item)
160 def _load_minor_classes(self, major_val: int, major_item: dict[str, Any]) -> None:
161 """Load minor classes for a specific major device class.
163 Args:
164 major_val: Major device class value
165 major_item: Dictionary containing major class data including minor classes
166 """
167 minor_list: list[dict[str, Any]] | None = major_item.get("minor")
168 if not isinstance(minor_list, list):
169 return
171 for minor_item in minor_list:
172 minor_val = minor_item.get("value")
173 minor_name = minor_item.get("name")
174 if minor_val is not None and minor_name:
175 self._minor_classes[(major_val, minor_val)] = MinorDeviceClassInfo(
176 value=minor_val,
177 name=minor_name,
178 major_class=major_val,
179 )
181 def decode_class_of_device(self, cod: int) -> ClassOfDeviceInfo:
182 """Decode 24-bit Class of Device value.
184 Extracts and decodes the major/minor device classes and service classes
185 from a 24-bit CoD value. Lazy loads the registry data on first call.
187 Args:
188 cod: 24-bit Class of Device value from advertising data
190 Returns:
191 ClassOfDeviceInfo with decoded device classification
193 Examples:
194 >>> registry = ClassOfDeviceRegistry()
195 >>> # Computer (major=1), Laptop (minor=3), Networking service (bit 17)
196 >>> info = registry.decode_class_of_device(0x02010C)
197 >>> info.major_class
198 'Computer (desktop, notebook, PDA, organizer, ...)'
199 >>> info.minor_class
200 'Laptop'
201 >>> info.service_classes
202 ['Networking (LAN, Ad hoc, ...)']
203 """
204 self._ensure_loaded()
206 # Extract fields using bit masks and shifts
207 service_class_bits = (cod & CoDBitMask.SERVICE_CLASS) >> CoDBitShift.SERVICE_CLASS
208 major_class = (cod & CoDBitMask.MAJOR_CLASS) >> CoDBitShift.MAJOR_CLASS
209 minor_class = (cod & CoDBitMask.MINOR_CLASS) >> CoDBitShift.MINOR_CLASS
211 # Decode service classes (bit mask - multiple bits can be set)
212 service_classes: list[str] = []
213 for bit_pos in range(11):
214 if service_class_bits & (1 << bit_pos):
215 # Map bit position 0-10 to actual bit positions 13-23
216 actual_bit_pos = bit_pos + CoDBitShift.SERVICE_CLASS
217 service_info = self._service_classes.get(actual_bit_pos)
218 if service_info:
219 service_classes.append(service_info.name)
221 # Decode major class
222 major_info = self._major_classes.get(major_class)
223 if major_info:
224 major_list = [major_info]
225 else:
226 # Create Unknown info for unknown major class
227 unknown_major = MajorDeviceClassInfo(
228 value=major_class,
229 name=f"Unknown (0x{major_class:02X})",
230 )
231 major_list = [unknown_major]
233 # Decode minor class
234 minor_info = self._minor_classes.get((major_class, minor_class))
235 minor_list = [minor_info] if minor_info else None
237 if minor_info:
238 minor_list = [minor_info]
239 else:
240 # Create Unknown info for unknown major class
241 unknown_minor = MinorDeviceClassInfo(
242 value=minor_class, name=f"Unknown (0x{minor_class:02X})", major_class=major_class
243 )
244 minor_list = [unknown_minor]
246 return ClassOfDeviceInfo(
247 major_class=major_list,
248 minor_class=minor_list,
249 service_classes=service_classes,
250 raw_value=cod,
251 )
254# Module-level singleton instance
255class_of_device_registry = ClassOfDeviceRegistry()