Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,9 @@ The translation layer must remain framework-agnostic to support multiple backend

### 8. Quality Gates Must Pass
**All linting must pass before claiming completion.**
- Run `./scripts/lint.sh --all` before every completion
- Run `./scripts/lint.sh --all` before every completion, docs don't need this linter script to be ran
- Fix issues, don't suppress them unless documented
- The linter script is slow, so grepping or tailing the output is banned, pipe it to a file and read the file instead
- Never hide real problems with disables
- If fixing linting errors, rerun only that linting tool to speed up iteration, i.e. `./scripts/lint.sh --mypy`. Then rerun all at the end.

Expand Down
26 changes: 14 additions & 12 deletions .github/instructions/python-implementation.instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,27 @@ applyTo: "**/*.py,**/pyproject.toml,**/requirements*.txt,**/setup.py"

# Python Implementation Guidelines

## CRITICAL MUST READ - Prohibited Practices

- Hardcoded UUIDs (use registry resolution)
- Conditional imports for core logic
- Use of TYPE_CHECKING
- Non top-level or lazy imports
- Untyped public function signatures
- Using hasattr or getattr when direct attribute access is possible
- Silent exception pass / bare `except:`
- Returning unstructured `dict` / `tuple` when a msgspec.Struct fits
- Magic numbers without an inline named constant or spec citation
- Parsing without pre-validating length

## Type Safety (ABSOLUTE REQUIREMENT)

**Every public function MUST have complete, explicit type hints.**

- ❌ **FORBIDDEN**: `def parse(data)` or `def get_value(self)`
- ✅ **REQUIRED**: `def parse(data: bytes) -> BatteryLevelData` and `def get_value(self) -> int | None`
- Return types are MANDATORY - no implicit returns
- Use modern union syntax: `Type | None` not `Optional[Type]`
- Use modern union syntax: `Type | None` not `Optional[Type]`. Use `from __future__ import annotations` for forward refs.
- Use msgspec.Struct for structured data - NEVER return raw `dict` or `tuple`
- `Any` type requires inline justification comment explaining why typing is impossible
- No gradual typing - all parameters and returns must be typed from the start
Expand Down Expand Up @@ -227,17 +240,6 @@ from bluetooth_sig.types import CharacteristicContext
from .base import BaseCharacteristic
```

## Prohibited Practices

- Hardcoded UUIDs (use registry resolution)
- Conditional imports for core logic
- Untyped public function signatures
- Using hasattr or getattr when direct attribute access is possible
- Silent exception pass / bare `except:`
- Returning unstructured `dict` / `tuple` when a msgspec.Struct fits
- Magic numbers without an inline named constant or spec citation
- Parsing without pre-validating length

## Quality Gates

**Before claiming completion:**
Expand Down
3 changes: 2 additions & 1 deletion .github/workflows/copilot-setup-steps.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,8 @@ jobs:
- name: Install system dependencies
run: |
sudo apt-get update
sudo apt-get install -y python3-dev
# Install build deps to compile C extensions like bluepy
sudo apt-get install -y build-essential cmake ninja-build pkg-config libdbus-1-dev libglib2.0-dev libudev-dev libbluetooth-dev python3-dev

- name: Install Python dependencies
run: |
Expand Down
3 changes: 2 additions & 1 deletion .github/workflows/lint-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,8 @@ jobs:
- name: 'Install system dependencies'
run: |
sudo apt-get update
sudo apt-get install -y shellcheck
# Install build deps to compile C extensions like bluepy
sudo apt-get install -y build-essential cmake ninja-build pkg-config libdbus-1-dev libglib2.0-dev libudev-dev libbluetooth-dev python3-dev

- name: 'Install Python dependencies'
run: |
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/test-coverage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ jobs:
- name: Install system dependencies
run: |
sudo apt-get update
sudo apt-get install -y build-essential cmake ninja-build pkg-config libdbus-1-dev libglib2.0-dev libudev-dev python3-dev
sudo apt-get install -y build-essential cmake ninja-build pkg-config libdbus-1-dev libglib2.0-dev libudev-dev libbluetooth-dev python3-dev

- name: Install Python dependencies
run: |
Expand Down Expand Up @@ -103,7 +103,7 @@ jobs:
- name: Install system dependencies
run: |
sudo apt-get update
sudo apt-get install -y build-essential cmake ninja-build pkg-config libdbus-1-dev libglib2.0-dev libudev-dev python3-dev
sudo apt-get install -y build-essential cmake ninja-build pkg-config libdbus-1-dev libglib2.0-dev libudev-dev libbluetooth-dev python3-dev

- name: Install dependencies
run: |
Expand Down
22 changes: 19 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ A pure Python library for Bluetooth SIG standards interpretation, providing comp

- ✅ **Standards-Based**: Official Bluetooth SIG YAML registry with automatic UUID resolution
- ✅ **Type-Safe**: Convert raw Bluetooth data to meaningful values with comprehensive typing
- ✅ **Modern Python**: Dataclass-based design with Python 3.9+ compatibility
- ✅ **Modern Python**: msgspec-based design with Python 3.9+ compatibility
- ✅ **Comprehensive**: Support for 70+ GATT characteristics across multiple service categories
- ✅ **Production Ready**: Extensive validation and comprehensive testing
- ✅ **Framework Agnostic**: Works with any BLE library (bleak, simplepyble, etc.)
Expand All @@ -38,8 +38,23 @@ print(service_info.name) # "Battery Service"
## Parse characteristic data

```python
battery_data = translator.parse_characteristic("2A19", bytearray([85]), descriptor_data=None)
# ============================================
# SIMULATED DATA - Replace with actual BLE read
# ============================================
SIMULATED_BATTERY_DATA = bytearray([85]) # Simulates 85% battery

# Use UUID from your BLE library
battery_data = translator.parse_characteristic(
"2A19", # UUID from your BLE library
SIMULATED_BATTERY_DATA
)
print(f"Battery: {battery_data.value}%") # "Battery: 85%"

# Alternative: Use CharacteristicName enum - convert to UUID first
from bluetooth_sig.types.gatt_enums import CharacteristicName
battery_uuid = translator.get_characteristic_uuid_by_name(CharacteristicName.BATTERY_LEVEL)
if battery_uuid:
result2 = translator.parse_characteristic(str(battery_uuid), SIMULATED_BATTERY_DATA)
```

## What This Library Does
Expand All @@ -62,6 +77,7 @@ print(f"Battery: {battery_data.value}%") # "Battery: 85%"
Works seamlessly with any BLE connection library:

```python
# SKIP: Requires BLE hardware and connection setup
from bleak import BleakClient
from bluetooth_sig import BluetoothSIGTranslator

Expand All @@ -72,7 +88,7 @@ async with BleakClient(address) as client:
raw_data = await client.read_gatt_char("2A19")

# bluetooth-sig handles parsing
result = translator.parse_characteristic("2A19", raw_data, descriptor_data=None)
result = translator.parse_characteristic("2A19", raw_data)
print(f"Battery: {result.value}%")
```

Expand Down
19 changes: 15 additions & 4 deletions docs/api/core.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,26 @@ The core API provides the main entry point for using the Bluetooth SIG Standards
```python
from bluetooth_sig import BluetoothSIGTranslator

# ============================================
# SIMULATED DATA - Replace with actual BLE read
# ============================================
SIMULATED_BATTERY_DATA = bytearray([85]) # Simulates 85% battery

translator = BluetoothSIGTranslator()

# Parse battery level - returns CharacteristicData
result = translator.parse_characteristic("2A19", bytearray([85]))
# Parse battery level using UUID from your BLE library - returns CharacteristicData
result = translator.parse_characteristic("2A19", SIMULATED_BATTERY_DATA)
print(f"Battery: {result.value}%") # Battery: 85%
print(f"Unit: {result.info.unit}") # Unit: %

# Alternative: Use CharacteristicName enum - convert to UUID first
from bluetooth_sig.types.gatt_enums import CharacteristicName
battery_uuid = translator.get_characteristic_uuid_by_name(CharacteristicName.BATTERY_LEVEL)
if battery_uuid:
result2 = translator.parse_characteristic(str(battery_uuid), SIMULATED_BATTERY_DATA)
```

The [parse_characteristic][bluetooth_sig.BluetoothSIGTranslator.parse_characteristic] method returns a [CharacteristicData][bluetooth_sig.types.CharacteristicData] object.
The [parse_characteristic][bluetooth_sig.BluetoothSIGTranslator.parse_characteristic] method returns a [CharacteristicData][bluetooth_sig.gatt.characteristics.base.CharacteristicData] object.

### UUID Resolution

Expand Down Expand Up @@ -81,7 +92,7 @@ except ValueRangeError:

These types are returned by the core API methods:

::: bluetooth_sig.types.CharacteristicData
::: bluetooth_sig.gatt.characteristics.base.CharacteristicData
options:
show_root_heading: true
heading_level: 3
Expand Down
31 changes: 24 additions & 7 deletions docs/api/gatt.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,13 @@ Use [GattServiceRegistry][] to register custom services.
```python
from bluetooth_sig.gatt.characteristics import BatteryLevelCharacteristic

# ============================================
# SIMULATED DATA - Replace with actual BLE read
# ============================================
SIMULATED_BATTERY_DATA = bytearray([85]) # Simulates 85% battery

char = BatteryLevelCharacteristic()
value = char.decode_value(bytearray([85]))
value = char.decode_value(SIMULATED_BATTERY_DATA)
print(f"Battery: {value}%") # Battery: 85%
```

Expand All @@ -79,8 +84,13 @@ print(f"Battery: {value}%") # Battery: 85%
```python
from bluetooth_sig.gatt.characteristics import TemperatureCharacteristic

# ============================================
# SIMULATED DATA - Replace with actual BLE read
# ============================================
SIMULATED_TEMP_DATA = bytearray([0x64, 0x09]) # Simulates 24.36°C

char = TemperatureCharacteristic()
value = char.decode_value(bytearray([0x64, 0x09]))
value = char.decode_value(SIMULATED_TEMP_DATA)
print(f"Temperature: {value}°C") # Temperature: 24.36°C
```

Expand All @@ -89,8 +99,13 @@ print(f"Temperature: {value}°C") # Temperature: 24.36°C
```python
from bluetooth_sig.gatt.characteristics import HumidityCharacteristic

# ============================================
# SIMULATED DATA - Replace with actual BLE read
# ============================================
SIMULATED_HUMIDITY_DATA = bytearray([0x3A, 0x13]) # Simulates 49.42%

char = HumidityCharacteristic()
value = char.decode_value(bytearray([0x3A, 0x13]))
value = char.decode_value(SIMULATED_HUMIDITY_DATA)
print(f"Humidity: {value}%") # Humidity: 49.42%
```

Expand All @@ -101,11 +116,12 @@ print(f"Humidity: {value}%") # Humidity: 49.42%
Raised when data is too short for the characteristic.

```python
from bluetooth_sig.gatt.exceptions import InsufficientDataError
from bluetooth_sig.gatt.characteristics import BatteryLevelCharacteristic

char = BatteryLevelCharacteristic()
try:
char.decode_value(bytearray([])) # Empty
except InsufficientDataError as e:
except ValueError as e:
print(f"Error: {e}")
```

Expand All @@ -114,11 +130,12 @@ except InsufficientDataError as e:
Raised when value is outside valid range.

```python
from bluetooth_sig.gatt.exceptions import ValueRangeError
from bluetooth_sig.gatt.characteristics import BatteryLevelCharacteristic

char = BatteryLevelCharacteristic()
try:
char.decode_value(bytearray([150])) # > 100%
except ValueRangeError as e:
except ValueError as e:
print(f"Error: {e}")
```

Expand Down
19 changes: 10 additions & 9 deletions docs/api/registry.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,15 +35,15 @@ from bluetooth_sig.types.gatt_enums import CharacteristicName, ServiceName

# Get characteristic info
char_info = uuid_registry.get_characteristic_info(
CharacteristicName.BATTERY_LEVEL
CharacteristicName.BATTERY_LEVEL.value
)
print(char_info.uuid) # "2A19"
print(char_info.name) # "Battery Level"

# Get service info
service_info = uuid_registry.get_service_info(ServiceName.BATTERY_SERVICE)
service_info = uuid_registry.get_service_info(ServiceName.BATTERY.value)
print(service_info.uuid) # "180F"
print(service_info.name) # "Battery Service"
print(service_info.name) # "Battery"
```

## Enumerations
Expand All @@ -59,31 +59,32 @@ CharacteristicName.TEMPERATURE # "Temperature"
CharacteristicName.HUMIDITY # "Humidity"

# Services
ServiceName.BATTERY_SERVICE # "Battery Service"
ServiceName.BATTERY # "Battery"
ServiceName.ENVIRONMENTAL_SENSING # "Environmental Sensing"
ServiceName.DEVICE_INFORMATION # "Device Information"
```

See [CharacteristicData][bluetooth_sig.types.CharacteristicData], [CharacteristicInfo][bluetooth_sig.types.CharacteristicInfo], and [ServiceInfo][bluetooth_sig.types.ServiceInfo] in the [Core API](core.md) for type definitions.
See [CharacteristicData][bluetooth_sig.gatt.characteristics.base.CharacteristicData], [CharacteristicInfo][bluetooth_sig.types.CharacteristicInfo], and [ServiceInfo][bluetooth_sig.types.ServiceInfo] in the [Core API](core.md) for type definitions.

## Custom Registration

Register custom characteristics and services:

```python
# SKIP: Example of custom registration API - requires custom classes to be defined
from bluetooth_sig import BluetoothSIGTranslator

translator = BluetoothSIGTranslator()

# Register custom characteristic
translator.register_custom_characteristic(
uuid="ACME0001",
translator.register_custom_characteristic_class(
uuid="12345678",
characteristic_class=MyCustomCharacteristic
)

# Register custom service
translator.register_custom_service(
uuid="ACME1000",
translator.register_custom_service_class(
uuid="ABCD1234",
service_class=MyCustomService
)
```
Expand Down
Loading
Loading