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

33 statements  

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

1"""Digital characteristic (0x2A56).""" 

2 

3from __future__ import annotations 

4 

5from enum import IntEnum 

6 

7from ...types import CharacteristicInfo 

8from ...types.uuid import BluetoothUUID 

9from ..context import CharacteristicContext 

10from .base import BaseCharacteristic 

11 

12 

13class DigitalSignalState(IntEnum): 

14 """2-bit digital signal state enumeration (Automation IO Service v1.0). 

15 

16 Per AIO spec §3.1, each digital signal is encoded as a 2-bit value: 

17 0b00: Inactive state (logical low / contact open) 

18 0b01: Active state (logical high / contact closed) 

19 0b10: Tri-state (if supported; ignored on write) 

20 0b11: Unknown (server cannot report; writes with this value are ignored by server) 

21 """ 

22 

23 INACTIVE = 0b00 

24 ACTIVE = 0b01 

25 TRISTATE = 0b10 

26 UNKNOWN = 0b11 

27 

28 

29class DigitalCharacteristic(BaseCharacteristic[tuple[DigitalSignalState, ...]]): 

30 """Digital characteristic (0x2A56). 

31 

32 org.bluetooth.characteristic.digital 

33 

34 Automation IO Service (AIOS) v1.0 §3.1: Array of n 2-bit digital signal values 

35 in little-endian bit order within packed octets. 

36 

37 Format: 

38 - Length: ceil(n/4) octets, where n = number of digital signals 

39 - Each 2-bit field encodes one signal state (see DigitalSignalState enum) 

40 - Bit order within each octet: LSB first (little-endian) 

41 Byte bits [1:0] = Signal 0, bits [3:2] = Signal 1, bits [5:4] = Signal 2, bits [7:6] = Signal 3 

42 

43 Specification: 

44 - Source: Bluetooth SIG Automation IO Service v1.0 (AIOS_v1.0) 

45 - Mandatory descriptor: Number of Digitals (0x2908) - specifies number of valid 2-bit fields 

46 - Optional descriptors: Value Trigger Setting, Time Trigger Setting 

47 """ 

48 

49 # Help the registry resolver find this characteristic by name 

50 _characteristic_name = "Digital" 

51 

52 min_length = 0 

53 allow_variable_length = True 

54 # TODO Remove once uuid is added to yaml files 

55 _info = CharacteristicInfo( 

56 uuid=BluetoothUUID(0x2A56), 

57 name="Digital", 

58 id="org.bluetooth.characteristic.digital", 

59 unit="", 

60 ) 

61 

62 def _decode_value( 

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

64 ) -> tuple[DigitalSignalState, ...]: 

65 """Decode packed 2-bit digital signal states from little-endian bytes. 

66 

67 Args: 

68 data: Raw bytes containing packed 2-bit signal values. 

69 ctx: Optional context (unused for Digital). 

70 validate: Whether to validate (currently unused; all 2-bit values are valid). 

71 

72 Returns: 

73 Tuple of DigitalSignalState enums, one per 2-bit field. 

74 Example: bytearray([0x05]) decodes to (ACTIVE, ACTIVE, INACTIVE, INACTIVE) 

75 because 0x05 = 0b0000_0101 

76 - bits [1:0] = 0b01 = ACTIVE 

77 - bits [3:2] = 0b01 = ACTIVE 

78 - bits [5:4] = 0b00 = INACTIVE 

79 - bits [7:6] = 0b00 = INACTIVE 

80 """ 

81 signals: list[DigitalSignalState] = [] 

82 

83 for byte in data: 

84 # Extract four 2-bit values from each byte (little-endian bit order) 

85 for shift in range(0, 8, 2): 

86 value = (byte >> shift) & 0x03 

87 signals.append(DigitalSignalState(value)) 

88 

89 return tuple(signals) 

90 

91 def _encode_value(self, data: tuple[DigitalSignalState, ...]) -> bytearray: 

92 """Encode digital signal states as packed little-endian 2-bit values. 

93 

94 Args: 

95 data: Tuple of DigitalSignalState values to encode. 

96 

97 Returns: 

98 Bytearray with packed 2-bit values (4 signals per byte). 

99 Example: (ACTIVE, ACTIVE, INACTIVE, INACTIVE) encodes to bytearray([0x05]) 

100 """ 

101 result = bytearray() 

102 

103 # Pack 4 signals per byte 

104 for i in range(0, len(data), 4): 

105 byte = 0 

106 

107 for j in range(4): 

108 if i + j < len(data): 

109 signal_value = int(data[i + j]) 

110 byte |= (signal_value & 0x03) << (j * 2) 

111 

112 result.append(byte) 

113 

114 return result