Skip to content

espressif: Issue with initial value for remote BLE/GATT characteristic in bleio #10683

@juergenpabel

Description

@juergenpabel

CircuitPython version and board name

Adafruit CircuitPython 10.0.3 on 2025-10-17; M5Stack DinMeter with ESP32S3

Code/REPL

import _bleio

from adafruit_ble import BLERadio
from adafruit_ble.attributes import Attribute
from adafruit_ble.advertising import Advertisement
from adafruit_ble.advertising.standard import ProvideServicesAdvertisement
from adafruit_ble.services import Service
from adafruit_ble.characteristics import Characteristic, ComplexCharacteristic
from adafruit_ble.uuid import VendorUUID

UUID_TODO_SERV = '023331ce-5455-49b0-98c9-107904d21918'
UUID_TODO_CHAR = '54783f34-f513-43f3-b13f-b7f212736c1f'

class TodoCharacteristic(ComplexCharacteristic):
    def __init__(self):
        super().__init__(uuid=VendorUUID(UUID_TODO_CHAR), properties=(Characteristic.INDICATE,), read_perm=Attribute.OPEN, write_perm=Attribute.OPEN, max_length=0,  fixed_length=False, initial_value=None)            
    def bind(self, service):
        bound_characteristic = super().bind(service)
        return _bleio.PacketBuffer(bound_characteristic, buffer_size=8)
    def __get__(self, obj, cls = None):
        if obj is None:
            return self
        return super().__get__(obj, cls)
    def __set__(self, obj, value):
        super().__set__(obj, value)

class TodoService(Service):
    uuid = VendorUUID(UUID_TODO_SERV)
    todo = TodoCharacteristic()
    def __init__(self, *, service = None):
        super().__init__(service=service)
        self.data = TodoService.todo.bind(self)


ble = BLERadio()
for adv in ble.start_scan(ProvideServicesAdvertisement, Advertisement, timeout=1):
    if isinstance(adv, ProvideServicesAdvertisement) and TodoService in adv.services:
        ble.stop_scan()
        ble_connection = ble.connect(adv)
        ble_service = ble_connection[TodoService]

Behavior

Traceback (most recent call last):
  File "<stdin>", line 5, in <module>
  File "/lib/adafruit_ble/__init__.py", line 101, in __getitem__
  File "/lib/adafruit_ble/__init__.py", line 66, in _discover_remote
_bleio.BluetoothError: Unknown BLE error: 7

Description

In this scenario we are a GATT client, simply discovering the remote services - but with (at least) one characteristic being read-only on the GATT server. What happens is a 0-byte long write on the characteristic - even if it declared as read-only (like with notify/indicate).

In _discovered_characteristic_cb() (ports/espressif/common-hal/_bleio/Connection.c) the following call is made:

    common_hal_bleio_characteristic_construct(
        characteristic, service, chr->val_handle, uuid,
        props, SECURITY_MODE_OPEN, SECURITY_MODE_OPEN,
        GATT_MAX_DATA_LENGTH, false,   // max_length, fixed_length: values don't matter for gattc, but don't use 0
        &mp_const_empty_bytes_bufinfo,
        NULL);

This leads to the BLE error shown above - the culprit is the second-last parameter ("initial_value_bufinfo", with value "&mp_const_empty_bytes_bufinfo") in the invocation of common_hal_bleio_characteristic_construct() (implemented in ports/espressif/common-hal/_bleio/Characteristic.c), as it always leads to a GATT write on the characteristic, because the following condition in common_hal_bleio_characteristic_construct() is always true:

    if (initial_value_bufinfo != NULL) {
        common_hal_bleio_characteristic_set_value(self, initial_value_bufinfo);
    }

Important notices:

  • The provided UUIDs in the Code/REPL section were randomly generated, it should be reproducible with any GATT server that has any read-only characteristic (during discovery).
  • The issue is not chip/model specific (like the esp32s3 I used) applies to any board with BLE/GATT in the espressif port.

Additional information

I am not entirely sure what the proper initialization state should be, presumably NULL because of the existing NULL-condition check in common_hal_bleio_characteristic_construct() - but on the other hand the implemented initial value of mp_const_empty_bytes_bufinfo was probably chosen for some reason (but maybe it was chosen under the assumption for a GATT server?). I guess another condition check whether the characteristic is read-only (and remote?) could be used to differentiate - but I am not sure whether GATT client vs. server is the "correct" differentiator here (I haven't looked at the "history" of these files or searched for related github issues).

Metadata

Metadata

Assignees

No one assigned

    Labels

    blebugespressifapplies to multiple Espressif chips

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions