Coverage for src/bluetooth_sig/gatt/characteristics/generic_access.py: 77%

39 statements  

« prev     ^ index     » next       coverage.py v7.11.0, created at 2025-10-30 00:10 +0000

1"""Generic Access Service characteristics.""" 

2 

3from __future__ import annotations 

4 

5from ..context import CharacteristicContext 

6from .base import BaseCharacteristic 

7from .utils import DataParser 

8 

9 

10class DeviceNameCharacteristic(BaseCharacteristic): 

11 """Device Name characteristic (0x2A00). 

12 

13 org.bluetooth.characteristic.gap.device_name 

14 

15 Device Name characteristic. 

16 """ 

17 

18 _characteristic_name: str = "Device Name" 

19 _manual_value_type = "string" # Override since decode_value returns str 

20 

21 def decode_value(self, data: bytearray, ctx: CharacteristicContext | None = None) -> str: 

22 """Parse device name string. 

23 

24 Args: 

25 data: Raw bytearray from BLE characteristic. 

26 ctx: Optional CharacteristicContext providing surrounding context (may be None). 

27 

28 Returns: 

29 Decoded device name string. 

30 

31 """ 

32 return DataParser.parse_utf8_string(data) 

33 

34 def encode_value(self, data: str) -> bytearray: 

35 """Encode device name value back to bytes. 

36 

37 Args: 

38 data: Device name as string 

39 

40 Returns: 

41 Encoded bytes representing the device name (UTF-8) 

42 

43 """ 

44 # Encode as UTF-8 bytes 

45 return bytearray(data.encode("utf-8")) 

46 

47 

48class AppearanceCharacteristic(BaseCharacteristic): 

49 """Appearance characteristic (0x2A01). 

50 

51 org.bluetooth.characteristic.gap.appearance 

52 

53 Appearance characteristic. 

54 """ 

55 

56 _characteristic_name: str = "Appearance" 

57 _manual_value_type = "int" # Override since decode_value returns int 

58 

59 min_length = 2 # Appearance(2) fixed length 

60 max_length = 2 # Appearance(2) fixed length 

61 allow_variable_length: bool = False # Fixed length 

62 

63 def decode_value(self, data: bytearray, ctx: CharacteristicContext | None = None) -> int: 

64 """Parse appearance value (uint16). 

65 

66 Args: 

67 data: Raw bytearray from BLE characteristic. 

68 ctx: Optional CharacteristicContext providing surrounding context (may be None). 

69 

70 Returns: 

71 Parsed appearance as integer. 

72 

73 """ 

74 return DataParser.parse_int16(data, 0, signed=False) 

75 

76 def encode_value(self, data: int) -> bytearray: 

77 """Encode appearance value back to bytes. 

78 

79 Args: 

80 data: Appearance value as integer 

81 

82 Returns: 

83 Encoded bytes representing the appearance 

84 

85 """ 

86 appearance = int(data) 

87 return DataParser.encode_int16(appearance, signed=False) 

88 

89 

90class ServiceChangedCharacteristic(BaseCharacteristic): 

91 """Service Changed characteristic (0x2A05). 

92 

93 org.bluetooth.characteristic.gatt.service_changed 

94 

95 Service Changed characteristic. 

96 """ 

97 

98 _characteristic_name: str = "Service Changed" 

99 _manual_value_type = "bytes" # Raw bytes for handle range 

100 

101 min_length = 4 # Start Handle(2) + End Handle(2) 

102 max_length = 4 # Fixed length 

103 allow_variable_length: bool = False # Fixed length 

104 

105 def decode_value(self, data: bytearray, ctx: CharacteristicContext | None = None) -> dict[str, int]: 

106 """Parse service changed value. 

107 

108 Args: 

109 data: Raw bytearray from BLE characteristic. 

110 ctx: Optional CharacteristicContext providing surrounding context (may be None). 

111 

112 Returns: 

113 Dict with start_handle and end_handle. 

114 

115 """ 

116 start_handle = DataParser.parse_int16(data, 0, signed=False) 

117 end_handle = DataParser.parse_int16(data, 2, signed=False) 

118 return {"start_handle": start_handle, "end_handle": end_handle} 

119 

120 def encode_value(self, data: dict[str, int]) -> bytearray: 

121 """Encode service changed value back to bytes. 

122 

123 Args: 

124 data: Dict with start_handle and end_handle 

125 

126 Returns: 

127 Encoded bytes 

128 

129 """ 

130 start_handle = int(data["start_handle"]) 

131 end_handle = int(data["end_handle"]) 

132 result = bytearray() 

133 result.extend(DataParser.encode_int16(start_handle, signed=False)) 

134 result.extend(DataParser.encode_int16(end_handle, signed=False)) 

135 return result