Refactor Bluetooth HID Mouse API
A nice, new, separated API for emulating a mouse is now here.
Kit redirection is explicit in it, too. This represents the first step
in ridding ourselves of the getattr hack and solving
http://crbug.com/764055 .
RN42 can now keep holding buttons while scrolling, too!
Remove the old HID keyboard/mouse API from the public interface in
PeripheralKit, and remove the pylint silencer on BluefruitLE.
The RN42 no longer clamps values to [-127,127]. The API is no longer
publicly exposed, so we're handling those checks elsewhere.
BUG=chromium:752719,chromium:764055
TEST=Run this code on the Chameleon board, with an RN42 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
Change-Id: I07b70f0c5fba4abde34f640199f7ccf4c916224d
Reviewed-on: https://chromium-review.googlesource.com/663788
Commit-Ready: Alexander Lent <alent@google.com>
Tested-by: Alexander Lent <alent@google.com>
Reviewed-by: Wai-Hong Tam <waihong@google.com>
Reviewed-by: Shyh-In Hwang <josephsih@chromium.org>
diff --git a/chameleond/utils/bluetooth_bluefruitle.py b/chameleond/utils/bluetooth_bluefruitle.py
index 875f9e8..f9bf77b 100644
--- a/chameleond/utils/bluetooth_bluefruitle.py
+++ b/chameleond/utils/bluetooth_bluefruitle.py
@@ -26,7 +26,6 @@
pass
-# pylint: disable=abstract-method
class BluefruitLE(PeripheralKit):
"""This is an abstraction of the Adafruit Bluefruit LE Friend kit.
diff --git a/chameleond/utils/bluetooth_hid.py b/chameleond/utils/bluetooth_hid.py
index 28a4b98..f23be3f 100644
--- a/chameleond/utils/bluetooth_hid.py
+++ b/chameleond/utils/bluetooth_hid.py
@@ -68,6 +68,9 @@
AttributeError if the attribute is not found.
(This is the default behavior and kits should follow it.)
"""
+ if name.startswith("Mouse"):
+ error = "Kit API is not public. Use public API from BluetoothHIDMouse."
+ raise AttributeError(error)
return getattr(self._kit, name)
def Init(self, factory_reset=True):
@@ -180,10 +183,9 @@
class BluetoothHIDMouse(BluetoothHID):
"""A bluetooth HID mouse emulator class."""
- # Definitions of buttons
- BUTTONS_RELEASED = 0x0
- LEFT_BUTTON = 0x01
- RIGHT_BUTTON = 0x02
+ # Max and min values for HID mouse report values
+ HID_MAX_REPORT_VALUE = 127
+ HID_MIN_REPORT_VALUE = -127
def __init__(self, authentication_mode, kit_impl):
"""Initialization of BluetoothHIDMouse
@@ -195,91 +197,102 @@
super(BluetoothHIDMouse, self).__init__(
PeripheralKit.MOUSE, authentication_mode, kit_impl)
- def Move(self, delta_x=0, delta_y=0, buttons=0):
- """Move the mouse (delta_x, delta_y) pixels with buttons status.
+ def _EnsureHIDValueInRange(self, value):
+ """Ensures given value is in the range [-127,127] (inclusive).
Args:
- delta_x: the pixels to move horizontally
- positive values: moving right; max value = 127.
- negative values: moving left; max value = -127.
- delta_y: the pixels to move vertically
- positive values: moving down; max value = 127.
- negative values: moving up; max value = -127.
- buttons: the press/release status of buttons
- """
- if delta_x or delta_y:
- mouse_codes = self.RawMouseCodes(buttons=buttons,
- x_stop=delta_x, y_stop=delta_y)
- self.SerialSendReceive(mouse_codes, msg='BluetoothHIDMouse.Move')
- time.sleep(self.send_delay)
+ value: The value that should be checked.
- def _PressButtons(self, buttons):
- """Press down the specified buttons
+ Raises:
+ BluetoothHIDException if value is outside of the acceptable range.
+ """
+ if value < self.HID_MIN_REPORT_VALUE or value > self.HID_MAX_REPORT_VALUE:
+ error = "Value %s is outside of acceptable range [-127,127]." % value
+ logging.error(error)
+ raise BluetoothHIDException(error)
+
+ def Move(self, delta_x=0, delta_y=0):
+ """Move the mouse (delta_x, delta_y) steps.
+
+ If buttons are being pressed, they 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:
- buttons: the buttons to press
- """
- if buttons:
- mouse_codes = self.RawMouseCodes(buttons=buttons)
- self.SerialSendReceive(mouse_codes, msg='BluetoothHIDMouse._PressButtons')
- time.sleep(self.send_delay)
+ 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.
- def _ReleaseButtons(self):
- """Release buttons."""
- mouse_codes = self.RawMouseCodes(buttons=self.BUTTONS_RELEASED)
- self.SerialSendReceive(mouse_codes, msg='BluetoothHIDMouse._ReleaseButtons')
+ Raises:
+ BluetoothHIDException if a given delta is not in [-127,127].
+ """
+ self._EnsureHIDValueInRange(delta_x)
+ self._EnsureHIDValueInRange(delta_y)
+ self._kit.MouseMove(delta_x, delta_y)
time.sleep(self.send_delay)
- def PressLeftButton(self):
- """Press the left button."""
- self._PressButtons(self.LEFT_BUTTON)
+ def _PressLeftButton(self):
+ """Press the left button"""
+ self._kit.MousePressButtons({PeripheralKit.MOUSE_BUTTON_LEFT})
+ time.sleep(self.send_delay)
- def ReleaseLeftButton(self):
- """Release the left button."""
- self._ReleaseButtons()
+ def _PressRightButton(self):
+ """Press the right button"""
+ self._kit.MousePressButtons({PeripheralKit.MOUSE_BUTTON_RIGHT})
+ time.sleep(self.send_delay)
- def PressRightButton(self):
- """Press the right button."""
- self._PressButtons(self.RIGHT_BUTTON)
-
- def ReleaseRightButton(self):
- """Release the right button."""
- self._ReleaseButtons()
+ def _ReleaseAllButtons(self):
+ """Press the right button"""
+ self._kit.MouseReleaseAllButtons()
+ time.sleep(self.send_delay)
def LeftClick(self):
"""Make a left click."""
- self.PressLeftButton()
- self.ReleaseLeftButton()
+ self._PressLeftButton()
+ self._ReleaseAllButtons()
def RightClick(self):
"""Make a right click."""
- self.PressRightButton()
- self.ReleaseRightButton()
+ self._PressRightButton()
+ self._ReleaseAllButtons()
def ClickAndDrag(self, delta_x=0, delta_y=0):
- """Click and drag (delta_x, delta_y)
+ """Left click, drag (delta_x, delta_y) steps, and release.
+
+ 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 pixels to move horizontally
- delta_y: the pixels to move vertically
- """
- self.PressLeftButton()
- # Keep the left button pressed while moving.
- self.Move(delta_x=delta_x, delta_y=delta_y, buttons=self.LEFT_BUTTON)
- self.ReleaseLeftButton()
+ 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.
- def Scroll(self, wheel):
- """Scroll the wheel.
+ Raises:
+ BluetoothHIDException if a given delta is not in [-127,127].
+ """
+ self._EnsureHIDValueInRange(delta_x)
+ self._EnsureHIDValueInRange(delta_y)
+ self._PressLeftButton()
+ self.Move(delta_x, delta_y)
+ self._ReleaseAllButtons()
+
+ def Scroll(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:
- wheel: the steps to scroll
- The scroll direction depends on which scroll method is employed,
- traditional scrolling or Australian scrolling.
+ 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.
"""
- if wheel:
- mouse_codes = self.RawMouseCodes(wheel=wheel)
- self.SerialSendReceive(mouse_codes, msg='BluetoothHIDMouse.Scroll')
- time.sleep(self.send_delay)
+ self._EnsureHIDValueInRange(steps)
+ self._kit.MouseScroll(steps)
+ time.sleep(self.send_delay)
def DemoBluetoothHIDKeyboard(remote_address, chars):
diff --git a/chameleond/utils/bluetooth_peripheral_kit.py b/chameleond/utils/bluetooth_peripheral_kit.py
index d8915ee..ab8eb9d 100644
--- a/chameleond/utils/bluetooth_peripheral_kit.py
+++ b/chameleond/utils/bluetooth_peripheral_kit.py
@@ -83,11 +83,18 @@
# The default PIN code
DEFAULT_PIN_CODE = None
+ # Mouse constants
+ MOUSE_VALUE_MIN = -127
+ MOUSE_VALUE_MAX = 127
+ MOUSE_BUTTON_LEFT = "MOUSE_BUTTON_LEFT"
+ MOUSE_BUTTON_RIGHT = "MOUSE_BUTTON_RIGHT"
+
def __init__(self):
self._command_mode = False
self._closed = False
self._serial = None
self._tty = None
+ self._buttons_pressed = set()
def __del__(self):
self.Close()
@@ -633,18 +640,74 @@
"""
raise NotImplementedError("Not implemented")
- # TODO(alent): Refactor this part of the API, it's too RN-42-specific!
+ # Helper methods for implementing a system that remembers button state
+ def _MouseButtonStateUnion(self, buttons_to_press):
+ """Add to the current set of pressed buttons.
- def RawKeyCodes(self, modifiers=None, keys=None):
+ Args:
+ buttons_to_press: A set of buttons, as PeripheralKit MOUSE_BUTTON_*
+ values, that will stay pressed.
+ """
+ self._buttons_pressed = self._buttons_pressed.union(buttons_to_press)
+
+ def _MouseButtonStateSubtract(self, buttons_to_release):
+ """Remove from the current set of pressed buttons.
+
+ Args:
+ buttons_to_release: A set of buttons, as PeripheralKit MOUSE_BUTTON_*
+ values, that will be released.
+ """
+ self._buttons_pressed = self._buttons_pressed.difference(buttons_to_release)
+
+ def _MouseButtonStateClear(self):
+ """Clear the mouse button pressed state."""
+ self._buttons_pressed = set()
+
+ # Methods starting with "Mouse" should not be exposed to Autotest directly,
+ # especially those dealing with button sets.
+ def MouseMove(self, delta_x, delta_y):
+ """Move the mouse (delta_x, delta_y) steps.
+
+ If buttons are being pressed, they 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.
+ """
raise NotImplementedError("Not implemented")
- def RawMouseCodes(self, buttons=0, x_stop=0, y_stop=0, wheel=0):
+ 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.
+ """
raise NotImplementedError("Not implemented")
- def PressShorthandCodes(self, modifiers=None, keys=None):
+ 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).
+ """
raise NotImplementedError("Not implemented")
- def ReleaseShorthandCodes(self):
+ def MouseReleaseAllButtons(self):
+ """Release all mouse buttons."""
raise NotImplementedError("Not implemented")
diff --git a/chameleond/utils/bluetooth_rn42.py b/chameleond/utils/bluetooth_rn42.py
index e13168c..a3006e1 100644
--- a/chameleond/utils/bluetooth_rn42.py
+++ b/chameleond/utils/bluetooth_rn42.py
@@ -126,6 +126,11 @@
RAW_REPORT_FORMAT_MOUSE_LENGTH = 5
RAW_REPORT_FORMAT_MOUSE_DESCRIPTOR = 2
+ # Definitions of mouse button HID encodings
+ RAW_HID_BUTTONS_RELEASED = 0x0
+ RAW_HID_LEFT_BUTTON = 0x01
+ RAW_HID_RIGHT_BUTTON = 0x02
+
# TODO(alent): Move std scan codes to PeripheralKit when Keyboard implemented
# modifiers
LEFT_CTRL = 0x01
@@ -956,7 +961,79 @@
''.join(real_scan_codes) +
padding_0s)
- def RawMouseCodes(self, buttons=0, x_stop=0, y_stop=0, wheel=0):
+ def _MouseButtonsRawHidValues(self):
+ """Gives the raw HID values for whatever buttons are pressed."""
+ currently_pressed = 0x0
+ for button in self._buttons_pressed:
+ if button == PeripheralKit.MOUSE_BUTTON_LEFT:
+ currently_pressed |= self.RAW_HID_LEFT_BUTTON
+ elif button == PeripheralKit.MOUSE_BUTTON_RIGHT:
+ currently_pressed |= self.RAW_HID_RIGHT_BUTTON
+ else:
+ error = "Unknown mouse button in state: %s" % button
+ logging.error(error)
+ raise RN42Exception(error)
+ return currently_pressed
+
+ def MouseMove(self, delta_x, delta_y):
+ """Move the mouse (delta_x, delta_y) steps.
+
+ If buttons are being pressed, they 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.
+ """
+ raw_buttons = self._MouseButtonsRawHidValues()
+ if delta_x or delta_y:
+ mouse_codes = self._RawMouseCodes(buttons=raw_buttons, x_stop=delta_x,
+ y_stop=delta_y)
+ self.SerialSendReceive(mouse_codes, msg='RN42: MouseMove')
+
+ 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.
+ """
+ raw_buttons = self._MouseButtonsRawHidValues()
+ if steps:
+ mouse_codes = self._RawMouseCodes(buttons=raw_buttons, wheel=steps)
+ self.SerialSendReceive(mouse_codes, msg='RN42: MouseScroll')
+
+ 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).
+ """
+ self._MouseButtonStateUnion(buttons)
+ raw_buttons = self._MouseButtonsRawHidValues()
+ if raw_buttons:
+ mouse_codes = self._RawMouseCodes(buttons=raw_buttons)
+ self.SerialSendReceive(mouse_codes, msg='RN42: MousePressButtons')
+
+ def MouseReleaseAllButtons(self):
+ """Release all mouse buttons."""
+ self._MouseButtonStateClear()
+ mouse_codes = self._RawMouseCodes(buttons=self.RAW_HID_BUTTONS_RELEASED)
+ self.SerialSendReceive(mouse_codes, msg='RN42: MouseReleaseAllButtons')
+
+ def _RawMouseCodes(self, buttons=0, x_stop=0, y_stop=0, wheel=0):
"""Generate the codes in mouse raw report format.
This method sends data in the raw report mode. The first start
@@ -965,7 +1042,7 @@
For example, generate the codes of moving cursor 100 pixels left
and 50 pixels down:
- codes = RawMouseCodes(x_stop=-100, y_stop=50)
+ codes = _RawMouseCodes(x_stop=-100, y_stop=50)
Args:
buttons: the buttons to press and release
@@ -979,23 +1056,17 @@
def SignedChar(value):
"""Converted the value to a legitimate signed character value.
+ Given value must be in [-127,127], or odd things will happen.
+
Args:
value: a signed integer
Returns:
a signed character value
"""
- if value <= -127:
- # If the negative value is too small, limit it to -127.
- # Then perform two's complement: -127 + 256 = 129
- return 129
- elif value < 0:
+ if value < 0:
# Perform two's complement.
return value + 256
- elif value > 127:
- # If the positive value is too large, limit it to 127
- # to prevent it from becoming negative.
- return 127
return value
return (chr(self.UART_INPUT_RAW_MODE) +