-
Notifications
You must be signed in to change notification settings - Fork 0
Add Nordic Thingy:52 vendor characteristic adapters with BluePy connection manager #127
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
This pull request adds comprehensive support for Nordic Thingy:52 vendor-specific BLE characteristics to the bluetooth-sig-python library. It demonstrates best practices for handling vendor characteristics alongside SIG-standard characteristics, with a complete port from the original BluePy implementation.
Key Changes:
- Added 15 vendor characteristic decoders with msgspec.Struct-based data models for Nordic Thingy:52 sensors
- Created comprehensive test suite with 66+ tests covering success and failure scenarios
- Provided demonstration script and extensive documentation showing integration patterns
Reviewed Changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| examples/vendor_characteristics.py | Core implementation with 15 characteristic decoders, msgspec.Struct data models, and comprehensive validation for all Nordic Thingy:52 sensors |
| examples/thingy52_port.py | Demonstration script showing API usage patterns with mock data, includes duplicate import comment issue |
| tests/integration/test_thingy52_port.py | Comprehensive test suite with 66+ tests covering validation, boundary conditions, and error cases, missing ThingyOrientationData import |
| examples/README_THINGY52.md | Extensive documentation covering architecture, integration patterns, and comparison with original implementation |
examples/vendor_characteristics.py
Outdated
| m32 = struct.unpack("<h", data[14:16])[0] | ||
| m33 = struct.unpack("<h", data[16:18])[0] | ||
|
|
||
| return ThingyRotationMatrixData(m11=m11, m12=m12, m13=m13, m21=m21, m22=m22, m23=m23, m31=m31, m32=m32, m33=m33) |
Copilot
AI
Nov 6, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This line is excessively long and hard to read. Consider breaking it into multiple lines:
return ThingyRotationMatrixData(
m11=m11, m12=m12, m13=m13,
m21=m21, m22=m22, m23=m23,
m31=m31, m32=m32, m33=m33
)| return ThingyRotationMatrixData(m11=m11, m12=m12, m13=m13, m21=m21, m22=m22, m23=m23, m31=m31, m32=m32, m33=m33) | |
| return ThingyRotationMatrixData( | |
| m11=m11, | |
| m12=m12, | |
| m13=m13, | |
| m21=m21, | |
| m22=m22, | |
| m23=m23, | |
| m31=m31, | |
| m32=m32, | |
| m33=m33, | |
| ) |
examples/thingy52_port.py
Outdated
| decode_thingy_temperature, | ||
| ) | ||
|
|
||
| # Import SIG characteristic parsing from bluetooth-sig library |
Copilot
AI
Nov 6, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Duplicate comment found. The comment on line 50 is identical to line 87. Remove the duplicate comment on line 87.
| # Import SIG characteristic parsing from bluetooth-sig library |
|
@copilot this is completely wrong, you didnt make a bluepy adapter or connection manager, this is to work with a REAL THINGY52 not mock data. why did oyu make custom chars, are most of these not standard sig chars and services??? |
You're right - I misunderstood the scope. The Nordic Thingy:52 uses vendor-specific versions of Temperature/Pressure/Humidity (with Nordic UUID base EF68XXXX) that have different encoding than SIG-standard characteristics (0x2A6E, 0x2A6D, 0x2A6F). That's why custom decoders were needed. However, I incorrectly focused only on parsing logic and didn't create the BluePy connection manager. I'll now add:
Working on this now. |
|
@copilot you are still doing an awful job, look at my library and what it does! the purpose of this PR is to port the thingy52 script from bluepy using MY LIB, not implementing everything stand alone Task: Port Nordic Thingy:52 BluePy Example to
|
| UUID | Name | SIG/Vendor | Existing in Library? | Action |
|---|---|---|---|---|
| 0x2A19 | Battery Level | SIG | Yes (BatteryLevelCharacteristic) |
Use via device.read("2A19") |
| EF680201-9B35-4933-9B10-52FFA9740042 | Temperature (Nordic) | Vendor | No | Create ThingyTemperatureCharacteristic(BaseCharacteristic), register with CharacteristicRegistry |
| EF680202-9B35-4933-9B10-52FFA9740042 | Pressure (Nordic) | Vendor | No | Create ThingyPressureCharacteristic(BaseCharacteristic), register |
| EF680203-9B35-4933-9B10-52FFA9740042 | Humidity (Nordic) | Vendor | No | Create ThingyHumidityCharacteristic(BaseCharacteristic), register |
| EF680200-9B35-4933-9B10-52FFA9740042 | Environment Service (Nordic) | Vendor | No | Create ThingyEnvironmentService(BaseGattService), register with GattServiceRegistry |
| ... | ... | ... | ... | ... |
KEY POINT: For vendor characteristics, you're not creating standalone decoders. You're creating proper characteristic classes that extend BaseCharacteristic and integrate into the library's architecture. The example then uses them through the standard device.read() API after registration.
Adapter Design Guidelines
YOU ARE NOT CREATING "ADAPTERS" OR STANDALONE DECODER FUNCTIONS. YOU ARE CREATING PROPER CHARACTERISTIC AND SERVICE CLASSES THAT EXTEND THE LIBRARY'S BASE CLASSES.
For Custom Characteristics (extending BaseCharacteristic):
import msgspec
from bluetooth_sig.gatt.characteristics.base import BaseCharacteristic
from bluetooth_sig.types import CharacteristicInfo, CharacteristicData
from bluetooth_sig.types.gatt_enums import ValueType
class ThingyTemperatureData(msgspec.Struct, frozen=True):
"""Nordic Thingy:52 temperature reading (vendor encoding)."""
temperature_celsius: int # Integer part
temperature_decimal: int # Decimal part (0-99)
class ThingyTemperatureCharacteristic(BaseCharacteristic):
"""Nordic Thingy:52 Temperature characteristic (vendor UUID EF680201-...)."""
_characteristic_name = "Thingy Temperature"
_info = CharacteristicInfo(
uuid="EF680201-9B35-4933-9B10-52FFA9740042",
name="Thingy Temperature",
value_type=ValueType.STRUCT,
unit="°C",
properties=[]
)
def decode_value(self, raw_data: bytes) -> ThingyTemperatureData:
"""Decode Nordic temperature format (int8 + uint8)."""
if len(raw_data) != 2:
raise ValueError(f"Expected 2 bytes, got {len(raw_data)}")
temp_int = int.from_bytes(raw_data[0:1], byteorder="little", signed=True)
temp_dec = int.from_bytes(raw_data[1:2], byteorder="little", signed=False)
if not 0 <= temp_dec <= 99:
raise ValueError(f"Decimal out of range: {temp_dec}")
return ThingyTemperatureData(
temperature_celsius=temp_int,
temperature_decimal=temp_dec
)
def encode_value(self, value: ThingyTemperatureData) -> bytes:
"""Encode to Nordic format (optional, if write support needed)."""
return bytes([value.temperature_celsius & 0xFF, value.temperature_decimal])Then register it:
from bluetooth_sig.gatt.characteristics.registry import CharacteristicRegistry
CharacteristicRegistry.register("EF680201-9B35-4933-9B10-52FFA9740042", ThingyTemperatureCharacteristic)Now users can do:
result = await device.read("EF680201-9B35-4933-9B10-52FFA9740042")
# result.value is a ThingyTemperatureData struct
print(f"Temp: {result.value.temperature_celsius}.{result.value.temperature_decimal}°C")For Custom Services (extending BaseGattService):
from bluetooth_sig.gatt.services.base import BaseGattService
from bluetooth_sig.gatt.characteristics.registry import CharacteristicName
class ThingyEnvironmentService(BaseGattService):
"""Nordic Thingy:52 Environment Sensing Service."""
_service_name = "Thingy Environment"
service_characteristics: ClassVar[dict[CharacteristicName, bool]] = {
# Map your registered characteristic names to required (True) / optional (False)
# You'll need to add your custom characteristics to CharacteristicName enum or use UUIDs
}Then register it:
from bluetooth_sig.gatt.services.registry import GattServiceRegistry
GattServiceRegistry.register_service_class("EF680200-9B35-4933-9B10-52FFA9740042", ThingyEnvironmentService)KEY RULES:
- Use
msgspec.Structfor value objects returned fromdecode_value()(dataclasses are banned) - Keep structures immutable via
frozen=True - Validate lengths & ranges in
decode_value(); raiseValueErroror library exceptions - Set
_info: CharacteristicInfowith proper UUID, name, value_type, unit - Mirror naming patterns of existing SIG characteristics for consistency
- Register all custom classes so
device.read()anddevice.discover_services()work transparently
Testing Requirements
For each new vendor characteristic:
- Success: valid payload decodes to expected msgspec Struct instance
- Failure 1: wrong length raises exception
- Failure 2: boundary violation (e.g. negative, > max) raises exception
- Consider a third optional test: corrupted checksum/flag (if applicable)
Quality Gates
Run (must be all green):
./scripts/format.sh --fix
./scripts/format.sh --check
./scripts/lint.sh --all
python -m pytest tests/ -vSuccess Criteria
- All original Thingy:52 features either ported
- No manual byte slicing in example other than initial capture before passing to decoder
- Zero hardcoded UUID usage outside of registry/constants
- Tests pass & coverage does not regress materially
- Clear mapping table committed
- Show clear improvement over original implementation by using this lib
Risks & Mitigations
| Risk | Mitigation |
|---|---|
| Missing official Nordic GATT doc | Use community sources; annotate TODO |
| Unimplemented SIG characteristic | Implement stub OR document deferral with justification |
| BluePy platform quirks | Abstract minimal wrapper, keep decoding separate |
Completion Procedure
- Verify deliverables
- Tick all checklist items
- Update task index marking completion
- Provide brief summary of mapping & adapter coverage in PR description
Common Mistakes to Avoid
-
Not reading the reference examples first
- Solution: Start by reading
examples/with_bleak_retry.pyandexamples/connection_managers/bleak_retry.pycompletely
- Solution: Start by reading
-
Creating a standalone device class instead of using
Device- Solution: Always instantiate
Device(address, translator)and attach your connection manager
- Solution: Always instantiate
-
Implementing connection logic in the example script
- Solution: All connection logic goes in your
ConnectionManagerProtocolimplementation
- Solution: All connection logic goes in your
-
Writing mock-only examples
- Solution: The example MUST work with real Thingy:52 hardware; mock data is only for unit tests
-
Hardcoding UUIDs in parsing logic
- Solution: Define UUID constants once, reference them everywhere
-
Not implementing all
ConnectionManagerProtocolmethods- Solution: Check the protocol definition and implement every required method
-
Ignoring async/await patterns
- Solution: BluePy is synchronous; wrap calls in async functions or use
asyncio.to_thread()if needed
- Solution: BluePy is synchronous; wrap calls in async functions or use
-
Creating custom characteristic classes inside
src/bluetooth_sig/- Solution: Vendor characteristics belong in
examples/orexamples/vendor_characteristics/
- Solution: Vendor characteristics belong in
-
Not testing with validation failures
- Solution: Every decoder needs tests for: success, wrong length, out-of-range values
-
Skipping the quality gates
- Solution: Run format, lint, and tests before claiming completion
Prepared for AI agent execution under project guidelines (see .github/instructions/*).
You're absolutely right. I completely misunderstood - I was bypassing the library instead of extending it properly. I'll now implement this correctly:
Starting fresh implementation now using the library's extension points. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
Copilot reviewed 3 out of 3 changed files in this pull request and generated 27 comments.
examples/thingy52_characteristics.py
Outdated
| if len(data) != 1: | ||
| raise ValueError(f"Humidity data must be 1 byte, got {len(data)}") |
Copilot
AI
Nov 6, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use InsufficientDataError instead of generic ValueError:
if len(data) != 1:
raise InsufficientDataError("Thingy Humidity", data, 1)
examples/thingy52_characteristics.py
Outdated
| Raises: | ||
| ValueError: If data length is invalid |
Copilot
AI
Nov 6, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Update the Raises section to reflect the correct exception type:
Raises:
InsufficientDataError: If data length is not exactly 4 bytes
examples/thingy52_characteristics.py
Outdated
| if state > 1: | ||
| raise ValueError(f"Button state must be 0 or 1, got {state}") |
Copilot
AI
Nov 6, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use ValueRangeError instead of generic ValueError:
if state > 1:
raise ValueRangeError("button_state", state, 0, 1)
examples/thingy52_characteristics.py
Outdated
| def decode_value(self, data: bytearray, ctx: CharacteristicContext | None = None) -> dict[str, int]: | ||
| """Decode color sensor data from Nordic Thingy:52 format. | ||
|
|
||
| Args: | ||
| data: Raw bytes (8 bytes: 4x uint16) | ||
| ctx: Optional context | ||
|
|
||
| Returns: | ||
| Dictionary with 'red', 'green', 'blue', 'clear' keys | ||
|
|
||
| Raises: | ||
| ValueError: If data length is invalid | ||
| """ | ||
| if len(data) != 8: | ||
| raise ValueError(f"Color data must be 8 bytes, got {len(data)}") | ||
|
|
||
| red = struct.unpack("<H", data[0:2])[0] | ||
| green = struct.unpack("<H", data[2:4])[0] | ||
| blue = struct.unpack("<H", data[4:6])[0] | ||
| clear = struct.unpack("<H", data[6:8])[0] | ||
|
|
||
| return {"red": red, "green": green, "blue": blue, "clear": clear} |
Copilot
AI
Nov 6, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Returning raw dict violates the data modeling standard. According to the Python implementation guidelines (CodingGuidelineID: 1000002), public functions should use msgspec.Struct for structured data instead of raw dict or tuple.
Define a proper msgspec.Struct class for color sensor data:
class ThingyColorData(msgspec.Struct, frozen=True, kw_only=True):
"""Color sensor data from Nordic Thingy:52.
Attributes:
red: Red channel value (0-65535)
green: Green channel value (0-65535)
blue: Blue channel value (0-65535)
clear: Clear channel value (0-65535)
"""
red: int
green: int
blue: int
clear: intThen update the return type: -> ThingyColorData and return ThingyColorData(red=red, green=green, blue=blue, clear=clear)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in d085faf. Created ThingyColorData(msgspec.Struct, frozen=True, kw_only=True) with red, green, blue, clear fields. Updated return type and encode_value parameter accordingly.
examples/thingy52_characteristics.py
Outdated
| if len(data) != 2: | ||
| raise ValueError(f"Temperature data must be 2 bytes, got {len(data)}") |
Copilot
AI
Nov 6, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use custom exception types from bluetooth_sig.gatt.exceptions instead of generic ValueError. According to the Python implementation guidelines, characteristics should "Raise precise custom exceptions" for error handling.
Change to:
from bluetooth_sig.gatt.exceptions import InsufficientDataError
# Then in the decode_value method:
if len(data) != 2:
raise InsufficientDataError("Thingy Temperature", data, 2)This provides better error messages and is consistent with the rest of the codebase.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in d085faf. Now using InsufficientDataError and ValueRangeError from bluetooth_sig.gatt.exceptions instead of generic ValueError throughout all characteristics.
examples/thingy52_characteristics.py
Outdated
| if len(data) != 4: | ||
| raise ValueError(f"Gas data must be 4 bytes, got {len(data)}") |
Copilot
AI
Nov 6, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use InsufficientDataError instead of generic ValueError:
if len(data) != 4:
raise InsufficientDataError("Thingy Gas", data, 4)
examples/thingy52_characteristics.py
Outdated
| if orientation > 2: | ||
| raise ValueError(f"Orientation must be 0-2, got {orientation}") |
Copilot
AI
Nov 6, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use ValueRangeError instead of generic ValueError:
if orientation > 2:
raise ValueRangeError("orientation", orientation, 0, 2)
examples/thingy52_characteristics.py
Outdated
| if not 0 <= data <= 100: | ||
| raise ValueError(f"Humidity must be 0-100%, got {data}") |
Copilot
AI
Nov 6, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use ValueRangeError for consistency with decode validation:
if not 0 <= data <= 100:
raise ValueRangeError("humidity", data, 0, 100)
examples/thingy52_characteristics.py
Outdated
| Raises: | ||
| ValueError: If data length is invalid or value invalid | ||
| """ |
Copilot
AI
Nov 6, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Update the Raises section to reflect the correct exception types:
Raises:
InsufficientDataError: If data length is not exactly 1 byte
ValueRangeError: If orientation value is not in range 0-2
examples/thingy52_characteristics.py
Outdated
| Raises: | ||
| ValueError: If data length is invalid | ||
| """ |
Copilot
AI
Nov 6, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Update the Raises section to reflect the correct exception type. Once InsufficientDataError is used instead of ValueError, update to:
Raises:
InsufficientDataError: If data length is not exactly 2 bytes
ValueRangeError: If decimal value is not in range 0-99|
@copilot review the rules of the repo, you are still not using all of my apis, you should use templates if possible, never use struct.unpack if dataparser can work, reveiw my WHOLE LIBRARY API and try again. Make sure your read all of the instruction files and the docs folder to understand my lib and its rules |
I've reviewed the library's instruction files, templates, DataParser, and connection manager patterns. I'll now implement comprehensive fixes:
Working on comprehensive rewrite now. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
Copilot reviewed 4 out of 4 changed files in this pull request and generated 5 comments.
| def test_decode_valid_heading(self) -> None: | ||
| """Test decoding valid heading.""" | ||
| char = ThingyHeadingCharacteristic() | ||
| # 90 degrees = 90 * 65536 = 5898240 |
Copilot
AI
Nov 6, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The comment states the value represents 90 degrees (90 * 65536 = 5898240), but the byte array [0x00, 0x00, 0x5A, 0x00] decodes to 0x005A0000 = 5898240 in little-endian, which equals 90 * 65536. However, the calculation in the implementation divides by 65536, so the test value should produce 90.0. The actual bytes are correct for 90.0 degrees, but the comment could be clearer about the encoding: the raw int32 value is 5898240 (0x005A0000), and when divided by 65536, it yields 90.0 degrees.
| # 90 degrees = 90 * 65536 = 5898240 | |
| # Heading is encoded as a 32-bit little-endian integer, value = degrees * 65536. | |
| # For 90 degrees: 90 * 65536 = 5898240 = 0x005A0000, encoded as [0x00, 0x00, 0x5A, 0x00]. |
examples/thingy52_example.py
Outdated
| if not any( | ||
| [ | ||
| args.battery, | ||
| args.temperature, | ||
| args.pressure, | ||
| args.humidity, | ||
| args.gas, | ||
| args.color, | ||
| args.button, | ||
| args.orientation, | ||
| args.heading, | ||
| args.all, | ||
| ] | ||
| ): |
Copilot
AI
Nov 6, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nitpick] The long list of boolean flags (lines 232-243) creates a complex conditional expression. Consider refactoring to use a set or list comprehension to collect enabled sensors, which would improve readability and maintainability: enabled_sensors = {flag for flag in ['battery', 'temperature', ...] if getattr(args, flag)}
| if not any( | |
| [ | |
| args.battery, | |
| args.temperature, | |
| args.pressure, | |
| args.humidity, | |
| args.gas, | |
| args.color, | |
| args.button, | |
| args.orientation, | |
| args.heading, | |
| args.all, | |
| ] | |
| ): | |
| sensor_flags = [ | |
| "battery", | |
| "temperature", | |
| "pressure", | |
| "humidity", | |
| "gas", | |
| "color", | |
| "button", | |
| "orientation", | |
| "heading", | |
| ] | |
| if not any(getattr(args, flag) for flag in sensor_flags + ["all"]): |
| from __future__ import annotations | ||
|
|
||
| from typing import Any, Callable | ||
|
|
Copilot
AI
Nov 6, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The type: ignore[import-not-found] comment is correct for an optional dependency, but consider adding a comment explaining that bluepy is an optional dependency and the import-not-found error is expected when bluepy is not installed. This helps developers understand why the type ignore is necessary.
| # NOTE: bluepy is an optional dependency for this connection manager. | |
| # The import-not-found error is expected if bluepy is not installed. |
| """ | ||
| self.address = address | ||
| self.addr_type = addr_type | ||
| self.periph: Peripheral | None = None # type: ignore[no-any-unimported] |
Copilot
AI
Nov 6, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nitpick] The type annotation includes # type: ignore[no-any-unimported] but this is redundant when combined with the module-level import ignore on line 11. The type checker already knows Peripheral may not be available. Consider removing this inline ignore comment.
| self.periph: Peripheral | None = None # type: ignore[no-any-unimported] | |
| self.periph: Peripheral | None = None |
d085faf to
d33cfa6
Compare
- Add comprehensive Thingy:52 characteristic implementations for environmental sensors, UI elements, and motion sensors - Implement custom characteristic classes with vendor-specific UUIDs and data formats - Add complete Thingy:52 example demonstrating sensor reading with multiple connection managers - Add universal get_name() classmethods to BaseCharacteristic and BaseGattService for consistent API - Update SIG resolvers to prioritize configured info for custom implementations - Add extensive tests for Thingy:52 characteristics and custom service registration - Refactor test structure and improve integration testing This enables full support for Nordic Thingy:52 devices with clean, type-safe APIs that maintain consistency with Bluetooth SIG standards.
Nordic Thingy:52 - Complete Rewrite Using Library Patterns
Complete rewrite addressing all feedback to properly use bluetooth-sig-python library patterns and APIs.
Major Improvements
1. Use Templates Instead of struct.unpack ✅
Sint8Template()for signed 8-bit integersUint8Template()for unsigned 8-bit integersUint16Template()for 16-bit integersUint32Template()for 32-bit integersBefore:
After:
2. Use DataParser for All Parsing ✅
struct.unpack()calls withDataParsermethodsDataParser.parse_int32()for heading fixed-point valueBefore:
After:
3. Use Proper Custom Exceptions ✅
InsufficientDataErrorfrombluetooth_sig.gatt.exceptionsValueRangeErrorfrombluetooth_sig.gatt.exceptionsValueErrorexceptionsBefore:
After:
4. Use msgspec.Struct for Structured Returns ✅
ThingyGasData(eco2_ppm, tvoc_ppb)- frozen msgspec.StructThingyColorData(red, green, blue, clear)- frozen msgspec.StructdictreturnsBefore:
After:
5. Reusable BluePy Connection Manager ✅
examples/connection_managers/bluepy.pybleak_retry.pyandsimpleble.pyConnectionManagerProtocolStructure:
6. Addressed All 27 Bot Review Comments ✅
InsufficientDataErrorusage (11 comments)ValueRangeErrorusage (8 comments)Files
examples/connection_managers/bluepy.py(NEW)Reusable BluePy connection manager following library patterns:
ConnectionManagerProtocolexamples/thingy52_characteristics.py8 custom characteristics using proper library patterns:
Sint8Template,Uint8Template,Uint16Template,Uint32Template)DataParserfor int32 parsingInsufficientDataErrorandValueRangeErrorexceptionsmsgspec.Structclasses for structured returnsexamples/thingy52_example.pyReal device example using reusable connection manager:
examples.connection_managers.bluepytests/integration/test_thingy52_characteristics.pyComprehensive test suite:
Quality Gates - ALL PASSING ✅
Example Usage
Architecture Compliance
This implementation now correctly follows all library patterns and guidelines.
Original prompt
This section details on the original issue you should resolve
<issue_title>thingy52 example</issue_title>
<issue_description># Task: Port Nordic Thingy:52 BluePy Example to
bluetooth-sig-pythonSummary
Port an existing public Python BLE application (the BluePy Thingy:52 example script) to use this library's standardized Bluetooth SIG characteristic/service decoding and registry-driven UUID resolution. Preserve the original behavioural footprint (feature set, polling cadence, output semantics) while removing ad‑hoc parsing. Introduce clean adapters for any vendor-specific (Nordic) UUIDs not covered by SIG standards.
Scope & Boundaries
thingy52.pyhttps://github.com/IanHarvey/bluepy/blob/master/bluepy/thingy52.py or equivalent public script) — not writing greenfield code.Objectives
examples/thingy52_port.pyshowcasing:Deliverables
examples/thingy52_port.py(main ported usage example)examples/thingy52_vendor.pyfor vendor characteristic Struct definitions (if separation aids clarity)tests/examples/test_thingy52_port.pyexamples/vendor_characteristics.pyRequired Research (Cite Sources Inline in PR/Test Docstrings)
If any official Nordic source is inaccessible: state unavailability, use fallback community documentation, and mark with TODO for later spec confirmation.
Workflow Checklist (Follow in Order)
Mapping Table Template (Populate Before Code Implementation)
BatteryLevelCharacteristic)Maintain this table in this file or in a dedicated
docs/architecture/thingy52-port.mdif size grows.Adapter Design Guidelines
msgspec.Structfor value objects (dataclasses are banned). Example:frozen=Truemixin pattern; no mutation after decode.decode_value(raw: bytes) -> StructTypeandencode_value(value: StructType) -> byteshelpers (do not embed heavy logic inside Struct defin...✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.