a2dp: Refactor and handle AVDTP mmi

Change-Id: Ib6384e12c81f8adc2d8f73bf945cf7efe4e250ee
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
+from ._description import assert_description, format_function
+from ._audio import AudioSignal
 
-def _ensure_connection(host, addr):
-    global _connection
-    if not _connection:
-        _connection = host.GetConnection(address=addr).connection
+AUDIO_AMPLITUDE = 0.8
 
-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
+class A2DPProxy:
+    connection: Optional[Connection] = None
+    sink: Optional[Sink] = None
+    source: Optional[Source] = None
 
-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)
+    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:
-            _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
+            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
-        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.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:
-          _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.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.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)
-        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 @@
 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;
   }
 }
 
-message StartRequest {
-  oneof response {
+// 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 {
+  // 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 @@
 
 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 {}