blob: 3aadac217f0913dfcd4b8dbeb386e4714da55181 [file] [log] [blame]
Joseph Hwang5d0dc642016-01-19 10:21:12 +08001# Copyright 2016 The Chromium OS Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5"""This module provides emulation of bluetooth HID devices."""
6
Joseph Hwangd3784cb2016-03-06 21:57:39 +08007import argparse
Joseph Hwang5d0dc642016-01-19 10:21:12 +08008import logging
9import sys
10import time
11
Alexander Lentce006df2017-08-17 17:22:24 -070012from bluetooth_peripheral_kit import PeripheralKit
13from bluetooth_rn42 import RN42
14from bluetooth_rn42 import RN42Exception
Joseph Hwang5d0dc642016-01-19 10:21:12 +080015
16
17class BluetoothHIDException(Exception):
18 """A dummpy exception class for Bluetooth HID class."""
19 pass
20
21
Alexander Lent7cbd1592017-08-08 20:12:02 -070022class BluetoothHID(object):
Joseph Hwang9f0e0242016-06-22 12:50:20 +080023 """A base bluetooth HID emulator class using RN-42 evaluation kit.
24
25 Note: every public member method should
26 return True or a non-None object if successful;
27 return False or Raise an exception otherwise.
28 """
Joseph Hwang5d0dc642016-01-19 10:21:12 +080029
Alexander Lentce006df2017-08-17 17:22:24 -070030 # TODO(josephsih): Find better way to use constants other than PeripheralKit
Joseph Hwangd13e5e72016-05-30 18:57:35 +080031 TMP_PIN_CODE = '0000' # A temporary pin code
Joseph Hwang5d0dc642016-01-19 10:21:12 +080032
33 SEND_DELAY_SECS = 0.2 # Need to sleep for a short while otherwise
34 # the bits may get lost during transmission.
Joseph Hwangd13e5e72016-05-30 18:57:35 +080035 INIT_SLEEP_SECS = 5 # Sleep after initialization for stabilization.
Joseph Hwang5d0dc642016-01-19 10:21:12 +080036
Alexander Lent7cbd1592017-08-08 20:12:02 -070037 def __init__(self, device_type, authentication_mode, kit_impl,
Joseph Hwang5d0dc642016-01-19 10:21:12 +080038 send_delay=SEND_DELAY_SECS):
39 """Initialization of BluetoothHID
40
41 Args:
42 device_type: the device type for emulation
43 authentication_mode: the authentication mode
Alexander Lent7cbd1592017-08-08 20:12:02 -070044 kit_impl: the implementation of a peripheral kit to be instantiated
Joseph Hwang5d0dc642016-01-19 10:21:12 +080045 send_delay: wait a while after sending data
46 """
Alexander Lent7cbd1592017-08-08 20:12:02 -070047 self._kit = kit_impl()
Joseph Hwang5d0dc642016-01-19 10:21:12 +080048 self.device_type = device_type
Joseph Hwangc1edeff2016-06-08 15:38:14 +080049 self.authentication_mode = authentication_mode
Joseph Hwang5d0dc642016-01-19 10:21:12 +080050 self.send_delay = send_delay
51
Alexander Lent7cbd1592017-08-08 20:12:02 -070052 # TODO(josephsih): Remove the use of __getattr__ after a refactor of the
53 # Chameleon-Autotest interface to eliminate kit-specific commands in tests.
54 def __getattr__(self, name):
55 """Gets the attribute of name from the owned peripheral kit instance
56
57 Allows calling methods (or getting attributes in general) on this class or
58 its subclasses that resolve to methods defined on the kit implementation.
59
60 Args:
61 name: The name of the attribute to be found.
62
63 Returns:
64 The attribute of the kit with given name, if it exists.
65 (This is the default behavior and kits should follow it.)
66
67 Raises:
68 AttributeError if the attribute is not found.
69 (This is the default behavior and kits should follow it.)
70 """
71 return getattr(self._kit, name)
72
Joseph Hwangd13e5e72016-05-30 18:57:35 +080073 def Init(self, factory_reset=True):
74 """Initialize the chip correctly.
Joseph Hwang5d0dc642016-01-19 10:21:12 +080075
Joseph Hwangd13e5e72016-05-30 18:57:35 +080076 Initialize the chip with proper HID register values.
Joseph Hwang5d0dc642016-01-19 10:21:12 +080077
Joseph Hwangd13e5e72016-05-30 18:57:35 +080078 Args:
79 factory_reset: True if a factory reset is needed.
80 False if we only want to reconnect the serial device.
81 """
82 # Create a new serial device every time since the serial driver
83 # on chameleon board is not very stable.
Alexander Lentce006df2017-08-17 17:22:24 -070084 result = self.CreateSerialDevice()
Joseph Hwang5d0dc642016-01-19 10:21:12 +080085
Joseph Hwangd13e5e72016-05-30 18:57:35 +080086 if factory_reset:
Alexander Lentbc790282017-08-23 15:04:12 -070087 # Enter command mode to issue commands.
88 # This must happen first, so that other commands work
Alexander Lentce006df2017-08-17 17:22:24 -070089 result = self.EnterCommandMode() and result
Alexander Lentbc790282017-08-23 15:04:12 -070090
Joseph Hwangd13e5e72016-05-30 18:57:35 +080091 # Do a factory reset to make sure it is in a known initial state.
92 # Do the factory reset before proceeding to set parameters below.
Alexander Lentce006df2017-08-17 17:22:24 -070093 result = self.FactoryReset() and result
Joseph Hwang5d0dc642016-01-19 10:21:12 +080094
Joseph Hwangd13e5e72016-05-30 18:57:35 +080095 # Set HID as the service profile.
Alexander Lentce006df2017-08-17 17:22:24 -070096 result = self.SetServiceProfileHID() and result
Joseph Hwang5d0dc642016-01-19 10:21:12 +080097
Joseph Hwangd13e5e72016-05-30 18:57:35 +080098 # Set the HID device type.
Alexander Lentce006df2017-08-17 17:22:24 -070099 result = self.SetHIDType(self.device_type) and result
Joseph Hwang5d0dc642016-01-19 10:21:12 +0800100
Joseph Hwangd13e5e72016-05-30 18:57:35 +0800101 # Set the default class of service.
Alexander Lentce006df2017-08-17 17:22:24 -0700102 result = self.SetDefaultClassOfService() and result
Joseph Hwang5d0dc642016-01-19 10:21:12 +0800103
Joseph Hwangd13e5e72016-05-30 18:57:35 +0800104 # Set the class of device (CoD) according to the hid device type.
Alexander Lentce006df2017-08-17 17:22:24 -0700105 result = self.SetClassOfDevice(self.device_type) and result
Joseph Hwangd13e5e72016-05-30 18:57:35 +0800106
107 # Set authentication to the specified mode.
Alexander Lentce006df2017-08-17 17:22:24 -0700108 result = self.SetAuthenticationMode(self.authentication_mode) and result
Joseph Hwangd13e5e72016-05-30 18:57:35 +0800109
110 # Set RN-42 to work as a slave.
Alexander Lentce006df2017-08-17 17:22:24 -0700111 result = self.SetSlaveMode() and result
Joseph Hwangd13e5e72016-05-30 18:57:35 +0800112
113 # Enable the connection status message so that we could get the message
114 # of connection/disconnection status.
Alexander Lentce006df2017-08-17 17:22:24 -0700115 result = self.EnableConnectionStatusMessage() and result
Joseph Hwangd13e5e72016-05-30 18:57:35 +0800116
117 # Set a temporary pin code for testing purpose.
Alexander Lentce006df2017-08-17 17:22:24 -0700118 result = self.SetPinCode(self.TMP_PIN_CODE) and result
Joseph Hwangd13e5e72016-05-30 18:57:35 +0800119
120 # Reboot so that the configurations above take effect.
Alexander Lentce006df2017-08-17 17:22:24 -0700121 result = self.Reboot() and result
Joseph Hwangd13e5e72016-05-30 18:57:35 +0800122
123 # Enter command mode again after reboot.
Alexander Lentce006df2017-08-17 17:22:24 -0700124 result = self.EnterCommandMode() and result
Joseph Hwangd13e5e72016-05-30 18:57:35 +0800125
126 time.sleep(self.INIT_SLEEP_SECS)
127
128 logging.info('A bluetooth HID "%s" device is connected.', self.device_type)
Alexander Lentce006df2017-08-17 17:22:24 -0700129 return result
Joseph Hwang5d0dc642016-01-19 10:21:12 +0800130
Joseph Hwang5d0dc642016-01-19 10:21:12 +0800131
132class BluetoothHIDKeyboard(BluetoothHID):
133 """A bluetooth HID keyboard emulator class."""
134
Alexander Lent7cbd1592017-08-08 20:12:02 -0700135 def __init__(self, authentication_mode, kit_impl):
Joseph Hwang5d0dc642016-01-19 10:21:12 +0800136 """Initialization of BluetoothHIDKeyboard
137
138 Args:
139 authentication_mode: the authentication mode
Alexander Lent7cbd1592017-08-08 20:12:02 -0700140 kit_impl: the implementation of a Bluetooth HID peripheral kit to use
Joseph Hwang5d0dc642016-01-19 10:21:12 +0800141 """
Alexander Lent7cbd1592017-08-08 20:12:02 -0700142 super(BluetoothHIDKeyboard, self).__init__(
Alexander Lentce006df2017-08-17 17:22:24 -0700143 PeripheralKit.KEYBOARD, authentication_mode, kit_impl)
Joseph Hwang5d0dc642016-01-19 10:21:12 +0800144
145 def Send(self, data):
146 """Send data to the remote host.
147
148 Args:
149 data: data to send to the remote host
Joseph Hwangcb5623b2016-02-17 18:56:34 +0800150 data could be either a string of printable ASCII characters or
151 a special key combination.
Joseph Hwang5d0dc642016-01-19 10:21:12 +0800152 """
153 # TODO(josephsih): should have a method to check the connection status.
154 # Currently, once RN-42 is connected to a remote host, all characters
155 # except chr(0) transmitted through the serial port are interpreted
156 # as characters to send to the remote host.
Joseph Hwang5d0dc642016-01-19 10:21:12 +0800157 logging.debug('HID device sending %r...', data)
158 self.SerialSendReceive(data, msg='BluetoothHID.Send')
159 time.sleep(self.send_delay)
160
Joseph Hwangcb5623b2016-02-17 18:56:34 +0800161 def SendKeyCombination(self, modifiers=None, keys=None):
162 """Send special key combinations to the remote host.
163
164 Args:
165 modifiers: a list of modifiers
166 keys: a list of scan codes of keys
167 """
168 press_codes = self.PressShorthandCodes(modifiers=modifiers, keys=keys)
169 release_codes = self.ReleaseShorthandCodes()
170 if press_codes and release_codes:
171 self.Send(press_codes)
172 self.Send(release_codes)
173 else:
174 logging.warn('modifers: %s and keys: %s are not valid', modifiers, keys)
175 return None
176
Joseph Hwang5d0dc642016-01-19 10:21:12 +0800177
Joseph Hwangd3784cb2016-03-06 21:57:39 +0800178class BluetoothHIDMouse(BluetoothHID):
179 """A bluetooth HID mouse emulator class."""
180
181 # Definitions of buttons
182 BUTTONS_RELEASED = 0x0
183 LEFT_BUTTON = 0x01
184 RIGHT_BUTTON = 0x02
185
Alexander Lent7cbd1592017-08-08 20:12:02 -0700186 def __init__(self, authentication_mode, kit_impl):
Joseph Hwangd3784cb2016-03-06 21:57:39 +0800187 """Initialization of BluetoothHIDMouse
188
189 Args:
190 authentication_mode: the authentication mode
Alexander Lent7cbd1592017-08-08 20:12:02 -0700191 kit_impl: the implementation of a Bluetooth HID peripheral kit to use
Joseph Hwangd3784cb2016-03-06 21:57:39 +0800192 """
Alexander Lent7cbd1592017-08-08 20:12:02 -0700193 super(BluetoothHIDMouse, self).__init__(
Alexander Lentce006df2017-08-17 17:22:24 -0700194 PeripheralKit.MOUSE, authentication_mode, kit_impl)
Joseph Hwangd3784cb2016-03-06 21:57:39 +0800195
Alexander Lentce006df2017-08-17 17:22:24 -0700196 def Move(self, delta_x=0, delta_y=0, buttons=0):
Joseph Hwangb21333a2016-09-01 17:20:45 +0800197 """Move the mouse (delta_x, delta_y) pixels with buttons status.
Joseph Hwangd3784cb2016-03-06 21:57:39 +0800198
199 Args:
200 delta_x: the pixels to move horizontally
201 positive values: moving right; max value = 127.
202 negative values: moving left; max value = -127.
203 delta_y: the pixels to move vertically
204 positive values: moving down; max value = 127.
205 negative values: moving up; max value = -127.
Alexander Lentce006df2017-08-17 17:22:24 -0700206 buttons: the press/release status of buttons
Joseph Hwangd3784cb2016-03-06 21:57:39 +0800207 """
208 if delta_x or delta_y:
Joseph Hwangb21333a2016-09-01 17:20:45 +0800209 mouse_codes = self.RawMouseCodes(buttons=buttons,
210 x_stop=delta_x, y_stop=delta_y)
Joseph Hwangd3784cb2016-03-06 21:57:39 +0800211 self.SerialSendReceive(mouse_codes, msg='BluetoothHIDMouse.Move')
212 time.sleep(self.send_delay)
213
214 def _PressButtons(self, buttons):
215 """Press down the specified buttons
216
217 Args:
218 buttons: the buttons to press
219 """
220 if buttons:
221 mouse_codes = self.RawMouseCodes(buttons=buttons)
222 self.SerialSendReceive(mouse_codes, msg='BluetoothHIDMouse._PressButtons')
223 time.sleep(self.send_delay)
224
225 def _ReleaseButtons(self):
226 """Release buttons."""
227 mouse_codes = self.RawMouseCodes(buttons=self.BUTTONS_RELEASED)
228 self.SerialSendReceive(mouse_codes, msg='BluetoothHIDMouse._ReleaseButtons')
229 time.sleep(self.send_delay)
230
231 def PressLeftButton(self):
232 """Press the left button."""
233 self._PressButtons(self.LEFT_BUTTON)
234
235 def ReleaseLeftButton(self):
236 """Release the left button."""
237 self._ReleaseButtons()
238
239 def PressRightButton(self):
240 """Press the right button."""
241 self._PressButtons(self.RIGHT_BUTTON)
242
243 def ReleaseRightButton(self):
244 """Release the right button."""
245 self._ReleaseButtons()
246
247 def LeftClick(self):
248 """Make a left click."""
249 self.PressLeftButton()
250 self.ReleaseLeftButton()
251
252 def RightClick(self):
253 """Make a right click."""
254 self.PressRightButton()
255 self.ReleaseRightButton()
256
257 def ClickAndDrag(self, delta_x=0, delta_y=0):
258 """Click and drag (delta_x, delta_y)
259
260 Args:
261 delta_x: the pixels to move horizontally
262 delta_y: the pixels to move vertically
263 """
264 self.PressLeftButton()
Joseph Hwangb21333a2016-09-01 17:20:45 +0800265 # Keep the left button pressed while moving.
Alexander Lentce006df2017-08-17 17:22:24 -0700266 self.Move(delta_x=delta_x, delta_y=delta_y, buttons=self.LEFT_BUTTON)
Joseph Hwangd3784cb2016-03-06 21:57:39 +0800267 self.ReleaseLeftButton()
268
269 def Scroll(self, wheel):
270 """Scroll the wheel.
271
272 Args:
273 wheel: the steps to scroll
274 The scroll direction depends on which scroll method is employed,
275 traditional scrolling or Australian scrolling.
276 """
277 if wheel:
278 mouse_codes = self.RawMouseCodes(wheel=wheel)
279 self.SerialSendReceive(mouse_codes, msg='BluetoothHIDMouse.Scroll')
280 time.sleep(self.send_delay)
Joseph Hwang5d0dc642016-01-19 10:21:12 +0800281
282
283def DemoBluetoothHIDKeyboard(remote_address, chars):
284 """A simple demo of acting as a HID keyboard.
285
286 This simple demo works only after the HID device has already paired
287 with the remote device such that a link key has been exchanged. Then
288 the HID device could connect directly to the remote host without
289 pin code and sends the message.
290
291 A full flow would be letting a remote host pair with the HID device
292 with the pin code of the HID device. Thereafter, either the host or
293 the HID device could request to connect. This is out of the scope of
294 this simple demo.
295
296 Args:
297 remote_address: the bluetooth address of the target remote device
298 chars: the characters to send
299 """
300 print 'Creating an emulated bluetooth keyboard...'
Alexander Lentce006df2017-08-17 17:22:24 -0700301 # TODO(josephsih): Refactor test code to remove need for RN42 import
302 keyboard = BluetoothHIDKeyboard(PeripheralKit.PIN_CODE_MODE, RN42)
Joseph Hwangc1edeff2016-06-08 15:38:14 +0800303 keyboard.Init()
Joseph Hwang5d0dc642016-01-19 10:21:12 +0800304
305 print 'Connecting to the remote address %s...' % remote_address
306 try:
307 if keyboard.ConnectToRemoteAddress(remote_address):
Joseph Hwangcb5623b2016-02-17 18:56:34 +0800308 # Send printable ASCII strings a few times.
309 for i in range(1, 4):
Joseph Hwang5d0dc642016-01-19 10:21:12 +0800310 print 'Sending "%s" for the %dth time...' % (chars, i)
311 keyboard.Send(chars + ' ' + str(i))
Joseph Hwangcb5623b2016-02-17 18:56:34 +0800312
313 # Demo special key combinations below.
314 print 'Create a new chrome tab.'
315 keyboard.SendKeyCombination(modifiers=[RN42.LEFT_CTRL],
316 keys=[RN42.SCAN_T])
317
318 print 'Navigate to Google page.'
319 keyboard.Send('www.google.com')
320 time.sleep(1)
321
322 print 'Search hello world.'
323 keyboard.Send('hello world')
324 time.sleep(1)
325
326 print 'Navigate back to the previous page.'
327 keyboard.SendKeyCombination(keys=[RN42.SCAN_F1])
328 time.sleep(1)
329
330 print 'Switch to the previous tab.'
331 keyboard.SendKeyCombination(modifiers=[RN42.LEFT_CTRL, RN42.LEFT_SHIFT],
332 keys=[RN42.SCAN_TAB])
Joseph Hwang5d0dc642016-01-19 10:21:12 +0800333 else:
334 print 'Something is wrong. Not able to connect to the remote address.'
335 print 'Have you already paired RN-42 with the remote host?'
336 finally:
337 print 'Disconnecting...'
338 keyboard.Disconnect()
339
340 print 'Closing the keyboard...'
341 keyboard.Close()
342
343
Joseph Hwangd3784cb2016-03-06 21:57:39 +0800344def DemoBluetoothHIDMouse(remote_address):
345 """A simple demo of acting as a HID mouse.
346
347 Args:
348 remote_address: the bluetooth address of the target remote device
349 """
350 print 'Creating an emulated bluetooth mouse...'
Alexander Lentce006df2017-08-17 17:22:24 -0700351 # TODO(josephsih): Refactor test code to remove need for RN42 import
352 mouse = BluetoothHIDMouse(PeripheralKit.PIN_CODE_MODE, RN42)
Joseph Hwangc1edeff2016-06-08 15:38:14 +0800353 mouse.Init()
Joseph Hwangd3784cb2016-03-06 21:57:39 +0800354
Joseph Hwang24ed76a2016-05-27 13:20:48 +0800355 connected = False
Joseph Hwangd3784cb2016-03-06 21:57:39 +0800356 print 'Connecting to the remote address %s...' % remote_address
357 try:
358 if mouse.ConnectToRemoteAddress(remote_address):
Joseph Hwang24ed76a2016-05-27 13:20:48 +0800359 connected = True
360
Joseph Hwangd3784cb2016-03-06 21:57:39 +0800361 print 'Click and drag horizontally.'
362 mouse.ClickAndDrag(delta_x=100)
363 time.sleep(1)
364
365 print 'Make a right click.'
366 mouse.RightClick()
367 time.sleep(1)
368
369 print 'Move the cursor upper left.'
370 mouse.Move(delta_x=-30, delta_y=-40)
371 time.sleep(1)
372
373 print 'Make a left click.'
374 mouse.LeftClick()
375 time.sleep(1)
376
377 print 'Move the cursor left.'
378 mouse.Move(delta_x=-100)
379 time.sleep(1)
380
381 print 'Move the cursor up.'
382 mouse.Move(delta_y=-90)
383 time.sleep(1)
384
385 print 'Move the cursor down right.'
386 mouse.Move(delta_x=100, delta_y=90)
387 time.sleep(1)
388
389 print 'Scroll in one direction.'
390 mouse.Scroll(-80)
391 time.sleep(1)
392
393 print 'Scroll in the opposite direction.'
394 mouse.Scroll(100)
395 else:
396 print 'Something is wrong. Not able to connect to the remote address.'
397 finally:
Joseph Hwang24ed76a2016-05-27 13:20:48 +0800398 if connected:
399 print 'Disconnecting...'
400 try:
401 mouse.Disconnect()
Alexander Lentce006df2017-08-17 17:22:24 -0700402 # TODO(josephsih): Refactor test code to remove need for RN42 import
Joseph Hwang24ed76a2016-05-27 13:20:48 +0800403 except RN42Exception:
404 # RN-42 may have already disconnected.
405 pass
Joseph Hwangd3784cb2016-03-06 21:57:39 +0800406
407 print 'Closing the mouse...'
408 mouse.Close()
409
410
411def _Parse():
412 """Parse the command line options."""
413 prog = sys.argv[0]
414 example_usage = ('Example:\n' +
415 ' python %s keyboard 00:11:22:33:44:55\n' % prog +
416 ' python %s mouse 00:11:22:33:44:55\n'% prog)
417 parser = argparse.ArgumentParser(
418 description='Emulate a HID device.\n' + example_usage,
419 formatter_class=argparse.RawTextHelpFormatter)
420 parser.add_argument('device',
421 choices=['keyboard', 'mouse'],
422 help='the device type to emulate')
423 parser.add_argument('remote_host_address',
424 help='the remote host address')
425 parser.add_argument('-c', '--chars_to_send',
426 default='echo hello world',
427 help='characters to send to the remote host')
428 args = parser.parse_args()
429
430 if len(args.remote_host_address.replace(':', '')) != 12:
431 print '"%s" is not a valid bluetooth address.' % args.remote_host_address
432 exit(1)
433
434 print ('Emulate a %s and connect to remote host at %s' %
435 (args.device, args.remote_host_address))
436 return args
437
438
439def Demo():
440 """Make demonstrations about how to use the HID emulation classes."""
441 args = _Parse()
442 device = args.device.lower()
443 if device == 'keyboard':
444 DemoBluetoothHIDKeyboard(args.remote_host_address, args.chars_to_send)
445 elif device == 'mouse':
446 DemoBluetoothHIDMouse(args.remote_host_address)
447 else:
448 args.print_help()
449
450
Joseph Hwang5d0dc642016-01-19 10:21:12 +0800451if __name__ == '__main__':
Joseph Hwangd3784cb2016-03-06 21:57:39 +0800452 Demo()