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

1"""Registry for Class of Device decoding. 

2 

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""" 

7 

8from __future__ import annotations 

9 

10from enum import IntEnum, IntFlag 

11from pathlib import Path 

12from typing import Any 

13 

14import msgspec 

15 

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) 

24 

25 

26class CoDBitMask(IntFlag): 

27 """Bit masks for extracting Class of Device fields. 

28 

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 """ 

35 

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) 

40 

41 

42class CoDBitShift(IntEnum): 

43 """Bit shift values for Class of Device fields.""" 

44 

45 SERVICE_CLASS = 13 

46 MAJOR_CLASS = 8 

47 MINOR_CLASS = 2 

48 

49 

50class ClassOfDeviceRegistry(BaseGenericRegistry[ClassOfDeviceInfo]): 

51 """Registry for Class of Device decoding with lazy loading. 

52 

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. 

56 

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. 

60 

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) 

66 

67 Thread Safety: 

68 This registry is thread-safe. Multiple threads can safely call 

69 decode_class_of_device() concurrently. 

70 

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 """ 

79 

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] = {} 

86 

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 

94 

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 

101 

102 self._load_yaml(yaml_path) 

103 self._loaded = True 

104 

105 def _load_yaml(self, yaml_path: Path) -> None: 

106 """Load and parse the class_of_device.yaml file. 

107 

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()) 

114 

115 if not data: 

116 return 

117 

118 self._load_service_classes(data) 

119 self._load_device_classes(data) 

120 

121 def _load_service_classes(self, data: dict[str, Any]) -> None: 

122 """Load service classes from YAML data. 

123 

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 

130 

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 ) 

139 

140 def _load_device_classes(self, data: dict[str, Any]) -> None: 

141 """Load major and minor device classes from YAML data. 

142 

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 

149 

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) 

159 

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. 

162 

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 

170 

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 ) 

180 

181 def decode_class_of_device(self, cod: int) -> ClassOfDeviceInfo: 

182 """Decode 24-bit Class of Device value. 

183 

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. 

186 

187 Args: 

188 cod: 24-bit Class of Device value from advertising data 

189 

190 Returns: 

191 ClassOfDeviceInfo with decoded device classification 

192 

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() 

205 

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 

210 

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) 

220 

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] 

232 

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 

236 

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] 

245 

246 return ClassOfDeviceInfo( 

247 major_class=major_list, 

248 minor_class=minor_list, 

249 service_classes=service_classes, 

250 raw_value=cod, 

251 ) 

252 

253 

254# Module-level singleton instance 

255class_of_device_registry = ClassOfDeviceRegistry()