Coverage for src / bluetooth_sig / types / advertising / transport_discovery.py: 100%
46 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-03-18 11:17 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-03-18 11:17 +0000
1"""Transport Discovery Data (AD 0x26, CSS Part A §1.10).
3Decodes the Transport Discovery Data AD type which carries one or more
4transport blocks describing available transport connections.
5"""
7from __future__ import annotations
9from enum import IntFlag
11import msgspec
13from bluetooth_sig.gatt.characteristics.utils import DataParser
15# Transport block header: org_id (1) + flags (1) + data_length (1)
16TRANSPORT_BLOCK_HEADER_LENGTH = 3
19class TDSFlags(IntFlag):
20 """Transport Discovery Service flags (CSS Part A §1.10).
22 Encoded in a single byte per transport block.
23 """
25 ROLE_NOT_SPECIFIED = 0x00
26 ROLE_SEEKER = 0x01
27 ROLE_PROVIDER = 0x02
28 ROLE_SEEKER_AND_PROVIDER = 0x03
29 INCOMPLETE = 0x04 # Bit 2: transport data is incomplete
30 STATE_OFF = 0x00 # Bits 3-4 = 0b00
31 STATE_ON = 0x08 # Bits 3-4 = 0b01
32 STATE_TEMPORARILY_UNAVAILABLE = 0x10 # Bits 3-4 = 0b10
35# Masks for extracting sub-fields from TDSFlags
36TDS_ROLE_MASK = TDSFlags.ROLE_SEEKER | TDSFlags.ROLE_PROVIDER
37TDS_STATE_MASK = TDSFlags.STATE_ON | TDSFlags.STATE_TEMPORARILY_UNAVAILABLE
40class TransportBlock(msgspec.Struct, frozen=True, kw_only=True):
41 """A single transport block within Transport Discovery Data.
43 Attributes:
44 organization_id: Organisation defining the transport data (1 = Bluetooth SIG).
45 flags: TDS flags — role, incomplete, and transport state.
46 transport_data: Organisation-specific transport payload.
48 """
50 organization_id: int
51 flags: TDSFlags
52 transport_data: bytes = b""
54 @property
55 def role(self) -> TDSFlags:
56 """Role bits (0-1): seeker, provider, both, or not specified."""
57 return self.flags & TDS_ROLE_MASK
59 @property
60 def is_incomplete(self) -> bool:
61 """Whether transport data is incomplete (bit 2)."""
62 return bool(self.flags & TDSFlags.INCOMPLETE)
64 @property
65 def transport_state(self) -> TDSFlags:
66 """Transport state (bits 3-4): off, on, or temporarily unavailable."""
67 return self.flags & TDS_STATE_MASK
70class TransportDiscoveryData(msgspec.Struct, frozen=True, kw_only=True):
71 """Transport Discovery Data (CSS Part A, §1.10).
73 Contains one or more transport blocks describing available transport
74 connections (e.g. Wi-Fi, classic Bluetooth).
76 Attributes:
77 blocks: List of parsed transport blocks.
79 """
81 blocks: list[TransportBlock] = msgspec.field(default_factory=list)
83 @classmethod
84 def decode(cls, data: bytes | bytearray) -> TransportDiscoveryData:
85 """Decode Transport Discovery Data AD.
87 Iterates over transport blocks until the buffer is exhausted.
88 DataParser raises ``InsufficientDataError`` if a block header is
89 truncated. Incomplete trailing blocks (fewer than
90 ``TRANSPORT_BLOCK_HEADER_LENGTH`` bytes remaining) are silently
91 skipped, matching real-world scanner behaviour.
93 Args:
94 data: Raw AD data bytes (excluding length and AD type).
96 Returns:
97 Parsed TransportDiscoveryData with transport blocks.
99 """
100 blocks: list[TransportBlock] = []
101 offset = 0
103 while offset + TRANSPORT_BLOCK_HEADER_LENGTH <= len(data):
104 org_id = DataParser.parse_int8(data, offset, signed=False)
105 tds_flags = TDSFlags(DataParser.parse_int8(data, offset + 1, signed=False))
106 transport_data_length = DataParser.parse_int8(data, offset + 2, signed=False)
107 offset += TRANSPORT_BLOCK_HEADER_LENGTH
109 end = min(offset + transport_data_length, len(data))
110 transport_data = bytes(data[offset:end])
111 offset = end
113 blocks.append(
114 TransportBlock(
115 organization_id=org_id,
116 flags=tds_flags,
117 transport_data=transport_data,
118 )
119 )
121 return cls(blocks=blocks)
124__all__ = [
125 "TDSFlags",
126 "TDS_ROLE_MASK",
127 "TDS_STATE_MASK",
128 "TRANSPORT_BLOCK_HEADER_LENGTH",
129 "TransportBlock",
130 "TransportDiscoveryData",
131]