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
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-03 16:41 +0000
1"""RC Settings characteristic (0x2B1E).
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)].
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)
14References:
15 Bluetooth SIG Reconnection Configuration Service v1.0.1, Section 3.2
16"""
18from __future__ import annotations
20from enum import IntEnum, IntFlag
22import msgspec
24from ..context import CharacteristicContext
25from .base import BaseCharacteristic
26from .utils import DataParser
29class RCSettingsFlags(IntFlag):
30 """RC Settings bitfield flags (RCS v1.0.1 Table 3.5)."""
32 LESC_ONLY = 0x0002
33 USE_OOB_PAIRING = 0x0004
34 READY_FOR_DISCONNECT = 0x0010
35 LIMITED_ACCESS = 0x0020
36 ACCESS_PERMITTED = 0x0040
39class AdvertisementMode(IntEnum):
40 """Advertisement mode (RCS v1.0.1 Table 3.5, byte 1 bits 0-1)."""
42 ADV_IND = 0x00
43 ADV_SCAN_IND = 0x01
44 ADV_NONCONN_IND = 0x02
45 ADV_DIRECT_IND = 0x03
48class RCSettingsData(msgspec.Struct, frozen=True, kw_only=True):
49 """Parsed RC Settings characteristic data.
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).
57 """
59 length: int
60 settings_flags: RCSettingsFlags
61 advertisement_mode: AdvertisementMode
62 e2e_crc: int | None = None
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)
71class RCSettingsCharacteristic(BaseCharacteristic[RCSettingsData]):
72 """RC Settings characteristic (0x2B1E).
74 org.bluetooth.characteristic.rc_settings
76 Structure: Length(1) + Settings(2) + optional E2E-CRC(2).
77 """
79 min_length = 3
80 max_length = 5
81 allow_variable_length = True
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)
90 settings_flags = RCSettingsFlags(settings_raw & _FLAGS_MASK)
91 advertisement_mode = AdvertisementMode((settings_raw >> _ADV_MODE_SHIFT) & _ADV_MODE_MASK)
93 e2e_crc: int | None = None
94 if len(data) >= _MIN_LENGTH_WITH_CRC:
95 e2e_crc = DataParser.parse_int16(data, 3, signed=False)
97 return RCSettingsData(
98 length=length,
99 settings_flags=settings_flags,
100 advertisement_mode=advertisement_mode,
101 e2e_crc=e2e_crc,
102 )
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))
109 settings_raw = int(data.settings_flags) | (int(data.advertisement_mode) << _ADV_MODE_SHIFT)
110 result.extend(DataParser.encode_int16(settings_raw, signed=False))
112 if data.e2e_crc is not None:
113 result.extend(DataParser.encode_int16(data.e2e_crc, signed=False))
115 return result