Adding New Characteristics¶
Learn how to extend the library with custom or newly standardized characteristics.
When to Add a Characteristic¶
You might need to add a characteristic when:
A new Bluetooth SIG standard characteristic is released
You’re working with a vendor-specific characteristic
You need custom parsing for a specific use case
Basic Structure¶
SIG characteristics extend :class:~bluetooth_sig.gatt.characteristics.base.BaseCharacteristic and are auto-discovered by UUID in the docstring:
from bluetooth_sig.gatt.characteristics.base import BaseCharacteristic
from bluetooth_sig.gatt.characteristics.utils.data_parser import DataParser
from bluetooth_sig.gatt.context import CharacteristicContext
class MyCharacteristic(BaseCharacteristic):
"""My Characteristic (0x2AXX).
org.bluetooth.characteristic.my_characteristic
"""
expected_length: int = 2
min_value: float = 0.0
max_value: float = 100.0
expected_type: type = float
def _decode_value(
self, data: bytearray, ctx: CharacteristicContext | None = None
) -> float:
raw = DataParser.parse_int16(data, 0, signed=False)
return raw * 0.01
def _encode_value(self, data: float) -> bytearray:
return DataParser.encode_int16(int(data / 0.01), signed=False)
Custom Characteristics¶
For vendor-specific characteristics, extend :class:~bluetooth_sig.gatt.characteristics.custom.CustomBaseCharacteristic:
from bluetooth_sig.gatt.characteristics.custom import CustomBaseCharacteristic
from bluetooth_sig.gatt.exceptions import (
InsufficientDataError,
ValueRangeError,
)
from bluetooth_sig.types import CharacteristicInfo
from bluetooth_sig.types.gatt_enums import ValueType
from bluetooth_sig.types.uuid import BluetoothUUID
class LightLevelCharacteristic(CustomBaseCharacteristic):
"""Light Level characteristic.
Reports ambient light level as a percentage (0-100%).
Format:
- 1 byte: uint8
- Range: 0-100
- Unit: percentage
"""
_info = CharacteristicInfo(
uuid=BluetoothUUID("ABCD"), # Your custom UUID
name="Light Level",
unit="%",
)
def _decode_value(self, data: bytearray) -> int:
"""Decode light level.
Args:
data: Raw bytes (1 byte expected)
Returns:
Light level percentage (0-100)
"""
# Validate length
if len(data) != 1:
raise InsufficientDataError(
f"Light Level requires exactly 1 byte, got {len(data)}"
)
# Parse
value = int(data[0])
# Validate range
if not 0 <= value <= 100:
raise ValueRangeError(f"Light level must be 0-100%, got {value}%")
return value
Example: Complex Multi-Field Characteristic¶
For characteristics with multiple fields:
from datetime import datetime
import msgspec
class SensorReading(msgspec.Struct, frozen=True, kw_only=True):
"""Multi-field sensor reading."""
temperature: float
humidity: float
pressure: float
timestamp: datetime
class MultiSensorCharacteristic(CustomBaseCharacteristic):
"""Multi-sensor characteristic with multiple fields."""
_info = CharacteristicInfo(uuid=BluetoothUUID("WXYZ"), name="Multi Sensor")
def _decode_value(self, data: bytearray) -> SensorReading:
"""Decode multi-field sensor data.
Format:
Bytes 0-1: Temperature (sint16, 0.01°C)
Bytes 2-3: Humidity (uint16, 0.01%)
Bytes 4-7: Pressure (uint32, 0.1 Pa)
Bytes 8-15: Timestamp (64-bit Unix timestamp)
"""
# Validate length
if len(data) != 16:
raise InsufficientDataError(
f"Multi Sensor requires 16 bytes, got {len(data)}"
)
# Parse temperature
temp_raw = int.from_bytes(data[0:2], byteorder="little", signed=True)
temperature = temp_raw * 0.01
# Parse humidity
hum_raw = int.from_bytes(data[2:4], byteorder="little", signed=False)
humidity = hum_raw * 0.01
# Parse pressure
press_raw = int.from_bytes(data[4:8], byteorder="little", signed=False)
pressure = press_raw * 0.1
# SKIP: Incomplete class definition example
# Parse timestamp
ts_raw = int.from_bytes(data[8:16], byteorder="little", signed=False)
timestamp = ts_raw # Unix timestamp
return SensorReading(
temperature=temperature,
humidity=humidity,
pressure=pressure,
timestamp=timestamp,
)
Testing Your Characteristic¶
Always add tests for your custom characteristic:
import pytest
from bluetooth_sig.gatt.exceptions import (
InsufficientDataError,
ValueRangeError,
)
class TestLightLevelCharacteristic:
"""Test Light Level characteristic."""
def test_valid_value(self):
"""Test valid light level."""
char = LightLevelCharacteristic()
result = char.parse_value(bytearray([50]))
assert result == 50
def test_boundary_values(self):
"""Test boundary values."""
char = LightLevelCharacteristic()
assert char.parse_value(bytearray([0])) == 0
assert char.parse_value(bytearray([100])) == 100
def test_insufficient_data(self):
"""Test error on insufficient data."""
char = LightLevelCharacteristic()
with pytest.raises(InsufficientDataError):
char.parse_value(bytearray([]))
def test_out_of_range(self):
"""Test error on out-of-range value."""
char = LightLevelCharacteristic()
with pytest.raises(ValueRangeError):
char.parse_value(bytearray([101]))
Contributing Back¶
If you’ve added a standard Bluetooth SIG characteristic, consider contributing it back:
Ensure your implementation follows the official specification
Add comprehensive tests
Add proper docstrings
Open a pull request
See Contributing Guide for details.
See Also¶
Architecture - Understand the design
Testing Guide - Testing best practices
API Reference - GATT layer details