Coverage for src / bluetooth_sig / gatt / characteristics / hid_information.py: 62%

37 statements  

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

1"""HID Information characteristic implementation.""" 

2 

3from __future__ import annotations 

4 

5from enum import IntFlag 

6 

7import msgspec 

8 

9from ..context import CharacteristicContext 

10from .base import BaseCharacteristic 

11from .utils import DataParser 

12 

13# Constants per Bluetooth HID specification 

14BCD_HID_MAX = 0xFFFF # uint16 max for BCD HID version 

15COUNTRY_CODE_MAX = 0xFF # uint8 max for country code 

16HID_INFO_DATA_LENGTH = 4 # Fixed data length: bcdHID(2) + bCountryCode(1) + Flags(1) 

17 

18 

19class HidInformationFlags(IntFlag): 

20 """HID Information flags as per Bluetooth HID specification.""" 

21 

22 REMOTE_WAKE = 0x01 # Bit 0: RemoteWake 

23 NORMALLY_CONNECTABLE = 0x02 # Bit 1: NormallyConnectable 

24 # Bits 2-7: Reserved 

25 

26 

27class HidInformationData(msgspec.Struct, frozen=True, kw_only=True): 

28 """Parsed data from HID Information characteristic. 

29 

30 Attributes: 

31 bcd_hid: HID version in BCD format (uint16) 

32 b_country_code: Country code (uint8) 

33 flags: HID information flags 

34 """ 

35 

36 bcd_hid: int # uint16 

37 b_country_code: int # uint8 

38 flags: HidInformationFlags 

39 

40 def __post_init__(self) -> None: 

41 """Validate HID information data.""" 

42 if not 0 <= self.bcd_hid <= BCD_HID_MAX: 

43 raise ValueError(f"bcdHID must be 0-{BCD_HID_MAX:#x}, got {self.bcd_hid}") 

44 if not 0 <= self.b_country_code <= COUNTRY_CODE_MAX: 

45 raise ValueError(f"bCountryCode must be 0-{COUNTRY_CODE_MAX:#x}, got {self.b_country_code}") 

46 

47 

48class HidInformationCharacteristic(BaseCharacteristic[HidInformationData]): 

49 """HID Information characteristic (0x2A4A). 

50 

51 org.bluetooth.characteristic.hid_information 

52 

53 HID Information characteristic. 

54 """ 

55 

56 expected_length: int = 4 # bcdHID(2) + bCountryCode(1) + Flags(1) 

57 min_length: int = 4 

58 max_length: int = 4 

59 

60 def _decode_value( 

61 self, data: bytearray, ctx: CharacteristicContext | None = None, *, validate: bool = True 

62 ) -> HidInformationData: 

63 """Parse HID information data. 

64 

65 Format: bcdHID(2) + bCountryCode(1) + Flags(1) 

66 

67 Args: 

68 data: Raw bytearray from BLE characteristic. 

69 ctx: Optional context. 

70 validate: Whether to validate ranges (default True) 

71 

72 Returns: 

73 HidInformationData containing parsed HID information. 

74 """ 

75 bcd_hid = DataParser.parse_int16(data, 0, signed=False) 

76 b_country_code = DataParser.parse_int8(data, 2, signed=False) 

77 flags_value = DataParser.parse_int8(data, 3, signed=False) 

78 flags = HidInformationFlags(flags_value) 

79 

80 return HidInformationData( 

81 bcd_hid=bcd_hid, 

82 b_country_code=b_country_code, 

83 flags=flags, 

84 ) 

85 

86 def _encode_value(self, data: HidInformationData) -> bytearray: 

87 """Encode HidInformationData back to bytes. 

88 

89 Args: 

90 data: HidInformationData instance to encode 

91 

92 Returns: 

93 Encoded bytes 

94 """ 

95 result = bytearray() 

96 result.extend(DataParser.encode_int16(data.bcd_hid, signed=False)) 

97 result.extend(DataParser.encode_int8(data.b_country_code, signed=False)) 

98 result.extend(DataParser.encode_int8(int(data.flags), signed=False)) 

99 return result