blob: 3aadac217f0913dfcd4b8dbeb386e4714da55181 [file] [log] [blame]
# Copyright 2016 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 emulation of bluetooth HID devices."""
import argparse
import logging
import sys
import time
from bluetooth_peripheral_kit import PeripheralKit
from bluetooth_rn42 import RN42
from bluetooth_rn42 import RN42Exception
class BluetoothHIDException(Exception):
"""A dummpy exception class for Bluetooth HID class."""
pass
class BluetoothHID(object):
"""A base bluetooth HID emulator class using RN-42 evaluation kit.
Note: every public member method should
return True or a non-None object if successful;
return False or Raise an exception otherwise.
"""
# TODO(josephsih): Find better way to use constants other than PeripheralKit
TMP_PIN_CODE = '0000' # A temporary pin code
SEND_DELAY_SECS = 0.2 # Need to sleep for a short while otherwise
# the bits may get lost during transmission.
INIT_SLEEP_SECS = 5 # Sleep after initialization for stabilization.
def __init__(self, device_type, authentication_mode, kit_impl,
send_delay=SEND_DELAY_SECS):
"""Initialization of BluetoothHID
Args:
device_type: the device type for emulation
authentication_mode: the authentication mode
kit_impl: the implementation of a peripheral kit to be instantiated
send_delay: wait a while after sending data
"""
self._kit = kit_impl()
self.device_type = device_type
self.authentication_mode = authentication_mode
self.send_delay = send_delay
# TODO(josephsih): Remove the use of __getattr__ after a refactor of the
# Chameleon-Autotest interface to eliminate kit-specific commands in tests.
def __getattr__(self, name):
"""Gets the attribute of name from the owned peripheral kit instance
Allows calling methods (or getting attributes in general) on this class or
its subclasses that resolve to methods defined on the kit implementation.
Args:
name: The name of the attribute to be found.
Returns:
The attribute of the kit with given name, if it exists.
(This is the default behavior and kits should follow it.)
Raises:
AttributeError if the attribute is not found.
(This is the default behavior and kits should follow it.)
"""
return getattr(self._kit, name)
def Init(self, factory_reset=True):
"""Initialize the chip correctly.
Initialize the chip with proper HID register values.
Args:
factory_reset: True if a factory reset is needed.
False if we only want to reconnect the serial device.
"""
# Create a new serial device every time since the serial driver
# on chameleon board is not very stable.
result = self.CreateSerialDevice()
if factory_reset:
# Enter command mode to issue commands.
# This must happen first, so that other commands work
result = self.EnterCommandMode() and result
# Do a factory reset to make sure it is in a known initial state.
# Do the factory reset before proceeding to set parameters below.
result = self.FactoryReset() and result
# Set HID as the service profile.
result = self.SetServiceProfileHID() and result
# Set the HID device type.
result = self.SetHIDType(self.device_type) and result
# Set the default class of service.
result = self.SetDefaultClassOfService() and result
# Set the class of device (CoD) according to the hid device type.
result = self.SetClassOfDevice(self.device_type) and result
# Set authentication to the specified mode.
result = self.SetAuthenticationMode(self.authentication_mode) and result
# Set RN-42 to work as a slave.
result = self.SetSlaveMode() 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
# Enter command mode again after reboot.
result = self.EnterCommandMode() and result
time.sleep(self.INIT_SLEEP_SECS)
logging.info('A bluetooth HID "%s" device is connected.', self.device_type)
return result
class BluetoothHIDKeyboard(BluetoothHID):
"""A bluetooth HID keyboard emulator class."""
def __init__(self, authentication_mode, kit_impl):
"""Initialization of BluetoothHIDKeyboard
Args:
authentication_mode: the authentication mode
kit_impl: the implementation of a Bluetooth HID peripheral kit to use
"""
super(BluetoothHIDKeyboard, self).__init__(
PeripheralKit.KEYBOARD, authentication_mode, kit_impl)
def Send(self, data):
"""Send data to the remote host.
Args:
data: data to send to the remote host
data could be either a string of printable ASCII characters or
a special key combination.
"""
# TODO(josephsih): should have a method to check the connection status.
# Currently, once RN-42 is connected to a remote host, all characters
# except chr(0) transmitted through the serial port are interpreted
# as characters to send to the remote host.
logging.debug('HID device sending %r...', data)
self.SerialSendReceive(data, msg='BluetoothHID.Send')
time.sleep(self.send_delay)
def SendKeyCombination(self, modifiers=None, keys=None):
"""Send special key combinations to the remote host.
Args:
modifiers: a list of modifiers
keys: a list of scan codes of keys
"""
press_codes = self.PressShorthandCodes(modifiers=modifiers, keys=keys)
release_codes = self.ReleaseShorthandCodes()
if press_codes and release_codes:
self.Send(press_codes)
self.Send(release_codes)
else:
logging.warn('modifers: %s and keys: %s are not valid', modifiers, keys)
return None
class BluetoothHIDMouse(BluetoothHID):
"""A bluetooth HID mouse emulator class."""
# Definitions of buttons
BUTTONS_RELEASED = 0x0
LEFT_BUTTON = 0x01
RIGHT_BUTTON = 0x02
def __init__(self, authentication_mode, kit_impl):
"""Initialization of BluetoothHIDMouse
Args:
authentication_mode: the authentication mode
kit_impl: the implementation of a Bluetooth HID peripheral kit to use
"""
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.
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)
def _PressButtons(self, buttons):
"""Press down the specified buttons
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)
def _ReleaseButtons(self):
"""Release buttons."""
mouse_codes = self.RawMouseCodes(buttons=self.BUTTONS_RELEASED)
self.SerialSendReceive(mouse_codes, msg='BluetoothHIDMouse._ReleaseButtons')
time.sleep(self.send_delay)
def PressLeftButton(self):
"""Press the left button."""
self._PressButtons(self.LEFT_BUTTON)
def ReleaseLeftButton(self):
"""Release the left button."""
self._ReleaseButtons()
def PressRightButton(self):
"""Press the right button."""
self._PressButtons(self.RIGHT_BUTTON)
def ReleaseRightButton(self):
"""Release the right button."""
self._ReleaseButtons()
def LeftClick(self):
"""Make a left click."""
self.PressLeftButton()
self.ReleaseLeftButton()
def RightClick(self):
"""Make a right click."""
self.PressRightButton()
self.ReleaseRightButton()
def ClickAndDrag(self, delta_x=0, delta_y=0):
"""Click and drag (delta_x, delta_y)
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()
def Scroll(self, wheel):
"""Scroll the wheel.
Args:
wheel: the steps to scroll
The scroll direction depends on which scroll method is employed,
traditional scrolling or Australian scrolling.
"""
if wheel:
mouse_codes = self.RawMouseCodes(wheel=wheel)
self.SerialSendReceive(mouse_codes, msg='BluetoothHIDMouse.Scroll')
time.sleep(self.send_delay)
def DemoBluetoothHIDKeyboard(remote_address, chars):
"""A simple demo of acting as a HID keyboard.
This simple demo works only after the HID device has already paired
with the remote device such that a link key has been exchanged. Then
the HID device could connect directly to the remote host without
pin code and sends the message.
A full flow would be letting a remote host pair with the HID device
with the pin code of the HID device. Thereafter, either the host or
the HID device could request to connect. This is out of the scope of
this simple demo.
Args:
remote_address: the bluetooth address of the target remote device
chars: the characters to send
"""
print 'Creating an emulated bluetooth keyboard...'
# TODO(josephsih): Refactor test code to remove need for RN42 import
keyboard = BluetoothHIDKeyboard(PeripheralKit.PIN_CODE_MODE, RN42)
keyboard.Init()
print 'Connecting to the remote address %s...' % remote_address
try:
if keyboard.ConnectToRemoteAddress(remote_address):
# Send printable ASCII strings a few times.
for i in range(1, 4):
print 'Sending "%s" for the %dth time...' % (chars, i)
keyboard.Send(chars + ' ' + str(i))
# Demo special key combinations below.
print 'Create a new chrome tab.'
keyboard.SendKeyCombination(modifiers=[RN42.LEFT_CTRL],
keys=[RN42.SCAN_T])
print 'Navigate to Google page.'
keyboard.Send('www.google.com')
time.sleep(1)
print 'Search hello world.'
keyboard.Send('hello world')
time.sleep(1)
print 'Navigate back to the previous page.'
keyboard.SendKeyCombination(keys=[RN42.SCAN_F1])
time.sleep(1)
print 'Switch to the previous tab.'
keyboard.SendKeyCombination(modifiers=[RN42.LEFT_CTRL, RN42.LEFT_SHIFT],
keys=[RN42.SCAN_TAB])
else:
print 'Something is wrong. Not able to connect to the remote address.'
print 'Have you already paired RN-42 with the remote host?'
finally:
print 'Disconnecting...'
keyboard.Disconnect()
print 'Closing the keyboard...'
keyboard.Close()
def DemoBluetoothHIDMouse(remote_address):
"""A simple demo of acting as a HID mouse.
Args:
remote_address: the bluetooth address of the target remote device
"""
print 'Creating an emulated bluetooth mouse...'
# TODO(josephsih): Refactor test code to remove need for RN42 import
mouse = BluetoothHIDMouse(PeripheralKit.PIN_CODE_MODE, RN42)
mouse.Init()
connected = False
print 'Connecting to the remote address %s...' % remote_address
try:
if mouse.ConnectToRemoteAddress(remote_address):
connected = True
print 'Click and drag horizontally.'
mouse.ClickAndDrag(delta_x=100)
time.sleep(1)
print 'Make a right click.'
mouse.RightClick()
time.sleep(1)
print 'Move the cursor upper left.'
mouse.Move(delta_x=-30, delta_y=-40)
time.sleep(1)
print 'Make a left click.'
mouse.LeftClick()
time.sleep(1)
print 'Move the cursor left.'
mouse.Move(delta_x=-100)
time.sleep(1)
print 'Move the cursor up.'
mouse.Move(delta_y=-90)
time.sleep(1)
print 'Move the cursor down right.'
mouse.Move(delta_x=100, delta_y=90)
time.sleep(1)
print 'Scroll in one direction.'
mouse.Scroll(-80)
time.sleep(1)
print 'Scroll in the opposite direction.'
mouse.Scroll(100)
else:
print 'Something is wrong. Not able to connect to the remote address.'
finally:
if connected:
print 'Disconnecting...'
try:
mouse.Disconnect()
# TODO(josephsih): Refactor test code to remove need for RN42 import
except RN42Exception:
# RN-42 may have already disconnected.
pass
print 'Closing the mouse...'
mouse.Close()
def _Parse():
"""Parse the command line options."""
prog = sys.argv[0]
example_usage = ('Example:\n' +
' python %s keyboard 00:11:22:33:44:55\n' % prog +
' python %s mouse 00:11:22:33:44:55\n'% prog)
parser = argparse.ArgumentParser(
description='Emulate a HID device.\n' + example_usage,
formatter_class=argparse.RawTextHelpFormatter)
parser.add_argument('device',
choices=['keyboard', 'mouse'],
help='the device type to emulate')
parser.add_argument('remote_host_address',
help='the remote host address')
parser.add_argument('-c', '--chars_to_send',
default='echo hello world',
help='characters to send to the remote host')
args = parser.parse_args()
if len(args.remote_host_address.replace(':', '')) != 12:
print '"%s" is not a valid bluetooth address.' % args.remote_host_address
exit(1)
print ('Emulate a %s and connect to remote host at %s' %
(args.device, args.remote_host_address))
return args
def Demo():
"""Make demonstrations about how to use the HID emulation classes."""
args = _Parse()
device = args.device.lower()
if device == 'keyboard':
DemoBluetoothHIDKeyboard(args.remote_host_address, args.chars_to_send)
elif device == 'mouse':
DemoBluetoothHIDMouse(args.remote_host_address)
else:
args.print_help()
if __name__ == '__main__':
Demo()