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¶
Every characteristic follows this pattern:
from bluetooth_sig.gatt.characteristics.base import BaseCharacteristic
from bluetooth_sig.gatt.exceptions import InsufficientDataError, ValueRangeError
class MyCharacteristic(BaseCharacteristic):
"""My Custom Characteristic (UUID: XXXX).
Specification: [Link to specification if available]
"""
def decode_value(self, data: bytearray) -> YourReturnType:
"""Decode characteristic data.
Args:
data: Raw bytes from BLE characteristic
Returns:
Parsed value in appropriate type
Raises:
InsufficientDataError: If data is too short
ValueRangeError: If value is out of range
"""
# 1. Validate length
if len(data) < expected_length:
raise InsufficientDataError(
f"My Characteristic requires at least {expected_length} bytes"
)
# 2. Parse data
value = parse_logic_here(data)
# 3. Validate range
if not is_valid(value):
raise ValueRangeError(f"Value {value} is out of range")
# 4. Return typed result
return value
Example: Simple Characteristic¶
Let's add a custom "Light Level" characteristic that reports brightness as a percentage:
from bluetooth_sig.gatt.characteristics.base import BaseCharacteristic
from bluetooth_sig.gatt.exceptions import InsufficientDataError, ValueRangeError
from bluetooth_sig.types import CharacteristicInfo
from bluetooth_sig.types.uuid import BluetoothUUID
class LightLevelCharacteristic(BaseCharacteristic):
"""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"
)
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
@property
def unit(self) -> str:
"""Return the unit for this characteristic."""
return "%"
Example: Complex Multi-Field Characteristic¶
For characteristics with multiple fields:
from dataclasses import dataclass
from datetime import datetime
@dataclass(frozen=True)
class SensorReading:
"""Multi-field sensor reading."""
temperature: float
humidity: float
pressure: float
timestamp: datetime
class MultiSensorCharacteristic(BaseCharacteristic):
"""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
# Parse timestamp
ts_raw = int.from_bytes(data[8:16], byteorder='little', signed=False)
timestamp = datetime.fromtimestamp(ts_raw)
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.decode_value(bytearray([50]))
assert result == 50
def test_boundary_values(self):
"""Test boundary values."""
char = LightLevelCharacteristic()
assert char.decode_value(bytearray([0])) == 0
assert char.decode_value(bytearray([100])) == 100
def test_insufficient_data(self):
"""Test error on insufficient data."""
char = LightLevelCharacteristic()
with pytest.raises(InsufficientDataError):
char.decode_value(bytearray([]))
def test_out_of_range(self):
"""Test error on out-of-range value."""
char = LightLevelCharacteristic()
with pytest.raises(ValueRangeError):
char.decode_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