blob: d6fc054ebdbc74e993b60f93dc2443be3c86c6f6 [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
Joseph Hwangd3784cb2016-03-06 21:57:39 +080012from bluetooth_rn42 import RN42, RN42Exception
Alexander Lent7cbd1592017-08-08 20:12:02 -070013# TODO(josephsih): Remove import when references to RN42* are no longer used
Joseph Hwang5d0dc642016-01-19 10:21:12 +080014
15
16class BluetoothHIDException(Exception):
17 """A dummpy exception class for Bluetooth HID class."""
18 pass
19
20
Alexander Lent7cbd1592017-08-08 20:12:02 -070021class BluetoothHID(object):
Joseph Hwang9f0e0242016-06-22 12:50:20 +080022 """A base bluetooth HID emulator class using RN-42 evaluation kit.
23
24 Note: every public member method should
25 return True or a non-None object if successful;
26 return False or Raise an exception otherwise.
27 """
Joseph Hwang5d0dc642016-01-19 10:21:12 +080028
Alexander Lent7cbd1592017-08-08 20:12:02 -070029 # the authentication mode
30 OPEN_MODE = RN42.OPEN_MODE
31 SSP_KEYBOARD_MODE = RN42.SSP_KEYBOARD_MODE
32 SSP_JUST_WORK_MODE = RN42.SSP_JUST_WORK_MODE
33 PIN_CODE_MODE = RN42.PIN_CODE_MODE
34 AUTHENTICATION_MODE = RN42.AUTHENTICATION_MODE
35
36 # Supported device types
37 KEYBOARD = RN42.KEYBOARD
38 GAMEPAD = RN42.GAMEPAD
39 MOUSE = RN42.MOUSE
40 COMBO = RN42.COMBO
41 JOYSTICK = RN42.JOYSTICK
42
43 # TODO(alent): The above constants are redirected to RN42 to allow
44 # other classes to pass these options on instantiation. Deduplication will
45 # be addressed by the second phase of the refactor. Remove this comment then.
46
Joseph Hwangd13e5e72016-05-30 18:57:35 +080047 TMP_PIN_CODE = '0000' # A temporary pin code
Joseph Hwang5d0dc642016-01-19 10:21:12 +080048
49 SEND_DELAY_SECS = 0.2 # Need to sleep for a short while otherwise
50 # the bits may get lost during transmission.
Joseph Hwangd13e5e72016-05-30 18:57:35 +080051 INIT_SLEEP_SECS = 5 # Sleep after initialization for stabilization.
Joseph Hwang5d0dc642016-01-19 10:21:12 +080052
Alexander Lent7cbd1592017-08-08 20:12:02 -070053 def __init__(self, device_type, authentication_mode, kit_impl,
Joseph Hwang5d0dc642016-01-19 10:21:12 +080054 send_delay=SEND_DELAY_SECS):
55 """Initialization of BluetoothHID
56
57 Args:
58 device_type: the device type for emulation
59 authentication_mode: the authentication mode
Alexander Lent7cbd1592017-08-08 20:12:02 -070060 kit_impl: the implementation of a peripheral kit to be instantiated
Joseph Hwang5d0dc642016-01-19 10:21:12 +080061 send_delay: wait a while after sending data
62 """
Alexander Lent7cbd1592017-08-08 20:12:02 -070063 self._kit = kit_impl()
Joseph Hwang5d0dc642016-01-19 10:21:12 +080064 self.device_type = device_type
Joseph Hwangc1edeff2016-06-08 15:38:14 +080065 self.authentication_mode = authentication_mode
Joseph Hwang5d0dc642016-01-19 10:21:12 +080066 self.send_delay = send_delay
67
Alexander Lent7cbd1592017-08-08 20:12:02 -070068 # TODO(josephsih): Remove the use of __getattr__ after a refactor of the
69 # Chameleon-Autotest interface to eliminate kit-specific commands in tests.
70 def __getattr__(self, name):
71 """Gets the attribute of name from the owned peripheral kit instance
72
73 Allows calling methods (or getting attributes in general) on this class or
74 its subclasses that resolve to methods defined on the kit implementation.
75
76 Args:
77 name: The name of the attribute to be found.
78
79 Returns:
80 The attribute of the kit with given name, if it exists.
81 (This is the default behavior and kits should follow it.)
82
83 Raises:
84 AttributeError if the attribute is not found.
85 (This is the default behavior and kits should follow it.)
86 """
87 return getattr(self._kit, name)
88
Joseph Hwangd13e5e72016-05-30 18:57:35 +080089 def Init(self, factory_reset=True):
90 """Initialize the chip correctly.
Joseph Hwang5d0dc642016-01-19 10:21:12 +080091
Joseph Hwangd13e5e72016-05-30 18:57:35 +080092 Initialize the chip with proper HID register values.
Joseph Hwang5d0dc642016-01-19 10:21:12 +080093
Joseph Hwangd13e5e72016-05-30 18:57:35 +080094 Args:
95 factory_reset: True if a factory reset is needed.
96 False if we only want to reconnect the serial device.
97 """
98 # Create a new serial device every time since the serial driver
99 # on chameleon board is not very stable.
100 self.CreateSerialDevice()
Joseph Hwang5d0dc642016-01-19 10:21:12 +0800101
Joseph Hwangd13e5e72016-05-30 18:57:35 +0800102 if factory_reset:
Alexander Lentbc790282017-08-23 15:04:12 -0700103 # Enter command mode to issue commands.
104 # This must happen first, so that other commands work
105 self.EnterCommandMode()
106
Joseph Hwangd13e5e72016-05-30 18:57:35 +0800107 # Do a factory reset to make sure it is in a known initial state.
108 # Do the factory reset before proceeding to set parameters below.
109 self.FactoryReset()
Joseph Hwang5d0dc642016-01-19 10:21:12 +0800110
Joseph Hwangd13e5e72016-05-30 18:57:35 +0800111 # Set HID as the service profile.
112 self.SetServiceProfileHID()
Joseph Hwang5d0dc642016-01-19 10:21:12 +0800113
Joseph Hwangd13e5e72016-05-30 18:57:35 +0800114 # Set the HID device type.
115 self.SetHIDDevice(self.device_type)
Joseph Hwang5d0dc642016-01-19 10:21:12 +0800116
Joseph Hwangd13e5e72016-05-30 18:57:35 +0800117 # Set the default class of service.
118 self.SetDefaultClassOfService()
Joseph Hwang5d0dc642016-01-19 10:21:12 +0800119
Joseph Hwangd13e5e72016-05-30 18:57:35 +0800120 # Set the class of device (CoD) according to the hid device type.
121 self.SetClassOfDevice(self.device_type)
122
123 # Set authentication to the specified mode.
124 self.SetAuthenticationMode(self.authentication_mode)
125
126 # Set RN-42 to work as a slave.
127 self.SetSlaveMode()
128
129 # Enable the connection status message so that we could get the message
130 # of connection/disconnection status.
131 self.EnableConnectionStatusMessage()
132
133 # Set a temporary pin code for testing purpose.
134 self.SetPinCode(self.TMP_PIN_CODE)
135
136 # Reboot so that the configurations above take effect.
137 self.Reboot()
138
139 # Enter command mode again after reboot.
140 self.EnterCommandMode()
141
142 time.sleep(self.INIT_SLEEP_SECS)
143
144 logging.info('A bluetooth HID "%s" device is connected.', self.device_type)
Joseph Hwang9f0e0242016-06-22 12:50:20 +0800145 return True
Joseph Hwang5d0dc642016-01-19 10:21:12 +0800146
Joseph Hwang5d0dc642016-01-19 10:21:12 +0800147 def SetHIDDevice(self, device_type):
148 """Set HID device to the specified device type.
149
150 Args:
151 device_type: the HID device type to emulate
152 """
153 if device_type == self.KEYBOARD:
154 self.SetHIDKeyboard()
155 elif device_type == self.GAMEPAD:
156 self.SetHIDGamepad()
157 elif device_type == self.MOUSE:
158 self.SetHIDMouse()
159 elif device_type == self.COMBO:
160 self.SetHIDCombo()
161 elif device_type == self.JOYSTICK:
162 self.SetHIDJoystick()
163
Joseph Hwang5d0dc642016-01-19 10:21:12 +0800164
165class BluetoothHIDKeyboard(BluetoothHID):
166 """A bluetooth HID keyboard emulator class."""
167
Alexander Lent7cbd1592017-08-08 20:12:02 -0700168 def __init__(self, authentication_mode, kit_impl):
Joseph Hwang5d0dc642016-01-19 10:21:12 +0800169 """Initialization of BluetoothHIDKeyboard
170
171 Args:
172 authentication_mode: the authentication mode
Alexander Lent7cbd1592017-08-08 20:12:02 -0700173 kit_impl: the implementation of a Bluetooth HID peripheral kit to use
Joseph Hwang5d0dc642016-01-19 10:21:12 +0800174 """
Alexander Lent7cbd1592017-08-08 20:12:02 -0700175 super(BluetoothHIDKeyboard, self).__init__(
176 BluetoothHID.KEYBOARD, authentication_mode, kit_impl)
Joseph Hwang5d0dc642016-01-19 10:21:12 +0800177
178 def Send(self, data):
179 """Send data to the remote host.
180
181 Args:
182 data: data to send to the remote host
Joseph Hwangcb5623b2016-02-17 18:56:34 +0800183 data could be either a string of printable ASCII characters or
184 a special key combination.
Joseph Hwang5d0dc642016-01-19 10:21:12 +0800185 """
186 # TODO(josephsih): should have a method to check the connection status.
187 # Currently, once RN-42 is connected to a remote host, all characters
188 # except chr(0) transmitted through the serial port are interpreted
189 # as characters to send to the remote host.
Joseph Hwang5d0dc642016-01-19 10:21:12 +0800190 logging.debug('HID device sending %r...', data)
191 self.SerialSendReceive(data, msg='BluetoothHID.Send')
192 time.sleep(self.send_delay)
193
Joseph Hwangcb5623b2016-02-17 18:56:34 +0800194 def SendKeyCombination(self, modifiers=None, keys=None):
195 """Send special key combinations to the remote host.
196
197 Args:
198 modifiers: a list of modifiers
199 keys: a list of scan codes of keys
200 """
201 press_codes = self.PressShorthandCodes(modifiers=modifiers, keys=keys)
202 release_codes = self.ReleaseShorthandCodes()
203 if press_codes and release_codes:
204 self.Send(press_codes)
205 self.Send(release_codes)
206 else:
207 logging.warn('modifers: %s and keys: %s are not valid', modifiers, keys)
208 return None
209
Joseph Hwang5d0dc642016-01-19 10:21:12 +0800210
Joseph Hwangd3784cb2016-03-06 21:57:39 +0800211class BluetoothHIDMouse(BluetoothHID):
212 """A bluetooth HID mouse emulator class."""
213
214 # Definitions of buttons
215 BUTTONS_RELEASED = 0x0
216 LEFT_BUTTON = 0x01
217 RIGHT_BUTTON = 0x02
218
Alexander Lent7cbd1592017-08-08 20:12:02 -0700219 def __init__(self, authentication_mode, kit_impl):
Joseph Hwangd3784cb2016-03-06 21:57:39 +0800220 """Initialization of BluetoothHIDMouse
221
222 Args:
223 authentication_mode: the authentication mode
Alexander Lent7cbd1592017-08-08 20:12:02 -0700224 kit_impl: the implementation of a Bluetooth HID peripheral kit to use
Joseph Hwangd3784cb2016-03-06 21:57:39 +0800225 """
Alexander Lent7cbd1592017-08-08 20:12:02 -0700226 super(BluetoothHIDMouse, self).__init__(
227 BluetoothHID.MOUSE, authentication_mode, kit_impl)
Joseph Hwangd3784cb2016-03-06 21:57:39 +0800228
Joseph Hwangb21333a2016-09-01 17:20:45 +0800229 def _Move(self, buttons=0, delta_x=0, delta_y=0):
230 """Move the mouse (delta_x, delta_y) pixels with buttons status.
Joseph Hwangd3784cb2016-03-06 21:57:39 +0800231
232 Args:
Joseph Hwangb21333a2016-09-01 17:20:45 +0800233 buttons: the press/release status of buttons
Joseph Hwangd3784cb2016-03-06 21:57:39 +0800234 delta_x: the pixels to move horizontally
235 positive values: moving right; max value = 127.
236 negative values: moving left; max value = -127.
237 delta_y: the pixels to move vertically
238 positive values: moving down; max value = 127.
239 negative values: moving up; max value = -127.
240 """
241 if delta_x or delta_y:
Joseph Hwangb21333a2016-09-01 17:20:45 +0800242 mouse_codes = self.RawMouseCodes(buttons=buttons,
243 x_stop=delta_x, y_stop=delta_y)
Joseph Hwangd3784cb2016-03-06 21:57:39 +0800244 self.SerialSendReceive(mouse_codes, msg='BluetoothHIDMouse.Move')
245 time.sleep(self.send_delay)
246
Joseph Hwangb21333a2016-09-01 17:20:45 +0800247 def Move(self, delta_x=0, delta_y=0):
248 """Move the mouse (delta_x, delta_y) pixels.
249
250 Pure cursor movement without changing any button status.
251
252 Args:
253 delta_x: the pixels to move horizontally
254 delta_y: the pixels to move vertically
255 """
256 self._Move(delta_x=delta_x, delta_y=delta_y)
257
Joseph Hwangd3784cb2016-03-06 21:57:39 +0800258 def _PressButtons(self, buttons):
259 """Press down the specified buttons
260
261 Args:
262 buttons: the buttons to press
263 """
264 if buttons:
265 mouse_codes = self.RawMouseCodes(buttons=buttons)
266 self.SerialSendReceive(mouse_codes, msg='BluetoothHIDMouse._PressButtons')
267 time.sleep(self.send_delay)
268
269 def _ReleaseButtons(self):
270 """Release buttons."""
271 mouse_codes = self.RawMouseCodes(buttons=self.BUTTONS_RELEASED)
272 self.SerialSendReceive(mouse_codes, msg='BluetoothHIDMouse._ReleaseButtons')
273 time.sleep(self.send_delay)
274
275 def PressLeftButton(self):
276 """Press the left button."""
277 self._PressButtons(self.LEFT_BUTTON)
278
279 def ReleaseLeftButton(self):
280 """Release the left button."""
281 self._ReleaseButtons()
282
283 def PressRightButton(self):
284 """Press the right button."""
285 self._PressButtons(self.RIGHT_BUTTON)
286
287 def ReleaseRightButton(self):
288 """Release the right button."""
289 self._ReleaseButtons()
290
291 def LeftClick(self):
292 """Make a left click."""
293 self.PressLeftButton()
294 self.ReleaseLeftButton()
295
296 def RightClick(self):
297 """Make a right click."""
298 self.PressRightButton()
299 self.ReleaseRightButton()
300
301 def ClickAndDrag(self, delta_x=0, delta_y=0):
302 """Click and drag (delta_x, delta_y)
303
304 Args:
305 delta_x: the pixels to move horizontally
306 delta_y: the pixels to move vertically
307 """
308 self.PressLeftButton()
Joseph Hwangb21333a2016-09-01 17:20:45 +0800309 # Keep the left button pressed while moving.
310 self._Move(buttons=self.LEFT_BUTTON, delta_x=delta_x, delta_y=delta_y)
Joseph Hwangd3784cb2016-03-06 21:57:39 +0800311 self.ReleaseLeftButton()
312
313 def Scroll(self, wheel):
314 """Scroll the wheel.
315
316 Args:
317 wheel: the steps to scroll
318 The scroll direction depends on which scroll method is employed,
319 traditional scrolling or Australian scrolling.
320 """
321 if wheel:
322 mouse_codes = self.RawMouseCodes(wheel=wheel)
323 self.SerialSendReceive(mouse_codes, msg='BluetoothHIDMouse.Scroll')
324 time.sleep(self.send_delay)
Joseph Hwang5d0dc642016-01-19 10:21:12 +0800325
326
327def DemoBluetoothHIDKeyboard(remote_address, chars):
328 """A simple demo of acting as a HID keyboard.
329
330 This simple demo works only after the HID device has already paired
331 with the remote device such that a link key has been exchanged. Then
332 the HID device could connect directly to the remote host without
333 pin code and sends the message.
334
335 A full flow would be letting a remote host pair with the HID device
336 with the pin code of the HID device. Thereafter, either the host or
337 the HID device could request to connect. This is out of the scope of
338 this simple demo.
339
340 Args:
341 remote_address: the bluetooth address of the target remote device
342 chars: the characters to send
343 """
344 print 'Creating an emulated bluetooth keyboard...'
Alexander Lent7cbd1592017-08-08 20:12:02 -0700345 keyboard = BluetoothHIDKeyboard(BluetoothHID.PIN_CODE_MODE, RN42)
Joseph Hwangc1edeff2016-06-08 15:38:14 +0800346 keyboard.Init()
Joseph Hwang5d0dc642016-01-19 10:21:12 +0800347
348 print 'Connecting to the remote address %s...' % remote_address
349 try:
350 if keyboard.ConnectToRemoteAddress(remote_address):
Joseph Hwangcb5623b2016-02-17 18:56:34 +0800351 # Send printable ASCII strings a few times.
352 for i in range(1, 4):
Joseph Hwang5d0dc642016-01-19 10:21:12 +0800353 print 'Sending "%s" for the %dth time...' % (chars, i)
354 keyboard.Send(chars + ' ' + str(i))
Joseph Hwangcb5623b2016-02-17 18:56:34 +0800355
356 # Demo special key combinations below.
357 print 'Create a new chrome tab.'
358 keyboard.SendKeyCombination(modifiers=[RN42.LEFT_CTRL],
359 keys=[RN42.SCAN_T])
360
361 print 'Navigate to Google page.'
362 keyboard.Send('www.google.com')
363 time.sleep(1)
364
365 print 'Search hello world.'
366 keyboard.Send('hello world')
367 time.sleep(1)
368
369 print 'Navigate back to the previous page.'
370 keyboard.SendKeyCombination(keys=[RN42.SCAN_F1])
371 time.sleep(1)
372
373 print 'Switch to the previous tab.'
374 keyboard.SendKeyCombination(modifiers=[RN42.LEFT_CTRL, RN42.LEFT_SHIFT],
375 keys=[RN42.SCAN_TAB])
Joseph Hwang5d0dc642016-01-19 10:21:12 +0800376 else:
377 print 'Something is wrong. Not able to connect to the remote address.'
378 print 'Have you already paired RN-42 with the remote host?'
379 finally:
380 print 'Disconnecting...'
381 keyboard.Disconnect()
382
383 print 'Closing the keyboard...'
384 keyboard.Close()
385
386
Joseph Hwangd3784cb2016-03-06 21:57:39 +0800387def DemoBluetoothHIDMouse(remote_address):
388 """A simple demo of acting as a HID mouse.
389
390 Args:
391 remote_address: the bluetooth address of the target remote device
392 """
393 print 'Creating an emulated bluetooth mouse...'
Alexander Lent7cbd1592017-08-08 20:12:02 -0700394 mouse = BluetoothHIDMouse(BluetoothHID.PIN_CODE_MODE, RN42)
Joseph Hwangc1edeff2016-06-08 15:38:14 +0800395 mouse.Init()
Joseph Hwangd3784cb2016-03-06 21:57:39 +0800396
Joseph Hwang24ed76a2016-05-27 13:20:48 +0800397 connected = False
Joseph Hwangd3784cb2016-03-06 21:57:39 +0800398 print 'Connecting to the remote address %s...' % remote_address
399 try:
400 if mouse.ConnectToRemoteAddress(remote_address):
Joseph Hwang24ed76a2016-05-27 13:20:48 +0800401 connected = True
402
Joseph Hwangd3784cb2016-03-06 21:57:39 +0800403 print 'Click and drag horizontally.'
404 mouse.ClickAndDrag(delta_x=100)
405 time.sleep(1)
406
407 print 'Make a right click.'
408 mouse.RightClick()
409 time.sleep(1)
410
411 print 'Move the cursor upper left.'
412 mouse.Move(delta_x=-30, delta_y=-40)
413 time.sleep(1)
414
415 print 'Make a left click.'
416 mouse.LeftClick()
417 time.sleep(1)
418
419 print 'Move the cursor left.'
420 mouse.Move(delta_x=-100)
421 time.sleep(1)
422
423 print 'Move the cursor up.'
424 mouse.Move(delta_y=-90)
425 time.sleep(1)
426
427 print 'Move the cursor down right.'
428 mouse.Move(delta_x=100, delta_y=90)
429 time.sleep(1)
430
431 print 'Scroll in one direction.'
432 mouse.Scroll(-80)
433 time.sleep(1)
434
435 print 'Scroll in the opposite direction.'
436 mouse.Scroll(100)
437 else:
438 print 'Something is wrong. Not able to connect to the remote address.'
439 finally:
Joseph Hwang24ed76a2016-05-27 13:20:48 +0800440 if connected:
441 print 'Disconnecting...'
442 try:
443 mouse.Disconnect()
444 except RN42Exception:
445 # RN-42 may have already disconnected.
446 pass
Joseph Hwangd3784cb2016-03-06 21:57:39 +0800447
448 print 'Closing the mouse...'
449 mouse.Close()
450
451
452def _Parse():
453 """Parse the command line options."""
454 prog = sys.argv[0]
455 example_usage = ('Example:\n' +
456 ' python %s keyboard 00:11:22:33:44:55\n' % prog +
457 ' python %s mouse 00:11:22:33:44:55\n'% prog)
458 parser = argparse.ArgumentParser(
459 description='Emulate a HID device.\n' + example_usage,
460 formatter_class=argparse.RawTextHelpFormatter)
461 parser.add_argument('device',
462 choices=['keyboard', 'mouse'],
463 help='the device type to emulate')
464 parser.add_argument('remote_host_address',
465 help='the remote host address')
466 parser.add_argument('-c', '--chars_to_send',
467 default='echo hello world',
468 help='characters to send to the remote host')
469 args = parser.parse_args()
470
471 if len(args.remote_host_address.replace(':', '')) != 12:
472 print '"%s" is not a valid bluetooth address.' % args.remote_host_address
473 exit(1)
474
475 print ('Emulate a %s and connect to remote host at %s' %
476 (args.device, args.remote_host_address))
477 return args
478
479
480def Demo():
481 """Make demonstrations about how to use the HID emulation classes."""
482 args = _Parse()
483 device = args.device.lower()
484 if device == 'keyboard':
485 DemoBluetoothHIDKeyboard(args.remote_host_address, args.chars_to_send)
486 elif device == 'mouse':
487 DemoBluetoothHIDMouse(args.remote_host_address)
488 else:
489 args.print_help()
490
491
Joseph Hwang5d0dc642016-01-19 10:21:12 +0800492if __name__ == '__main__':
Joseph Hwangd3784cb2016-03-06 21:57:39 +0800493 Demo()