aboutsummaryrefslogtreecommitdiff
path: root/tests/hfp_test.py
diff options
context:
space:
mode:
Diffstat (limited to 'tests/hfp_test.py')
-rw-r--r--tests/hfp_test.py470
1 files changed, 444 insertions, 26 deletions
diff --git a/tests/hfp_test.py b/tests/hfp_test.py
index dc28180..83b0d35 100644
--- a/tests/hfp_test.py
+++ b/tests/hfp_test.py
@@ -19,8 +19,9 @@ import asyncio
import logging
import os
import pytest
+import pytest_asyncio
-from typing import Tuple
+from typing import Tuple, Optional
from .test_utils import TwoDevices
from bumble import core
@@ -36,9 +37,93 @@ logger = logging.getLogger(__name__)
# -----------------------------------------------------------------------------
+def _default_hf_configuration() -> hfp.HfConfiguration:
+ return hfp.HfConfiguration(
+ supported_hf_features=[
+ hfp.HfFeature.CODEC_NEGOTIATION,
+ hfp.HfFeature.ESCO_S4_SETTINGS_SUPPORTED,
+ hfp.HfFeature.HF_INDICATORS,
+ hfp.HfFeature.ENHANCED_CALL_STATUS,
+ hfp.HfFeature.THREE_WAY_CALLING,
+ hfp.HfFeature.CLI_PRESENTATION_CAPABILITY,
+ ],
+ supported_hf_indicators=[
+ hfp.HfIndicator.ENHANCED_SAFETY,
+ hfp.HfIndicator.BATTERY_LEVEL,
+ ],
+ supported_audio_codecs=[
+ hfp.AudioCodec.CVSD,
+ hfp.AudioCodec.MSBC,
+ ],
+ )
+
+
+# -----------------------------------------------------------------------------
+def _default_hf_sdp_features() -> hfp.HfSdpFeature:
+ return (
+ hfp.HfSdpFeature.WIDE_BAND
+ | hfp.HfSdpFeature.THREE_WAY_CALLING
+ | hfp.HfSdpFeature.CLI_PRESENTATION_CAPABILITY
+ )
+
+
+# -----------------------------------------------------------------------------
+def _default_ag_configuration() -> hfp.AgConfiguration:
+ return hfp.AgConfiguration(
+ supported_ag_features=[
+ hfp.AgFeature.HF_INDICATORS,
+ hfp.AgFeature.IN_BAND_RING_TONE_CAPABILITY,
+ hfp.AgFeature.REJECT_CALL,
+ hfp.AgFeature.CODEC_NEGOTIATION,
+ hfp.AgFeature.ESCO_S4_SETTINGS_SUPPORTED,
+ hfp.AgFeature.ENHANCED_CALL_STATUS,
+ hfp.AgFeature.THREE_WAY_CALLING,
+ ],
+ supported_ag_indicators=[
+ hfp.AgIndicatorState.call(),
+ hfp.AgIndicatorState.service(),
+ hfp.AgIndicatorState.callsetup(),
+ hfp.AgIndicatorState.callsetup(),
+ hfp.AgIndicatorState.signal(),
+ hfp.AgIndicatorState.roam(),
+ hfp.AgIndicatorState.battchg(),
+ ],
+ supported_hf_indicators=[
+ hfp.HfIndicator.ENHANCED_SAFETY,
+ hfp.HfIndicator.BATTERY_LEVEL,
+ ],
+ supported_ag_call_hold_operations=[
+ hfp.CallHoldOperation.ADD_HELD_CALL,
+ hfp.CallHoldOperation.HOLD_ALL_ACTIVE_CALLS,
+ hfp.CallHoldOperation.HOLD_ALL_CALLS_EXCEPT,
+ hfp.CallHoldOperation.RELEASE_ALL_ACTIVE_CALLS,
+ hfp.CallHoldOperation.RELEASE_ALL_HELD_CALLS,
+ hfp.CallHoldOperation.RELEASE_SPECIFIC_CALL,
+ hfp.CallHoldOperation.CONNECT_TWO_CALLS,
+ ],
+ supported_audio_codecs=[hfp.AudioCodec.CVSD, hfp.AudioCodec.MSBC],
+ )
+
+
+# -----------------------------------------------------------------------------
+def _default_ag_sdp_features() -> hfp.AgSdpFeature:
+ return (
+ hfp.AgSdpFeature.WIDE_BAND
+ | hfp.AgSdpFeature.IN_BAND_RING_TONE_CAPABILITY
+ | hfp.AgSdpFeature.THREE_WAY_CALLING
+ )
+
+
+# -----------------------------------------------------------------------------
async def make_hfp_connections(
- hf_config: hfp.Configuration,
-) -> Tuple[hfp.HfProtocol, hfp.HfpProtocol]:
+ hf_config: Optional[hfp.HfConfiguration] = None,
+ ag_config: Optional[hfp.AgConfiguration] = None,
+):
+ if not hf_config:
+ hf_config = _default_hf_configuration()
+ if not ag_config:
+ ag_config = _default_ag_configuration()
+
# Setup devices
devices = TwoDevices()
await devices.setup_connection()
@@ -55,38 +140,371 @@ async def make_hfp_connections(
# Setup HFP connection
hf = hfp.HfProtocol(client_dlc, hf_config)
- ag = hfp.HfpProtocol(server_dlc)
- return hf, ag
+ ag = hfp.AgProtocol(server_dlc, ag_config)
+
+ await hf.initiate_slc()
+ return (hf, ag)
# -----------------------------------------------------------------------------
+@pytest_asyncio.fixture
+async def hfp_connections():
+ hf, ag = await make_hfp_connections()
+ hf_loop_task = asyncio.create_task(hf.run())
+
+ try:
+ yield (hf, ag)
+ finally:
+ # Close the coroutine.
+ hf.unsolicited_queue.put_nowait(None)
+ await hf_loop_task
+# -----------------------------------------------------------------------------
@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))'
+async def test_slc_with_minimal_features():
+ hf, ag = await make_hfp_connections(
+ hfp.HfConfiguration(
+ supported_audio_codecs=[],
+ supported_hf_features=[],
+ supported_hf_indicators=[],
+ ),
+ hfp.AgConfiguration(
+ supported_ag_call_hold_operations=[],
+ supported_ag_features=[],
+ supported_ag_indicators=[
+ hfp.AgIndicatorState(
+ indicator=hfp.AgIndicator.CALL,
+ supported_values={0, 1},
+ current_status=0,
)
- elif line.startswith('AT+CIND?'):
- ag.send_response_line('+CIND: 0,0,1,4,1,5,0')
- ag.send_response_line('OK')
+ ],
+ supported_hf_indicators=[],
+ supported_audio_codecs=[],
+ ),
+ )
- ag_task = asyncio.create_task(ag_loop())
+ assert hf.supported_ag_features == ag.supported_ag_features
+ assert hf.supported_hf_features == ag.supported_hf_features
+ assert hf.supported_ag_call_hold_operations == ag.supported_ag_call_hold_operations
+ for a, b in zip(hf.ag_indicators, ag.ag_indicators):
+ assert a.indicator == b.indicator
+ assert a.current_status == b.current_status
- await hf.initiate_slc()
- ag_task.cancel()
+
+# -----------------------------------------------------------------------------
+@pytest.mark.asyncio
+async def test_slc(hfp_connections: Tuple[hfp.HfProtocol, hfp.AgProtocol]):
+ hf, ag = hfp_connections
+
+ assert hf.supported_ag_features == ag.supported_ag_features
+ assert hf.supported_hf_features == ag.supported_hf_features
+ assert hf.supported_ag_call_hold_operations == ag.supported_ag_call_hold_operations
+ for a, b in zip(hf.ag_indicators, ag.ag_indicators):
+ assert a.indicator == b.indicator
+ assert a.current_status == b.current_status
+
+
+# -----------------------------------------------------------------------------
+@pytest.mark.asyncio
+async def test_ag_indicator(hfp_connections: Tuple[hfp.HfProtocol, hfp.AgProtocol]):
+ hf, ag = hfp_connections
+
+ future = asyncio.get_running_loop().create_future()
+ hf.on('ag_indicator', future.set_result)
+
+ ag.update_ag_indicator(hfp.AgIndicator.CALL, 1)
+
+ indicator: hfp.AgIndicatorState = await future
+ assert indicator.current_status == 1
+ assert indicator.indicator == hfp.AgIndicator.CALL
+
+
+# -----------------------------------------------------------------------------
+@pytest.mark.asyncio
+async def test_hf_indicator(hfp_connections: Tuple[hfp.HfProtocol, hfp.AgProtocol]):
+ hf, ag = hfp_connections
+
+ future = asyncio.get_running_loop().create_future()
+ ag.on('hf_indicator', future.set_result)
+
+ await hf.execute_command('AT+BIEV=2,100')
+
+ indicator: hfp.HfIndicatorState = await future
+ assert indicator.current_status == 100
+
+
+# -----------------------------------------------------------------------------
+@pytest.mark.asyncio
+async def test_codec_negotiation(
+ hfp_connections: Tuple[hfp.HfProtocol, hfp.AgProtocol]
+):
+ hf, ag = hfp_connections
+
+ futures = [
+ asyncio.get_running_loop().create_future(),
+ asyncio.get_running_loop().create_future(),
+ ]
+ hf.on('codec_negotiation', futures[0].set_result)
+ ag.on('codec_negotiation', futures[1].set_result)
+ await ag.negotiate_codec(hfp.AudioCodec.MSBC)
+
+ assert await futures[0] == await futures[1]
+
+
+# -----------------------------------------------------------------------------
+@pytest.mark.asyncio
+async def test_dial(hfp_connections: Tuple[hfp.HfProtocol, hfp.AgProtocol]):
+ hf, ag = hfp_connections
+ NUMBER = 'ATD123456789'
+
+ future = asyncio.get_running_loop().create_future()
+ ag.on('dial', future.set_result)
+ await hf.execute_command(f'ATD{NUMBER}')
+
+ number: str = await future
+ assert number == NUMBER
+
+
+# -----------------------------------------------------------------------------
+@pytest.mark.asyncio
+async def test_answer(hfp_connections: Tuple[hfp.HfProtocol, hfp.AgProtocol]):
+ hf, ag = hfp_connections
+
+ future = asyncio.get_running_loop().create_future()
+ ag.on('answer', lambda: future.set_result(None))
+ await hf.answer_incoming_call()
+
+ await future
+
+
+# -----------------------------------------------------------------------------
+@pytest.mark.asyncio
+async def test_reject_incoming_call(
+ hfp_connections: Tuple[hfp.HfProtocol, hfp.AgProtocol]
+):
+ hf, ag = hfp_connections
+
+ future = asyncio.get_running_loop().create_future()
+ ag.on('hang_up', lambda: future.set_result(None))
+ await hf.reject_incoming_call()
+
+ await future
+
+
+# -----------------------------------------------------------------------------
+@pytest.mark.asyncio
+async def test_terminate_call(hfp_connections: Tuple[hfp.HfProtocol, hfp.AgProtocol]):
+ hf, ag = hfp_connections
+
+ future = asyncio.get_running_loop().create_future()
+ ag.on('hang_up', lambda: future.set_result(None))
+ await hf.terminate_call()
+
+ await future
+
+
+# -----------------------------------------------------------------------------
+@pytest.mark.asyncio
+async def test_query_calls_without_calls(
+ hfp_connections: Tuple[hfp.HfProtocol, hfp.AgProtocol]
+):
+ hf, ag = hfp_connections
+
+ assert await hf.query_current_calls() == []
+
+
+# -----------------------------------------------------------------------------
+@pytest.mark.asyncio
+async def test_query_calls_with_calls(
+ hfp_connections: Tuple[hfp.HfProtocol, hfp.AgProtocol]
+):
+ hf, ag = hfp_connections
+ ag.calls.append(
+ hfp.CallInfo(
+ index=1,
+ direction=hfp.CallInfoDirection.MOBILE_ORIGINATED_CALL,
+ status=hfp.CallInfoStatus.ACTIVE,
+ mode=hfp.CallInfoMode.VOICE,
+ multi_party=hfp.CallInfoMultiParty.NOT_IN_CONFERENCE,
+ number='123456789',
+ )
+ )
+
+ assert await hf.query_current_calls() == ag.calls
+
+
+# -----------------------------------------------------------------------------
+@pytest.mark.asyncio
+@pytest.mark.parametrize(
+ "operation,",
+ (
+ hfp.CallHoldOperation.RELEASE_ALL_HELD_CALLS,
+ hfp.CallHoldOperation.RELEASE_ALL_ACTIVE_CALLS,
+ hfp.CallHoldOperation.HOLD_ALL_ACTIVE_CALLS,
+ hfp.CallHoldOperation.ADD_HELD_CALL,
+ hfp.CallHoldOperation.CONNECT_TWO_CALLS,
+ ),
+)
+async def test_hold_call_without_call_index(
+ hfp_connections: Tuple[hfp.HfProtocol, hfp.AgProtocol],
+ operation: hfp.CallHoldOperation,
+):
+ hf, ag = hfp_connections
+ call_hold_future = asyncio.get_running_loop().create_future()
+ ag.on("call_hold", lambda op, index: call_hold_future.set_result((op, index)))
+
+ await hf.execute_command(f"AT+CHLD={operation.value}")
+
+ assert (await call_hold_future) == (operation, None)
+
+
+# -----------------------------------------------------------------------------
+@pytest.mark.asyncio
+@pytest.mark.parametrize(
+ "operation,",
+ (
+ hfp.CallHoldOperation.RELEASE_SPECIFIC_CALL,
+ hfp.CallHoldOperation.HOLD_ALL_CALLS_EXCEPT,
+ ),
+)
+async def test_hold_call_with_call_index(
+ hfp_connections: Tuple[hfp.HfProtocol, hfp.AgProtocol],
+ operation: hfp.CallHoldOperation,
+):
+ hf, ag = hfp_connections
+ call_hold_future = asyncio.get_running_loop().create_future()
+ ag.on("call_hold", lambda op, index: call_hold_future.set_result((op, index)))
+ ag.calls.append(
+ hfp.CallInfo(
+ index=1,
+ direction=hfp.CallInfoDirection.MOBILE_ORIGINATED_CALL,
+ status=hfp.CallInfoStatus.ACTIVE,
+ mode=hfp.CallInfoMode.VOICE,
+ multi_party=hfp.CallInfoMultiParty.NOT_IN_CONFERENCE,
+ number='123456789',
+ )
+ )
+
+ await hf.execute_command(f"AT+CHLD={operation.value.replace('x', '1')}")
+
+ assert (await call_hold_future) == (operation, 1)
+
+
+# -----------------------------------------------------------------------------
+@pytest.mark.asyncio
+async def test_ring(hfp_connections: Tuple[hfp.HfProtocol, hfp.AgProtocol]):
+ hf, ag = hfp_connections
+ ring_future = asyncio.get_running_loop().create_future()
+ hf.on("ring", lambda: ring_future.set_result(None))
+
+ ag.send_ring()
+
+ await ring_future
+
+
+# -----------------------------------------------------------------------------
+@pytest.mark.asyncio
+async def test_speaker_volume(hfp_connections: Tuple[hfp.HfProtocol, hfp.AgProtocol]):
+ hf, ag = hfp_connections
+ speaker_volume_future = asyncio.get_running_loop().create_future()
+ hf.on("speaker_volume", speaker_volume_future.set_result)
+
+ ag.set_speaker_volume(10)
+
+ assert await speaker_volume_future == 10
+
+
+# -----------------------------------------------------------------------------
+@pytest.mark.asyncio
+async def test_microphone_volume(
+ hfp_connections: Tuple[hfp.HfProtocol, hfp.AgProtocol]
+):
+ hf, ag = hfp_connections
+ microphone_volume_future = asyncio.get_running_loop().create_future()
+ hf.on("microphone_volume", microphone_volume_future.set_result)
+
+ ag.set_microphone_volume(10)
+
+ assert await microphone_volume_future == 10
+
+
+# -----------------------------------------------------------------------------
+@pytest.mark.asyncio
+async def test_cli_notification(hfp_connections: Tuple[hfp.HfProtocol, hfp.AgProtocol]):
+ hf, ag = hfp_connections
+ cli_notification_future = asyncio.get_running_loop().create_future()
+ hf.on("cli_notification", cli_notification_future.set_result)
+
+ ag.send_cli_notification(
+ hfp.CallLineIdentification(number="\"123456789\"", type=129, alpha="\"Bumble\"")
+ )
+
+ assert await cli_notification_future == hfp.CallLineIdentification(
+ number="123456789", type=129, alpha="Bumble", subaddr="", satype=None
+ )
+
+
+# -----------------------------------------------------------------------------
+@pytest.mark.asyncio
+async def test_voice_recognition_from_hf(
+ hfp_connections: Tuple[hfp.HfProtocol, hfp.AgProtocol]
+):
+ hf, ag = hfp_connections
+ voice_recognition_future = asyncio.get_running_loop().create_future()
+ ag.on("voice_recognition", voice_recognition_future.set_result)
+
+ await hf.execute_command("AT+BVRA=1")
+
+ assert await voice_recognition_future == hfp.VoiceRecognitionState.ENABLE
+
+
+# -----------------------------------------------------------------------------
+@pytest.mark.asyncio
+async def test_voice_recognition_from_ag(
+ hfp_connections: Tuple[hfp.HfProtocol, hfp.AgProtocol]
+):
+ hf, ag = hfp_connections
+ voice_recognition_future = asyncio.get_running_loop().create_future()
+ hf.on("voice_recognition", voice_recognition_future.set_result)
+
+ ag.send_response("+BVRA: 1")
+
+ assert await voice_recognition_future == hfp.VoiceRecognitionState.ENABLE
+
+
+# -----------------------------------------------------------------------------
+@pytest.mark.asyncio
+async def test_hf_sdp_record():
+ devices = TwoDevices()
+ await devices.setup_connection()
+
+ devices[0].sdp_service_records[1] = hfp.make_hf_sdp_records(
+ 1, 2, _default_hf_configuration(), hfp.ProfileVersion.V1_8
+ )
+
+ assert await hfp.find_hf_sdp_record(devices.connections[1]) == (
+ 2,
+ hfp.ProfileVersion.V1_8,
+ _default_hf_sdp_features(),
+ )
+
+
+# -----------------------------------------------------------------------------
+@pytest.mark.asyncio
+async def test_ag_sdp_record():
+ devices = TwoDevices()
+ await devices.setup_connection()
+
+ devices[0].sdp_service_records[1] = hfp.make_ag_sdp_records(
+ 1, 2, _default_ag_configuration(), hfp.ProfileVersion.V1_8
+ )
+
+ assert await hfp.find_ag_sdp_record(devices.connections[1]) == (
+ 2,
+ hfp.ProfileVersion.V1_8,
+ _default_ag_sdp_features(),
+ )
# -----------------------------------------------------------------------------