diff options
-rw-r--r-- | mmi2grpc/__init__.py | 40 | ||||
-rw-r--r-- | mmi2grpc/_audio.py | 79 | ||||
-rw-r--r-- | mmi2grpc/_description.py | 41 | ||||
-rw-r--r-- | mmi2grpc/a2dp.py | 649 | ||||
-rw-r--r-- | proto/blueberry/a2dp.proto | 179 | ||||
-rw-r--r-- | proto/blueberry/host.proto | 65 |
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 {} |