Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit fe1f0527 authored by Himanshu Rawat's avatar Himanshu Rawat Committed by Automerger Merge Worker
Browse files

Merge changes Idabb8939,Ibbd8e9a1,If246e311,I788c1f6b,I76858fd8 into main am: 2110c70f

parents ebfb3eef 2110c70f
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -42,6 +42,7 @@
    <option name="test-tag" value="BumbleBluetoothTests" />
    <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
        <option name="package" value="android.bluetooth" />
        <option name="hidden-api-checks" value="false" />
    </test>

    <!-- Only run if the Bluetooth Mainline module is installed. -->
+410 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package android.bluetooth;

import static com.google.common.truth.Truth.assertThat;

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.ParcelUuid;
import android.platform.test.annotations.RequiresFlagsEnabled;
import android.platform.test.flag.junit.CheckFlagsRule;
import android.platform.test.flag.junit.DeviceFlagsValueProvider;
import android.util.Log;

import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;

import com.android.bluetooth.flags.Flags;
import com.android.compatibility.common.util.AdoptShellPermissionsRule;

import com.google.common.util.concurrent.SettableFuture;

import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;

import pandora.HIDGrpc;
import pandora.HostProto.AdvertiseRequest;
import pandora.HostProto.OwnAddressType;

/** Test cases for {@link Hid Host}. */
@RunWith(AndroidJUnit4.class)
public class HidHostDualModeTest {
    private static final String TAG = "HidHostDualModeTest";
    private SettableFuture<Integer> mFutureConnectionIntent,
            mFutureBondIntent,
            mFutureHandShakeIntent,
            mFutureReportIntent,
            mFutureProtocolModeIntent,
            mFutureTransportIntent;
    private SettableFuture<Boolean> mFutureHogpServiceIntent;
    private BluetoothDevice mDevice;
    private BluetoothHidHost mHidService;
    private BluetoothHeadset mHfpService;
    private BluetoothA2dp mA2dpService;
    private final Context mContext = ApplicationProvider.getApplicationContext();
    private final BluetoothManager mManager = mContext.getSystemService(BluetoothManager.class);
    private final BluetoothAdapter mAdapter = mManager.getAdapter();
    private HIDGrpc.HIDBlockingStub mHidBlockingStub;
    private byte mReportId;
    private static final int KEYBD_RPT_ID = 1;
    private static final int KEYBD_RPT_SIZE = 9;
    private static final int MOUSE_RPT_ID = 2;
    private static final int MOUSE_RPT_SIZE = 4;

    @Rule(order = 0)
    public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();

    @Rule(order = 1)
    public final AdoptShellPermissionsRule mPermissionRule = new AdoptShellPermissionsRule();

    @Rule(order = 2)
    public final PandoraDevice mBumble = new PandoraDevice();

    private BroadcastReceiver mHidStateReceiver =
            new BroadcastReceiver() {
                @Override
                public void onReceive(Context context, Intent intent) {
                    switch (intent.getAction()) {
                        case BluetoothHidHost.ACTION_CONNECTION_STATE_CHANGED:
                            int state =
                                    intent.getIntExtra(
                                            BluetoothProfile.EXTRA_STATE, BluetoothAdapter.ERROR);
                            int transport =
                                    intent.getIntExtra(
                                            BluetoothDevice.EXTRA_TRANSPORT,
                                            BluetoothDevice.TRANSPORT_AUTO);
                            Log.i(
                                    TAG,
                                    "Connection state change: "
                                            + state
                                            + "transport: "
                                            + transport);
                            if (state == BluetoothProfile.STATE_CONNECTED
                                    || state == BluetoothProfile.STATE_DISCONNECTED) {
                                if (mFutureConnectionIntent != null) {
                                    mFutureConnectionIntent.set(state);
                                }
                                if (state == BluetoothProfile.STATE_CONNECTED
                                        && mFutureTransportIntent != null) {
                                    mFutureTransportIntent.set(transport);
                                }
                            }
                            break;
                        case BluetoothDevice.ACTION_PAIRING_REQUEST:
                            mBumble.getRemoteDevice().setPairingConfirmation(true);
                            break;
                        case BluetoothDevice.ACTION_BOND_STATE_CHANGED:
                            int bondState =
                                    intent.getIntExtra(
                                            BluetoothDevice.EXTRA_BOND_STATE,
                                            BluetoothDevice.ERROR);
                            Log.i(TAG, "Bond state change:" + bondState);
                            if (bondState == BluetoothDevice.BOND_BONDED
                                    || bondState == BluetoothDevice.BOND_NONE) {
                                if (mFutureBondIntent != null) {
                                    mFutureBondIntent.set(bondState);
                                }
                            }
                            break;
                        case BluetoothDevice.ACTION_UUID:
                            ParcelUuid[] parcelUuids =
                                    intent.getParcelableArrayExtra(
                                            BluetoothDevice.EXTRA_UUID, ParcelUuid.class);
                            for (int i = 0; i < parcelUuids.length; i++) {
                                Log.d(TAG, "UUIDs : index=" + i + " uuid=" + parcelUuids[i]);
                                if (parcelUuids[i].equals(BluetoothUuid.HOGP)) {
                                    if (mFutureHogpServiceIntent != null) {
                                        mFutureHogpServiceIntent.set(true);
                                    }
                                }
                            }
                            break;
                        case BluetoothHidHost.ACTION_PROTOCOL_MODE_CHANGED:
                            int protocolMode =
                                    intent.getIntExtra(
                                            BluetoothHidHost.EXTRA_PROTOCOL_MODE,
                                            BluetoothHidHost.PROTOCOL_UNSUPPORTED_MODE);
                            Log.i(TAG, "Protocol mode:" + protocolMode);
                            if (mFutureProtocolModeIntent != null) {
                                mFutureProtocolModeIntent.set(protocolMode);
                            }
                            break;
                        case BluetoothHidHost.ACTION_HANDSHAKE:
                            int handShake =
                                    intent.getIntExtra(
                                            BluetoothHidHost.EXTRA_STATUS,
                                            BluetoothHidDevice.ERROR_RSP_UNKNOWN);
                            Log.i(TAG, "Handshake status:" + handShake);
                            if (mFutureHandShakeIntent != null) {
                                mFutureHandShakeIntent.set(handShake);
                            }
                            break;
                        case BluetoothHidHost.ACTION_REPORT:
                            byte[] report = intent.getByteArrayExtra(BluetoothHidHost.EXTRA_REPORT);
                            int reportSize =
                                    intent.getIntExtra(
                                            BluetoothHidHost.EXTRA_REPORT_BUFFER_SIZE, 0);
                            mReportId = report[0];
                            if (mFutureReportIntent != null) {
                                mFutureReportIntent.set((reportSize - 1));
                            }
                            break;
                        default:
                            break;
                    }
                }
            };

    // These callbacks run on the main thread.
    private final BluetoothProfile.ServiceListener mBluetoothProfileServiceListener =
            new BluetoothProfile.ServiceListener() {

                @Override
                public void onServiceConnected(int profile, BluetoothProfile proxy) {
                    switch (profile) {
                        case BluetoothProfile.HEADSET:
                            mHfpService = (BluetoothHeadset) proxy;
                            break;
                        case BluetoothProfile.A2DP:
                            mA2dpService = (BluetoothA2dp) proxy;
                            break;
                        case BluetoothProfile.HID_HOST:
                            mHidService = (BluetoothHidHost) proxy;
                            break;
                        default:
                            break;
                    }
                }

                @Override
                public void onServiceDisconnected(int profile) {}
            };

    @Before
    public void setUp() throws Exception {
        final IntentFilter filter = new IntentFilter();
        filter.addAction(BluetoothHidHost.ACTION_CONNECTION_STATE_CHANGED);
        filter.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED);
        filter.addAction(BluetoothDevice.ACTION_PAIRING_REQUEST);
        filter.addAction(BluetoothDevice.ACTION_UUID);
        filter.addAction(BluetoothHidHost.ACTION_PROTOCOL_MODE_CHANGED);
        filter.addAction(BluetoothHidHost.ACTION_HANDSHAKE);
        filter.addAction(BluetoothHidHost.ACTION_REPORT);
        mContext.registerReceiver(mHidStateReceiver, filter);
        mAdapter.getProfileProxy(
                mContext, mBluetoothProfileServiceListener, BluetoothProfile.HID_HOST);
        mAdapter.getProfileProxy(mContext, mBluetoothProfileServiceListener, BluetoothProfile.A2DP);
        mAdapter.getProfileProxy(
                mContext, mBluetoothProfileServiceListener, BluetoothProfile.HEADSET);
        mHidBlockingStub = mBumble.hidBlocking();
        AdvertiseRequest request =
                AdvertiseRequest.newBuilder()
                        .setLegacy(true)
                        .setConnectable(true)
                        .setOwnAddressType(OwnAddressType.RANDOM)
                        .build();
        mBumble.hostBlocking().advertise(request);

        mFutureConnectionIntent = SettableFuture.create();

        mDevice = mBumble.getRemoteDevice();
        assertThat(mDevice.createBond()).isTrue();
        assertThat(mFutureConnectionIntent.get()).isEqualTo(BluetoothProfile.STATE_CONNECTED);
        if (mA2dpService != null) {
            assertThat(
                            mA2dpService.setConnectionPolicy(
                                    mDevice, BluetoothProfile.CONNECTION_POLICY_FORBIDDEN))
                    .isTrue();
        }
        if (mHfpService != null) {
            assertThat(
                            mHfpService.setConnectionPolicy(
                                    mDevice, BluetoothProfile.CONNECTION_POLICY_FORBIDDEN))
                    .isTrue();
        }
        mFutureHogpServiceIntent = SettableFuture.create();
        assertThat(mFutureHogpServiceIntent.get()).isTrue();
        assertThat(mHidService.getPreferredTransport(mDevice))
                .isEqualTo(BluetoothDevice.TRANSPORT_BREDR);
        // LE transport
        mFutureTransportIntent = SettableFuture.create();
        mHidService.setPreferredTransport(mDevice, BluetoothDevice.TRANSPORT_LE);
        // Verifies BREDR transport Disconnected
        mFutureConnectionIntent = SettableFuture.create();
        assertThat(mFutureConnectionIntent.get()).isEqualTo(BluetoothProfile.STATE_DISCONNECTED);

        assertThat(mFutureTransportIntent.get()).isEqualTo(BluetoothDevice.TRANSPORT_LE);
        assertThat(mHidService.getPreferredTransport(mDevice))
                .isEqualTo(BluetoothDevice.TRANSPORT_LE);
    }

    @After
    public void tearDown() throws Exception {
        if (mDevice.getBondState() == BluetoothDevice.BOND_BONDED) {
            mFutureBondIntent = SettableFuture.create();
            mDevice.removeBond();
            assertThat(mFutureBondIntent.get()).isEqualTo(BluetoothDevice.BOND_NONE);
        }
        mContext.unregisterReceiver(mHidStateReceiver);
    }

    /**
     * Test HID Preferred transport selection Test case
     *
     * <ol>
     *   <li>1. Android to creates bonding and HID connected with default transport.
     *   <li>2. Android switch the transport to LE and Verifies the transport
     *   <li>3. Android switch the transport to BR/EDR and Verifies the transport
     * </ol>
     */
    @Test
    @RequiresFlagsEnabled({
        Flags.FLAG_ALLOW_SWITCHING_HID_AND_HOGP,
        Flags.FLAG_SAVE_INITIAL_HID_CONNECTION_POLICY
    })
    public void setPreferredTransportTest() throws Exception {

        // BREDR transport
        mFutureTransportIntent = SettableFuture.create();
        mHidService.setPreferredTransport(mDevice, BluetoothDevice.TRANSPORT_BREDR);
        // Verifies LE transport Disconnected
        mFutureConnectionIntent = SettableFuture.create();
        assertThat(mFutureConnectionIntent.get()).isEqualTo(BluetoothProfile.STATE_DISCONNECTED);

        assertThat(mFutureTransportIntent.get()).isEqualTo(BluetoothDevice.TRANSPORT_BREDR);
        assertThat(mHidService.getPreferredTransport(mDevice))
                .isEqualTo(BluetoothDevice.TRANSPORT_BREDR);
    }

    /**
     * Test Get Report
     *
     * <ol>
     *   <li>1. Android creates bonding and connect the HID Device
     *   <li>2. Android get report and verifies the report
     * </ol>
     */
    @Test
    @RequiresFlagsEnabled({
        Flags.FLAG_ALLOW_SWITCHING_HID_AND_HOGP,
        Flags.FLAG_SAVE_INITIAL_HID_CONNECTION_POLICY
    })
    public void hogpGetReportTest() throws Exception {

        // Keyboard report
        byte id = KEYBD_RPT_ID;
        mHidService.getReport(mDevice, BluetoothHidHost.REPORT_TYPE_INPUT, id, (int) 0);
        mFutureReportIntent = SettableFuture.create();
        assertThat(mFutureReportIntent.get()).isEqualTo(KEYBD_RPT_SIZE);
        assertThat(mReportId).isEqualTo(KEYBD_RPT_ID);

        // Mouse report
        id = MOUSE_RPT_ID;
        mHidService.getReport(mDevice, BluetoothHidHost.REPORT_TYPE_INPUT, id, (int) 0);
        mFutureReportIntent = SettableFuture.create();
        assertThat(mFutureReportIntent.get()).isEqualTo(MOUSE_RPT_SIZE);
        assertThat(mReportId).isEqualTo(MOUSE_RPT_ID);
    }

    /**
     * Test Get Protocol mode
     *
     * <ol>
     *   <li>1. Android creates bonding and connect the HID Device
     *   <li>2. Android Gets the Protocol mode and verifies the mode
     * </ol>
     */
    @Test
    @RequiresFlagsEnabled({
        Flags.FLAG_ALLOW_SWITCHING_HID_AND_HOGP,
        Flags.FLAG_SAVE_INITIAL_HID_CONNECTION_POLICY
    })
    public void hogpGetProtocolModeTest() throws Exception {
        mHidService.getProtocolMode(mDevice);
        mFutureProtocolModeIntent = SettableFuture.create();
        assertThat(mFutureProtocolModeIntent.get())
                .isEqualTo(BluetoothHidHost.PROTOCOL_REPORT_MODE);
    }

    /**
     * Test Set Protocol mode
     *
     * <ol>
     *   <li>1. Android creates bonding and connect the HID Device
     *   <li>2. Android Sets the Protocol mode and verifies the mode
     * </ol>
     */
    @Test
    @RequiresFlagsEnabled({
        Flags.FLAG_ALLOW_SWITCHING_HID_AND_HOGP,
        Flags.FLAG_SAVE_INITIAL_HID_CONNECTION_POLICY
    })
    public void hogpSetProtocolModeTest() throws Exception {
        mHidService.setProtocolMode(mDevice, BluetoothHidHost.PROTOCOL_BOOT_MODE);
        mFutureHandShakeIntent = SettableFuture.create();
        assertThat(mFutureHandShakeIntent.get()).isEqualTo(BluetoothHidDevice.ERROR_RSP_SUCCESS);
    }

    /**
     * Test Set Report
     *
     * <ol>
     *   <li>1. Android creates bonding and connect the HID Device
     *   <li>2. Android Set report and verifies the report
     * </ol>
     */
    @Test
    @RequiresFlagsEnabled({
        Flags.FLAG_ALLOW_SWITCHING_HID_AND_HOGP,
        Flags.FLAG_SAVE_INITIAL_HID_CONNECTION_POLICY
    })
    public void hogpSetReportTest() throws Exception {
        // Keyboard report
        mHidService.setReport(mDevice, BluetoothHidHost.REPORT_TYPE_INPUT, "010203040506070809");
        mFutureHandShakeIntent = SettableFuture.create();
        assertThat(mFutureHandShakeIntent.get()).isEqualTo(BluetoothHidDevice.ERROR_RSP_SUCCESS);
        // Mouse report
        mHidService.setReport(mDevice, BluetoothHidHost.REPORT_TYPE_INPUT, "02030405");
        mFutureHandShakeIntent = SettableFuture.create();
        assertThat(mFutureHandShakeIntent.get()).isEqualTo(BluetoothHidDevice.ERROR_RSP_SUCCESS);
    }

    /**
     * Test Virtual Unplug from Hid Host
     *
     * <ol>
     *   <li>1. Android creates bonding and connect the HID Device
     *   <li>2. Android Virtual Unplug and verifies Bonding
     * </ol>
     */
    @Test
    @RequiresFlagsEnabled({
        Flags.FLAG_ALLOW_SWITCHING_HID_AND_HOGP,
        Flags.FLAG_SAVE_INITIAL_HID_CONNECTION_POLICY
    })
    public void hogpVirtualUnplugFromHidHostTest() throws Exception {
        mHidService.virtualUnplug(mDevice);
        mFutureBondIntent = SettableFuture.create();
        assertThat(mFutureBondIntent.get()).isEqualTo(BluetoothDevice.BOND_NONE);
    }
}
+294 −60

File changed.

Preview size limit exceeded, changes collapsed.

+25 −1
Original line number Diff line number Diff line
@@ -15,8 +15,11 @@ service HID {
  rpc VirtualCableUnplugHost(google.protobuf.Empty) returns (google.protobuf.Empty);
  // Send a SET_REPORT command, acting as a HID host, to a connected HID device
  rpc SendHostReport(SendHostReportRequest) returns (SendHostReportResponse);
  // receive Protocol Mode Event
  rpc OnSetProtocolMode(google.protobuf.Empty) returns (stream ProtocolModeEvent);
  // receive Report Event
  rpc OnSetReport(google.protobuf.Empty) returns (stream ReportEvent);
}

// Enum values match those in BluetoothHidHost.java
enum HidReportType {
  HID_REPORT_TYPE_UNSPECIFIED = 0;
@@ -24,6 +27,17 @@ enum HidReportType {
  HID_REPORT_TYPE_OUTPUT = 2;
  HID_REPORT_TYPE_FEATURE = 3;
}
// Enum values match those in BluetoothHidHost.java
enum ProtocolMode {
  PROTOCOL_REPORT_MODE = 0;
  PROTOCOL_BOOT_MODE = 1;
  PROTOCOL_UNSUPPORTED_MODE = 255;
}
enum HidReportId {
  HID_KEYBD_RPT_ID = 0;
  HID_MOUSE_RPT_ID = 1;
  HID_INVALID_RPT_ID = 3;
}

message SendHostReportRequest {
  bytes address = 1;
@@ -34,3 +48,13 @@ message SendHostReportRequest {
message SendHostReportResponse {

}

message ProtocolModeEvent {
  ProtocolMode protocol_mode = 1;
}

message ReportEvent {
  HidReportType report_type = 1;
  HidReportId report_id = 2;
  string report_data = 3;
}
+83 −6
Original line number Diff line number Diff line
@@ -11,6 +11,14 @@ from google.protobuf import empty_pb2 # pytype: disable=pyi-error
from pandora_experimental.hid_grpc_aio import HIDServicer

from bumble.pandora import utils
from pandora_experimental.hid_pb2 import (
    ProtocolModeEvent,
    ReportEvent,
    PROTOCOL_REPORT_MODE,
    PROTOCOL_BOOT_MODE,
    PROTOCOL_UNSUPPORTED_MODE,
)

from bumble.core import (
    BT_BR_EDR_TRANSPORT,
    BT_L2CAP_PROTOCOL_ID,
@@ -489,20 +497,30 @@ def sdp_records():

# -----------------------------------------------------------------------------
def hogp_device(device):
    global input_report_characteristic
    # Create an 'input report' characteristic to send keyboard reports to the host
    input_report_characteristic = Characteristic(
    input_report_kb_characteristic = Characteristic(
        GATT_REPORT_CHARACTERISTIC,
        Characteristic.Properties.READ | Characteristic.Properties.WRITE | Characteristic.Properties.NOTIFY,
        Characteristic.READABLE | Characteristic.WRITEABLE,
        bytes([0, 0, 0, 0, 0, 0, 0, 0]),
        bytes([0, 0, 0, 0, 0, 0, 0, 0, 0]),
        [Descriptor(
            GATT_REPORT_REFERENCE_DESCRIPTOR,
            Descriptor.READABLE,
            bytes([0x01, HID_INPUT_REPORT]),
        )],
    )

    # Create an 'input report' characteristic to send mouse reports to the host
    input_report_mouse_characteristic = Characteristic(
        GATT_REPORT_CHARACTERISTIC,
        Characteristic.Properties.READ | Characteristic.Properties.WRITE | Characteristic.Properties.NOTIFY,
        Characteristic.READABLE | Characteristic.WRITEABLE,
        bytes([0, 0, 0, 0]),
        [Descriptor(
            GATT_REPORT_REFERENCE_DESCRIPTOR,
            Descriptor.READABLE,
            bytes([0x02, HID_INPUT_REPORT]),
        )],
    )
    # Create an 'output report' characteristic to receive keyboard reports from the host
    output_report_characteristic = Characteristic(
        GATT_REPORT_CHARACTERISTIC,
@@ -558,7 +576,8 @@ def hogp_device(device):
                    Characteristic.READABLE,
                    HID_KEYBOARD_REPORT_MAP,
                ),
                input_report_characteristic,
                input_report_kb_characteristic,
                input_report_mouse_characteristic,
                output_report_characteristic,
            ],
        ),
@@ -630,6 +649,12 @@ def on_set_report_cb(report_id: int, report_type: int, report_size: int, data: b
    logging.info("SET_REPORT report_id: " + str(report_id) + "report_type: " + str(report_type) + "report_size " +
                 str(report_size) + "data:" + str(data))

    report = ReportEvent()
    report.report_type = report_type
    report.report_id = report_id
    report.report_data = str(data.hex())
    hid_report_queue.put_nowait(report)

    if report_type == Message.ReportType.FEATURE_REPORT:
        retValue.status = hid_device.GetSetReturn.ERR_INVALID_PARAMETER
    elif report_type == Message.ReportType.INPUT_REPORT:
@@ -657,7 +682,15 @@ def on_get_protocol_cb():
def on_set_protocol_cb(protocol: int):
    retValue = hid_device.GetSetStatus()
    # We do not support SET_PROTOCOL.
    logging.info(f"SET_PROTOCOL report_id: {protocol}")
    logging.info(f"SET_PROTOCOL mode: {protocol}")
    mode = ProtocolModeEvent()
    if protocol == PROTOCOL_REPORT_MODE:
        mode.protocol_mode = PROTOCOL_REPORT_MODE
    elif protocol == PROTOCOL_BOOT_MODE:
        mode.protocol_mode = PROTOCOL_BOOT_MODE
    else:
        mode.protocol_mode = PROTOCOL_UNSUPPORTED_MODE
    hid_protoMode_queue.put_nowait(mode)
    retValue.status = hid_device.GetSetReturn.ERR_UNSUPPORTED_REQUEST
    return retValue

@@ -667,6 +700,9 @@ def on_virtual_cable_unplug_cb():
    asyncio.create_task(handle_virtual_cable_unplug())


hid_protoMode_queue = None


# This class implements the Hid Pandora interface.
class HIDService(HIDServicer):

@@ -676,6 +712,7 @@ class HIDService(HIDServicer):
        super().__init__()
        self.device = device
        self.device.sdp_service_records.update(sdp_records())
        self.event_queue: Optional[asyncio.Queue[ProtocolModeEvent]] = None
        hogp_device(self.device)
        logging.info(f'Hid device register: ')
        global hid_device
@@ -742,3 +779,43 @@ class HIDService(HIDServicer):
            logging.exception(f'Device does not exist')
            raise e
        return empty_pb2.Empty()

    @utils.rpc
    async def OnSetProtocolMode(self, request: empty_pb2.Empty,
                                context: grpc.ServicerContext) -> AsyncGenerator[ProtocolModeEvent, None]:
        logging.info(f'OnSetProtocolMode')

        if self.event_queue is not None:
            raise RuntimeError('already streaming OnSetProtocolMode events')

        self.event_queue = asyncio.Queue()
        global hid_protoMode_queue
        hid_protoMode_queue = self.event_queue

        try:
            while event := await hid_protoMode_queue.get():
                yield event

        finally:
            self.event_queue = None
            hid_protoMode_queue = None

    @utils.rpc
    async def OnSetReport(self, request: empty_pb2.Empty,
                          context: grpc.ServicerContext) -> AsyncGenerator[ReportEvent, None]:
        logging.info(f'OnSetReport')

        if self.event_queue is not None:
            raise RuntimeError('already streaming OnSetReport events')

        self.event_queue = asyncio.Queue()
        global hid_report_queue
        hid_report_queue = self.event_queue

        try:
            while event := await hid_report_queue.get():
                yield event

        finally:
            self.event_queue = None
            hid_report_queue = None