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

ad_structures.core

local_name, service_uuids, manufacturer_data, service_data

Properties

ad_structures.properties

tx_power, flags, appearance

Location

ad_structures.location

indoor_positioning, transport_discovery_data

Security

ad_structures.security

encrypted_advertising_data

Mesh

ad_structures.mesh

mesh_message, mesh_beacon, broadcast_name

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:

  1. PDU Parsing (AdvertisingPDUParser) — Raw bytes → AD structures

  2. Payload 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