Compare commits

...

16 Commits

Author SHA1 Message Date
zxzxwu
88ef65a4e2 Merge pull request #798 from khsiao-google/update
HFP: Change configuration attribute types to Sequence
2025-10-22 13:52:20 +08:00
zxzxwu
324b26d8f2 Merge pull request #801 from zyanwu-google/feat/intel_ddc
feat(intel): clarify firmware/DDC flow and preserve driver metadata
2025-10-22 13:51:16 +08:00
zyanwu-google
c657494362 feat(intel): clarify firmware/DDC flow and preserve driver metadata
- Add explanatory comments across intel driver to clarify metadata parsing.
- Ensure driver selection preserves runtime options (e.g. "intel/ddc_override:AABB")
  so driver-specific metadata is passed through to the host and available to
  drivers via host.hci_metadata.
- Ensure transport parsing regex and metadata extraction so transport/source
  metadata is populated and visible to drivers.
- Example usage: passing [driver=intel/ddc_override:AABB] will be preserved and
  can be consumed by the Intel driver to apply a DDC override blob.
2025-10-21 09:00:38 +00:00
khsiao-google
11505f08b7 [Typing] Change to Sequence 2025-10-20 08:47:40 +00:00
khsiao-google
9bf9ed5f59 [Typing] Change list to Iterable 2025-10-10 15:32:06 +00:00
zxzxwu
0fa517a4f6 Merge pull request #793 from zain2983/main
Minor fixes
2025-10-03 15:54:13 +08:00
Z1
a11962a487 Minor fixes 2025-10-02 19:26:30 +00:00
zxzxwu
32d448edf3 Merge pull request #790 from markusjellitsch/task/fix-cis-reconnect
Fix - Allow re-creation of CIS link when not successfull
2025-09-26 19:55:49 +08:00
markus
3d615b13ce fix accessing pending_cis dict 2025-09-26 12:38:38 +02:00
Markus Jellitsch
1ad92dc759 Update bumble/device.py
Co-authored-by: zxzxwu <92432172+zxzxwu@users.noreply.github.com>
2025-09-26 12:25:50 +02:00
markus
aacfd4328c satisfy the linter, return None 2025-09-26 12:02:54 +02:00
markus
6aa1f5211c use local cis_link.handle to the pop the dict 2025-09-26 11:13:52 +02:00
markus
df8e454ee5 pop cis link only when cis created successfully 2025-09-26 10:58:37 +02:00
Gilles Boccon-Gibod
aec50ac616 Merge pull request #789 from google/gbg/nrf-uart-flow-control 2025-09-26 09:34:33 +02:00
zxzxwu
6e6b4cd4b2 Merge pull request #773 from wescande/main
HAP: wait for MTU to process reconnection event
2025-09-26 01:36:45 +08:00
William Escande
8a5f6a61d5 HAP: wait for MTU to process reconnection event
When HAP reconnect, it sends indication of all events that happen during
the disconnection.
But it should wait for the profile to be ready and for the MTU to have
been negotiated or else the remote may not be ready yet.

As a side effect of this, the current GattServer doesn't re-populate the
handle of subscriber during a reconnection, we have to bypass this check
to send the notification
2025-09-16 16:18:16 -07:00
8 changed files with 86 additions and 21 deletions

View File

@@ -50,7 +50,7 @@ Bumble is easiest to use with a dedicated USB dongle.
This is because internal Bluetooth interfaces tend to be locked down by the operating system.
You can use the [usb_probe](/docs/mkdocs/src/apps_and_tools/usb_probe.md) tool (all platforms) or `lsusb` (Linux or macOS) to list the available USB devices on your system.
See the [USB Transport](/docs/mkdocs/src/transports/usb.md) page for details on how to refer to USB devices. Also, if your are on a mac, see [these instructions](docs/mkdocs/src/platforms/macos.md).
See the [USB Transport](/docs/mkdocs/src/transports/usb.md) page for details on how to refer to USB devices. Also, if you are on a mac, see [these instructions](docs/mkdocs/src/platforms/macos.md).
## License

View File

@@ -2263,8 +2263,6 @@ class Device(utils.CompositeEventEmitter):
EVENT_CONNECTION_FAILURE = "connection_failure"
EVENT_SCO_REQUEST = "sco_request"
EVENT_INQUIRY_COMPLETE = "inquiry_complete"
EVENT_REMOTE_NAME = "remote_name"
EVENT_REMOTE_NAME_FAILURE = "remote_name_failure"
EVENT_SCO_CONNECTION = "sco_connection"
EVENT_SCO_CONNECTION_FAILURE = "sco_connection_failure"
EVENT_CIS_REQUEST = "cis_request"
@@ -4727,7 +4725,7 @@ class Device(utils.CompositeEventEmitter):
self, cis_acl_pairs: Sequence[tuple[int, Connection]]
) -> list[CisLink]:
for cis_handle, acl_connection in cis_acl_pairs:
cis_id, cig_id = self._pending_cis.pop(cis_handle)
cis_id, cig_id = self._pending_cis[cis_handle]
self.cis_links[cis_handle] = CisLink(
device=self,
acl_connection=acl_connection,
@@ -4743,6 +4741,7 @@ class Device(utils.CompositeEventEmitter):
}
def on_cis_establishment(cis_link: CisLink) -> None:
self._pending_cis.pop(cis_link.handle)
if pending_future := pending_cis_establishments.get(cis_link.handle):
pending_future.set_result(cis_link)

View File

@@ -49,6 +49,10 @@ async def get_driver_for_host(host: Host) -> Optional[Driver]:
driver_classes: dict[str, type[Driver]] = {"rtk": rtk.Driver, "intel": intel.Driver}
probe_list: Iterable[str]
if driver_name := host.hci_metadata.get("driver"):
# The "driver" metadata may include runtime options after a '/' (for example
# "intel/ddc=..."). Keep only the base driver name (the portion before the
# first slash) so it matches a key in driver_classes (e.g. "intel").
driver_name = driver_name.split("/")[0]
# Only probe a single driver
probe_list = [driver_name]
else:

View File

@@ -459,6 +459,10 @@ class Driver(common.Driver):
== ModeOfOperation.OPERATIONAL
):
logger.debug("firmware already loaded")
# If the firmeare is already loaded, still attempt to load any
# device configuration (DDC). DDC can be applied independently of a
# firmware reload and may contain runtime overrides or patches.
await self.load_ddc_if_any()
return
# We only support some platforms and variants.
@@ -598,17 +602,39 @@ class Driver(common.Driver):
await self.reset_complete.wait()
logger.debug("reset complete")
# Load the device config if there is one.
await self.load_ddc_if_any(firmware_base_name)
async def load_ddc_if_any(self, firmware_base_name: Optional[str] = None) -> None:
"""
Check for and load any Device Data Configuration (DDC) blobs.
Args:
firmware_base_name: Base name of the selected firmware (e.g. "ibt-XXXX-YYYY").
If None, don't attempt to look up a .ddc file that
corresponds to the firmware image.
Priority:
1. If a ddc_override was provided via driver metadata, use it (highest priority).
2. Otherwise, if firmware_base_name is provided, attempt to find a .ddc file
that corresponds to the selected firmware image.
3. Finally, if a ddc_addon was provided, append/load it after the primary DDC.
"""
# If an explicit DDC override was supplied, use it and skip file lookup.
if self.ddc_override:
logger.debug("loading overridden DDC")
await self.load_device_config(self.ddc_override)
else:
ddc_name = f"{firmware_base_name}.ddc"
ddc_path = _find_binary_path(ddc_name)
if ddc_path:
logger.debug(f"loading DDC from {ddc_path}")
ddc_data = ddc_path.read_bytes()
await self.load_device_config(ddc_data)
# Only attempt .ddc file lookup if a firmware_base_name was provided.
if firmware_base_name is None:
logger.debug(
"no firmware_base_name provided; skipping .ddc file lookup"
)
else:
ddc_name = f"{firmware_base_name}.ddc"
ddc_path = _find_binary_path(ddc_name)
if ddc_path:
logger.debug(f"loading DDC from {ddc_path}")
ddc_data = ddc_path.read_bytes()
await self.load_device_config(ddc_data)
if self.ddc_addon:
logger.debug("loading DDC addon")
await self.load_device_config(self.ddc_addon)

View File

@@ -489,9 +489,9 @@ STATUS_CODES = {
@dataclasses.dataclass
class HfConfiguration:
supported_hf_features: list[HfFeature]
supported_hf_indicators: list[HfIndicator]
supported_audio_codecs: list[AudioCodec]
supported_hf_features: collections.abc.Sequence[HfFeature]
supported_hf_indicators: collections.abc.Sequence[HfIndicator]
supported_audio_codecs: collections.abc.Sequence[AudioCodec]
@dataclasses.dataclass
@@ -753,7 +753,7 @@ class HfProtocol(utils.EventEmitter):
# Build local features.
self.supported_hf_features = sum(configuration.supported_hf_features)
self.supported_audio_codecs = configuration.supported_audio_codecs
self.supported_audio_codecs = list(configuration.supported_audio_codecs)
self.hf_indicators = {
indicator: HfIndicatorState(indicator=indicator)

View File

@@ -273,12 +273,19 @@ class HearingAccessService(gatt.TemplateService):
def on_disconnection(_reason) -> None:
self.currently_connected_clients.discard(connection)
@connection.on(connection.EVENT_CONNECTION_ATT_MTU_UPDATE)
def on_mtu_update(*_: Any) -> None:
self.on_incoming_connection(connection)
@connection.on(connection.EVENT_CONNECTION_ENCRYPTION_CHANGE)
def on_encryption_change(*_: Any) -> None:
self.on_incoming_connection(connection)
@connection.on(connection.EVENT_PAIRING)
def on_pairing(*_: Any) -> None:
self.on_incoming_paired_connection(connection)
self.on_incoming_connection(connection)
if connection.peer_resolvable_address:
self.on_incoming_paired_connection(connection)
self.on_incoming_connection(connection)
self.hearing_aid_features_characteristic = gatt.Characteristic(
uuid=gatt.GATT_HEARING_AID_FEATURES_CHARACTERISTIC,
@@ -315,9 +322,30 @@ class HearingAccessService(gatt.TemplateService):
]
)
def on_incoming_paired_connection(self, connection: Connection):
def on_incoming_connection(self, connection: Connection):
'''Setup initial operations to handle a remote bonded HAP device'''
# TODO Should we filter on HAP device only ?
if not connection.is_encrypted:
logging.debug(f'HAS: {connection.peer_address} is not encrypted')
return
if not connection.peer_resolvable_address:
logging.debug(f'HAS: {connection.peer_address} is not paired')
return
if connection.att_mtu < 49:
logging.debug(
f'HAS: {connection.peer_address} invalid MTU={connection.att_mtu}'
)
return
if connection.peer_address in self.currently_connected_clients:
logging.debug(
f'HAS: Already connected to {connection.peer_address} nothing to do'
)
return
self.currently_connected_clients.add(connection)
if (
connection.peer_address
@@ -457,6 +485,7 @@ class HearingAccessService(gatt.TemplateService):
connection,
self.hearing_aid_preset_control_point,
value=op_list[0].to_bytes(len(op_list) == 1),
force=True, # TODO GATT notification subscription should be persistent
)
# Remove item once sent, and keep the non sent item in the list
op_list.pop(0)

View File

@@ -84,7 +84,12 @@ async def open_transport(name: str) -> Transport:
scheme, *tail = name.split(':', 1)
spec = tail[0] if tail else None
metadata = None
if spec and (m := re.search(r'\[(\w+=\w+(?:,\w+=\w+)*,?)\]', spec)):
# If a spec is provided, check for a metadata section in square brackets.
# The regex captures a comma-separated list of key=value pairs (allowing an
# optional trailing comma). The key is matched by \w+ and the value by [^,\]]+,
# meaning the value may contain any character except a comma or a closing
# bracket (']').
if spec and (m := re.search(r'\[(\w+=[^,\]]+(?:,\w+=[^,\]]+)*,?)\]', spec)):
metadata_str = m.group(1)
if m.start() == 0:
# <metadata><spec>

View File

@@ -82,7 +82,6 @@ async def hap_client():
)
await devices.setup_connection()
# TODO negotiate MTU > 49 to not truncate preset names
# Mock encryption.
devices.connections[0].encryption = 1 # type: ignore
@@ -93,6 +92,9 @@ async def hap_client():
)
peer = device.Peer(devices.connections[1]) # type: ignore
await peer.request_mtu(49)
peer2 = device.Peer(devices.connections[0]) # type: ignore
await peer2.request_mtu(49)
hap_client = await peer.discover_service_and_create_proxy(
hap.HearingAccessServiceProxy
)