aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDavid Duarte <licorne@google.com>2021-12-22 13:09:23 +0000
committerDavid Duarte <licorne@google.com>2022-01-10 14:07:04 +0000
commit3819001d8d9b86da41d493e7c85777845f47019b (patch)
treeb32ce3b8b0ac05f5e56f79c4119594314e760b03
parent2fed870203ce3567f58e3506d769d2edc80184c6 (diff)
downloadmmi2grpc-3819001d8d9b86da41d493e7c85777845f47019b.tar.gz
a2dp: Refactor and handle AVDTP mmi
Change-Id: Ib6384e12c81f8adc2d8f73bf945cf7efe4e250ee
-rw-r--r--mmi2grpc/__init__.py40
-rw-r--r--mmi2grpc/_audio.py79
-rw-r--r--mmi2grpc/_description.py41
-rw-r--r--mmi2grpc/a2dp.py649
-rw-r--r--proto/blueberry/a2dp.proto179
-rw-r--r--proto/blueberry/host.proto65
6 files changed, 931 insertions, 122 deletions
diff --git a/mmi2grpc/__init__.py b/mmi2grpc/__init__.py
index dac3702..b62f2ee 100644
--- a/mmi2grpc/__init__.py
+++ b/mmi2grpc/__init__.py
@@ -1,27 +1,39 @@
+from typing import Optional
import grpc
-from . import a2dp
+import time
+import sys
+import textwrap
+
+from .a2dp import A2DPProxy
from blueberry.host_grpc import Host
GRPC_PORT = 8999
+_a2dp: Optional[A2DPProxy] = None
+
-def run(profile: str, interaction_id: str, test: str, pts_addr: bytes):
- channel = grpc.insecure_channel(f'localhost:{GRPC_PORT}')
- print(f'{profile} mmi: {interaction_id}')
- if profile == "A2DP":
- a2dp.interact(channel, interaction_id, test, pts_addr)
- channel.close()
+def run(profile: str, interaction_id: str, test: str, description: str, pts_addr: bytes):
+ global _a2dp
+ print(f'{profile} mmi: {interaction_id}', file=sys.stderr)
+ if profile in ('A2DP', 'AVDTP'):
+ if not _a2dp:
+ _a2dp = A2DPProxy(grpc.insecure_channel(f'localhost:{GRPC_PORT}'))
+ return _a2dp.interact(interaction_id, test, description, pts_addr)
def reset():
- channel = grpc.insecure_channel(f'localhost:{GRPC_PORT}')
- Host(channel).Reset(wait_for_ready=True)
- channel.close()
+ global a2dp
+ a2dp = None
+ with grpc.insecure_channel(f'localhost:{GRPC_PORT}') as channel:
+ Host(channel).Reset(wait_for_ready=True)
def read_local_address() -> bytes:
- channel = grpc.insecure_channel(f'localhost:{GRPC_PORT}')
- bluetooth_address = Host(channel).ReadLocalAddress(wait_for_ready=True)
- channel.close()
- return bluetooth_address.address
+ with grpc.insecure_channel(f'localhost:{GRPC_PORT}') as channel:
+ try:
+ return Host(channel).ReadLocalAddress(wait_for_ready=True).address
+ except grpc.RpcError:
+ print('Retry')
+ time.sleep(5)
+ return Host(channel).ReadLocalAddress(wait_for_ready=True).address
diff --git a/mmi2grpc/_audio.py b/mmi2grpc/_audio.py
new file mode 100644
index 0000000..92e06df
--- /dev/null
+++ b/mmi2grpc/_audio.py
@@ -0,0 +1,79 @@
+import itertools
+import math
+import os
+from threading import Thread
+
+import numpy as np
+from scipy.io import wavfile
+
+
+def _fixup_wav_header(path):
+ WAV_RIFF_SIZE_OFFSET = 4
+ WAV_DATA_SIZE_OFFSET = 40
+
+ with open(path, 'r+b') as f:
+ f.seek(0, os.SEEK_END)
+ file_size = f.tell()
+ for offset in [WAV_RIFF_SIZE_OFFSET, WAV_DATA_SIZE_OFFSET]:
+ size = file_size - offset - 4
+ f.seek(offset)
+ f.write(size.to_bytes(4, byteorder='little'))
+
+
+SINE_FREQUENCY = 440
+SINE_DURATION = 0.1
+
+WAV_FILE = "/tmp/audiodata"
+
+
+class AudioSignal:
+ def __init__(self, transport, amplitude, fs):
+ self.transport = transport
+ self.amplitude = amplitude
+ self.fs = fs
+ self.thread = None
+
+ def start(self):
+ self.thread = Thread(target=self._run)
+ self.thread.start()
+
+ def _run(self):
+ sine = self._generate_sine(SINE_FREQUENCY, SINE_DURATION)
+
+ # Interleaved audio
+ stereo = np.zeros(sine.size * 2, dtype=sine.dtype)
+ stereo[0::2] = sine
+
+ # Send 4 second of audio
+ audio = itertools.repeat(stereo.tobytes(), int(4 / SINE_DURATION))
+
+ self.transport(audio)
+
+ def _generate_sine(self, f, duration):
+ sine = self.amplitude * \
+ np.sin(2 * np.pi * np.arange(self.fs * duration) * (f / self.fs))
+ s16le = (sine * 32767).astype("<i2")
+ return s16le
+
+ def verify(self):
+ assert self.thread is not None
+ self.thread.join()
+ self.thread = None
+
+ _fixup_wav_header(WAV_FILE)
+
+ samplerate, data = wavfile.read(WAV_FILE)
+ # Take one second of audio after the first second
+ audio = data[samplerate:samplerate*2, 0].astype(np.float) / 32767
+ assert(len(audio) == samplerate)
+
+ spectrum = np.abs(np.fft.fft(audio))
+ frequency = np.fft.fftfreq(samplerate, d=1/samplerate)
+ amplitudes = spectrum / (samplerate/2)
+ index = np.where(frequency == SINE_FREQUENCY)
+ amplitude = amplitudes[index][0]
+
+ match_amplitude = math.isclose(
+ amplitude, self.amplitude, rel_tol=1e-03)
+
+ return match_amplitude
diff --git a/mmi2grpc/_description.py b/mmi2grpc/_description.py
new file mode 100644
index 0000000..0f3219b
--- /dev/null
+++ b/mmi2grpc/_description.py
@@ -0,0 +1,41 @@
+import functools
+import unittest
+import textwrap
+
+COMMENT_WIDTH = 80 - 8 # 80 cols - 8 indentation space
+
+
+def assert_description(f):
+ @functools.wraps(f)
+ def wrapper(*args, **kwargs):
+ description = textwrap.fill(
+ kwargs["description"], COMMENT_WIDTH, replace_whitespace=False)
+ docstring = textwrap.dedent(f.__doc__ or "")
+
+ if docstring.strip() != description.strip():
+ print(f'Expected description of {f.__name__}:')
+ print(description)
+
+ # Generate AssertionError
+ test = unittest.TestCase()
+ test.maxDiff = None
+ test.assertMultiLineEqual(
+ docstring.strip(), description.strip(),
+ f'description does not match with function docstring of {f.__name__}')
+
+ f(*args, **kwargs)
+ return wrapper
+
+
+def format_function(id, description):
+ wrapped = textwrap.fill(description, COMMENT_WIDTH,
+ replace_whitespace=False)
+ return (
+ f'@assert_description\n'
+ f'def {id}(self, **kwargs):\n'
+ f' """\n'
+ f'{textwrap.indent(wrapped, " ")}\n'
+ f' """\n'
+ f'\n'
+ f' return "OK"\n'
+ )
diff --git a/mmi2grpc/a2dp.py b/mmi2grpc/a2dp.py
index 60a55fd..6f3ba7c 100644
--- a/mmi2grpc/a2dp.py
+++ b/mmi2grpc/a2dp.py
@@ -1,101 +1,572 @@
+import time
+import os
+import textwrap
from typing import Optional
-from grpc import Channel
+import grpc
from blueberry.a2dp_grpc import A2DP
from blueberry.host_grpc import Host
-from blueberry.a2dp_pb2 import Sink, Source
+from blueberry.a2dp_pb2 import Sink, Source, PlaybackAudioRequest
from blueberry.host_pb2 import Connection
-_connection: Optional[Connection] = None
-_sink: Optional[Sink] = None
-_source: Optional[Source] = None
-
-def _ensure_connection(host, addr):
- global _connection
- if not _connection:
- _connection = host.GetConnection(address=addr).connection
-
-def _ensure_sink_open(host, a2dp, addr):
- global _connection, _sink, _source
- _ensure_connection(host, addr)
- if not _sink:
- _sink = a2dp.OpenSink(connection=_connection).sink
-
-def _ensure_source_open(host, a2dp, addr):
- global _connection, _source
- _ensure_connection(host, addr)
- if not _source:
- _source = a2dp.OpenSource(connection=_connection).source
-
-def interact(channel: Channel, interaction_id: str, test: str, pts_addr: bytes):
- global _connection, _sink, _source
- a2dp = A2DP(channel)
- host = Host(channel)
- if interaction_id == "TSC_AVDTP_mmi_iut_accept_connect":
- host.SetConnectable(connectable=True)
- elif interaction_id == "TSC_AVDTP_mmi_iut_initiate_start":
- _ensure_connection(host, pts_addr)
- if "SNK" in test:
- _ensure_sink_open(host, a2dp, pts_addr)
- a2dp.Start(sink=_sink)
- if "SRC" in test:
- _ensure_source_open(host, a2dp, pts_addr)
- a2dp.Close(source=_source)
- elif interaction_id == "TSC_AVDTP_mmi_iut_initiate_out_of_range":
- _ensure_connection(host, pts_addr)
- host.Disconnect(connection=_connection)
- _connection = None
- _sink = None
- _source = None
- elif interaction_id == "TSC_AVDTP_mmi_iut_accept_discover":
- pass
- elif interaction_id == "TSC_AVDTP_mmi_iut_initiate_set_configuration":
- _connection = host.Connect(address=pts_addr).connection
+from ._description import assert_description, format_function
+from ._audio import AudioSignal
+
+AUDIO_AMPLITUDE = 0.8
+
+
+class A2DPProxy:
+ connection: Optional[Connection] = None
+ sink: Optional[Sink] = None
+ source: Optional[Source] = None
+
+ def __init__(self, channel):
+ self.host = Host(channel)
+ self.a2dp = A2DP(channel)
+
+ def convert_frame(data): return PlaybackAudioRequest(
+ data=data, source=self.source)
+ self.audio = AudioSignal(
+ lambda frames: self.a2dp.PlaybackAudio(map(convert_frame, frames)),
+ AUDIO_AMPLITUDE,
+ 44100
+ )
+
+ def interact(self, id: str, test: str, description: str, pts_addr: bytes):
+ try:
+ return getattr(self, id)(test=test, description=description, pts_addr=pts_addr)
+ except AttributeError:
+ code = format_function(id, description)
+ assert False, f'Unhandled mmi {id}\n{code}'
+
+ @assert_description
+ def TSC_AVDTP_mmi_iut_accept_connect(self, test: str, pts_addr: bytes, **kwargs):
+ """
+ If necessary, take action to accept the AVDTP Signaling Channel
+ Connection initiated by the tester.
+
+ Description: Make sure the IUT
+ (Implementation Under Test) is in a state to accept incoming Bluetooth
+ connections. Some devices may need to be on a specific screen, like a
+ Bluetooth settings screen, in order to pair with PTS. If the IUT is
+ still having problems pairing with PTS, try running a test case where
+ the IUT connects to PTS to establish pairing.
+ """
+
if "SRC" in test:
- _source = a2dp.OpenSource(connection=_connection).source
- if "SNK" in test:
- _sink = a2dp.OpenSink(connection=_connection).sink
- elif interaction_id == "TSC_AVDTP_mmi_iut_initiate_open_stream":
- _ensure_connection(host, pts_addr)
- if "SNK" in test:
- _sink = a2dp.OpenSink(connection=_connection).sink
+ self.connection = self.host.WaitConnection(
+ address=pts_addr).connection
+ try:
+ if "INT" in test:
+ self.source = self.a2dp.OpenSource(
+ connection=self.connection).source
+ else:
+ self.source = self.a2dp.WaitSource(
+ connection=self.connection).source
+ except:
+ pass
+ else:
+ self.connection = self.host.WaitConnection(
+ address=pts_addr).connection
+ try:
+ self.sink = self.a2dp.WaitSink(
+ connection=self.connection).sink
+ except:
+ pass
+ return "OK"
+
+ @assert_description
+ def TSC_AVDTP_mmi_iut_initiate_discover(self, **kwargs):
+ """
+ Send a discover command to PTS.
+
+ Action: If the IUT (Implementation
+ Under Test) is already connected to PTS, attempting to send or receive
+ streaming media should trigger this action. If the IUT is not connected
+ to PTS, attempting to connect may trigger this action.
+ """
+
+ return "OK"
+
+ @assert_description
+ def TSC_AVDTP_mmi_iut_initiate_start(self, test: str, **kwargs):
+ """
+ Send a start command to PTS.
+
+ Action: If the IUT (Implementation Under
+ Test) is already connected to PTS, attempting to send or receive
+ streaming media should trigger this action. If the IUT is not connected
+ to PTS, attempting to connect may trigger this action.
+ """
+
if "SRC" in test:
- _source = a2dp.OpenSource(connection=_connection).source
- elif interaction_id == "TSC_AVDTP_mmi_iut_initiate_close_stream":
- _ensure_connection(host, pts_addr)
- if "SNK" in test:
- _ensure_sink_open(host, a2dp, pts_addr)
- a2dp.Close(sink=_sink)
- _sink = None
+ self.a2dp.Start(source=self.source)
+ else:
+ self.a2dp.Start(sink=self.sink)
+ return "OK"
+
+ @assert_description
+ def TSC_AVDTP_mmi_iut_initiate_suspend(self, test: str, **kwargs):
+ """
+ Suspend the streaming channel.
+ """
+
if "SRC" in test:
- _ensure_source_open(host, a2dp, pts_addr)
- a2dp.Close(source=_source)
- _source = None
- elif interaction_id == "TSC_AVDTP_mmi_iut_initiate_suspend":
- _ensure_connection(host, pts_addr)
- if "SNK" in test:
- _ensure_sink_open(host, a2dp, pts_addr)
- a2dp.Suspend(sink=_sink)
+ self.a2dp.Suspend(source=self.source)
+ else:
+ assert False
+ return "OK"
+
+ @assert_description
+ def TSC_AVDTP_mmi_iut_initiate_close_stream(self, test: str, **kwargs):
+ """
+ Close the streaming channel.
+
+ Action: Disconnect the streaming channel,
+ or close the Bluetooth connection to the PTS.
+ """
+
if "SRC" in test:
- _ensure_source_open(host, a2dp, pts_addr)
- a2dp.suspend(source=_source)
- elif interaction_id == "TSC_AVDTP_mmi_iut_accept_close_stream":
- pass
- elif interaction_id == "TSC_AVDTP_mmi_iut_accept_get_capabilities":
- pass
- elif interaction_id == "TSC_AVDTP_mmi_iut_accept_set_configuration":
- pass
- elif interaction_id == "TSC_AVDTP_mmi_iut_accept_open_stream":
- pass
- elif interaction_id == "TSC_AVDTP_mmi_iut_accept_start":
- pass
- elif interaction_id == "TSC_AVDTP_mmi_iut_confirm_streaming":
- pass
- elif interaction_id == "TSC_AVDTP_mmi_iut_accept_reconnect":
- pass
- elif interaction_id == "TSC_AVDTP_mmi_iut_accept_suspend":
- pass
- else:
- print(f'MMI NOT IMPLEMENTED: {interaction_id}')
+ self.a2dp.Close(source=self.source)
+ self.source = None
+ else:
+ self.a2dp.Close(sink=self.sink)
+ self.sink = None
+ return "OK"
+
+ @assert_description
+ def TSC_AVDTP_mmi_iut_initiate_out_of_range(self, pts_addr: bytes, **kwargs):
+ """
+ Move the IUT out of range to create a link loss scenario.
+
+ Action: This
+ can be also be done by placing the IUT or PTS in an RF shielded box.
+ """
+
+ if self.connection is None:
+ self.connection = self.host.GetConnection(
+ address=pts_addr).connection
+ self.host.Disconnect(connection=self.connection)
+ self.connection = None
+ self.sink = None
+ self.source = None
+ return "OK"
+
+ @assert_description
+ def TSC_AVDTP_mmi_iut_begin_streaming(self, test: str, **kwargs):
+ """
+ Begin streaming media ...
+
+ Note: If the IUT has suspended the stream
+ please restart the stream to begin streaming media.
+ """
+
+ if test == "AVDTP/SRC/ACP/SIG/SMG/BI-29-C":
+ time.sleep(2) # TODO: Remove, AVRCP SegFault
+ if test in ("A2DP/SRC/CC/BV-09-I", "A2DP/SRC/SET/BV-04-I",
+ "AVDTP/SRC/ACP/SIG/SMG/BV-18-C", "AVDTP/SRC/ACP/SIG/SMG/BV-20-C",
+ "AVDTP/SRC/ACP/SIG/SMG/BV-22-C"):
+ time.sleep(1) # TODO: Remove, AVRCP SegFault
+ if test == "A2DP/SRC/SUS/BV-01-I":
+ # Stream is not suspended when we receive the interaction
+ time.sleep(1)
+
+ self.a2dp.Start(source=self.source)
+ self.audio.start()
+ return "OK"
+
+ @assert_description
+ def TSC_AVDTP_mmi_iut_initiate_media(self, **kwargs):
+ """
+ Take action if necessary to start streaming media to the tester.
+ """
+
+ self.audio.start()
+ return "OK"
+
+ @assert_description
+ def TSC_AVDTP_mmi_iut_initiate_stream_media(self, **kwargs):
+ """
+ Stream media to PTS. If the IUT is a SNK, wait for PTS to start
+ streaming media.
+
+ Action: If the IUT (Implementation Under Test) is
+ already connected to PTS, attempting to send or receive streaming media
+ should trigger this action. If the IUT is not connected to PTS,
+ attempting to connect may trigger this action.
+ """
+
+ self.audio.start()
+ return "OK"
+
+ @assert_description
+ def TSC_AVDTP_mmi_user_verify_media_playback(self, **kwargs):
+ """
+ Is the test system properly playing back the media being sent by the
+ IUT?
+ """
+
+ result = self.audio.verify()
+ assert result
+
+ return "Yes" if result else "No"
+
+ @assert_description
+ def TSC_AVDTP_mmi_iut_initiate_get_capabilities(self, **kwargs):
+ """
+ Send a get capabilities command to PTS.
+
+ Action: If the IUT
+ (Implementation Under Test) is already connected to PTS, attempting to
+ send or receive streaming media should trigger this action. If the IUT
+ is not connected to PTS, attempting to connect may trigger this action.
+ """
+
+ # This will be done as part as the a2dp.OpenSource or a2dp.WaitSource
+ return "OK"
+
+ @assert_description
+ def TSC_AVDTP_mmi_iut_accept_discover(self, **kwargs):
+ """
+ If necessary, take action to accept the AVDTP Discover operation
+ initiated by the tester.
+ """
+
+ return "OK"
+
+ @assert_description
+ def TSC_AVDTP_mmi_iut_initiate_set_configuration(self, **kwargs):
+ """
+ Send a set configuration command to PTS.
+
+ Action: If the IUT
+ (Implementation Under Test) is already connected to PTS, attempting to
+ send or receive streaming media should trigger this action. If the IUT
+ is not connected to PTS, attempting to connect may trigger this action.
+ """
+
+ return "OK"
+
+ @assert_description
+ def TSC_AVDTP_mmi_iut_accept_close_stream(self, **kwargs):
+ """
+ If necessary, take action to accept the AVDTP Close operation initiated
+ by the tester.
+ """
+
+ return "OK"
+
+ @assert_description
+ def TSC_AVDTP_mmi_iut_accept_abort(self, **kwargs):
+ """
+ If necessary, take action to accept the AVDTP Abort operation initiated
+ by the tester..
+ """
+
+ return "OK"
+
+ @assert_description
+ def TSC_AVDTP_mmi_iut_accept_get_all_capabilities(self, **kwargs):
+ """
+ If necessary, take action to accept the AVDTP Get All Capabilities
+ operation initiated by the tester.
+ """
+
+ return "OK"
+
+ @assert_description
+ def TSC_AVDTP_mmi_iut_accept_get_capabilities(self, **kwargs):
+ """
+ If necessary, take action to accept the AVDTP Get Capabilities operation
+ initiated by the tester.
+ """
+
+ return "OK"
+
+ @assert_description
+ def TSC_AVDTP_mmi_iut_accept_set_configuration(self, **kwargs):
+ """
+ If necessary, take action to accept the AVDTP Set Configuration
+ operation initiated by the tester.
+ """
+
+ return "OK"
+
+ @assert_description
+ def TSC_AVDTP_mmi_iut_accept_get_configuration(self, **kwargs):
+ """
+ Take action to accept the AVDTP Get Configuration command from the
+ tester.
+ """
+
+ return "OK"
+
+ @assert_description
+ def TSC_AVDTP_mmi_iut_accept_open_stream(self, **kwargs):
+ """
+ If necessary, take action to accept the AVDTP Open operation initiated
+ by the tester.
+ """
+
+ return "OK"
+
+ @assert_description
+ def TSC_AVDTP_mmi_iut_accept_start(self, **kwargs):
+ """
+ If necessary, take action to accept the AVDTP Start operation initiated
+ by the tester.
+ """
+
+ return "OK"
+
+ @assert_description
+ def TSC_AVDTP_mmi_iut_accept_suspend(self, **kwargs):
+ """
+ If necessary, take action to accept the AVDTP Suspend operation
+ initiated by the tester.
+ """
+
+ return "OK"
+
+ @assert_description
+ def TSC_AVDTP_mmi_iut_accept_reconfigure(self, **kwargs):
+ """
+ If necessary, take action to accept the AVDTP Reconfigure operation
+ initiated by the tester.
+ """
+
+ return "OK"
+
+ @assert_description
+ def TSC_AVDTP_mmi_iut_accept_media_transports(self, **kwargs):
+ """
+ Take action to accept transport channels for the recently configured
+ media stream.
+ """
+
+ return "OK"
+
+ @assert_description
+ def TSC_AVDTP_mmi_iut_confirm_streaming(self, **kwargs):
+ """
+ Is the IUT (Implementation Under Test) receiving streaming media from
+ PTS?
+
+ Action: Press 'Yes' if the IUT is receiving streaming data from
+ the PTS (in some cases the sound may not be clear, this is normal).
+ """
+
+ # TODO: verify
+ return "OK"
+
+ @assert_description
+ def TSC_AVDTP_mmi_iut_initiate_open_stream(self, **kwargs):
+ """
+ Open a streaming media channel.
+
+ Action: If the IUT (Implementation
+ Under Test) is already connected to PTS, attempting to send or receive
+ streaming media should trigger this action. If the IUT is not connected
+ to PTS, attempting to connect may trigger this action.
+ """
+
+ return "OK"
+
+ @assert_description
+ def TSC_AVDTP_mmi_iut_accept_reconnect(self, pts_addr: bytes, **kwargs):
+ """
+ Press OK when the IUT (Implementation Under Test) is ready to allow the
+ PTS to reconnect the AVDTP signaling channel.
+
+ Action: Press OK when the
+ IUT is ready to accept Bluetooth connections again.
+ """
+
+ return "OK"
+
+ @assert_description
+ def TSC_AVDTP_mmi_iut_initiate_get_all_capabilities(self, **kwargs):
+ """
+ Send a GET ALL CAPABILITIES command to PTS.
+
+ Action: If the IUT
+ (Implementation Under Test) is already connected to PTS, attempting to
+ send or receive streaming media should trigger this action. If the IUT
+ is not connected to PTS, attempting to connect may trigger this action.
+ """
+
+ return "OK"
+
+ @assert_description
+ def TSC_AVDTP_mmi_tester_verifying_suspend(self, **kwargs):
+ """
+ Please wait while the tester verifies the IUT does not send media during
+ suspend ...
+ """
+
+ return "Yes"
+
+ @assert_description
+ def TSC_A2DP_mmi_user_confirm_optional_data_attribute(self, **kwargs):
+ """
+ Tester found the optional SDP attribute named 'Supported Features'.
+ Press 'Yes' if the data displayed below is correct.
+
+ Value: 0x0001
+ """
+
+ # TODO: Extract and verify attribute name and value from description
+ return "OK"
+
+ @assert_description
+ def TSC_A2DP_mmi_user_confirm_optional_string_attribute(self, **kwargs):
+ """
+ Tester found the optional SDP attribute named 'Service Name'. Press
+ 'Yes' if the string displayed below is correct.
+
+ Value: Advanced Audio
+ Source
+ """
+
+ # TODO: Extract and verify attribute name and value from description
+ return "OK"
+
+ @assert_description
+ def TSC_A2DP_mmi_user_confirm_no_optional_attribute_support(self, **kwargs):
+ """
+ Tester could not find the optional SDP attribute named 'Provider Name'.
+ Is this correct?
+ """
+
+ # TODO: Extract and verify attribute name from description
+ return "OK"
+
+ @assert_description
+ def TSC_AVDTPEX_mmi_iut_accept_delayreport(self, **kwargs):
+ """
+ Take action if necessary to accept the Delay Reportl command from the
+ tester.
+ """
+
+ return "OK"
+
+ @assert_description
+ def TSC_AVDTPEX_mmi_iut_initiate_media_transport_connect(self, **kwargs):
+ """
+ Take action to initiate an AVDTP media transport.
+ """
+
+ return "OK"
+
+ @assert_description
+ def TSC_AVDTPEX_mmi_user_confirm_SIG_SMG_BV_28_C(self, **kwargs):
+ """
+ Were all the service capabilities reported to the upper tester valid?
+ """
+
+ # TODO: verify
+ return "Yes"
+
+ @assert_description
+ def TSC_AVDTPEX_mmi_iut_reject_invalid_command(self, **kwargs):
+ """
+ Take action to reject the invalid command sent by the tester.
+ """
+
+ return "OK"
+
+ @assert_description
+ def TSC_AVDTPEX_mmi_iut_reject_open(self, **kwargs):
+ """
+ Take action to reject the invalid OPEN command sent by the tester.
+ """
+
+ return "OK"
+
+ @assert_description
+ def TSC_AVDTPEX_mmi_iut_reject_start(self, **kwargs):
+ """
+ Take action to reject the invalid START command sent by the tester.
+ """
+
+ return "OK"
+
+ @assert_description
+ def TSC_AVDTPEX_mmi_iut_reject_suspend(self, **kwargs):
+ """
+ Take action to reject the invalid SUSPEND command sent by the tester.
+ """
+
+ return "OK"
+
+ @assert_description
+ def TSC_AVDTPEX_mmi_iut_reject_reconfigure(self, **kwargs):
+ """
+ Take action to reject the invalid or incompatible RECONFIGURE command
+ sent by the tester.
+ """
+
+ return "OK"
+
+ @assert_description
+ def TSC_AVDTPEX_mmi_iut_reject_get_all_capabilities(self, **kwargs):
+ """
+ Take action to reject the invalid GET ALL CAPABILITIES command with the
+ error code BAD_LENGTH.
+ """
+
+ return "OK"
+
+ @assert_description
+ def TSC_AVDTPEX_mmi_iut_reject_get_capabilities(self, **kwargs):
+ """
+ Take action to reject the invalid GET CAPABILITIES command with the
+ error code BAD_LENGTH.
+ """
+
+ return "OK"
+
+ @assert_description
+ def TSC_AVDTPEX_mmi_iut_reject_set_configuration(self, **kwargs):
+ """
+ Take action to reject the SET CONFIGURATION sent by the tester. The IUT
+ is expected to respond with SEP_IN_USE because the SEP requested was
+ previously configured.
+ """
+
+ return "OK"
+
+ def TSC_AVDTPEX_mmi_iut_reject_get_configuration(self, **kwargs):
+ """
+ Take action to reject the GET CONFIGURATION sent by the tester. The IUT
+ is expected to respond with BAD_ACP_SEID because the SEID requested was
+ not previously configured.
+ """
+
+ return "OK"
+
+ @assert_description
+ def TSC_AVDTPEX_mmi_iut_reject_close(self, **kwargs):
+ """
+ Take action to reject the invalid CLOSE command sent by the tester.
+ """
+
+ return "OK"
+
+ @assert_description
+ def TSC_AVDTPEX_mmi_user_confirm_SIG_SMG_BV_18_C(self, **kwargs):
+ """
+ Did the IUT receive media with the following information?
+
+ - V = RTP_Ver
+ - P = 0 (no padding bits)
+ - X = 0 (no extension)
+ - CC = 0 (no
+ contributing source)
+ - M = 0
+ """
+
+ # TODO: verify
+ return "OK"
diff --git a/proto/blueberry/a2dp.proto b/proto/blueberry/a2dp.proto
index 803ab0a..a5e1b1a 100644
--- a/proto/blueberry/a2dp.proto
+++ b/proto/blueberry/a2dp.proto
@@ -3,76 +3,235 @@ syntax = "proto3";
package blueberry;
import "blueberry/host.proto";
-
+import "google/protobuf/wrappers.proto";
+
+// Service to trigger A2DP (Advanced Audio Distribution Profile) procedures.
+//
+// Requirement for the implementor:
+// - Streams must not be automaticaly opened, even if discovered.
+// - The `Host` service should be implemented
+//
+// References:
+// - [A2DP] Bluetooth SIG, Specification of the Bluetooth System,
+// Advanced Audio Distribution, Version 1.3 or Later
+// - [AVDTP] Bluetooth SIG, Specification of the Bluetooth System,
+// Audio/Video Distribution Transport Protocol, Version 1.3 or Later
service A2DP {
+ // Open a stream from a local **Source** endpoint to a remote **Sink**
+ // endpoint.
+ //
+ // The returned source should be in the AVDTP_OPEN state (see [AVDTP] 9.1).
+ // The function must block until the stream has reached this state
+ //
+ // A cancellation of this call must result in aborting the current
+ // AVDTP procedure (see [AVDTP] 9.9)
rpc OpenSource(OpenSourceRequest) returns (OpenSourceResponse);
+ // Open a stream from a local **Sink** endpoint to a remote **Source**
+ // endpoint.
+ //
+ // The returned sink must be in the AVDTP_OPEN state (see [AVDTP] 9.1).
+ // The function must block until the stream has reached this state
+ //
+ // A cancellation of this call must result in aborting the current
+ // AVDTP procedure (see [AVDTP] 9.9)
rpc OpenSink(OpenSinkRequest) returns (OpenSinkResponse);
+ // Wait for a stream from a local **Source** endpoint to
+ // a remote **Sink** endpoint to open.
+ //
+ // If the peer has opened a source prior to this call, the server will
+ // return it. The server must return the same source only once.
+ rpc WaitSource(WaitSourceRequest) returns (WaitSourceResponse);
+ // Wait for a stream from a local **Sink** endpoint to
+ // a remote **Source** endpoint to open.
+ //
+ // If the peer has opened a sink prior to this call, the server will
+ // return it. The server must return the same sink only once.
+ rpc WaitSink(WaitSinkRequest) returns (WaitSinkResponse);
+ // Get if the stream is suspended
+ rpc IsSuspended(IsSuspendedRequest) returns (google.protobuf.BoolValue);
+ // Start a suspended stream.
rpc Start(StartRequest) returns (StartResponse);
+ // Suspend a started stream.
rpc Suspend(SuspendRequest) returns (SuspendResponse);
+ // Close a stream, the source or sink tokens must not be reused afterwards.
rpc Close(CloseRequest) returns (CloseResponse);
- rpc Abort(AbortRequest) returns (AbortResponse);
+ // Get the `AudioEncoding` value of a stream
+ rpc GetAudioEncoding(GetAudioEncodingRequest) returns (GetAudioEncodingResponse);
+ // Playback audio by a `Source`
+ rpc PlaybackAudio(stream PlaybackAudioRequest) returns (PlaybackAudioResponse);
+ // Capture audio from a `Sink`
+ rpc CaptureAudio(CaptureAudioRequest)returns (stream CaptureAudioResponse);
+}
+
+// Audio encoding formats.
+enum AudioEncoding {
+ // Interleaved stereo frames with 16-bit signed little-endian linear PCM
+ // samples at 44100Hz sample rate
+ PCM_S16_LE_44K1_STEREO = 0;
+ // Interleaved stereo frames with 16-bit signed little-endian linear PCM
+ // samples at 48000Hz sample rate
+ PCM_S16_LE_48K_STEREO = 1;
}
+// A Token representing a Source stream (see [A2DP] 2.2).
+// It's acquired via an OpenSource on the A2DP service.
message Source {
+ // Opaque value filled by the GRPC server, must not
+ // be modified nor crafted.
bytes cookie = 1;
}
+// A Token representing a Sink stream (see [A2DP] 2.2).
+// It's acquired via an OpenSink on the A2DP service.
message Sink {
+ // Opaque value filled by the GRPC server, must not
+ // be modified nor crafted.
bytes cookie = 1;
}
+// Request for the `OpenSource` method.
message OpenSourceRequest {
+ // The connection that will open the stream.
Connection connection = 1;
}
+// Response for the `OpenSource` method.
message OpenSourceResponse {
- oneof response {
+ // Result of the `OpenSource` call:
+ // - If successfull: a Source
+ oneof result {
Source source = 1;
}
}
+// Request for the `OpenSink` method.
message OpenSinkRequest {
+ // The connection that will open the stream.
Connection connection = 1;
}
+// Response for the `OpenSink` method.
message OpenSinkResponse {
- oneof response {
+ // Result of the `OpenSink` call:
+ // - If successfull: a Sink
+ oneof result {
+ Sink sink = 1;
+ }
+}
+
+// Request for the `WaitSource` method.
+message WaitSourceRequest {
+ // The connection that is awaiting the stream.
+ Connection connection = 1;
+}
+
+// Response for the `WaitSource` method.
+message WaitSourceResponse {
+ // Result of the `WaitSource` call:
+ // - If successfull: a Source
+ oneof result {
+ Source source = 1;
+ }
+}
+
+// Request for the `WaitSink` method.
+message WaitSinkRequest {
+ // The connection that is awaiting the stream.
+ Connection connection = 1;
+}
+
+// Response for the `WaitSink` method.
+message WaitSinkResponse {
+ // Result of the `WaitSink` call:
+ // - If successfull: a Sink
+ oneof result {
+ Sink sink = 1;
+ }
+}
+
+// Request for the `IsSuspended` method.
+message IsSuspendedRequest {
+ // The stream on which the function will check if it's suspended
+ oneof target {
Sink sink = 1;
+ Source source = 2;
}
}
+// Request for the `Start` method.
message StartRequest {
- oneof response {
+ // Target of the start, either a Sink or a Source.
+ oneof target {
Sink sink = 1;
Source source = 2;
}
}
+// Response for the `Start` method.
message StartResponse {}
+// Request for the `Suspend` method.
message SuspendRequest {
- oneof response {
+ // Target of the suspend, either a Sink or a Source.
+ oneof target {
Sink sink = 1;
Source source = 2;
}
}
+// Response for the `Suspend` method.
message SuspendResponse {}
+// Request for the `Close` method.
message CloseRequest {
- oneof response {
+ // Target of the close, either a Sink or a Source.
+ oneof target {
Sink sink = 1;
Source source = 2;
}
}
+// Response for the `Close` method.
message CloseResponse {}
-message AbortRequest {
- oneof response {
+// Request for the `GetAudioEncoding` method.
+message GetAudioEncodingRequest {
+ // The stream on which the function will read the `AudioEncoding`.
+ oneof target {
Sink sink = 1;
Source source = 2;
}
}
-message AbortResponse {} \ No newline at end of file
+// Response for the `GetAudioEncoding` method.
+message GetAudioEncodingResponse {
+ // Audio encoding of the stream.
+ AudioEncoding encoding = 1;
+}
+
+// Request for the `PlaybackAudio` method.
+message PlaybackAudioRequest {
+ // Source that will playback audio.
+ Source source = 1;
+ // Audio data to playback.
+ // The audio data must be encoded in the specified `AudioEncoding` value
+ // obtained in response of a `GetAudioEncoding` method call.
+ bytes data = 2;
+}
+
+// Response for the `PlaybackAudio` method.
+message PlaybackAudioResponse {}
+
+// Request for the `CaptureAudio` method.
+message CaptureAudioRequest {
+ // Sink that will capture audio
+ Sink sink = 1;
+}
+
+// Response for the `CaptureAudio` method.
+message CaptureAudioResponse {
+ // Captured audio data.
+ // The audio data is encoded in the specified `AudioEncoding` value
+ // obained in response of a `GetAudioEncoding` method call.
+ bytes data = 1;
+}
diff --git a/proto/blueberry/host.proto b/proto/blueberry/host.proto
index 61ddd8a..bf80ab9 100644
--- a/proto/blueberry/host.proto
+++ b/proto/blueberry/host.proto
@@ -4,51 +4,98 @@ package blueberry;
import "google/protobuf/empty.proto";
+// Service to trigger Bluetooth Host procedures
+//
+// At startup, the Host must be in BR/EDR connectable mode
+// (see GAP connectability modes)
service Host {
+ // Reset the host.
+ // **After** responding to this command, the GRPC server should loose
+ // all its state.
+ // This is comparable to a process restart or an hardware reset.
+ // The GRPC server might take some time to be available after
+ // this command.
rpc Reset(google.protobuf.Empty) returns (google.protobuf.Empty);
+ // Create an ACL BR/EDR connection to a peer.
+ // This should send a CreateConnection on the HCI level.
+ // If the two devices have not established a previous bond,
+ // the peer must be discoverable.
rpc Connect(ConnectRequest) returns (ConnectResponse);
+ // Get an active ACL BR/EDR connection to a peer.
rpc GetConnection(GetConnectionRequest) returns (GetConnectionResponse);
+ // Wait for an ACL BR/EDR connection from a peer.
+ rpc WaitConnection(WaitConnectionRequest) returns (WaitConnectionResponse);
+ // Disconnect an ACL BR/EDR connection. The Connection must not be reused afterwards.
rpc Disconnect(DisconnectRequest) returns (DisconnectResponse);
+ // Read the local Bluetooth device address.
+ // This should return the same value as a Read BD_ADDR HCI command.
rpc ReadLocalAddress(google.protobuf.Empty) returns (ReadLocalAddressResponse);
- rpc SetConnectable(SetConnectableRequest) returns (SetConnectableResponse);
}
+// A Token representing an ACL connection.
+// It's acquired via a Connect on the Host service.
message Connection {
+ // Opaque value filled by the GRPC server, must not
+ // be modified nor crafted.
bytes cookie = 1;
}
+// Request of the `Connect` method.
message ConnectRequest {
+ // Peer Bluetooth Device Address as array of 6 bytes.
bytes address = 1;
}
+// Response of the `Connect` method.
message ConnectResponse {
- oneof response {
+ // Result of the `Connect` call:
+ // - If successfull: a Connection
+ oneof result {
Connection connection = 1;
}
}
+// Request of the `GetConnection` method.
message GetConnectionRequest {
+ // Peer Bluetooth Device Address as array of 6 bytes.
bytes address = 1;
}
+// Response of the `GetConnection` method.
message GetConnectionResponse {
- oneof response {
+ // Result of the `GetConnection` call:
+ // - If successfull: a Connection
+ oneof result {
Connection connection = 1;
}
}
+// Request of the `WaitConnection` method.
+message WaitConnectionRequest {
+ // Peer Bluetooth Device Address as array of 6 bytes.
+ bytes address = 1;
+}
+
+// Response of the `WaitConnection` method.
+message WaitConnectionResponse {
+ // Result of the `WaitConnection` call:
+ // - If successfull: a Connection
+ oneof result {
+ Connection connection = 1;
+ }
+}
+
+// Request of the `Disconnect` method.
message DisconnectRequest {
+ // Connection that should be disconnected.
Connection connection = 1;
}
+// Response of the `Disconnect` method.
message DisconnectResponse {}
+// Response of the `ReadLocalAddress` method.
message ReadLocalAddressResponse {
+ // Local Bluetooth Device Address as array of 6 bytes.
bytes address = 1;
}
-
-message SetConnectableRequest {
- bool connectable = 1;
-}
-
-message SetConnectableResponse {}