Coverage for src / bluetooth_sig / advertising / sig_characteristic_interpreter.py: 100%

37 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-03-18 11:17 +0000

1"""SIG characteristic interpreter for standard service data. 

2 

3Built-in interpreter that uses CharacteristicRegistry to parse SIG-standard 

4service data from BLE advertisements. Automatically handles any UUID 

5registered in CharacteristicRegistry. 

6 

7Based on Bluetooth SIG GATT Specification Supplement for characteristic 

8data formats. 

9""" 

10 

11from __future__ import annotations 

12 

13import logging 

14from typing import Any 

15 

16import msgspec 

17 

18from bluetooth_sig.advertising.base import ( 

19 AdvertisingData, 

20 DataSource, 

21 InterpreterInfo, 

22 PayloadInterpreter, 

23) 

24from bluetooth_sig.advertising.exceptions import AdvertisingParseError 

25from bluetooth_sig.advertising.state import DeviceAdvertisingState 

26from bluetooth_sig.gatt.characteristics.registry import CharacteristicRegistry 

27from bluetooth_sig.types.uuid import BluetoothUUID 

28 

29logger = logging.getLogger(__name__) 

30 

31 

32class SIGCharacteristicData(msgspec.Struct, kw_only=True, frozen=True): 

33 """Parsed SIG characteristic data from service data advertisement. 

34 

35 Attributes: 

36 uuid: The characteristic UUID that was parsed. 

37 characteristic_name: Human-readable characteristic name. 

38 parsed_value: The parsed characteristic value (type varies by characteristic). 

39 

40 """ 

41 

42 uuid: BluetoothUUID 

43 characteristic_name: str 

44 # Any is justified: Runtime UUID dispatch means return type varies per characteristic 

45 parsed_value: Any 

46 

47 

48class SIGCharacteristicInterpreter(PayloadInterpreter[SIGCharacteristicData]): 

49 """Interprets service data using SIG characteristic definitions. 

50 

51 Automatically handles any UUID registered in CharacteristicRegistry. 

52 No encryption support (SIG characteristics are not encrypted in service data). 

53 

54 This interpreter checks service data UUIDs against the CharacteristicRegistry 

55 and parses the payload using the corresponding characteristic class. 

56 

57 Example:: 

58 from bluetooth_sig.advertising.base import AdvertisingData 

59 

60 interpreter = SIGCharacteristicInterpreter("AA:BB:CC:DD:EE:FF") 

61 state = DeviceAdvertisingState(address="AA:BB:CC:DD:EE:FF") 

62 

63 # Create advertising data from BLE packet 

64 ad_data = AdvertisingData( 

65 service_data={BluetoothUUID("0000180f-0000-1000-8000-00805f9b34fb"): bytes([75])}, 

66 rssi=-60, 

67 ) 

68 

69 try: 

70 data = interpreter.interpret(ad_data, state) 

71 print(f"Parsed: {data.parsed_value}") 

72 except AdvertisingParseError as e: 

73 print(f"Parse failed: {e}") 

74 

75 """ 

76 

77 _info = InterpreterInfo( 

78 name="SIG Characteristic", 

79 data_source=DataSource.SERVICE, 

80 ) 

81 _is_base_class = False 

82 

83 @classmethod 

84 def supports(cls, advertising_data: AdvertisingData) -> bool: 

85 """Check if any service data UUID matches a registered characteristic. 

86 

87 Args: 

88 advertising_data: Complete advertising data from BLE packet. 

89 

90 Returns: 

91 True if at least one service data UUID has a registered characteristic. 

92 

93 """ 

94 for uuid in advertising_data.service_data: 

95 if CharacteristicRegistry.get_characteristic_class_by_uuid(uuid) is not None: 

96 return True 

97 return False 

98 

99 def interpret( 

100 self, 

101 advertising_data: AdvertisingData, 

102 state: DeviceAdvertisingState, 

103 ) -> SIGCharacteristicData: 

104 """Interpret service data using SIG characteristic definitions. 

105 

106 Finds the first service data UUID that matches a registered characteristic 

107 and parses the payload using that characteristic class. 

108 

109 Args: 

110 advertising_data: Complete advertising data from BLE packet. 

111 state: Current device advertising state (unused, no encryption). 

112 

113 Returns: 

114 SIGCharacteristicData with parsed characteristic value. 

115 

116 Raises: 

117 AdvertisingParseError: If no matching characteristic or parsing fails. 

118 

119 """ 

120 del state # Required by interface but not used in this implementation 

121 for uuid, payload in advertising_data.service_data.items(): 

122 char_class = CharacteristicRegistry.get_characteristic_class_by_uuid(uuid) 

123 if char_class is None: 

124 continue 

125 

126 try: 

127 char_instance = char_class() 

128 parsed_value = char_instance.parse_value(payload) 

129 

130 return SIGCharacteristicData( 

131 uuid=uuid, 

132 characteristic_name=char_class.__name__, 

133 parsed_value=parsed_value, 

134 ) 

135 except Exception as e: 

136 logger.warning( 

137 "Failed to parse SIG characteristic %s: %s", 

138 char_class.__name__, 

139 e, 

140 ) 

141 raise AdvertisingParseError( 

142 message=f"Failed to parse {char_class.__name__}: {e}", 

143 raw_data=payload, 

144 interpreter_name=self._info.name, 

145 ) from e 

146 

147 # No matching characteristic found 

148 raise AdvertisingParseError( 

149 message="No matching SIG characteristic found in service data", 

150 interpreter_name=self._info.name, 

151 )