Advertising Parsing¶
Parse BLE advertising packets to extract device information, service UUIDs, and manufacturer data before establishing a connection.
When to Use Advertising Parsing¶
Device scanning — Identify nearby BLE devices by name or services
Beacon detection — Read iBeacon, Eddystone, or custom beacon data
Passive monitoring — Collect broadcast data without connecting
Pre-connection filtering — Check if a device supports required services
Basic Usage¶
from bluetooth_sig.advertising import AdvertisingPDUParser
parser = AdvertisingPDUParser()
# raw_bytes comes from your BLE library (bleak, simplepyble, etc.)
# Example: Flags (0x02, 0x01, 0x06) + Complete Local Name "Test"
raw_bytes = bytearray([0x02, 0x01, 0x06, 0x05, 0x09, 0x54, 0x65, 0x73, 0x74])
advertising_data = parser.parse_advertising_data(raw_bytes)
# Access parsed fields
if advertising_data.ad_structures.core.local_name:
print(f"Device: {advertising_data.ad_structures.core.local_name}")
Extracting Advertising Fields¶
The AdvertisingData object organises fields into logical groups. Here’s how to extract common fields:
from bluetooth_sig import BluetoothSIGTranslator
from bluetooth_sig.advertising import AdvertisingPDUParser
translator = BluetoothSIGTranslator()
parser = AdvertisingPDUParser()
# Example: Flags + Local Name "Sensor" + Battery Service UUID (0x180F)
raw_bytes = bytearray(
[
0x02,
0x01,
0x06, # Flags
0x07,
0x09,
0x53,
0x65,
0x6E,
0x73,
0x6F,
0x72, # Complete Local Name "Sensor"
0x03,
0x03,
0x0F,
0x18, # Complete List of 16-bit Service UUIDs: 0x180F
]
)
advertising_data = parser.parse_advertising_data(raw_bytes)
# Device name
name = advertising_data.ad_structures.core.local_name
# Service UUIDs - identify what the device supports
for uuid in advertising_data.ad_structures.core.service_uuids:
service_info = translator.get_service_info_by_uuid(str(uuid))
if service_info:
print(f"{uuid.short_form}: {service_info.name}")
# Manufacturer data - vendor-specific payloads
for (
company_id,
data,
) in advertising_data.ad_structures.core.manufacturer_data.items():
print(f"Company 0x{company_id:04X}: {data.hex()}")
# Service data - characteristic values broadcast without connection
for (
service_uuid,
data,
) in advertising_data.ad_structures.core.service_data.items():
print(f"Service {service_uuid}: {data.hex()}")
# Signal strength
tx_power = advertising_data.ad_structures.properties.tx_power
rssi = advertising_data.rssi
Available field groups:
Group |
Path |
Contents |
|---|---|---|
Core |
|
|
Properties |
|
|
Location |
|
|
Security |
|
|
Mesh |
|
|
Extended Advertising (BLE 5.0+)¶
The parser automatically detects extended advertising PDUs (BLE 5.0+), which support larger payloads and additional metadata:
from bluetooth_sig.advertising import AdvertisingPDUParser
parser = AdvertisingPDUParser()
# Standard advertising data
raw_bytes = bytearray(
[
0x02,
0x01,
0x06, # Flags
0x05,
0x09,
0x54,
0x65,
0x73,
0x74, # Complete Local Name "Test"
]
)
advertising_data = parser.parse_advertising_data(raw_bytes)
# Check for extended advertising fields
if advertising_data.extended:
print(f"Extended payload: {advertising_data.extended.extended_payload.hex()}")
print(f"Aux packets: {len(advertising_data.extended.auxiliary_packets)}")
Filtering Devices¶
By Service UUID¶
from bluetooth_sig import BluetoothSIGTranslator
from bluetooth_sig.types.gatt_enums import ServiceName
translator = BluetoothSIGTranslator()
battery_uuid = translator.get_service_uuid_by_name(ServiceName.BATTERY)
def has_battery_service(advertising_data):
return battery_uuid in advertising_data.ad_structures.core.service_uuids
By Manufacturer¶
APPLE_COMPANY_ID = 0x004C
def is_apple_device(advertising_data):
return (
APPLE_COMPANY_ID
in advertising_data.ad_structures.core.manufacturer_data
)
By Name Pattern¶
import re
def matches_name_pattern(advertising_data, pattern):
name = advertising_data.ad_structures.core.local_name
if name:
return re.match(pattern, name) is not None
return False
# Example: Create sample advertising data to test the filter
from bluetooth_sig.advertising import AdvertisingPDUParser
parser = AdvertisingPDUParser()
# Advertising data with name "Sensor_42"
sample_bytes = bytearray(
[
0x02,
0x01,
0x06,
0x0A,
0x09,
0x53,
0x65,
0x6E,
0x73,
0x6F,
0x72,
0x5F,
0x34,
0x32,
]
)
advertising_data = parser.parse_advertising_data(sample_bytes)
# Find devices starting with "Sensor_"
if matches_name_pattern(advertising_data, r"Sensor_\d+"):
print("Found sensor device")
Custom Advertising Interpreters¶
For vendor-specific advertising protocols (BTHome, Xiaomi, RuuviTag, etc.), create custom interpreters that convert raw advertising data into typed sensor readings.
Architecture Overview¶
The library uses a two-layer architecture:
PDU Parsing (
AdvertisingPDUParser) — Raw bytes → AD structuresPayload Interpretation (
PayloadInterpreter) — AD structures → typed results
Custom interpreters implement the second layer.
Creating a Custom Interpreter¶
import msgspec
from bluetooth_sig.advertising import (
DataSource,
DeviceAdvertisingState,
InterpreterInfo,
PayloadInterpreter,
)
from bluetooth_sig.advertising.base import AdvertisingData
from bluetooth_sig.types.company import ManufacturerData
class MySensorData(msgspec.Struct):
"""Typed result from my sensor protocol."""
temperature: float
humidity: float
battery: int
class MySensorInterpreter(PayloadInterpreter[MySensorData]):
"""Interpreter for My Sensor advertising protocol."""
_info = InterpreterInfo(
company_id=0x1234, # Your company ID
name="My Sensor",
data_source=DataSource.MANUFACTURER,
)
@classmethod
def supports(cls, advertising_data: AdvertisingData) -> bool:
"""Check if this interpreter handles the advertisement."""
return 0x1234 in advertising_data.manufacturer_data
def interpret(
self,
advertising_data: AdvertisingData,
state: DeviceAdvertisingState,
) -> MySensorData:
"""Parse manufacturer data into typed result."""
manufacturer_entry = advertising_data.manufacturer_data[0x1234]
data = manufacturer_entry.payload
# Parse your protocol (example format)
temperature = int.from_bytes(data[0:2], "little") / 100.0
humidity = int.from_bytes(data[2:4], "little") / 100.0
battery = data[4]
return MySensorData(
temperature=temperature,
humidity=humidity,
battery=battery,
)
Service-Based Interpreters¶
For protocols using service data instead of manufacturer data:
import msgspec
from bluetooth_sig.advertising import (
DataSource,
DeviceAdvertisingState,
InterpreterInfo,
PayloadInterpreter,
)
from bluetooth_sig.advertising.base import AdvertisingData
from bluetooth_sig.types.uuid import BluetoothUUID
BTHOME_UUID = BluetoothUUID("0000fcd2-0000-1000-8000-00805f9b34fb")
class BTHomeData(msgspec.Struct):
"""Typed result from BTHome protocol."""
temperature: float | None = None
humidity: float | None = None
battery: int | None = None
class BTHomeInterpreter(PayloadInterpreter[BTHomeData]):
"""Example BTHome-style interpreter."""
_info = InterpreterInfo(
service_uuid=BTHOME_UUID,
name="BTHome",
data_source=DataSource.SERVICE,
)
@classmethod
def supports(cls, advertising_data: AdvertisingData) -> bool:
return BTHOME_UUID in advertising_data.service_data
def interpret(
self,
advertising_data: AdvertisingData,
state: DeviceAdvertisingState,
) -> BTHomeData:
data = advertising_data.service_data[BTHOME_UUID]
# Parse BTHome format (simplified example)
return BTHomeData(temperature=22.5, humidity=45.0, battery=85)
Auto-Registration¶
Interpreters are automatically registered when defined (via __init_subclass__). The registry routes advertisements to the appropriate interpreter.
Using the Registry¶
from bluetooth_sig.advertising import (
AdvertisingPDUParser,
DeviceAdvertisingState,
payload_interpreter_registry,
)
from bluetooth_sig.advertising.base import AdvertisingData
parser = AdvertisingPDUParser()
# Example: Advertising data with manufacturer data (Company ID 0x1234)
# Payload: temp=100 (0x0064), humidity=50 (0x0032), battery=85 (0x55)
raw_bytes = bytearray(
[
0x02,
0x01,
0x06, # Flags
0x08,
0xFF,
0x34,
0x12,
0x64,
0x00,
0x32,
0x00,
0x55, # Manufacturer data (5 bytes)
]
)
pdu_data = parser.parse_advertising_data(raw_bytes)
# Build AdvertisingData for interpreter routing
# pdu_data.ad_structures.core.manufacturer_data already contains ManufacturerData objects
advertising_data = AdvertisingData(
manufacturer_data=pdu_data.ad_structures.core.manufacturer_data,
service_data=pdu_data.ad_structures.core.service_data,
local_name=pdu_data.ad_structures.core.local_name,
rssi=pdu_data.rssi,
)
# Find interpreter for this advertisement
interpreter_class = payload_interpreter_registry.find_interpreter_class(advertising_data)
if interpreter_class:
# Create interpreter instance for this device
interpreter = interpreter_class(mac_address="AA:BB:CC:DD:EE:FF")
# Create state for this device (caller-managed)
state = DeviceAdvertisingState()
# Interpret the data
result = interpreter.interpret(advertising_data, state)
print(f"Interpreted: {result}")
Encrypted Advertising¶
For protocols with encrypted payloads, set the bindkey in the device state:
# SKIP: Depends on MySensorInterpreter class defined in previous examples
from bluetooth_sig.advertising import DeviceAdvertisingState
interpreter = MySensorInterpreter(mac_address="AA:BB:CC:DD:EE:FF")
# Create state with bindkey for encrypted devices
state = DeviceAdvertisingState()
state.encryption.bindkey = bytes.fromhex("0123456789ABCDEF0123456789ABCDEF")
# The interpret() method checks state.encryption.bindkey for decryption
Stateful Interpreters¶
State is managed externally by the caller via DeviceAdvertisingState:
# SKIP: Depends on classes defined in previous examples
from bluetooth_sig.advertising import DeviceAdvertisingState
from bluetooth_sig.advertising.base import AdvertisingData
class StatefulInterpreter(PayloadInterpreter[MySensorData]):
def interpret(
self,
advertising_data: AdvertisingData,
state: DeviceAdvertisingState,
) -> MySensorData:
# Access packet state from caller-managed state
last_packet_id = state.packets.last_packet_id or 0
# Parse current packet
manufacturer_entry = advertising_data.manufacturer_data[0x1234]
current_id = manufacturer_entry.payload[0]
if current_id <= last_packet_id:
raise ValueError("Replay detected")
# Update state (mutable, changes visible to caller)
state.packets.last_packet_id = current_id
# Continue parsing...
return MySensorData(temperature=0.0, humidity=0.0, battery=0)
Unregistering Interpreters¶
# SKIP: Depends on MySensorInterpreter class defined in previous examples
from bluetooth_sig.advertising import payload_interpreter_registry
# Remove a specific interpreter
payload_interpreter_registry.unregister(MySensorInterpreter)
See Also¶
API Overview — Conceptual overview
Import Patterns — Correct import statements
examples/advertising_parsing.py — Complete working example