Working with Characteristics¶
GATT characteristics are the primary data containers in Bluetooth Low Energy. This guide covers parsing, encoding, registry lookup, and creating custom characteristics.
When to Use Characteristic Classes¶
Type-safe parsing — Get full IDE inference on parsed values
Known devices — When you know what characteristics the device has
Encoding data — Build bytes from structured values
Notifications — Parse incoming notification data
Parsing Characteristic Data¶
Direct Class Usage (Type-Safe)¶
When you know the characteristic type, instantiate the class directly:
from bluetooth_sig.gatt.characteristics import (
BatteryLevelCharacteristic,
HeartRateMeasurementCharacteristic,
TemperatureCharacteristic,
)
# Simple characteristic: returns int
battery = BatteryLevelCharacteristic()
level = battery.parse_value(bytearray([85])) # IDE knows: int
print(f"Battery: {level}%")
# Complex characteristic: returns structured data
heart_rate = HeartRateMeasurementCharacteristic()
hr_data = heart_rate.parse_value(
bytearray([0x00, 72])
) # IDE knows: HeartRateData
print(f"Heart rate: {hr_data.heart_rate} bpm")
print(f"Sensor contact: {hr_data.sensor_contact}") # Full autocomplete
Dynamic Parsing (UUID Strings)¶
When discovering unknown devices, use the translator:
from bluetooth_sig import BluetoothSIGTranslator
translator = BluetoothSIGTranslator()
# Parse by UUID - returns Any
value = translator.parse_characteristic("2A19", bytearray([85]))
# Get characteristic info separately
info = translator.get_characteristic_info_by_uuid("2A19")
print(f"{info.name}: {value}") # "Battery Level: 85"
Encoding Data¶
Convert values back to bytes for writing to devices:
from bluetooth_sig.gatt.characteristics import (
BatteryLevelCharacteristic,
HeartRateMeasurementCharacteristic,
)
from bluetooth_sig.gatt.characteristics.heart_rate_measurement import (
HeartRateData,
)
# Simple encoding
battery = BatteryLevelCharacteristic()
encoded = battery.build_value(85)
print(f"Bytes: {encoded.hex()}") # "55"
# Complex encoding
heart_rate = HeartRateMeasurementCharacteristic()
data = HeartRateData(
heart_rate=72,
sensor_contact=True,
energy_expended=None,
rr_intervals=[],
)
encoded_hr = heart_rate.build_value(data)
print(f"Bytes: {encoded_hr.hex()}")
Characteristic Registry¶
The CharacteristicRegistry provides dynamic lookup:
from bluetooth_sig.gatt.characteristics import CharacteristicRegistry
# Check if UUID is a known characteristic
char_class = CharacteristicRegistry.get_characteristic_class_by_uuid("2A19")
if char_class:
print(f"Found: {char_class.__name__}") # BatteryLevelCharacteristic
# Get instance from UUID
instance = CharacteristicRegistry.get_characteristic(uuid="2A19")
if instance:
value = instance.parse_value(bytearray([85]))
print(f"Parsed: {value}")
# Get by name
from bluetooth_sig.gatt.characteristics.registry import CharacteristicName
char_class = CharacteristicRegistry.get_characteristic_class(
CharacteristicName.BATTERY_LEVEL
)
Custom Characteristics¶
For vendor-specific characteristics not defined by the Bluetooth SIG.
Runtime Registration¶
Register a custom characteristic class at runtime. This example shows a sensor characteristic with multiple fields and validation:
import msgspec
from bluetooth_sig.gatt.characteristics import CharacteristicRegistry
from bluetooth_sig.gatt.characteristics.custom import CustomBaseCharacteristic
from bluetooth_sig.gatt.context import CharacteristicContext
from bluetooth_sig.types import CharacteristicInfo
from bluetooth_sig.types.uuid import BluetoothUUID
class SensorReading(msgspec.Struct, frozen=True, kw_only=True):
"""Structured data from vendor sensor."""
temperature: float # Celsius
humidity: float # Percentage
battery: int # 0-100
class VendorSensorCharacteristic(CustomBaseCharacteristic):
"""Custom characteristic for vendor environmental sensor.
Data format (6 bytes):
Bytes 0-1: Temperature (sint16, 0.01°C resolution)
Bytes 2-3: Humidity (uint16, 0.01% resolution)
Byte 4: Battery level (uint8, 0-100%)
Byte 5: Reserved
"""
_info = CharacteristicInfo(
uuid=BluetoothUUID("12345678-1234-1234-1234-123456789ABC"),
name="Vendor Environmental Sensor",
)
min_length = 6
expected_type = SensorReading
def _decode_value(
self, data: bytearray, ctx: CharacteristicContext | None = None, *, validate: bool = True
) -> SensorReading:
# Parse temperature (signed 16-bit, 0.01°C resolution)
temp_raw = int.from_bytes(data[0:2], "little", signed=True)
temperature = temp_raw * 0.01
# Parse humidity (unsigned 16-bit, 0.01% resolution)
humidity_raw = int.from_bytes(data[2:4], "little", signed=False)
humidity = humidity_raw * 0.01
# Parse battery (0-100%)
battery = data[4]
return SensorReading(
temperature=temperature,
humidity=humidity,
battery=battery,
)
def _encode_value(self, value: SensorReading) -> bytearray:
result = bytearray(6)
# Encode temperature
temp_raw = int(value.temperature / 0.01)
result[0:2] = temp_raw.to_bytes(2, "little", signed=True)
# Encode humidity
humidity_raw = int(value.humidity / 0.01)
result[2:4] = humidity_raw.to_bytes(2, "little", signed=False)
# Encode battery
result[4] = value.battery
# Reserved byte
result[5] = 0x00
return result
# Register with the registry
CharacteristicRegistry.register_characteristic_class(
uuid="12345678-1234-1234-1234-123456789ABC",
char_cls=VendorSensorCharacteristic,
)
# Use it
sensor = VendorSensorCharacteristic()
# Decode: raw bytes → structured data
raw_data = bytearray([0xC4, 0x09, 0xDC, 0x0D, 0x5A, 0x00]) # 25°C, 35.5%, 90%
reading = sensor.parse_value(raw_data)
print(
f"Temp: {reading.temperature}°C, Humidity: {reading.humidity}%, Battery: {reading.battery}%"
)
# Encode: structured data → raw bytes
new_reading = SensorReading(temperature=22.5, humidity=45.0, battery=85)
encoded = sensor.build_value(new_reading)
print(f"Encoded: {encoded.hex()}")
Cleanup Custom Registrations¶
from bluetooth_sig.gatt.characteristics import CharacteristicRegistry
# Remove specific registration (if it was registered)
CharacteristicRegistry.unregister_characteristic_class(
"12345678-1234-1234-1234-123456789ABC"
)
# Clear all custom registrations (useful for testing)
CharacteristicRegistry.clear_custom_registrations()
Handling Notifications¶
Parse characteristic data from BLE notifications:
# SKIP: Requires BLE hardware and async context for start_notify
from bluetooth_sig.gatt.characteristics import (
HeartRateMeasurementCharacteristic,
)
heart_rate = HeartRateMeasurementCharacteristic()
def on_notification(sender, data: bytearray):
"""Handle incoming heart rate notification."""
hr_data = heart_rate.parse_value(data)
print(f"Heart Rate: {hr_data.heart_rate} bpm")
if hr_data.rr_intervals:
avg_rr = sum(hr_data.rr_intervals) / len(hr_data.rr_intervals)
print(f"Avg RR interval: {avg_rr:.0f} ms")
# With bleak
await client.start_notify(str(heart_rate.uuid), on_notification)
Context-Dependent Parsing¶
Some characteristics require context from other characteristics. When using
translator.process_services(), dependencies are resolved automatically. For
manual parsing, you can provide context explicitly:
from bluetooth_sig.gatt.characteristics import (
BodySensorLocationCharacteristic,
HeartRateMeasurementCharacteristic,
)
from bluetooth_sig.gatt.context import CharacteristicContext
# ============================================
# SIMULATED DATA - Replace with actual BLE reads
# ============================================
location_bytes = bytearray([0x01]) # Chest sensor location
measurement_bytes = bytearray([0x00, 72]) # 72 bpm heart rate
# Read feature characteristic first
location = BodySensorLocationCharacteristic()
location_value = location.parse_value(location_bytes)
print(f"Sensor location: {location_value}")
# Create context with feature data for dependent characteristics
ctx = CharacteristicContext(
other_characteristics={"body_sensor_location": location_value},
)
# Parse measurement with context
hr = HeartRateMeasurementCharacteristic()
hr_data = hr.parse_value(measurement_bytes, ctx=ctx)
print(f"Heart rate: {hr_data.heart_rate} bpm")
Tip: For automatic dependency resolution, use
translator.process_services()which builds context from all discovered characteristics. See Working with Services for details.
Error Handling¶
Handle parsing errors gracefully:
from bluetooth_sig.gatt.characteristics import BatteryLevelCharacteristic
from bluetooth_sig.gatt.exceptions import (
CharacteristicEncodeError,
CharacteristicParseError,
)
battery = BatteryLevelCharacteristic()
try:
# Invalid data (wrong length)
value = battery.parse_value(bytearray([]))
except CharacteristicParseError as e:
print(f"Parse error: {e}")
try:
# Invalid value (out of range)
encoded = battery.build_value(150) # Battery max is 100
except CharacteristicEncodeError as e:
print(f"Encode error: {e}")
See Also¶
API Overview — When to use each parsing approach
Adding Characteristics — Extend the library with new SIG characteristics
Supported Characteristics — Complete list of implemented characteristics
BLE Integration — Full integration patterns with BLE libraries