Add bluetooth_nrf52.py and other changes to support nRF52-based BLE HID server

BUG=chromium:923046
TEST=run bluetooth_AdapterHIDReports.ble_mouse on setup that includes
nRF52 devkit attached to Fizz.
e.g test_that -b vayne vaynemanor bluetooth_AdapterHIDReports.ble_mouse --autotest_dir=. --args "chameleon_host=vaynemanor-chameleon"

Change-Id: If8092b82f3420fa0e0e03c2c2c5bacf48df5f4a2
Reviewed-on: https://chromium-review.googlesource.com/1450029
Commit-Ready: Neeraj Poojary <npoojary@google.com>
Tested-by: Neeraj Poojary <npoojary@google.com>
Reviewed-by: Shyh-In Hwang <josephsih@chromium.org>
diff --git a/chameleond/devices/bluetooth_hid_flow.py b/chameleond/devices/bluetooth_hid_flow.py
index 97f3367..4a0caa0 100644
--- a/chameleond/devices/bluetooth_hid_flow.py
+++ b/chameleond/devices/bluetooth_hid_flow.py
@@ -15,6 +15,7 @@
 from chameleond.utils.bluetooth_hid import BluetoothHIDMouse
 from chameleond.utils.bluetooth_peripheral_kit import PeripheralKit
 from chameleond.utils.bluetooth_rn42 import RN42
+from chameleond.utils.bluetooth_nrf52 import nRF52
 
 
 class BluetoothHIDFlow(chameleon_device.Flow):
@@ -210,3 +211,21 @@
                               BluefruitLE.USB_VID, BluefruitLE.USB_PID)
     BluetoothHIDMouse.__init__(self, PeripheralKit.SSP_JUST_WORK_MODE,
                                BluefruitLE)
+
+
+class BleHIDMouseFlow(BluetoothHIDFlow, BluetoothHIDMouse):
+  """A flow object that emulates a BLE mouse device."""
+
+  DRIVER = nRF52.DRIVER
+  DRIVER_MODULE = nRF52.DRIVER_MODULE # See explanation in bluetooth_nrf52.py
+
+  def __init__(self, port_id, usb_ctrl):
+    """Initializes a BleHIDMouseFlow object.
+
+    Args:
+      port_id: the port id that represents the type of port used.
+      usb_ctrl: a USBController object that BluetoothHIDFlow references to.
+    """
+    BluetoothHIDFlow.__init__(self, port_id, 'BleMouse', usb_ctrl,
+                              nRF52.USB_VID, nRF52.USB_PID)
+    BluetoothHIDMouse.__init__(self, PeripheralKit.PIN_CODE_MODE, nRF52)
diff --git a/chameleond/drivers/fpga_tio.py b/chameleond/drivers/fpga_tio.py
index 82d3bf9..3b90265 100644
--- a/chameleond/drivers/fpga_tio.py
+++ b/chameleond/drivers/fpga_tio.py
@@ -97,6 +97,9 @@
         bluetooth_hid_flow.BluetoothHOGMouseFlow.DRIVER)
     self._bluetooth_a2dp_sink_ctrl = lazy(usb.USBController)(
         bluetooth_a2dp.BluetoothA2DPSinkFlow.DRIVER)
+    # See explanation for using DRIVER_MODULE in bluetooth_nrf52.py
+    self._ble_hid_ctrl = lazy(usb.USBController)(
+        bluetooth_hid_flow.BleHIDMouseFlow.DRIVER_MODULE)
 
     if platform == 'chromeos':
       self._devices = self.init_devices_for_chromeos()
@@ -122,6 +125,8 @@
     self.printer = self._device_manager.GetChameleonDevice(ids.USB_PRINTER)
     self.bluetooth_a2dp_sink = self._device_manager.GetChameleonDevice(
         ids.BLUETOOTH_A2DP_SINK)
+    self.ble_mouse = self._device_manager.GetChameleonDevice(
+        ids.BLE_MOUSE)
     self._flow_manager = flow_manager.FlowManager(self._flows)
 
     self.Reset()
@@ -137,6 +142,9 @@
         ids.BLUETOOTH_A2DP_SINK:
             bluetooth_a2dp.BluetoothA2DPSinkFlow(
                 ids.BLUETOOTH_A2DP_SINK, self._bluetooth_a2dp_sink_ctrl),
+        ids.BLE_MOUSE:
+            bluetooth_hid_flow.BleHIDMouseFlow(
+                ids.BLE_MOUSE, self._ble_hid_ctrl),
     }
     return devices
 
diff --git a/chameleond/utils/bluetooth_nrf52.py b/chameleond/utils/bluetooth_nrf52.py
new file mode 100644
index 0000000..8fbb200
--- /dev/null
+++ b/chameleond/utils/bluetooth_nrf52.py
@@ -0,0 +1,704 @@
+# Copyright 2019 The Chromium OS Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+"""
+This module provides an abstraction of the Nordic nRF52 bluetooth low energy
+kit.
+"""
+
+from __future__ import print_function
+
+import logging
+import time
+
+import common
+import serial_utils
+from bluetooth_peripheral_kit import PeripheralKit
+from bluetooth_peripheral_kit import PeripheralKitException
+
+
+class nRF52Exception(PeripheralKitException):
+  """A dummy exception class for nRF52 class."""
+  pass
+
+
+class nRF52(PeripheralKit):
+  """This is an abstraction of Nordic's nRF52 Dongle and the C application
+     that implements BLE mouse and keyboard functionality.
+
+     SDK: https://www.nordicsemi.com/Software-and-Tools/Software/nRF5-SDK
+
+     See autotest-private/nRF52/ble_app_hids/README for information about
+     using the SDK to compile the application.
+  """
+
+  # Serial port settings (override)
+  BAUDRATE = 115200
+  DRIVER = 'cdc_acm'
+  # Driver name in udev is 'cdc_acm', but builtin module is 'cdc-acm.ko'
+  # So we need to look for cdc_acm when searching by driver,
+  # but looking in builtins requires searching by 'cdc-acm'.
+  DRIVER_MODULE = 'cdc-acm'
+  BAUDRATE = 115200
+  USB_VID = '1d6b'
+  USB_PID = '0002'
+
+  # A newline can just be a '\n' to denote the end of a command
+  NEWLINE = '\n'
+  CMD_FS = ' '                                      # Command field separator
+
+  # Supported device types
+  MOUSE = 'MOUSE'
+  KEYBOARD = 'KEYBOARD'
+
+  RESET_SLEEP_SECS = 1
+
+  # Mouse button constants
+  MOUSE_BUTTON_LEFT_BIT = 1
+  MOUSE_BUTTON_RIGHT_BIT = 2
+
+  # Specific Commands
+  # Reboot the nRF52
+  CMD_REBOOT = "RBT"
+  # Reset the nRF52 and erase all previous bonds
+  CMD_FACTORY_RESET = "FRST"
+  # Return the name that is sent in advertisement packets
+  CMD_GET_ADVERTISED_NAME = "GN"
+  # Return the nRF52 firmware version
+  CMD_GET_FIRMWARE_VERSION = "GV"
+  # Return the Bluetooth address of the nRF52
+  CMD_GET_NRF52_MAC = "GM"
+  # Return the address of the device connected (if there exists a connection)
+  CMD_GET_REMOTE_CONNECTION_MAC = "GC"
+  # Return the status of the nRF52's connection with a central device
+  CMD_GET_CONNECTION_STATUS = "GS"
+
+  # Return the type of device the HID service is set
+  CMD_GET_DEVICE_TYPE = "GD"
+  # Set the nRF52 HID service to mouse
+  CMD_SET_MOUSE = "SM"
+  # Set the nRF52 HID service to keyboard
+  CMD_SET_KEYBOARD = "SK"
+  # Start HID service emulation
+  CMD_START_HID_EM = "START"
+  # Start HID service emulation
+  CMD_STOP_HID_EM = "STOP"
+  # Start advertising with the current settings (HID type)
+  CMD_START_ADVERTISING = "ADV"
+
+  # Press (or clear) one or more buttons (left/right)
+  CMD_MOUSE_BUTTON = "B"
+  # Click the left and/or right button of the mouse
+  CMD_MOUSE_CLICK = "C"
+  # Move the mouse along x and/or y axis
+  CMD_MOUSE_MOVE = "M"
+  # Scrolling the mouse wheel up/down
+  CMD_MOUSE_SCROLL = "S"
+
+  def GetCapabilities(self):
+    """What can this kit do/not do that tests need to adjust for?
+
+    Returns:
+      A dictionary from PeripheralKit.CAP_* strings to an appropriate value.
+      See above (CAP_*) for details.
+    """
+    return {PeripheralKit.CAP_TRANSPORTS: [PeripheralKit.TRANSPORT_LE],
+            PeripheralKit.CAP_HAS_PIN: False,
+            PeripheralKit.CAP_INIT_CONNECT: False}
+
+  def EnterCommandMode(self):
+    """Make the kit enter command mode.
+
+    The application on the nRF52 Dongle is always in command mode, so this
+    method will just create a serial connection if necessary
+
+    Returns:
+      True if the kit successfully entered command mode.
+
+    Raises:
+      nRF52Exception if there is an error in creating the serial connection
+    """
+    if self._serial is None:
+      self.CreateSerialDevice()
+    if not self._command_mode:
+      self._command_mode = True
+    return True
+
+  def LeaveCommandMode(self, force=False):
+    """Make the kit leave command mode.
+
+    As above, the nRF52 application is always in command mode.
+
+    Args:
+      force: True if we want to ignore potential errors and leave command mode
+            regardless of those errors
+
+    Returns:
+      True if the kit successfully left command mode.
+    """
+    if self._command_mode or force:
+      self._command_mode = False
+    return True
+
+  def Reboot(self):
+    """Reboot the nRF52 Dongle.
+
+    Does not erase the bond information.
+
+    Returns:
+      True if the kit rebooted successfully.
+    """
+    self.SerialSendReceive(self.CMD_REBOOT,
+                           msg='rebooting nRF52')
+    time.sleep(self.RESET_SLEEP_SECS)
+    return True
+
+  def FactoryReset(self):
+    """Factory reset the nRF52 Dongle.
+
+    Erase the bond information and reboot.
+
+    Returns:
+      True if the kit is reset successfully.
+    """
+    self.SerialSendReceive(self.CMD_FACTORY_RESET,
+                           msg='factory reset nRF52')
+    time.sleep(self.RESET_SLEEP_SECS)
+    return True
+
+  def GetAdvertisedName(self):
+    """Get the name advertised by the nRF52.
+
+    Returns:
+      The device name that the application uses in advertising
+    """
+    return self.SerialSendReceive(self.CMD_GET_ADVERTISED_NAME,
+                                  msg='getting advertised name')
+
+  def GetFirmwareVersion(self):
+    """Get the firmware version of the kit.
+
+    This is useful for checking what features are supported if we want to
+    support muliple versions of some kit.
+
+    For nRF52, returns the Link Layer Version (8 corresponds to BT 4.2),
+    Nordic Company ID (89), and Firmware ID (135).
+
+    Returns:
+      The firmware version of the kit.
+    """
+    return self.SerialSendReceive(self.CMD_GET_FIRMWARE_VERSION,
+                                  msg='getting firmware version')
+
+  def GetOperationMode(self):
+    """Get the operation mode.
+
+    This is master/slave in Bluetooth BR/EDR; the Bluetooth LE equivalent is
+    central/peripheral. For legacy reasons, we call it MASTER or SLAVE only.
+    Not all kits may support all modes.
+
+    nRF52 only supports peripheral role
+
+    Returns:
+      The operation mode of the kit.
+    """
+    logging.debug('GetOperationMode is a NOP on nRF52')
+    return "SLAVE"
+
+  def SetMasterMode(self):
+    """Set the kit to master/central mode.
+
+    nRF52 application only acts as a peripheral
+
+    Returns:
+      True if master/central mode was set successfully.
+
+    Raises:
+      A kit-specific exception if master/central mode is unsupported.
+    """
+    error_msg = 'Failed to set master/central mode'
+    logging.error(error_msg)
+    raise nRF52Exception(error_msg)
+
+  def SetSlaveMode(self):
+    """Set the kit to slave/peripheral mode.
+
+    Silently succeeds, because the nRF52 application is always a peripheral
+
+    Returns:
+      True if slave/peripheral mode was set successfully.
+
+    Raises:
+      A kit-specific exception if slave/peripheral mode is unsupported.
+    """
+    logging.debug('SetSlaveMode is a NOP on nRF52')
+    return True
+
+  def GetAuthenticationMode(self):
+    """Get the authentication mode.
+
+    This specifies how the device will authenticate with the DUT, for example,
+    a PIN code may be used.
+
+    Not supported on nRF52 application.
+
+    Returns:
+      None as the nRF52 does not support an Authentication mode.
+    """
+    logging.debug('GetAuthenticationMode is a NOP on nRF52')
+    return None
+
+  def SetAuthenticationMode(self, mode):
+    """Set the authentication mode to the specified mode.
+
+    If mode is PIN_CODE_MODE, implementations must ensure the default PIN
+    is set by calling _SetDefaultPinCode() as appropriate.
+
+    Not supported on nRF52 application.
+
+    Args:
+      mode: the desired authentication mode (specified in PeripheralKit)
+
+    Returns:
+      True if the mode was set successfully,
+
+    Raises:
+      A kit-specific exception if given mode is not supported.
+    """
+    error_msg = 'nRF52 does not support authentication mode'
+    logging.error(error_msg)
+    raise nRF52Exception(error_msg)
+
+  def GetPinCode(self):
+    """Get the pin code.
+
+    Returns:
+      A string representing the pin code,
+      None if there is no pin code stored.
+    """
+    warn_msg = 'nRF52 does not support PIN code mode, no PIN exists'
+    logging.warn(warn_msg)
+    return None
+
+  def SetPinCode(self, pin):
+    """Set the pin code.
+
+    Not support on nRF52 application.
+
+    Returns:
+      True if the pin code is set successfully,
+
+    Raises:
+      A kit-specifc exception if the pin code is invalid.
+    """
+    error_msg = 'nRF52 does not support PIN code mode'
+    logging.error(error_msg)
+    raise nRF52Exception(error_msg)
+
+  def GetServiceProfile(self):
+    """Get the service profile.
+
+    Unrelated to HID for the nRF52 application, so ignore for now
+
+    Returns:
+      The service profile currently in use (as per constant in PeripheralKit)
+    """
+    logging.debug('GetServiceProfile is a NOP on nRF52')
+    return "HID"
+
+  def SetServiceProfileSPP(self):
+    """Set SPP as the service profile.
+
+    Unrelated to HID for the nRF52 application, so ignore for now
+
+    Returns:
+      True if the service profile was set to SPP successfully.
+
+    Raises:
+      A kit-specifc exception if unsuppported.
+    """
+    error_msg = 'Failed to set SPP service profile'
+    logging.error(error_msg)
+    raise nRF52Exception(error_msg)
+
+  def SetServiceProfileHID(self):
+    """Set HID as the service profile.
+
+    nRF52 application only does HID at the moment. Silently succeeds
+
+    Returns:
+      True if the service profile was set to HID successfully.
+    """
+    logging.debug('SetServiceProfileHID is a NOP on nRF52')
+    return True
+
+  def GetLocalBluetoothAddress(self):
+    """Get the address advertised by the nRF52, which is the MAC address.
+
+    Address is returned as XX:XX:XX:XX:XX:XX
+
+    Returns:
+      The address of the nRF52 if successful or None if it fails
+    """
+    address = self.SerialSendReceive(self.CMD_GET_NRF52_MAC,
+                                     msg='getting local MAC address')
+    return address
+
+  def GetRemoteConnectedBluetoothAddress(self):
+    """Get the address of the device that is connected to the nRF52.
+
+    Address is returned as XX:XX:XX:XX:XX:XX
+    If not connected, nRF52 will return 00:00:00:00:00:00
+
+    Returns:
+      The address of the connected device or a null address if successful.
+      None if the serial receiving fails
+    """
+    address = self.SerialSendReceive(self.CMD_GET_REMOTE_CONNECTION_MAC,
+                                     msg='getting remote MAC address')
+    if len(address) == 17:
+      return address
+    else:
+      logging.error('remote connection address is invalid: %s', raw_address)
+      return None
+
+  def GetConnectionStatus(self):
+    """Get whether the nRF52 is connected to another device.
+
+    nRF52 returns a string 'INVALID' or 'CONNECTED'
+
+    Returns:
+      True if the nRF52 is connected to another device
+    """
+    result = self.SerialSendReceive(self.CMD_GET_CONNECTION_STATUS,
+                                    msg = 'getting connection status')
+    return result == 'CONNECTED'
+
+  def EnableConnectionStatusMessage(self):
+    """Enable the connection status message.
+
+    On some kits, this is required to use connection-related methods.
+
+    Not supported by the nRF52 application for now. This could be
+    changed so that Connection Status Messages are sent by nRF52.
+
+    Returns:
+      True if enabling the connection status message successfully.
+    """
+    logging.debug('EnableConnectionStatusMessage is a NOP on nRF52')
+    return True
+
+  def DisableConnectionStatusMessage(self):
+    """Disable the connection status message.
+
+    Not supported by the nRF52 application for now. This could be
+    changed so that Connection Status Messages are sent by nRF52.
+
+    Returns:
+      True if disabling the connection status message successfully.
+    """
+    logging.debug('DisableConnectionStatusMessage is a NOP on nRF52')
+    return True
+
+  def GetHIDDeviceType(self):
+    """Get the HID device type.
+
+    Returns:
+      A string representing the HID device type
+    """
+    return self.SerialSendReceive(self.CMD_GET_DEVICE_TYPE,
+                                  msg='getting HID device type')
+
+  def SetHIDType(self, device_type):
+    """Set HID type to the specified device type.
+
+    Args:
+      device_type: the HID type to emulate, from PeripheralKit
+                   (MOUSE, KEYBOARD)
+
+    Returns:
+      True if successful
+
+    Raises:
+      A kit-specific exception if that device type is not supported.
+    """
+    if device_type == self.MOUSE:
+      result = self.SerialSendReceive(self.CMD_SET_MOUSE,
+                             msg='setting mouse as HID type')
+      print(result)
+    elif device_type == self.KEYBOARD:
+      self.SerialSendReceive(self.CMD_SET_KEYBOARD,
+                             msg='setting keyboard as HID type')
+    else:
+      msg = "Failed to set HID type, not supported: %s" % device_type
+      logging.error(msg)
+      raise nRF52Exception(msg)
+    return True
+
+  def GetClassOfService(self):
+    """Get the class of service, if supported.
+
+    Not supported on nRF52
+
+    Returns:
+      None, the only reasonable value for BLE-only devices
+    """
+    logging.debug('GetClassOfService is a NOP on nRF52')
+    return None
+
+  def SetClassOfService(self, class_of_service):
+    """Set the class of service, if supported.
+
+    The class of service is a number usually assigned by the Bluetooth SIG.
+    Usually supported only on BR/EDR kits.
+
+    Not supported on nRF52, but fake it
+
+    Args:
+      class_of_service: A decimal integer representing the class of service.
+
+    Returns:
+      True as this action is not supported.
+    """
+    logging.debug('SetClassOfService is a NOP on nRF52')
+    return True
+
+  def GetClassOfDevice(self):
+    """Get the class of device, if supported.
+
+    The kit uses a hexadeciaml string to represent the class of device.
+    It is converted to a decimal number as the return value.
+    The class of device is a number usually assigned by the Bluetooth SIG.
+    Usually supported only on BR/EDR kits.
+
+    Not supported on nRF52, so None
+
+    Returns:
+      None, the only reasonable value for BLE-only devices.
+    """
+    logging.debug('GetClassOfDevice is a NOP on nRF52')
+    return None
+
+  def SetClassOfDevice(self, device_type):
+    """Set the class of device, if supported.
+
+    The class of device is a number usually assigned by the Bluetooth SIG.
+    Usually supported only on BR/EDR kits.
+
+    Not supported on nRF52, but fake it.
+
+    Args:
+      device_type: A decimal integer representing the class of device.
+
+    Returns:
+      True as this action is not supported.
+    """
+    logging.debug('SetClassOfDevice is a NOP on nRF52')
+    return True
+
+  def SetRemoteAddress(self, remote_address):
+    """Set the remote Bluetooth address.
+
+    (Usually this will be the device under test that we want to connect with,
+    where the kit starts the connection.)
+
+    Not supported on nRF52 HID application.
+
+    Args:
+      remote_address: the remote Bluetooth MAC address, which must be given as
+                      12 hex digits with colons between each pair.
+                      For reference: '00:29:95:1A:D4:6F'
+
+    Returns:
+      True if the remote address was set successfully.
+
+    Raises:
+      PeripheralKitException if the given address was malformed.
+    """
+    error_msg = 'Failed to set remote address'
+    logging.error(error_msg)
+    raise nRF52Exception(error_msg)
+
+  def Connect(self):
+    """Connect to the stored remote bluetooth address.
+
+    In the case of a timeout (or a failure causing an exception), the caller
+    is responsible for retrying when appropriate.
+
+    Not supported on nRF52 HID application.
+
+    Returns:
+      True if connecting to the stored remote address succeeded, or
+      False if a timeout occurs.
+    """
+    error_msg = 'Failed to connect to remote device'
+    logging.error(error_msg)
+    raise nRF52Exception(error_msg)
+
+  def Disconnect(self):
+    """Disconnect from the remote device.
+
+    Specifically, this causes the peripheral emulation kit to disconnect from
+    the remote connected device, usually the DUT.
+
+    Returns:
+      True if disconnecting from the remote device succeeded.
+    """
+    self.SerialSendReceive(self.CMD_DISCONNECT,
+                           msg='disconnect')
+    return True
+
+  def StartAdvertising(self):
+    """Command the nRF52 to begin advertising with its current settings.
+
+    Returns:
+      True if successful.
+    """
+    self.SerialSendReceive(self.CMD_START_ADVERTISING,
+                           msg='start advertising')
+    return True
+
+  def MouseMove(self, delta_x, delta_y):
+    """Move the mouse (delta_x, delta_y) steps.
+
+    Buttons currently pressed will stay pressed during this operation.
+    This move is relative to the current position by the HID standard.
+    Valid step values must be in the range [-127,127].
+
+    Args:
+      delta_x: The number of steps to move horizontally.
+               Negative values move left, positive values move right.
+      delta_y: The number of steps to move vertically.
+               Negative values move up, positive values move down.
+
+    Returns:
+      True if successful.
+    """
+    command = self.CMD_MOUSE_MOVE + self.CMD_FS
+    command += str(delta_x) + self.CMD_FS + str(delta_y)
+    message = 'moving BLE mouse ' + str(delta_x) + " " + str(delta_y)
+    result = self.SerialSendReceive(command, msg=message)
+    return True
+
+  def MouseScroll(self, steps):
+    """Scroll the mouse wheel steps number of steps.
+
+    Buttons currently pressed will stay pressed during this operation.
+    Valid step values must be in the range [-127,127].
+
+    Args:
+      steps: The number of steps to scroll the wheel.
+             With traditional scrolling:
+               Negative values scroll down, positive values scroll up.
+             With reversed (formerly "Australian") scrolling this is reversed.
+
+    Returns:
+      True if successful.
+    """
+    command = self.CMD_MOUSE_SCROLL + self.CMD_FS
+    command += self.CMD_FS
+    command += str(steps) + self.CMD_FS
+    message = 'scrolling BLE mouse'
+    result = self.SerialSendReceive(command, msg=message)
+    return True
+
+  def MouseHorizontalScroll(self, steps):
+    """Horizontally scroll the mouse wheel steps number of steps.
+
+    Buttons currently pressed will stay pressed during this operation.
+    Valid step values must be in the range [-127,127].
+
+    There is no nRF52 limitation for implementation. If we can program
+    the correct HID event report to emulate horizontal scrolling, this
+    can be supported.
+    **** Not implemented ****
+    Args:
+      steps: The number of steps to scroll the wheel.
+             With traditional scrolling:
+               Negative values scroll left, positive values scroll right.
+             With reversed (formerly "Australian") scrolling this is reversed.
+
+    Returns:
+      True if successful.
+    """
+    return True
+
+  def _MouseButtonCodes(self):
+    """Gives the letter codes for whatever buttons are pressed.
+
+    Returns:
+      A int w/ bits representing pressed buttons.
+    """
+    currently_pressed = 0
+    for button in self._buttons_pressed:
+      if button == PeripheralKit.MOUSE_BUTTON_LEFT:
+        currently_pressed += self.MOUSE_BUTTON_LEFT_BIT
+      elif button == PeripheralKit.MOUSE_BUTTON_RIGHT:
+        currently_pressed += self.MOUSE_BUTTON_RIGHT_BIT
+      else:
+        error = "Unknown mouse button in state: %s" % button
+        logging.error(error)
+        raise nRF52Exception(error)
+    return currently_pressed
+
+  def MousePressButtons(self, buttons):
+    """Press the specified mouse buttons.
+
+    The kit will continue to press these buttons until otherwise instructed, or
+    until its state has been reset.
+
+    Args:
+      buttons: A set of buttons, as PeripheralKit MOUSE_BUTTON_* values, that
+               will be pressed (and held down).
+
+    Returns:
+      True if successful.
+    """
+    self._MouseButtonStateUnion(buttons)
+    button_codes = self._MouseButtonCodes()
+    command = self.CMD_MOUSE_BUTTON + self.CMD_FS
+    command += str(button_codes)
+    message = 'pressing BLE mouse buttons'
+    result = self.SerialSendReceive(command, msg=message)
+    return True
+
+  def MouseReleaseAllButtons(self):
+    """Release all mouse buttons.
+
+    Returns:
+      True if successful.
+    """
+    self._MouseButtonStateClear()
+    command = self.CMD_MOUSE_BUTTON + self.CMD_FS
+    command += '0'
+    message = 'releasing all BLE HOG mouse buttons'
+    result = self.SerialSendReceive(command, msg=message)
+    return True
+
+  def Reset(self):
+    result = self.SerialSendReceive(nRF52.CMD_REBOOT, msg='reset nRF52')
+    return True
+
+  def SetModeMouse(self):
+    self.EnterCommandMode()
+    result = self.SerialSendReceive(nRF52.CMD_SET_MOUSE, msg='set nRF52 mouse')
+    return True
+
+  def GetKitInfo(self, connect_separately=False, test_reset=False):
+    """A simple demo of getting kit information."""
+    if connect_separately:
+      print('create serial device: %s' % self.CreateSerialDevice())
+    if test_reset:
+      print('factory reset: %s' % self.FactoryReset())
+    self.EnterCommandMode()
+    print('advertised name: %s' % self.GetAdvertisedName())
+    print('firmware version: %s' % self.GetFirmwareVersion())
+    print('local bluetooth address: %s' % self.GetLocalBluetoothAddress())
+    print('connection status: %s' % self.GetConnectionStatus())
+    # The class of service/device is None for LE kits (it is BR/EDR-only)
+
+
+if __name__ == '__main__':
+  kit_instance = nRF52()
+  kit_instance.GetKitInfo()
diff --git a/chameleond/utils/bluetooth_peripheral_kit.py b/chameleond/utils/bluetooth_peripheral_kit.py
index 8d66d66..1b32a05 100644
--- a/chameleond/utils/bluetooth_peripheral_kit.py
+++ b/chameleond/utils/bluetooth_peripheral_kit.py
@@ -10,7 +10,6 @@
 
 import serial_utils
 
-
 class PeripheralKitException(Exception):
   """A dummpy exception class for the PeripheralKit class."""
   pass
diff --git a/chameleond/utils/ids.py b/chameleond/utils/ids.py
index 81ba5cb..f8b95d8 100644
--- a/chameleond/utils/ids.py
+++ b/chameleond/utils/ids.py
@@ -30,6 +30,7 @@
 BLUETOOTH_HOG_JOYSTICK = 24
 USB_PRINTER = 25
 BLUETOOTH_A2DP_SINK = 26
+BLE_MOUSE = 27
 
 # device names
 DEVICE_NAMES = {
@@ -59,6 +60,7 @@
     BLUETOOTH_HOG_JOYSTICK: 'bluetooth_hog_joystick',
     USB_PRINTER: 'usb_printer',
     BLUETOOTH_A2DP_SINK: 'bluetooth_a2dp_sink',
+    BLE_MOUSE: 'ble_mouse',
 }
 
 
diff --git a/chameleond/utils/usb.py b/chameleond/utils/usb.py
index 59fc668..5808f45 100644
--- a/chameleond/utils/usb.py
+++ b/chameleond/utils/usb.py
@@ -6,6 +6,7 @@
 import copy
 import logging
 import re
+import subprocess
 
 import chameleon_common  # pylint: disable=W0611
 from chameleond.utils import system_tools
@@ -28,6 +29,9 @@
   MODPROBE_NO_ACTION = 1  # command is redundant and no error occurred
   MODPROBE_DUPLICATED = 2  # module is already inserted/removed from the kernel
 
+  # List of builtin drivers
+  _builtins = ''
+
   def __init__(self, module):
     """Initializes a USBAudioController object.
 
@@ -35,7 +39,17 @@
     the module is not in kernel at initialization.
     """
     self._module = module
-    system_tools.SystemTools.Call('modprobe', '-r', self._module)
+    # Trying to remove a builtin module will result in an error.
+    # So check for builtins with "cat /lib/modules/`uname -r`/modules.builtin"
+    linux_ver = subprocess.check_output('uname -r',
+                                        stderr=subprocess.STDOUT,
+                                        shell=True).strip('\n')
+    checkforbuiltins_cmd = 'cat /lib/modules/' + linux_ver + '/modules.builtin'
+    self._builtins = subprocess.check_output(checkforbuiltins_cmd,
+                                             stderr=subprocess.STDOUT,
+                                             shell=True)
+    if self._module not in self._builtins:
+      system_tools.SystemTools.Call('modprobe', '-r', self._module)
 
   @property
   def _is_modprobed(self):
@@ -70,6 +84,11 @@
     Returns:
       The status code of modprobe result.
     """
+
+    # Check for builtin driver before calling modprobe to install
+    if self._module in self._builtins:
+      return
+
     args_list = self._MakeArgsForInsertModule()
     process = system_tools.SystemTools.RunInSubprocess('modprobe', *args_list)
     logging.info('Modprobe command is run with arguments: %s', str(args_list))