Coverage for src / bluetooth_sig / gatt / characteristics / rc_settings.py: 100%

47 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-04-03 16:41 +0000

1"""RC Settings characteristic (0x2B1E). 

2 

3Structure per RCS v1.0.1, Section 3.2 (Table 3.4): 

4 Length (uint8, 1 octet) + Settings (2 octets) + [E2E-CRC (uint16, 0 or 2 octets)]. 

5 

6Settings bitfield (Table 3.5): 

7 Byte 0, bit 1: LESC Only 

8 Byte 0, bit 2: Use OOB Pairing 

9 Byte 0, bit 4: Ready for Disconnect 

10 Byte 0, bit 5: Limited Access 

11 Byte 0, bit 6: Access Permitted 

12 Byte 1, bits 0-1: Advertisement Mode (2-bit field) 

13 

14References: 

15 Bluetooth SIG Reconnection Configuration Service v1.0.1, Section 3.2 

16""" 

17 

18from __future__ import annotations 

19 

20from enum import IntEnum, IntFlag 

21 

22import msgspec 

23 

24from ..context import CharacteristicContext 

25from .base import BaseCharacteristic 

26from .utils import DataParser 

27 

28 

29class RCSettingsFlags(IntFlag): 

30 """RC Settings bitfield flags (RCS v1.0.1 Table 3.5).""" 

31 

32 LESC_ONLY = 0x0002 

33 USE_OOB_PAIRING = 0x0004 

34 READY_FOR_DISCONNECT = 0x0010 

35 LIMITED_ACCESS = 0x0020 

36 ACCESS_PERMITTED = 0x0040 

37 

38 

39class AdvertisementMode(IntEnum): 

40 """Advertisement mode (RCS v1.0.1 Table 3.5, byte 1 bits 0-1).""" 

41 

42 ADV_IND = 0x00 

43 ADV_SCAN_IND = 0x01 

44 ADV_NONCONN_IND = 0x02 

45 ADV_DIRECT_IND = 0x03 

46 

47 

48class RCSettingsData(msgspec.Struct, frozen=True, kw_only=True): 

49 """Parsed RC Settings characteristic data. 

50 

51 Attributes: 

52 length: Length field (uint8). 

53 settings_flags: Settings bitfield flags. 

54 advertisement_mode: Advertisement mode from byte 1 bits 0-1. 

55 e2e_crc: E2E-CRC value (None if not present). 

56 

57 """ 

58 

59 length: int 

60 settings_flags: RCSettingsFlags 

61 advertisement_mode: AdvertisementMode 

62 e2e_crc: int | None = None 

63 

64 

65_ADV_MODE_SHIFT = 8 

66_ADV_MODE_MASK = 0x03 

67_FLAGS_MASK = 0x0076 

68_MIN_LENGTH_WITH_CRC = 5 # 1 (length) + 2 (settings) + 2 (CRC) 

69 

70 

71class RCSettingsCharacteristic(BaseCharacteristic[RCSettingsData]): 

72 """RC Settings characteristic (0x2B1E). 

73 

74 org.bluetooth.characteristic.rc_settings 

75 

76 Structure: Length(1) + Settings(2) + optional E2E-CRC(2). 

77 """ 

78 

79 min_length = 3 

80 max_length = 5 

81 allow_variable_length = True 

82 

83 def _decode_value( 

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

85 ) -> RCSettingsData: 

86 """Parse RC Settings data per RCS v1.0.1 Section 3.2.""" 

87 length = DataParser.parse_int8(data, 0, signed=False) 

88 settings_raw = DataParser.parse_int16(data, 1, signed=False) 

89 

90 settings_flags = RCSettingsFlags(settings_raw & _FLAGS_MASK) 

91 advertisement_mode = AdvertisementMode((settings_raw >> _ADV_MODE_SHIFT) & _ADV_MODE_MASK) 

92 

93 e2e_crc: int | None = None 

94 if len(data) >= _MIN_LENGTH_WITH_CRC: 

95 e2e_crc = DataParser.parse_int16(data, 3, signed=False) 

96 

97 return RCSettingsData( 

98 length=length, 

99 settings_flags=settings_flags, 

100 advertisement_mode=advertisement_mode, 

101 e2e_crc=e2e_crc, 

102 ) 

103 

104 def _encode_value(self, data: RCSettingsData) -> bytearray: 

105 """Encode RC Settings data.""" 

106 result = bytearray() 

107 result.extend(DataParser.encode_int8(data.length, signed=False)) 

108 

109 settings_raw = int(data.settings_flags) | (int(data.advertisement_mode) << _ADV_MODE_SHIFT) 

110 result.extend(DataParser.encode_int16(settings_raw, signed=False)) 

111 

112 if data.e2e_crc is not None: 

113 result.extend(DataParser.encode_int16(data.e2e_crc, signed=False)) 

114 

115 return result