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

37 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-01-11 20:14 +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 

58 def _decode_value(self, data: bytearray, ctx: CharacteristicContext | None = None) -> HidInformationData: 

59 """Parse HID information data. 

60 

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

62 

63 Args: 

64 data: Raw bytearray from BLE characteristic. 

65 ctx: Optional context. 

66 

67 Returns: 

68 HidInformationData containing parsed HID information. 

69 """ 

70 if len(data) != HID_INFO_DATA_LENGTH: 

71 raise ValueError(f"HID Information data must be exactly {HID_INFO_DATA_LENGTH} bytes, got {len(data)}") 

72 

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

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

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

76 flags = HidInformationFlags(flags_value) 

77 

78 return HidInformationData( 

79 bcd_hid=bcd_hid, 

80 b_country_code=b_country_code, 

81 flags=flags, 

82 ) 

83 

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

85 """Encode HidInformationData back to bytes. 

86 

87 Args: 

88 data: HidInformationData instance to encode 

89 

90 Returns: 

91 Encoded bytes 

92 """ 

93 result = bytearray() 

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

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

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

97 return result