Architectural Decision Records¶
This document explains key architectural decisions made in the bluetooth-sig-python library.
ADR-001: Registry-Driven Resolution¶
Context: Need to support 70+ Bluetooth SIG characteristics without manual UUID mapping.
Decision: Automatic discovery via pkgutil.walk_packages() with enum-based registry.
Rationale:
Adding a new characteristic requires zero boilerplate
Compile-time safety through enum validation
Registry stays synchronized with implementation automatically
Consequences:
✅ Zero maintenance overhead for UUID mappings
✅ Type-safe characteristic lookups
⚠️ First access incurs 10-50ms discovery cost (one-time)
Alternatives Considered:
Manual UUID dictionary: High maintenance, error-prone
Module-level registration: Requires explicit imports
ADR-002: Double-Checked Locking for Lazy Initialization¶
Context: Registry initialization is expensive but must be thread-safe in concurrent applications.
Decision: Implement double-checked locking pattern with threading.RLock.
Rationale:
Most accesses are reads after initialization
Lock contention should only occur during first access
RLock allows reentrant calls from same thread
Consequences:
✅ Lock-free reads after initialization (<0.1ms)
✅ Thread-safe lazy loading
⚠️ Slightly more complex initialization code
Performance:
Cold (first access): 10-50ms (one-time cost)
Warm (subsequent): <0.1ms (lock-free fast path)
ADR-003: Template Composition Over Inheritance¶
Context: Many characteristics share identical parsing logic (uint16, float32, etc.).
Decision: Use composition with _template attribute instead of inheritance hierarchies.
Rationale:
Avoids deep inheritance trees
Templates are reusable and testable independently
Clear separation: template handles encoding, characteristic handles validation
Consequences:
✅ Flat class hierarchy
✅ Reusable parsing strategies
✅ Easy to test templates independently
⚠️ Slight indirection in understanding data flow
Example:
from bluetooth_sig.gatt.characteristics.base import BaseCharacteristic
from bluetooth_sig.gatt.characteristics.templates import ScaledSint16Template
class Temperature(BaseCharacteristic):
_template = ScaledSint16Template(scale_factor=0.01) # Composition
expected_length = 2
# Little-endian: [low_byte, high_byte] = [0x00, 0x0A] = 0x0A00 = 2560
# 2560 * 0.01 = 25.6°C
temperature_data = bytearray([0x00, 0x0A]) # 2560 in little-endian
temp_char = Temperature()
result = temp_char.parse_value(temperature_data)
print(f"Temperature: {result}°C") # Temperature: 25.6°C
ADR-004: Declarative Validation¶
Context: Manual validation in every decode_value() method is verbose and inconsistent.
Decision: Use class-level validation attributes that BaseCharacteristic enforces.
Rationale:
DRY principle: validation logic in one place
Consistent error messages across all characteristics
Self-documenting: attributes describe constraints clearly
Consequences:
✅ Reduced boilerplate in characteristic implementations
✅ Consistent validation behaviour
✅ Easy to understand constraints
⚠️ Less flexibility for complex conditional validation
Validation Attributes:
expected_length: Enforce exact byte lengthmin_value/max_value: Range validationallowed_values: Whitelist specific values
ADR-005: Framework-Agnostic Design¶
Context: Users integrate with diverse BLE libraries (bleak, simplepyble, bluepy, etc.).
Decision: Never import BLE backend libraries in src/bluetooth_sig/ code.
Rationale:
Avoids coupling to specific BLE library versions
Users choose their preferred connection manager
Library focuses on standards interpretation only
Consequences:
✅ Works with any BLE library
✅ No dependency version conflicts
✅ Clear separation of concerns
⚠️ Users must handle connection management separately
Boundary:
User's BLE Library → bytes → bluetooth-sig → structured data
ADR-006: Bluetooth SIG Git Submodule¶
Context: Need authoritative Bluetooth SIG specification data.
Decision: Use official bluetooth_sig/ repository as git submodule.
Rationale:
Single source of truth for UUID definitions
Automatic updates when Bluetooth SIG publishes changes
No manual YAML maintenance
Consequences:
✅ Always synchronized with official specifications
✅ Zero maintenance for UUID registry
⚠️ Adds ~5MB to repository size
Mitigation: Clear documentation in README and setup guides.
ADR-007: msgspec for Data Structures¶
Context: Need fast, type-safe data structures for parsed results.
Decision: Use msgspec.Struct for characteristic data classes.
Rationale:
5-10x faster than standard dataclasses
Compact memory representation
Built-in validation and type checking
JSON serialization support
Consequences:
✅ Excellent performance
✅ Strong typing with runtime validation
✅ Easy JSON serialization for APIs
⚠️ Additional dependency
ADR-008: Progressive API Levels¶
Context: Users have varying needs from simple parsing to advanced customization.
Decision: Provide four progressive API levels without breaking changes.
Rationale:
Low barrier to entry (Level 1: direct characteristic usage)
Advanced features available when needed
Backward compatible across levels
API Levels:
Direct characteristic parsing
UUID-based parsing via translator
Metadata queries without parsing
Custom characteristic registration
Consequences:
✅ Easy for beginners
✅ Powerful for advanced users
✅ No breaking changes when advancing levels
ADR-009: Lazy YAML Loading¶
Context: Loading all Bluetooth SIG YAML files at startup is slow.
Decision: Load YAML files on first access per registry type.
Rationale:
Most applications only use subset of characteristics
Startup time must be minimal
Trade startup time for runtime overhead
Consequences:
✅ Fast application startup
✅ Load only what’s needed
⚠️ First UUID lookup is slower (10-30ms)
Optimization: YAML loading is cached after first access.
ADR-010: Singleton Pattern for Translators¶
Context: Multiple translator instances would cause unnecessary resource duplication.
Decision: Implement singleton pattern for BluetoothSIGTranslator and registries.
Rationale:
Registry data should be shared across application
Prevent memory waste from duplicate YAML loading
Simplify API (no need to pass translator instances)
Consequences:
✅ Efficient resource usage
✅ Simple API (
get_instance())✅ Consistent state across application
⚠️ Global state (acceptable for read-only registry data)
Summary¶
These architectural decisions prioritize:
Performance: Lazy loading, caching, efficient data structures
Maintainability: Auto-discovery, declarative patterns, DRY
Flexibility: Framework-agnostic, progressive APIs
Standards Compliance: Official Bluetooth SIG data source
For implementation details, see Internal Architecture.