Migration Guide: Adopt the Bluetooth SIG Standards Library¶
This guide shows how to migrate existing BLE code to use the library’s parsing. Examples are based on real-world applications like heart rate monitors, environmental sensors, and smart home devices.
The library stays backend-agnostic: you keep your connection code (Bleak, SimplePyBLE, etc.) and drop the parser into your reads/notifications.
Who is this for?¶
You already read GATT characteristics via a BLE client (Bleak, SimplePyBLE, Home Assistant, etc.)
You manually parse bytes today and want robust, spec-aligned parsing with validation and types
You’re building health monitors, environmental sensors, or smart home devices
Real-World Example 1: Heart Rate Monitor¶
Based on: Polar H10, Fitbit, Apple Watch integrations
Before (manual parsing - typical fitness app pattern):
# SKIP: Migration "before" example (anti-pattern)
# Common pattern from real fitness app integrations
async with BleakClient(device_address) as client:
def heart_rate_handler(sender, data: bytearray):
# Manual bit manipulation from BLE Heart Rate spec
flags = data[0]
hr_format = flags & 0x01
if hr_format == 0:
heart_rate = data[1]
offset = 2
else:
heart_rate = int.from_bytes(data[1:3], "little")
offset = 3
# Energy expended (if present)
energy = None
if flags & 0x08:
energy = int.from_bytes(data[offset : offset + 2], "little")
offset += 2
# RR intervals (if present)
rr_intervals = []
if flags & 0x10:
while offset < len(data):
rr = int.from_bytes(data[offset : offset + 2], "little")
rr_intervals.append(rr / 1024.0) # Convert to seconds
offset += 2
print(f"HR: {heart_rate} bpm, Energy: {energy} kJ, RR: {rr_intervals}")
await client.start_notify("2A37", heart_rate_handler)
After (with bluetooth-sig-python):
import asyncio
from bluetooth_sig import BluetoothSIGTranslator
async def main():
translator = BluetoothSIGTranslator()
# Note: This is a simplified example - in production, use actual BleakClient
def heart_rate_handler(sender, data: bytearray):
# Returns HeartRateData directly - type-safe dataclass
hr_data = translator.parse_characteristic("2A37", data)
print(f"HR: {hr_data.heart_rate} bpm")
if hr_data.energy_expended:
print(f"Energy: {hr_data.energy_expended} kJ")
if hr_data.rr_intervals:
print(f"RR intervals: {hr_data.rr_intervals} seconds")
# Test with sample data
heart_rate_handler(None, bytearray([0x00, 0x55]))
asyncio.run(main())
What improved:
✅ No manual bit manipulation or offset tracking
✅ Automatic unit conversions (RR intervals to seconds)
✅ Type-safe access to all fields with autocomplete
✅ Validates against spec (catches malformed data)
Real-World Example 2: Environmental Sensor Dashboard¶
Based on: Xiaomi LYWSD03MMC, SwitchBot Meter, Govee sensors
Before (manual parsing - typical home automation pattern):
# SKIP: Migration "before" example (anti-pattern)
# Common pattern from home automation projects (Home Assistant, openHAB)
async with BleakClient(device_address) as client:
# Read battery level
battery_bytes = await client.read_gatt_char("2A19")
battery = battery_bytes[0]
# Read temperature
temp_bytes = await client.read_gatt_char("2A6E")
temp_raw = int.from_bytes(temp_bytes[0:2], "little", signed=True)
temperature = temp_raw / 100.0 # Convert to °C
# Read humidity
humidity_bytes = await client.read_gatt_char("2A6F")
humidity = int.from_bytes(humidity_bytes[0:2], "little") / 100.0
print(f"Battery: {battery}%, Temp: {temperature}°C, Humidity: {humidity}%")
After (with bluetooth-sig-python):
import asyncio
from bluetooth_sig import BluetoothSIGTranslator
async def main():
translator = BluetoothSIGTranslator()
# ============================================
# SIMULATED DATA - Replace with actual BLE reads
# ============================================
SIMULATED_BATTERY_DATA = bytearray([85]) # Simulates 85% battery
SIMULATED_TEMP_DATA = bytearray([0x64, 0x09]) # Simulates 24.04°C
SIMULATED_HUMIDITY_DATA = bytearray([0x3A, 0x13]) # Simulates 49.22%
# Parse each characteristic - returns values directly
battery = translator.parse_characteristic(
"2A19", SIMULATED_BATTERY_DATA
) # int
temp = translator.parse_characteristic(
"2A6E", SIMULATED_TEMP_DATA
) # float
humidity = translator.parse_characteristic(
"2A6F", SIMULATED_HUMIDITY_DATA
) # float
print(f"Battery: {battery}%")
print(f"Temp: {temp}°C")
print(f"Humidity: {humidity}%")
asyncio.run(main())
What improved:
✅ Single parse call for multiple characteristics
✅ Automatic unit handling (no hardcoded divisors)
✅ Cleaner, more maintainable code
✅ Consistent error handling across all values
Real-World Example 3: Glucose Meter with Context¶
Based on: OneTouch Verio, AccuChek Guide, Abbott FreeStyle
Before (manual pairing logic):
# SKIP: Migration "before" example (anti-pattern)
# Typical medical device integration pattern
async with BleakClient(device_address) as client:
measurements = {}
contexts = {}
def measurement_handler(sender, data: bytearray):
seq = int.from_bytes(data[1:3], "little")
# Complex flag parsing...
glucose = int.from_bytes(data[3:5], "little")
measurements[seq] = glucose / 100000.0 # mg/dL conversion
def context_handler(sender, data: bytearray):
seq = int.from_bytes(data[1:3], "little")
# More flag parsing...
carbs = data[3]
contexts[seq] = carbs
await client.start_notify(
"2A18", measurement_handler
) # Glucose Measurement
await client.start_notify("2A34", context_handler) # Glucose Context
# Manual pairing logic
await asyncio.sleep(5)
for seq in measurements:
if seq in contexts:
print(
f"Seq {seq}: {measurements[seq]} mg/dL, Carbs: {contexts[seq]}g"
)
After (with automatic dependency resolution):
# SKIP: Requires BLE hardware and bleak client connection
import asyncio
from bluetooth_sig import BluetoothSIGTranslator
async def main():
translator = BluetoothSIGTranslator()
gm_uuid = "2A18"
gmc_uuid = "2A34"
measurements_cache = {}
contexts_cache = {}
# Example of handling paired characteristics
def combined_handler(char_uuid: str, data: bytearray):
# Parse immediately to get the sequence number
# Returns the parsed value directly (GlucoseMeasurement or GlucoseMeasurementContext)
parsed = translator.parse_characteristic(char_uuid, data)
# Cache using the parsed sequence number
if char_uuid == gm_uuid:
seq = parsed.sequence_number
measurements_cache[seq] = data
elif char_uuid == gmc_uuid:
seq = parsed.sequence_number
contexts_cache[seq] = data
# Parse pairs when we have matching sequence numbers
matching_seqs = set(measurements_cache.keys()) & set(
contexts_cache.keys()
)
for seq in matching_seqs:
results = translator.parse_characteristics(
{
gm_uuid: measurements_cache[seq],
gmc_uuid: contexts_cache[seq],
}
)
gm = results[gm_uuid] # GlucoseMeasurementData directly
gmc = results[gmc_uuid] # GlucoseMeasurementContextData directly
print(f"Seq {seq}: Glucose {gm.glucose_concentration} {gm.unit}")
print(f"Seq {seq}: Carbs {gmc.carbohydrate} g")
# Remove processed entries
del measurements_cache[seq]
del contexts_cache[seq]
await client.start_notify(
gm_uuid, lambda s, d: combined_handler(gm_uuid, d)
)
await client.start_notify(
gmc_uuid, lambda s, d: combined_handler(gmc_uuid, d)
)
What improved:
✅ Automatic validation of sequence number pairing
✅ Type-safe access to complex nested structures
✅ Proper unit handling (mol/L vs mg/dL)
✅ No manual offset calculations
Real-World Example 4: Device Service Discovery¶
Based on: Bleak’s service_explorer.py pattern
Before (reading and manually parsing all characteristics):
# SKIP: Migration "before" example (anti-pattern)
# Common pattern from BLE exploration tools
async with BleakClient(device_address) as client:
for service in client.services:
print(f"[Service] {service.uuid}")
for char in service.characteristics:
if "read" in char.properties:
try:
value = await client.read_gatt_char(char)
# Manual parsing depends on UUID
if char.uuid == "2A19":
print(f" Battery: {value[0]}%")
elif char.uuid == "2A37":
hr = value[1]
print(f" Heart Rate: {hr} bpm")
else:
print(f" Raw: {value.hex()}")
except Exception as e:
print(f" Error: {e}")
After (with automatic parsing):
import asyncio
from bluetooth_sig import BluetoothSIGTranslator
from bluetooth_sig.gatt.exceptions import CharacteristicParseError
# ============================================
# SIMULATED DATA - Replace with actual BLE reads
# ============================================
SIMULATED_BATTERY_DATA = bytearray([85]) # Simulates 85% battery
SIMULATED_HR_DATA = bytearray([0x00, 0x55]) # Simulates heart rate measurement
async def main():
translator = BluetoothSIGTranslator()
# Example: parse known characteristics using UUIDs
characteristics = {
"2A19": SIMULATED_BATTERY_DATA, # Battery
"2A37": SIMULATED_HR_DATA, # Heart Rate
}
for uuid, raw_bytes in characteristics.items():
try:
result = translator.parse_characteristic(uuid, raw_bytes)
info = translator.get_characteristic_info_by_uuid(uuid)
# For simple types (int/float), result is the value directly
# For complex types, result is a dataclass with fields
if hasattr(result, "heart_rate"):
print(f" {info.name}: {result.heart_rate} bpm")
else:
print(f" {info.name}: {result}")
except CharacteristicParseError:
print(f" {uuid}: {raw_bytes.hex()} (parse failed)")
asyncio.run(main())
What improved:
✅ Automatic characteristic identification by UUID
✅ No UUID-specific parsing logic needed
✅ Graceful fallback for unknown characteristics
✅ Human-readable characteristic names
Batch Parsing (Dependencies)¶
If characteristics depend on each other (e.g., Glucose Measurement + Context), parse them together.
Minimal path (no adapters)¶
from bluetooth_sig import BluetoothSIGTranslator
# Example data (replace with actual BLE reads)
bpm_bytes = bytearray(
[0x00, 0x78, 0x00, 0x50, 0x00, 0x46, 0x00]
) # Blood Pressure
battery_bytes = bytearray([85]) # Battery Level
translator = BluetoothSIGTranslator()
bpm_uuid = "2A35"
battery_uuid = "2A19"
values = {
bpm_uuid: bpm_bytes,
battery_uuid: battery_bytes,
}
results = translator.parse_characteristics(values)
print(f"Blood Pressure: {results[bpm_uuid]}")
print(f"Battery: {results[battery_uuid]}%")
With Connection Managers¶
Example adapters are provided in examples/connection_managers/ as references.
from bluetooth_sig import BluetoothSIGTranslator
# Example data (replace with actual BLE reads)
heart_rate_bytes = bytearray([0x00, 75]) # Heart Rate 75 bpm
battery_bytes = bytearray([92]) # Battery 92%
translator = BluetoothSIGTranslator()
# Simple batch parsing
char_data = {
"2A37": heart_rate_bytes,
"2A19": battery_bytes,
}
results = translator.parse_characteristics(char_data)
print(f"Heart Rate: {results['2A37']}")
print(f"Battery: {results['2A19']}%")
With descriptors/services (adapters in examples)¶
Bleak:
examples/connection_managers/bleak_utils.pySimplePyBLE:
examples/connection_managers/simpleble.py
# SKIP: Requires BLE connection (client.services from bleak)
from bluetooth_sig import CharacteristicName
from bluetooth_sig.types.io import to_parse_inputs
from examples.connection_managers.bleak_utils import bleak_services_to_batch
gm_uuid = "2A18"
gmc_uuid = "2A34"
services = client.services
values = {gm_uuid: gm_bytes, gmc_uuid: gmc_bytes}
batch = bleak_services_to_batch(services, values_by_uuid=values)
char_data, desc_data = to_parse_inputs(batch)
results = translator.parse_characteristics(
char_data, descriptor_data=desc_data
)
Note: Example adapters are provided as references; update them as needed for your project/backend versions.
Device + Connection Manager (larger apps)¶
For applications managing multiple devices or complex workflows, use the Device pattern:
# SKIP: Needs real BLE device
from bluetooth_sig import BluetoothSIGTranslator
from bluetooth_sig.device import Device
from bluetooth_sig.types.gatt_enums import CharacteristicName
translator = BluetoothSIGTranslator()
device = Device(address, translator)
# Your manager implements ConnectionManagerProtocol (connect/read/write/notify/etc.)
from bluetooth_sig.gatt.characteristics import BatteryLevelCharacteristic
from examples.connection_managers.bleak_retry import (
BleakRetryClientManager,
)
battery = BatteryLevelCharacteristic()
manager = BleakRetryClientManager(address)
device.attach_connection_manager(manager)
await device.connect()
# Type-safe read with characteristic class
parsed = await device.read(BatteryLevelCharacteristic)
await device.disconnect()
Migration Checklist¶
✅ Keep your BLE client code (Bleak/SimplePyBLE) unchanged
✅ After each read or in notification callbacks, call
parse_characteristic(uuid, bytes)✅ For multiple related characteristics, call
parse_characteristics({uuid: bytes})✅ Include descriptors when needed (via adapters or
descriptor_datamapping)✅ For larger apps, adopt a
ConnectionManagerProtocol+Devicepattern
Where are the example adapters?¶
examples/connection_managers/bleak_utils.py→bleak_services_to_batch(...)examples/connection_managers/simpleble.py→simpleble_services_to_batch(...)
These adapters are intentionally kept in examples to avoid hard dependencies. Copy or adapt them for your project; they may require updates to match your specific library versions and object shapes.
Adoption in Real Projects¶
Home Assistant Integration Pattern¶
# SKIP: Requires Home Assistant
from homeassistant.components import bluetooth
from bluetooth_sig.gatt.characteristics import TemperatureCharacteristic
class MySensorEntity(SensorEntity):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._temp = TemperatureCharacteristic()
def _async_handle_bluetooth_event(
self, service_info: BluetoothServiceInfoBleak, change: BluetoothChange
) -> None:
"""Handle bluetooth callback."""
# Your existing connection/read logic
char_data = self._read_characteristic(str(self._temp.uuid))
# Type-safe parsing - IDE knows value is float
value = self._temp.parse_value(bytearray(char_data))
self._attr_native_value = value
self._attr_native_unit_of_measurement = self._temp.unit
SimplePyBLE Pattern¶
# SKIP: Requires SimplePyBLE
import simplepyble
from bluetooth_sig.gatt.characteristics import BatteryLevelCharacteristic
battery = BatteryLevelCharacteristic()
peripheral = simplepyble.Peripheral()
peripheral.connect_to_address(device_address)
for service in peripheral.services():
for characteristic in service.characteristics():
try:
data = peripheral.read(service.uuid(), characteristic.uuid())
value = translator.parse_characteristic(
characteristic.uuid(), data
)
info = translator.get_characteristic_info_by_uuid(
characteristic.uuid()
)
if info:
print(f"{info.name}: {value} {info.unit or ''}")
except Exception as e:
print(f"Error reading {characteristic.uuid()}: {e}")
Troubleshooting¶
Unknown UUID: Ensure you’re using the correct SIG UUID (short or full).
Parse failed: Check spec constraints (length, range);
result.error_messageexplains the issue.Descriptor-dependent parsing: Pass descriptor bytes via
descriptor_dataor use the example adapters.Advertising data: Use
AdvertisingPDUParserfor parsing advertising packets—see Advertising Parsing Guide.Type hints not working: Ensure you’re using Python 3.10+ for best type checking support.
Next Steps¶
See Usage Guide for detailed API documentation
Check examples/ for complete working applications
Review supported characteristics to see what’s available
Pairing Dependent Characteristics (Blood Pressure Example)¶
Some BLE profiles send related data in separate notifications that must be paired. For example, during a Blood Pressure measurement, the device sends Intermediate Cuff Pressure notifications repeatedly as the cuff inflates, then a final Blood Pressure Measurement. If multiple measurement sessions overlap (or you have multiple devices), you need to pair notifications correctly.
The DependencyPairingBuffer helper buffers notifications and groups them using a caller-defined key (timestamp, sequence number, session ID, etc.).
Real-World Scenario: Blood Pressure Monitor¶
# SKIP: Requires BLE hardware connection
from datetime import datetime
from bluetooth_sig import BluetoothSIGTranslator
from bluetooth_sig.stream import DependencyPairingBuffer
from bluetooth_sig.types.gatt_enums import CharacteristicName
translator = BluetoothSIGTranslator()
bpm_uuid = "2A35"
icp_uuid = "2A36"
def group_by_timestamp(uuid: str, parsed) -> datetime:
"""Group notifications by their timestamp - same session = same timestamp.
Args:
uuid: The characteristic UUID (for reference)
parsed: The parsed value directly (BloodPressureMeasurement or IntermediateCuffPressure)
"""
# Both characteristics have optional timestamp fields
if hasattr(parsed, "optional_fields") and parsed.optional_fields.timestamp:
return parsed.optional_fields.timestamp
# Fallback: use current time or synthetic session tracking
raise ValueError("Timestamp required for grouping")
def on_measurement_complete(results):
"""Called when both BPM and ICP arrive for the same timestamp.
Results is dict[str, Any] where values are parsed dataclasses directly.
"""
bpm = results[bpm_uuid] # BloodPressureMeasurement directly
icp = results[icp_uuid] # IntermediateCuffPressure directly
print(f"Blood Pressure: {bpm.systolic}/{bpm.diastolic} mmHg")
print(f"Max Cuff Pressure: {icp.current_cuff_pressure} mmHg")
# Store to database, update UI, etc.
buffer = DependencyPairingBuffer(
translator=translator,
required_uuids={bpm_uuid, icp_uuid},
group_key=group_by_timestamp,
on_pair=on_measurement_complete,
)
# In your notification handlers (notifications arrive in any order)
async with BleakClient(device_address) as client:
await client.start_notify(
bpm_uuid, lambda s, d: buffer.ingest(bpm_uuid, d)
)
await client.start_notify(
icp_uuid, lambda s, d: buffer.ingest(icp_uuid, d)
)
# Buffer automatically pairs when both arrive with matching timestamps
await asyncio.sleep(30) # Wait for measurements
Why This is Useful¶
Without the buffer: You’d manually track which ICP belongs to which BPM, especially when:
Multiple measurement sessions overlap (nurse takes measurements from multiple patients)
Notifications arrive out of order (common with BLE)
Network delays cause interleaving
With the buffer: Automatically groups by timestamp, handles out-of-order arrival, and triggers your callback only when complete pairs arrive.
Paired Characteristics Example¶
Group characteristics by a key (e.g., timestamp, sequence number) and trigger a callback when all required characteristics arrive:
from bluetooth_sig import BluetoothSIGTranslator
from bluetooth_sig.stream import DependencyPairingBuffer
translator = BluetoothSIGTranslator()
hr_uuid = "2A37" # Heart Rate Measurement
bat_uuid = "2A19" # Battery Level
received_pairs = []
def on_pair(results):
"""Handle complete pair of characteristics."""
hr = results[hr_uuid] # HeartRateData
bat = results[bat_uuid] # int
received_pairs.append(results)
print(f"Heart Rate: {hr.heart_rate} bpm, Battery: {bat}%")
buffer = DependencyPairingBuffer(
translator=translator,
required_uuids={hr_uuid, bat_uuid},
group_key=lambda uuid, parsed: 0, # All go to same group
on_pair=on_pair,
)
# Example data (in real use, comes from BLE notifications)
hr_bytes = bytearray([0x00, 75]) # Heart rate 75 bpm
bat_bytes = bytearray([85]) # Battery 85%
# Ingest data (order-independent)
buffer.ingest(hr_uuid, hr_bytes)
buffer.ingest(bat_uuid, bat_bytes) # Triggers on_pair callback
print(f"Total received: {len(received_pairs)}")
Notes¶
Generic: Works with any paired characteristics - you define the grouping key
Validation: Implement profile-specific validation (sequence matching, etc.) in your
on_paircallbackBackend-agnostic: Buffer only handles UUID strings and bytes; keep subscription management in your BLE client
Order-independent: Notifications can arrive in any order; buffer waits for both before calling your callback