From 3852aa056bc982b943b0e3905a0e6d47a73c742d Mon Sep 17 00:00:00 2001 From: Josh Wu Date: Thu, 7 Sep 2023 22:39:05 +0800 Subject: [PATCH 1/2] Bring HfpProtocol back --- bumble/hfp.py | 59 ++++++++++++++++++++++++++++++++++ examples/run_hfp_gateway.py | 64 ++----------------------------------- 2 files changed, 62 insertions(+), 61 deletions(-) diff --git a/bumble/hfp.py b/bumble/hfp.py index 6d9e428..26da6e2 100644 --- a/bumble/hfp.py +++ b/bumble/hfp.py @@ -15,16 +15,19 @@ # ----------------------------------------------------------------------------- # Imports # ----------------------------------------------------------------------------- +import collections.abc import logging import asyncio import dataclasses import enum import traceback +import warnings from typing import Dict, List, Union, Set from . import at from . import rfcomm +from bumble.colors import color from bumble.core import ( ProtocolError, BT_GENERIC_AUDIO_SERVICE, @@ -49,6 +52,62 @@ from bumble.sdp import ( logger = logging.getLogger(__name__) +# ----------------------------------------------------------------------------- +# Protocol Support +# ----------------------------------------------------------------------------- + +# ----------------------------------------------------------------------------- +class HfpProtocol: + dlc: rfcomm.DLC + buffer: str + lines: collections.deque + lines_available: asyncio.Event + + def __init__(self, dlc: rfcomm.DLC) -> None: + warnings.warn("See HfProtocol", DeprecationWarning) + self.dlc = dlc + self.buffer = '' + self.lines = collections.deque() + self.lines_available = asyncio.Event() + + dlc.sink = self.feed + + def feed(self, data: Union[bytes, str]) -> None: + # Convert the data to a string if needed + if isinstance(data, bytes): + data = data.decode('utf-8') + + logger.debug(f'<<< Data received: {data}') + + # Add to the buffer and look for lines + self.buffer += data + while (separator := self.buffer.find('\r')) >= 0: + line = self.buffer[:separator].strip() + self.buffer = self.buffer[separator + 1 :] + if len(line) > 0: + self.on_line(line) + + def on_line(self, line: str) -> None: + self.lines.append(line) + self.lines_available.set() + + def send_command_line(self, line: str) -> None: + logger.debug(color(f'>>> {line}', 'yellow')) + self.dlc.write(line + '\r') + + def send_response_line(self, line: str) -> None: + logger.debug(color(f'>>> {line}', 'yellow')) + self.dlc.write('\r\n' + line + '\r\n') + + async def next_line(self) -> str: + await self.lines_available.wait() + line = self.lines.popleft() + if not self.lines: + self.lines_available.clear() + logger.debug(color(f'<<< {line}', 'green')) + return line + + # ----------------------------------------------------------------------------- # Normative protocol definitions # ----------------------------------------------------------------------------- diff --git a/examples/run_hfp_gateway.py b/examples/run_hfp_gateway.py index eac5473..13a2ed9 100644 --- a/examples/run_hfp_gateway.py +++ b/examples/run_hfp_gateway.py @@ -16,11 +16,9 @@ # Imports # ----------------------------------------------------------------------------- import asyncio -import collections import sys import os import logging -from typing import Union from bumble.colors import color @@ -32,8 +30,7 @@ from bumble.core import ( BT_RFCOMM_PROTOCOL_ID, BT_BR_EDR_TRANSPORT, ) -from bumble import rfcomm -from bumble.rfcomm import Client +from bumble import rfcomm, hfp from bumble.sdp import ( Client as SDP_Client, DataElement, @@ -47,61 +44,6 @@ from bumble.sdp import ( logger = logging.getLogger(__name__) -# ----------------------------------------------------------------------------- -# Protocol Support -# ----------------------------------------------------------------------------- - -# ----------------------------------------------------------------------------- -class HfpProtocol: - dlc: rfcomm.DLC - buffer: str - lines: collections.deque - lines_available: asyncio.Event - - def __init__(self, dlc: rfcomm.DLC) -> None: - self.dlc = dlc - self.buffer = '' - self.lines = collections.deque() - self.lines_available = asyncio.Event() - - dlc.sink = self.feed - - def feed(self, data: Union[bytes, str]) -> None: - # Convert the data to a string if needed - if isinstance(data, bytes): - data = data.decode('utf-8') - - logger.debug(f'<<< Data received: {data}') - - # Add to the buffer and look for lines - self.buffer += data - while (separator := self.buffer.find('\r')) >= 0: - line = self.buffer[:separator].strip() - self.buffer = self.buffer[separator + 1 :] - if len(line) > 0: - self.on_line(line) - - def on_line(self, line: str) -> None: - self.lines.append(line) - self.lines_available.set() - - def send_command_line(self, line: str) -> None: - logger.debug(color(f'>>> {line}', 'yellow')) - self.dlc.write(line + '\r') - - def send_response_line(self, line: str) -> None: - logger.debug(color(f'>>> {line}', 'yellow')) - self.dlc.write('\r\n' + line + '\r\n') - - async def next_line(self) -> str: - await self.lines_available.wait() - line = self.lines.popleft() - if not self.lines: - self.lines_available.clear() - logger.debug(color(f'<<< {line}', 'green')) - return line - - # ----------------------------------------------------------------------------- # pylint: disable-next=too-many-nested-blocks async def list_rfcomm_channels(device, connection): @@ -241,7 +183,7 @@ async def main(): # Create a client and start it print('@@@ Starting to RFCOMM client...') - rfcomm_client = Client(device, connection) + rfcomm_client = rfcomm.Client(device, connection) rfcomm_mux = await rfcomm_client.start() print('@@@ Started') @@ -256,7 +198,7 @@ async def main(): return # Protocol loop (just for testing at this point) - protocol = HfpProtocol(session) + protocol = hfp.HfpProtocol(session) while True: line = await protocol.next_line() From 838d10a09dff8334007051d6f0c5cc65464cfdfd Mon Sep 17 00:00:00 2001 From: Josh Wu Date: Thu, 7 Sep 2023 23:20:16 +0800 Subject: [PATCH 2/2] Add HFP tests --- tests/hfp_test.py | 100 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 tests/hfp_test.py diff --git a/tests/hfp_test.py b/tests/hfp_test.py new file mode 100644 index 0000000..bc6ccaf --- /dev/null +++ b/tests/hfp_test.py @@ -0,0 +1,100 @@ +# Copyright 2021-2022 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 asyncio +import logging +import os +import pytest + +from typing import Tuple + +from .test_utils import TwoDevices +from bumble import hfp +from bumble import rfcomm + + +# ----------------------------------------------------------------------------- +# Logging +# ----------------------------------------------------------------------------- +logger = logging.getLogger(__name__) + + +# ----------------------------------------------------------------------------- +async def make_hfp_connections( + hf_config: hfp.Configuration, +) -> Tuple[hfp.HfProtocol, hfp.HfpProtocol]: + # Setup devices + devices = TwoDevices() + await devices.setup_connection() + + # Setup RFCOMM channel + wait_dlc = asyncio.get_running_loop().create_future() + rfcomm_channel = rfcomm.Server(devices.devices[0]).listen( + lambda dlc: wait_dlc.set_result(dlc) + ) + assert devices.connections[0] + assert devices.connections[1] + client_mux = await rfcomm.Client(devices.devices[1], devices.connections[1]).start() + + client_dlc = await client_mux.open_dlc(rfcomm_channel) + server_dlc = await wait_dlc + + # Setup HFP connnection + hf = hfp.HfProtocol(client_dlc, hf_config) + ag = hfp.HfpProtocol(server_dlc) + return hf, ag + + +# ----------------------------------------------------------------------------- + + +@pytest.mark.asyncio +async def test_slc(): + hf_config = hfp.Configuration( + supported_hf_features=[], supported_hf_indicators=[], supported_audio_codecs=[] + ) + hf, ag = await make_hfp_connections(hf_config) + + async def ag_loop(): + while line := await ag.next_line(): + if line.startswith('AT+BRSF'): + ag.send_response_line('+BRSF: 0') + elif line.startswith('AT+CIND=?'): + ag.send_response_line( + '+CIND: ("call",(0,1)),("callsetup",(0-3)),("service",(0-1)),' + '("signal",(0-5)),("roam",(0,1)),("battchg",(0-5)),' + '("callheld",(0-2))' + ) + elif line.startswith('AT+CIND?'): + ag.send_response_line('+CIND: 0,0,1,4,1,5,0') + ag.send_response_line('OK') + + ag_task = asyncio.create_task(ag_loop()) + + await hf.initiate_slc() + ag_task.cancel() + + +# ----------------------------------------------------------------------------- +async def run(): + await test_slc() + + +# ----------------------------------------------------------------------------- +if __name__ == '__main__': + logging.basicConfig(level=os.environ.get('BUMBLE_LOGLEVEL', 'INFO').upper()) + asyncio.run(run())