Add Bluefruit LE Friend kit to Chameleon

BluefruitLE should be minimally-equivalent to RN42, with the caveats
mentioned in the class docstring. (The Bluefruit LE Friend has a
higher-level AT command set, with fewer features.) This patch allows the
BluefruitLE kit to be used in tests for pairing, but does not yet add
full keyboard, mouse, or gamepad support.

Note that this kit is not currently hooked up to anything.

Note also that some of the less-sensical API changes in the refactor
were rolled back, and inconsistencies were addressed. In general,
failures should fail loudly now, using exceptions. Hopefully, this will
help diagnose flakes.

BUG=chromium:752719
TEST=First, locate the source for chameleond on a Chameleon board, cd
there, and run the self-test with only a Bluefruit LE kit attached:
$ IFS="^^^";export PS1=`echo $PS1|sed s/w/W/`;unset IFS
$ cd \
/usr/lib/python2.7/site-packages/chameleond-0.0.2-py2.7.egg/chameleond/
$ python utils/bluetooth_bluefruitle.py
Check that it doesn't crash, prints sensible results.

Second, test that current functionality is not broken:
Run this code on the Chameleon board, with only RN-42 attached:
$ make && make remote-install CHAMELEON_HOST=$CHAMELEON_IP
Execute the non-flaky non-stress tests that use this code,
see that the tests pass:
$ test_that --board ${BOARD} --args "chameleon_host=${CHAMELEON_IP}" \
${DUT_IP} bluetooth_AdapterPairing.mouse \
bluetooth_AdapterPairing.mouse.pairing_twice \
bluetooth_AdapterHIDReports.mouse

Third, test that this actually WAI with future patches:
Apply CL:611780, CL:602900, and CL:639850 to your chroot.
Execute the non-flaky non-stress tests that use this code,
see that things work as much as possible without Autotest adjustments.
Note that only the Bluefruit LE Friend should be attached.
$ test_that --board ${BOARD} --args "chameleon_host=${CHAMELEON_IP}" \
${DUT_IP} bluetooth_AdapterPairing.le.mouse \
bluetooth_AdapterPairing.le.mouse.pairing_twice \
bluetooth_AdapterHIDReports.le.mouse

Change-Id: Ie651cf1d35609b7421cc3d02bce75306cae4ba91
Reviewed-on: https://chromium-review.googlesource.com/625478
Commit-Ready: Alexander Lent <alent@google.com>
Tested-by: Alexander Lent <alent@google.com>
Reviewed-by: Wai-Hong Tam <waihong@google.com>
diff --git a/chameleond/utils/bluetooth_bluefruitle.py b/chameleond/utils/bluetooth_bluefruitle.py
new file mode 100644
index 0000000..82246b2
--- /dev/null
+++ b/chameleond/utils/bluetooth_bluefruitle.py
@@ -0,0 +1,661 @@
+# Copyright 2017 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 a Bluefruit LE Friend kit.
+
+This module was implemented so as to reuse as much as possible of the interface
+from the RN42 abstraction. Since the AT command set of the Bluefruit LE Friend
+is generally simpler and higher-level than that of the RN42, and is
+LE-specific, some commands are no-ops, and some just fake out certain
+functionality.
+"""
+# TODO(josephsih): Attempt to get features we need added to the firmware.
+
+
+import logging
+import time
+
+from bluetooth_peripheral_kit import GetKitInfo
+from bluetooth_peripheral_kit import PeripheralKit
+from bluetooth_peripheral_kit import PeripheralKitException
+
+
+class BluefruitLEException(PeripheralKitException):
+  """A dummy exception for BluefruitLE-related tasks"""
+  pass
+
+
+# pylint: disable=abstract-method
+class BluefruitLE(PeripheralKit):
+  """This is an abstraction of the Adafruit Bluefruit LE Friend kit.
+
+  This was written specifically for the v2 hardare running the v0.7.7 firmware.
+  Check the version with the command 'ATI', or by calling GetFirmwareVersion().
+  For more details, see:
+  https://learn.adafruit.com/introducing-adafruit-ble-bluetooth-low-energy-friend?view=all
+  """
+
+  # Serial port settings (override)
+  # TODO(alent): Target: 115200, but every factory reset drops to 9600
+  BAUDRATE = 9600
+  # TODO(josephsih): Future versions of this chip have a different driver?
+
+  # Timing info
+  RESET_SLEEP_SECONDS = 3
+
+  # HID Types that the Bleufruit LE Combines into one
+  UNDISTINGUISHABLE_HID_TYPES = [PeripheralKit.KEYBOARD,
+                                 PeripheralKit.MOUSE,
+                                 PeripheralKit.COMBO]
+
+  # A reason for not being able to do something
+  UNSUPPORTED_REASON = "Not supported by Bluefruit LE as of v0.7.7"
+
+  # Common Command Parts
+  AT = 'AT'
+  RESULT_OK = 'OK'
+  RESULT_ERROR = 'ERROR'
+  SUFFIX_EXISTS = '?'
+  SUFFIX_ENABLE = '=1'
+
+  # Specific Commands
+  CMD_FACTORY_RESET = '+FACTORYRESET'
+  CMD_GET_DEVICE_NAME = '+GAPDEVNAME'
+  CMD_INFO = 'I'
+  CMD_PARTIAL_RESET = 'Z'
+  CMD_GET_CONNECTION_STATUS = '+GAPGETCONN'
+  CMD_GET_LOCAL_ADDRESS = '+BLEGETADDR'
+  CMD_GET_REMOTE_ADDRESS = '+BLEGETPEERADDR'
+  CMD_DISCONNECT = '+GAPDISCONNECT'
+  CMD_BLE_HID_ENABLE = '+BLEHIDEN'
+  CMD_BLE_HID_GAMEPAD_ENABLE = '+BLEHIDGAMEPADEN'
+
+  def _ValidateAndExtractResult(self, command, result, validate_only, message):
+    """Validate Bluefruit LE command result, and extract return value.
+
+    This only works for commands that return OK in all meaningful,
+    recoverable-error result cases, and for commands that also may return a
+    single-line result (See valdate_only).
+
+    The Bluefruit LE kit, unlike the RN42, has echo enabled by default.
+    So, for a setter command AT+SOMETHING=1, we get:
+    AT+SOMETHING=1\\r\\n
+    OK\\r\\n
+    But for a getter command AT+SOMETHING, we might get:
+    AT+SOMETHING\\r\\n
+    1\\r\\n
+    OK\\r\\n
+    [Note that \\ above should be read as a single backslash.]
+    This method validates and optionally extracts a result.
+
+    Args:
+      command: The command sent with SerialSendReceive
+      result: The result of the SerialSendReceive call
+      validate_only: Do not extract a result when True, just confirm success
+      message: A SerialSendReceive-stlye message to put into debug logs.
+
+    Returns:
+      True if validate_only and validation succeeds, otherwise the string
+      returned by the command if validation succeeds
+
+    Raises:
+      BluefruitLEException if validation fails.
+    """
+    # TODO(josephsih): Make this optionally handle more lines and optionally
+    # handle ERROR as a bool result.
+    result_parts = result.split(self.NEWLINE)
+    actual_length = len(result_parts)
+    expected_length = 2 if validate_only else 3
+    ok_index = 1 if validate_only else 2
+    if actual_length != expected_length:
+      values = (message, expected_length, actual_length, result)
+      error = "Incorrect number of lines in %s, wanted %s, got %s: %s" % values
+      logging.error(error)
+      raise BluefruitLEException(error)
+    if result_parts[0] != command: # Command always echoed first
+      values = (message, command, result)
+      error = "Unexpected command echo in %s, wanted %s, got: %s" % values
+      logging.error(error)
+      raise BluefruitLEException(error)
+    if result_parts[ok_index] != self.RESULT_OK:
+      values = (message, self.RESULT_OK, result)
+      error = "Not-OK command result in %s, wanted %s, got: %s" % values
+      logging.error(error)
+      raise BluefruitLEException(error)
+    else:
+      return True if validate_only else result_parts[1]
+
+  def __init__(self):
+    """Initialize the state of this kit abstraction.
+
+    Initially unknown, but current code assumes an adapter reset, more or less.
+    This seems reasonable as some, but not all, state is lost across reboots,
+    and this object is generally only create on daemon restarts, which can
+    include reboots.
+    """
+    super(BluefruitLE, self).__init__()
+    # The HID type when the Bluefruit can't distinguish (mouse/keyboard/combo)
+    # This is because it's always a combo internally
+    # Note it's Appearance value is (apparently always a keyboard like this?)
+    self._hid_fake_type = None
+
+  # TODO(alent): Run AT+MODESWITCHEN=local,0 to disable mode switch. (This would
+  # prevent us from leaving command mode if we get +++, w/o escaping + to \+.)
+  # TODO(alent): Way to detect mode switch or mode is wrong?
+  def EnterCommandMode(self):
+    """Make the kit enter command mode.
+
+    Enter command mode, creating the serial connection if necessary.
+    This must happen before other methods can be called, as they generally rely
+    on sending commands.
+
+    Long story short, the Bluefruit LE Friend has a physical mode switch,
+    so when it starts up it should be set to command mode (assuming that the
+    switch was set properly).
+    It can switch at runtime with +++\\r\\n over the USB tty, unless disabled.
+    We never *need* to enter/leave command mode, unlike the RN42, so no-op it.
+    [Note that \\ above should be read as a single backslash.]
+
+    Returns:
+      True if the kit succeessfully entered command mode.
+
+    Raises:
+      PeripheralKitException if there is an error in serial communication or
+      if the kit gives an unexpected response.
+      A kit-specific Exception if something else goes wrong.
+    """
+    if not self._serial:
+      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, we never switch out of command mode.
+
+    Args:
+      force: True if we want to ignore potential errors and attempt to
+             leave command mode regardless.
+
+    Returns:
+      True if the kit left command mode successfully.
+    """
+    if self._command_mode or force:
+      self._command_mode = False
+    return True
+
+  def Reboot(self):
+    """Reboot (or partially reset) the chip.
+
+    Rebooting or resetting the kit is required to make some settings take
+    effect after they are changed.
+
+    This destroys bonding data! Only do this when breaking the bond with the
+    remote device under test is acceptable.
+
+    Returns:
+      True if the kit rebooted successfully.
+
+    Raises:
+      A kit-specifc exception if something goes wrong.
+    """
+    command = self.AT + self.CMD_PARTIAL_RESET
+    message = '(partially) resetting Bluefruit LE'
+    result = self.SerialSendReceive(command, msg=message)
+    return self._ValidateAndExtractResult(command, result, True, message)
+
+  def FactoryReset(self):
+    """Factory reset the chip.
+
+    Reset the chip to the factory defaults.
+
+    Returns:
+      True if the kit is reset successfully.
+
+    Raises:
+      A kit-specifc exception if something goes wrong.
+    """
+    command = self.AT + self.CMD_FACTORY_RESET
+    message = 'factory reset Bluefruit LE'
+    result = self.SerialSendReceive(command, msg=message)
+    # TODO(alent): Need the wait?
+    time.sleep(self.RESET_SLEEP_SECONDS)
+    return self._ValidateAndExtractResult(command, result, True, message)
+
+  def GetChipName(self):
+    """Get the name advertised by the kit.
+
+    Returns:
+      The name that the kit advertises to other Bluetooth devices.
+    """
+    command = self.AT + self.CMD_GET_DEVICE_NAME
+    message = 'getting the advertisied name of the kit'
+    result = self.SerialSendReceive(command, msg=message)
+    return self._ValidateAndExtractResult(command, result, False, message)
+
+  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.
+
+    An example result is below:
+    ATI\\r\\n\\r\\n
+    BLEFRIEND32\\r\\n
+    nRF51822 QFACA10\\r\\n
+    6C280528C970FCDF\\r\\n
+    0.7.7\\r\\n
+    0.7.7\\r\\n
+    Dec 13 2016\\r\\n
+    S110 8.0.0, 0.2\\r\\n
+    OK
+    [Note that \\ above should be read as a single backslash.]
+
+    Returns:
+      The firmware version of the kit.
+    """
+    # TODO(alent): Generalize _ValidateAndExtractResult to do this?
+    result = self.SerialSendReceive(self.AT + self.CMD_INFO,
+                                    msg='getting Board Info')
+    info = result.split(self.NEWLINE)
+    # The 5th line of result contains the version that we want, probably.
+    return info[4]
+
+  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.
+
+    The Bluefruit LE Friend does not support the central role, only peripheral.
+
+    Returns:
+      The operation mode of the kit.
+    """
+    # TODO(alent): Better way to propagate this constant?
+    # TODO(alent): Is PERIPHERAL more appropriate for BLE? Does this matter?
+    logging.debug("GetOperationMode is a NOP on BluefruitLE")
+    return "SLAVE"
+
+  def SetMasterMode(self):
+    """Set the kit to central mode.
+
+    In BLE, this would be the Central role.
+    The Bluefruit LE Friend firmware can't do this.
+
+    Returns:
+      True if central mode was set successfully.
+
+    Raises:
+      A kit-specific exception if central mode is unsupported.
+    """
+    error_msg = "Failed to set master/central mode: " + self.UNSUPPORTED_REASON
+    logging.error(error_msg)
+    raise BluefruitLEException(error_msg)
+
+  def SetSlaveMode(self):
+    """Set the kit to slave/peripheral mode.
+
+    Silently succeeds, because the Bleufruit LE 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 BluefruitLE")
+    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.
+
+    Returns:
+      The authentication mode of the kit (from the choices in PeripheralKit).
+    """
+    logging.debug("GetAuthenticationMode is a NOP on BluefruitLE")
+    # TODO(alent): Fake PIN code necessary to make existing code work?
+    # TODO(alent): implement NONE?
+    return PeripheralKit.SSP_JUST_WORK_MODE
+
+  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.
+
+    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.
+    """
+    if mode == PeripheralKit.SSP_JUST_WORK_MODE:
+      return True
+    else:
+      error_msg = "Bluefruit LE does not support authentication mode: %s" % mode
+      error_msg = error_msg + ": " + self.UNSUPPORTED_REASON
+      logging.error(error_msg)
+      raise BluefruitLEException(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 = "Bluefruit LE does not support PIN code mode, none exists: "
+    warn_msg = warn_msg + self.UNSUPPORTED_REASON
+    logging.warn(warn_msg)
+    return None
+
+  def SetPinCode(self, pin):
+    """Set the pin code.
+
+    This is not supported.
+
+    Returns:
+      True if the pin code is set successfully,
+      False if the pin code is invalid.
+    """
+    warn_msg = "Bluefruit LE does not support PIN code mode, none exists: "
+    warn_msg = warn_msg + self.UNSUPPORTED_REASON
+    logging.warn(warn_msg)
+    return False
+
+  def GetServiceProfile(self):
+    """Get the service profile.
+
+    Returns:
+      The service profile currently in use (as per constant in PeripheralKit)
+    """
+    # TODO(alent): Move this constant to PeripheralKit?
+    logging.debug("GetServiceProfile is a NOP on BluefruitLE")
+    return "HID"
+
+  def SetServiceProfileSPP(self):
+    """Set SPP as the service profile.
+
+    In BLE, this would be something like a UART service.
+    The Bluefruit LE Friend firmware can do that, but,
+    the GATT profile is a proprietary Nordic Semiconductor one.
+    For now, unrelated to our HID efforts, so don't bother.
+
+    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: " + self.UNSUPPORTED_REASON
+    logging.error(error_msg)
+    raise BluefruitLEException(error_msg)
+
+  def SetServiceProfileHID(self):
+    """Set HID as the service profile.
+
+    This is currently a NOP on BluefruitLE, as it currently does only HID.
+
+    Returns:
+      True if the service profile was set to HID successfully.
+    """
+    logging.debug("GetAuthenticationMode is a NOP on BluefruitLE")
+    return True
+
+  def GetLocalBluetoothAddress(self):
+    """Get the local (kit's) Bluetooth MAC address.
+
+    The kit should always return a valid MAC address in the proper format:
+    12 digits with colons between each pair, like so: '00:06:66:75:A9:6F'
+
+    Returns:
+      The Bluetooth MAC address of the kit
+    """
+    command = self.AT + self.CMD_GET_LOCAL_ADDRESS
+    message = 'getting local (BluefruitLE\'s) MAC address'
+    result = self.SerialSendReceive(command, msg=message)
+    return self._ValidateAndExtractResult(command, result, False, message)
+
+  def GetConnectionStatus(self):
+    """Get the connection status.
+
+    This indicates that the kit is connected to a remote device, usually the
+    DUT.
+
+    The kit will give us a 0 or 1 as a string, which we can parse into a bool.
+
+    Returns:
+      True if the kit is connected to a remote device.
+    """
+    command = self.AT + self.CMD_GET_CONNECTION_STATUS
+    message = 'getting connection status'
+    result = self.SerialSendReceive(command, msg=message)
+    extracted = self._ValidateAndExtractResult(command, result, False, message)
+    return extracted == '1'
+
+  def EnableConnectionStatusMessage(self):
+    """No-op enable connection status message.
+
+    This does nothing and is not extant or necessary on the Bluefruit LE Friend.
+
+    Returns:
+      True
+    """
+    logging.debug("EnableConnectionStatusMessage is a NOP on BluefruitLE")
+    return True
+
+  def DisableConnectionStatusMessage(self):
+    """No-op disable connection status message.
+
+    This does nothing and is not extant or necessary on the Bluefruit LE Friend.
+
+    Returns:
+      True
+    """
+    logging.debug("DisableConnectionStatusMessage is a NOP on BluefruitLE")
+    return True
+
+  def GetRemoteConnectedBluetoothAddress(self):
+    """Get the Bluetooth MAC address of the current connected remote host.
+
+    On the Bluefruit LE, the docs indicate that AP+BLEGETPEERADDR, should give
+    ERROR if not connected. For some reason, I get garbage instead, even when
+    the device is not bonded. These semantics might differ slightly, but let's
+    just use connection status instead of the buggy command.
+    Maybe this will change in firmware versions > v0.7.7.
+
+    Returns:
+      The Bluetooth MAC address of the remote connected device if applicable,
+      or None if there is no remote connected device. If not None, this will
+      be properly formatted as a 12-digit MAC address with colons.
+    """
+    # TODO(josephsih): Investigate why this doesn't work
+    # Not connected, do nothing
+    if not self.GetConnectionStatus():
+      return None
+    # Otherwise, run the command:
+    command = self.AT + self.CMD_GET_REMOTE_ADDRESS
+    message = 'getting remote device\'s (DUT\'s) Bluetooth MAC'
+    result = self.SerialSendReceive(command, msg=message)
+    return self._ValidateAndExtractResult(command, result, False, message)
+
+  def GetHIDDeviceType(self):
+    # TODO(alent): Better documentation.
+    """Get the HID type.
+
+    The kit will give us a 0 or 1 as a string, which we can parse into a bool.
+
+    Returns:
+      A string representing the HID type (from PeripheralKit)
+    """
+    command_hid = self.AT + self.CMD_BLE_HID_ENABLE
+    message_hid = 'getting HID enabled status, to determine device type'
+    result_hid = self.SerialSendReceive(command_hid, msg=message_hid)
+    extracted_hid = self._ValidateAndExtractResult(command_hid, result_hid,
+                                                   False, message_hid)
+    is_combo = extracted_hid == '1'
+    command_gamepad = self.AT + self.CMD_BLE_HID_GAMEPAD_ENABLE
+    message_gamepad = 'getting gamepad enabled status, to determine device type'
+    result_gamepad = self.SerialSendReceive(command_gamepad,
+                                            msg=message_gamepad)
+    extracted_gamepad = self._ValidateAndExtractResult(command_gamepad,
+                                                       result_gamepad, False,
+                                                       message_gamepad)
+    is_gamepad = extracted_gamepad == '1'
+    if is_gamepad:
+      return PeripheralKit.GAMEPAD
+    elif is_combo and self._hid_fake_type:
+      return self._hid_fake_type
+    else:
+      # TODO(alent): Formally describe error in this API.
+      logging.error("Current HID Type is None")
+      return None
+
+  def SetHIDType(self, device_type):
+    """Set HID type to the specified device type.
+
+    Args:
+      device_type: the HID type to emulate, from PeripheralKit
+
+    Returns:
+      True if successful
+
+    Raises:
+      A kit-specific exception if that device type is not supported.
+    """
+    device_needs_faking = device_type in self.UNDISTINGUISHABLE_HID_TYPES
+    if device_needs_faking:
+      command_of_type = self.CMD_BLE_HID_ENABLE
+    elif device_type == PeripheralKit.GAMEPAD:
+      command_of_type = self.CMD_BLE_HID_GAMEPAD_ENABLE
+    else:
+      error_msg = "Failed to set HID type, not supported: %s" % device_type
+      logging.error(error_msg)
+      raise BluefruitLEException(error_msg)
+    command = self.AT + command_of_type + self.SUFFIX_ENABLE
+    message = 'setting %s as HID type' % device_type
+    result = self.SerialSendReceive(command, msg=message)
+    extracted = self._ValidateAndExtractResult(command, result, True, message)
+    if extracted:
+      if device_needs_faking:
+        self._hid_fake_type = device_type
+      else:
+        self._hid_fake_type = None
+    return extracted
+
+  def GetClassOfService(self):
+    """Get the class of service, if supported.
+
+    Not supported on Bluefruit LE, so None.
+
+    Returns:
+      None, the only reasonable value for BLE-only devices.
+    """
+    logging.debug("GetClassOfService is a NOP on BluefruitLE")
+    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.
+
+    Not supported on Bluefruit LE, 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 BluefruitLE")
+    return True
+
+  def GetClassOfDevice(self):
+    """Get the class of device, if supported.
+
+    Not supported on Bluefruit LE, so None.
+
+    Returns:
+      None, the only reasonable value for BLE-only devices.
+    """
+    logging.debug("GetClassOfDevice is a NOP on BluefruitLE")
+    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.
+
+    Not supported on Bluefruit LE, 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 BluefruitLE")
+    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.)
+
+    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: " + self.UNSUPPORTED_REASON
+    logging.error(error_msg)
+    raise BluefruitLEException(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.
+
+    Returns:
+      True if connecting to the stored remote address succeeded, or
+      False if a timeout occurs.
+    """
+    error_msg = "Failed to connect to remote device: " + self.UNSUPPORTED_REASON
+    logging.error(error_msg)
+    raise BluefruitLEException(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.
+    """
+    command = self.AT + self.CMD_DISCONNECT
+    message = 'disconnecting from the remote device (probably the DUT)'
+    result = self.SerialSendReceive(command, msg=message)
+    return self._ValidateAndExtractResult(command, result, True, message)
+
+
+if __name__ == '__main__':
+  GetKitInfo(BluefruitLE)
diff --git a/chameleond/utils/bluetooth_hid.py b/chameleond/utils/bluetooth_hid.py
index 3aadac2..17ccbb3 100644
--- a/chameleond/utils/bluetooth_hid.py
+++ b/chameleond/utils/bluetooth_hid.py
@@ -110,13 +110,15 @@
       # Set RN-42 to work as a slave.
       result = self.SetSlaveMode() and result
 
+      # Set a temporary pin code for testing purpose.
+      # Only do this when we want to use a pin code.
+      if self.authentication_mode == PeripheralKit.PIN_CODE_MODE:
+        result = self.SetPinCode(self.TMP_PIN_CODE) and result
+
       # Enable the connection status message so that we could get the message
       # of connection/disconnection status.
       result = self.EnableConnectionStatusMessage() and result
 
-      # Set a temporary pin code for testing purpose.
-      result = self.SetPinCode(self.TMP_PIN_CODE) and result
-
       # Reboot so that the configurations above take effect.
       result = self.Reboot() and result
 
diff --git a/chameleond/utils/bluetooth_peripheral_kit.py b/chameleond/utils/bluetooth_peripheral_kit.py
index d30e69d..0ac654e 100644
--- a/chameleond/utils/bluetooth_peripheral_kit.py
+++ b/chameleond/utils/bluetooth_peripheral_kit.py
@@ -17,7 +17,12 @@
 
 
 class PeripheralKit(object):
-  """A generalized abstraction of a Bluetooth peripheral emulation kit"""
+  """A generalized abstraction of a Bluetooth peripheral emulation kit
+
+  Note: every public member method should
+        return True or a non-None object if successful;
+        return False or Raise an exception (preferable) otherwise.
+  """
 
   # Serial port settings
   # Kit implementations should set these constants appropriately.
@@ -218,6 +223,9 @@
 
     Returns:
       True if the kit rebooted successfully.
+
+    Raises:
+      A kit-specifc exception if something goes wrong.
     """
     raise NotImplementedError("Not Implemented")
 
@@ -227,8 +235,7 @@
     Returns:
       a string representing the serial port.
     """
-    # TODO(josephsih): Give better info without storing
-    # and returning the port.
+    # TODO(josephsih): Give better info without storing and returning the port.
     return self._port
 
   def FactoryReset(self):
@@ -238,6 +245,9 @@
 
     Returns:
       True if the kit is reset successfully.
+
+    Raises:
+      A kit-specifc exception if something goes wrong.
     """
     raise NotImplementedError("Not Implemented")
 
@@ -280,8 +290,9 @@
 
     Returns:
       True if master/central mode was set successfully.
-      False if the kit does not support master/central mode. The client
-      should adjust accordingly.
+
+    Raises:
+      A kit-specific exception if master/central mode is unsupported.
     """
     raise NotImplementedError("Not Implemented")
 
@@ -290,8 +301,9 @@
 
     Returns:
       True if slave/peripheral mode was set successfully.
-      False if the kit does not support slave/peripheral mode. The client
-      should adjust accordingly.
+
+    Raises:
+      A kit-specific exception if slave/peripheral mode is unsupported.
     """
     raise NotImplementedError("Not Implemented")
 
@@ -318,7 +330,9 @@
 
     Returns:
       True if the mode was set successfully,
-      False if the mode is not supported.
+
+    Raises:
+      A kit-specific exception if given mode is not supported.
     """
     raise NotImplementedError("Not Implemented")
 
@@ -336,7 +350,9 @@
 
     Returns:
       True if the pin code is set successfully,
-      False if the pin code is invalid.
+
+    Raises:
+      A kit-specifc exception if the pin code is invalid.
     """
     raise NotImplementedError("Not Implemented")
 
@@ -362,6 +378,9 @@
 
     Returns:
       True if the service profile was set to SPP successfully.
+
+    Raises:
+      A kit-specifc exception if unsuppported.
     """
     raise NotImplementedError("Not Implemented")
 
@@ -387,8 +406,8 @@
   def GetConnectionStatus(self):
     """Get the connection status.
 
-    This indicates that the kit is connected to a remote device,
-    usually the DUT.
+    This indicates that the kit is connected to a remote device, usually the
+    DUT.
 
     Returns:
       True if the kit is connected to a remote device.
@@ -419,7 +438,7 @@
 
     Returns:
       The Bluetooth MAC address of the remote connected device if applicable,
-      or None if there is no remote connected device. If not none, this will
+      or None if there is no remote connected device. If not None, this will
       be properly formatted as a 12-digit MAC address with colons.
     """
     raise NotImplementedError("Not Implemented")
@@ -476,6 +495,9 @@
     Returns:
       True if the class of service was set successfully, or if this action is
       not supported.
+
+    Raises:
+      A kit-specific expection if the class of service is not supported.
     """
     raise NotImplementedError("Not Implemented")
 
@@ -516,11 +538,14 @@
     Returns:
       True if the class of device was set successfully, or if this action is
       not supported.
+
+    Raises:
+      A kit-specific expection if the class of device is not supported.
     """
     raise NotImplementedError("Not Implemented")
 
   # TODO(alent): How to handle not supported by kit in the API?
-  # TODO(alent): Implement/require validation logic
+  # TODO(alent): Implement/require validation logic?
   def SetRemoteAddress(self, remote_address):
     """Set the remote Bluetooth address.
 
@@ -540,6 +565,7 @@
     """
     raise NotImplementedError("Not Implemented")
 
+  # TODO(alent): API consistency with False vs exception
   def Connect(self):
     """Connect to the stored remote bluetooth address.
 
@@ -547,7 +573,7 @@
     is responsible for retrying when appropriate.
 
     Returns:
-      True if connecting to the stored remote address successfully, or
+      True if connecting to the stored remote address succeeded, or
       False if a timeout occurs.
     """
     raise NotImplementedError("Not implemented")
@@ -565,7 +591,7 @@
                       For reference: '00:29:95:1A:D4:6F'
 
     Returns:
-      True if connecting to the remote address successfully; otherwise, False.
+      True if connecting to the remote address succeeded
     """
     return self.SetRemoteAddress(remote_address) and self.Connect()
 
@@ -576,7 +602,7 @@
     the remote connected device, usually the DUT.
 
     Returns:
-      True if disconnecting from the remote device successfully.
+      True if disconnecting from the remote device succeeded.
     """
     raise NotImplementedError("Not implemented")
 
diff --git a/chameleond/utils/bluetooth_rn42.py b/chameleond/utils/bluetooth_rn42.py
index 83d45fa..bb3f4cb 100644
--- a/chameleond/utils/bluetooth_rn42.py
+++ b/chameleond/utils/bluetooth_rn42.py
@@ -25,10 +25,6 @@
   configure it to use the HID protocol to emulate a keyboard, a mouse,
   or a combo of both.
 
-  Note: every public member method should
-        return True or a non-None object if successful;
-        return False or Raise an exception otherwise.
-
   For user guide about serial control of the kit, refer to
   http://ww1.microchip.com/downloads/en/DeviceDoc/50002325A.pdf
 
@@ -441,8 +437,6 @@
 
     Returns:
       True if master mode was set successfully.
-      False if the kit does not support master mode. The client
-      should adjust accordingly.
     """
     self.SerialSendReceive(self.CMD_SET_MASTER_MODE,
                            expect=self.AOK,
@@ -454,8 +448,6 @@
 
     Returns:
       True if slave mode was set successfully.
-      False if the kit does not support slave mode. The client
-      should adjust accordingly.
     """
     self.SerialSendReceive(self.CMD_SET_SLAVE_MODE,
                            expect=self.AOK,
@@ -486,10 +478,14 @@
 
     Returns:
       True if the mode was set successfully,
-      False if the mode is not supported.
+
+    Raises:
+      A kit-specific Exception if something goes wrong.
     """
     if mode not in self.AUTHENTICATION_MODE:
-      return False
+      error_msg = 'Unsupported Authentication mode: %s' % mode
+      logging.error(error_msg)
+      raise RN42Exception(error_msg)
 
     digit_mode = self.AUTHENTICATION_MODE.get(mode)
 
@@ -515,12 +511,15 @@
 
     Returns:
       True if the pin code is set successfully,
-      False if the pin code is invalid.
+
+    Raises:
+      A kit-specifc exception if the pin code is invalid.
     """
     if len(pin) > self.MAX_PIN_LEN:
-      logging.warn('The pin code "%s" is longer than max length (%d).',
-                   pin, self.MAX_PIN_LEN)
-      return False
+      vals = (pin, self.MAX_PIN_LEN)
+      msg = 'The pin code "%s" is longer than max length (%d).' % vals
+      logging.warn(msg)
+      raise RN42Exception(msg)
 
     result = self.SerialSendReceive(self.CMD_SET_PIN_CODE + pin,
                                     msg='setting pin code')
@@ -584,8 +583,8 @@
   def GetConnectionStatus(self):
     """Get the connection status.
 
-    This indicates that the kit is connected to a remote device,
-    usually the DUT.
+    This indicates that the kit is connected to a remote device, usually the
+    DUT.
 
     the connection status returned from the kit could be
     '0,0,0': not connected
@@ -639,7 +638,7 @@
 
     Returns:
       The Bluetooth MAC address of the remote connected device if applicable,
-      or None if there is no remote connected device. If not none, this will
+      or None if there is no remote connected device. If not None, this will
       be properly formatted as a 12-digit MAC address with colons.
     """
     result = self.SerialSendReceive(self.CMD_GET_REMOTE_CONNECTED_BLUETOOTH_MAC,
@@ -742,6 +741,9 @@
     Returns:
       True if the class of service was set successfully, or if this action is
       not supported.
+
+    Raises:
+      A kit-specific expection if the class of service is not supported.
     """
     result = self.SerialSendReceive(
         self.CMD_SET_CLASS_OF_SERVICE + self._To4DigitHex(class_of_service),
@@ -786,12 +788,16 @@
     Returns:
       True if the class of device was set successfully, or if this action is
       not supported.
+
+    Raises:
+      A kit-specific expection if the class of device is not supported.
     """
     if device_type in self.CLASS_OF_DEVICE:
       return self._SetClassOfDevice(self.CLASS_OF_DEVICE.get(device_type))
     else:
-      logging.error('device type is not supported: %s', device_type)
-      return False
+      error_msg = 'device type is not supported: %s' % device_type
+      logging.error(error_msg)
+      raise RN42Exception(error_msg)
 
   def SetRemoteAddress(self, remote_address):
     """Set the remote Bluetooth address.
@@ -822,15 +828,15 @@
     In the case of a timeout (or a failure causing an exception), the caller
     is responsible for retrying when appropriate.
 
+    When a connection command is issued, it first returns a response 'TRYING'.
+    If the connection is successful, it returns something like
+    '...%CONNECT,6C29951AD46F,0'  where '6C29951AD46F' is the remote_address
+    after a few seconds.
+
     Returns:
-      True if connecting to the stored remote address successfully, or
+      True if connecting to the stored remote address succeeded, or
       False if a timeout occurs.
     """
-    # When a connection command is issued, it first returns a response 'TRYING'.
-    # If the connection is successful, it returns something like
-    # '...%CONNECT,6C29951AD46F,0'  where '6C29951AD46F' is the remote_address
-    # after a few seconds.
-
     # Expect an immediately 'TRYING' response.
     self.SerialSendReceive(self.CMD_CONNECT_REMOTE_ADDRESS,
                            expect='TRYING',
@@ -861,7 +867,7 @@
     the remote connected device, usually the DUT.
 
     Returns:
-      True if disconnecting from the remote device successfully.
+      True if disconnecting from the remote device succeeded.
     """
     # This is done by sending a 0x0.
     # A '%DISCONNECT' string would be received as a response.
@@ -1029,5 +1035,6 @@
     """
     return chr(self.UART_INPUT_SHORTHAND_MODE) + chr(0x0)
 
+
 if __name__ == '__main__':
   GetKitInfo(RN42)