diff --git a/bumble/controller.py b/bumble/controller.py index 374f138..eb20292 100644 --- a/bumble/controller.py +++ b/bumble/controller.py @@ -1151,7 +1151,28 @@ class Controller: ''' See Bluetooth spec Vol 4, Part E - 7.4.3 Read Local Supported Features Command ''' - return bytes([HCI_SUCCESS]) + self.lmp_features + return bytes([HCI_SUCCESS]) + self.lmp_features[:8] + + def on_hci_read_local_extended_features_command(self, command): + ''' + See Bluetooth spec Vol 4, Part E - 7.4.4 Read Local Extended Features Command + ''' + if command.page_number * 8 > len(self.lmp_features): + return bytes([HCI_INVALID_HCI_COMMAND_PARAMETERS_ERROR]) + return ( + bytes( + [ + # Status + HCI_SUCCESS, + # Page number + command.page_number, + # Max page number + len(self.lmp_features) // 8 - 1, + ] + ) + # Features of the current page + + self.lmp_features[command.page_number * 8 : (command.page_number + 1) * 8] + ) def on_hci_read_buffer_size_command(self, _command): ''' @@ -1522,6 +1543,20 @@ class Controller: } return bytes([HCI_SUCCESS]) + def on_hci_le_read_maximum_advertising_data_length_command(self, _command): + ''' + See Bluetooth spec Vol 4, Part E - 7.8.57 LE Read Maximum Advertising Data + Length Command + ''' + return struct.pack(' bool: return (self.local_le_features & feature) == feature + def supports_lmp_features(self, feature: hci.LmpFeatureMask) -> bool: + return self.local_lmp_features & (feature) == feature + @property def supported_le_features(self): return [ diff --git a/tests/host_test.py b/tests/host_test.py new file mode 100644 index 0000000..5170497 --- /dev/null +++ b/tests/host_test.py @@ -0,0 +1,62 @@ +# Copyright 2021-2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# ----------------------------------------------------------------------------- +# Imports +# ----------------------------------------------------------------------------- +import logging +import pytest + +from bumble.controller import Controller +from bumble.host import Host +from bumble.transport import AsyncPipeSink + +# ----------------------------------------------------------------------------- +# Logging +# ----------------------------------------------------------------------------- +logger = logging.getLogger(__name__) + + +# ----------------------------------------------------------------------------- +@pytest.mark.asyncio +@pytest.mark.parametrize( + 'supported_commands, lmp_features', + [ + ( + # Default commands + '2000800000c000000000e4000000a822000000000000040000f7ffff7f000000' + '30f0f9ff01008004000000000000000000000000000000000000000000000000', + # Only LE LMP feature + '0000000060000000', + ), + ( + # All commands + 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff' + 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff', + # 3 pages of LMP features + '000102030405060708090A0B0C0D0E0F011112131415161718191A1B1C1D1E1F', + ), + ], +) +async def test_reset(supported_commands: str, lmp_features: str): + controller = Controller('C') + controller.supported_commands = bytes.fromhex(supported_commands) + controller.lmp_features = bytes.fromhex(lmp_features) + host = Host(controller, AsyncPipeSink(controller)) + + await host.reset() + + assert host.local_lmp_features == int.from_bytes( + bytes.fromhex(lmp_features), 'little' + )