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
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-03 16:41 +0000
1"""Digital characteristic (0x2A56)."""
3from __future__ import annotations
5from enum import IntEnum
7from ...types import CharacteristicInfo
8from ...types.uuid import BluetoothUUID
9from ..context import CharacteristicContext
10from .base import BaseCharacteristic
13class DigitalSignalState(IntEnum):
14 """2-bit digital signal state enumeration (Automation IO Service v1.0).
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 """
23 INACTIVE = 0b00
24 ACTIVE = 0b01
25 TRISTATE = 0b10
26 UNKNOWN = 0b11
29class DigitalCharacteristic(BaseCharacteristic[tuple[DigitalSignalState, ...]]):
30 """Digital characteristic (0x2A56).
32 org.bluetooth.characteristic.digital
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.
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
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 """
49 # Help the registry resolver find this characteristic by name
50 _characteristic_name = "Digital"
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 )
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.
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).
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] = []
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))
89 return tuple(signals)
91 def _encode_value(self, data: tuple[DigitalSignalState, ...]) -> bytearray:
92 """Encode digital signal states as packed little-endian 2-bit values.
94 Args:
95 data: Tuple of DigitalSignalState values to encode.
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()
103 # Pack 4 signals per byte
104 for i in range(0, len(data), 4):
105 byte = 0
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)
112 result.append(byte)
114 return result