blob: e356ebe4c1393f7bdaf960888b8df9f852de9ed6 [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:
103 # Do a factory reset to make sure it is in a known initial state.
104 # Do the factory reset before proceeding to set parameters below.
105 self.FactoryReset()
Joseph Hwang5d0dc642016-01-19 10:21:12 +0800106
Joseph Hwangd13e5e72016-05-30 18:57:35 +0800107 # Enter command mode to issue commands.
108 self.EnterCommandMode()
Joseph Hwang5d0dc642016-01-19 10:21:12 +0800109
Joseph Hwangd13e5e72016-05-30 18:57:35 +0800110 # Set HID as the service profile.
111 self.SetServiceProfileHID()
Joseph Hwang5d0dc642016-01-19 10:21:12 +0800112
Joseph Hwangd13e5e72016-05-30 18:57:35 +0800113 # Set the HID device type.
114 self.SetHIDDevice(self.device_type)
Joseph Hwang5d0dc642016-01-19 10:21:12 +0800115
Joseph Hwangd13e5e72016-05-30 18:57:35 +0800116 # Set the default class of service.
117 self.SetDefaultClassOfService()
Joseph Hwang5d0dc642016-01-19 10:21:12 +0800118
Joseph Hwangd13e5e72016-05-30 18:57:35 +0800119 # Set the class of device (CoD) according to the hid device type.
120 self.SetClassOfDevice(self.device_type)
121
122 # Set authentication to the specified mode.
123 self.SetAuthenticationMode(self.authentication_mode)
124
125 # Set RN-42 to work as a slave.
126 self.SetSlaveMode()
127
128 # Enable the connection status message so that we could get the message
129 # of connection/disconnection status.
130 self.EnableConnectionStatusMessage()
131
132 # Set a temporary pin code for testing purpose.
133 self.SetPinCode(self.TMP_PIN_CODE)
134
135 # Reboot so that the configurations above take effect.
136 self.Reboot()
137
138 # Enter command mode again after reboot.
139 self.EnterCommandMode()
140
141 time.sleep(self.INIT_SLEEP_SECS)
142
143 logging.info('A bluetooth HID "%s" device is connected.', self.device_type)
Joseph Hwang9f0e0242016-06-22 12:50:20 +0800144 return True
Joseph Hwang5d0dc642016-01-19 10:21:12 +0800145
Joseph Hwang5d0dc642016-01-19 10:21:12 +0800146 def SetHIDDevice(self, device_type):
147 """Set HID device to the specified device type.
148
149 Args:
150 device_type: the HID device type to emulate
151 """
152 if device_type == self.KEYBOARD:
153 self.SetHIDKeyboard()
154 elif device_type == self.GAMEPAD:
155 self.SetHIDGamepad()
156 elif device_type == self.MOUSE:
157 self.SetHIDMouse()
158 elif device_type == self.COMBO:
159 self.SetHIDCombo()
160 elif device_type == self.JOYSTICK:
161 self.SetHIDJoystick()
162
Joseph Hwang5d0dc642016-01-19 10:21:12 +0800163
164class BluetoothHIDKeyboard(BluetoothHID):
165 """A bluetooth HID keyboard emulator class."""
166
Alexander Lent7cbd1592017-08-08 20:12:02 -0700167 def __init__(self, authentication_mode, kit_impl):
Joseph Hwang5d0dc642016-01-19 10:21:12 +0800168 """Initialization of BluetoothHIDKeyboard
169
170 Args:
171 authentication_mode: the authentication mode
Alexander Lent7cbd1592017-08-08 20:12:02 -0700172 kit_impl: the implementation of a Bluetooth HID peripheral kit to use
Joseph Hwang5d0dc642016-01-19 10:21:12 +0800173 """
Alexander Lent7cbd1592017-08-08 20:12:02 -0700174 super(BluetoothHIDKeyboard, self).__init__(
175 BluetoothHID.KEYBOARD, authentication_mode, kit_impl)
Joseph Hwang5d0dc642016-01-19 10:21:12 +0800176
177 def Send(self, data):
178 """Send data to the remote host.
179
180 Args:
181 data: data to send to the remote host
Joseph Hwangcb5623b2016-02-17 18:56:34 +0800182 data could be either a string of printable ASCII characters or
183 a special key combination.
Joseph Hwang5d0dc642016-01-19 10:21:12 +0800184 """
185 # TODO(josephsih): should have a method to check the connection status.
186 # Currently, once RN-42 is connected to a remote host, all characters
187 # except chr(0) transmitted through the serial port are interpreted
188 # as characters to send to the remote host.
Joseph Hwang5d0dc642016-01-19 10:21:12 +0800189 logging.debug('HID device sending %r...', data)
190 self.SerialSendReceive(data, msg='BluetoothHID.Send')
191 time.sleep(self.send_delay)
192
Joseph Hwangcb5623b2016-02-17 18:56:34 +0800193 def SendKeyCombination(self, modifiers=None, keys=None):
194 """Send special key combinations to the remote host.
195
196 Args:
197 modifiers: a list of modifiers
198 keys: a list of scan codes of keys
199 """
200 press_codes = self.PressShorthandCodes(modifiers=modifiers, keys=keys)
201 release_codes = self.ReleaseShorthandCodes()
202 if press_codes and release_codes:
203 self.Send(press_codes)
204 self.Send(release_codes)
205 else:
206 logging.warn('modifers: %s and keys: %s are not valid', modifiers, keys)
207 return None
208
Joseph Hwang5d0dc642016-01-19 10:21:12 +0800209
Joseph Hwangd3784cb2016-03-06 21:57:39 +0800210class BluetoothHIDMouse(BluetoothHID):
211 """A bluetooth HID mouse emulator class."""
212
213 # Definitions of buttons
214 BUTTONS_RELEASED = 0x0
215 LEFT_BUTTON = 0x01
216 RIGHT_BUTTON = 0x02
217
Alexander Lent7cbd1592017-08-08 20:12:02 -0700218 def __init__(self, authentication_mode, kit_impl):
Joseph Hwangd3784cb2016-03-06 21:57:39 +0800219 """Initialization of BluetoothHIDMouse
220
221 Args:
222 authentication_mode: the authentication mode
Alexander Lent7cbd1592017-08-08 20:12:02 -0700223 kit_impl: the implementation of a Bluetooth HID peripheral kit to use
Joseph Hwangd3784cb2016-03-06 21:57:39 +0800224 """
Alexander Lent7cbd1592017-08-08 20:12:02 -0700225 super(BluetoothHIDMouse, self).__init__(
226 BluetoothHID.MOUSE, authentication_mode, kit_impl)
Joseph Hwangd3784cb2016-03-06 21:57:39 +0800227
Joseph Hwangb21333a2016-09-01 17:20:45 +0800228 def _Move(self, buttons=0, delta_x=0, delta_y=0):
229 """Move the mouse (delta_x, delta_y) pixels with buttons status.
Joseph Hwangd3784cb2016-03-06 21:57:39 +0800230
231 Args:
Joseph Hwangb21333a2016-09-01 17:20:45 +0800232 buttons: the press/release status of buttons
Joseph Hwangd3784cb2016-03-06 21:57:39 +0800233 delta_x: the pixels to move horizontally
234 positive values: moving right; max value = 127.
235 negative values: moving left; max value = -127.
236 delta_y: the pixels to move vertically
237 positive values: moving down; max value = 127.
238 negative values: moving up; max value = -127.
239 """
240 if delta_x or delta_y:
Joseph Hwangb21333a2016-09-01 17:20:45 +0800241 mouse_codes = self.RawMouseCodes(buttons=buttons,
242 x_stop=delta_x, y_stop=delta_y)
Joseph Hwangd3784cb2016-03-06 21:57:39 +0800243 self.SerialSendReceive(mouse_codes, msg='BluetoothHIDMouse.Move')
244 time.sleep(self.send_delay)
245
Joseph Hwangb21333a2016-09-01 17:20:45 +0800246 def Move(self, delta_x=0, delta_y=0):
247 """Move the mouse (delta_x, delta_y) pixels.
248
249 Pure cursor movement without changing any button status.
250
251 Args:
252 delta_x: the pixels to move horizontally
253 delta_y: the pixels to move vertically
254 """
255 self._Move(delta_x=delta_x, delta_y=delta_y)
256
Joseph Hwangd3784cb2016-03-06 21:57:39 +0800257 def _PressButtons(self, buttons):
258 """Press down the specified buttons
259
260 Args:
261 buttons: the buttons to press
262 """
263 if buttons:
264 mouse_codes = self.RawMouseCodes(buttons=buttons)
265 self.SerialSendReceive(mouse_codes, msg='BluetoothHIDMouse._PressButtons')
266 time.sleep(self.send_delay)
267
268 def _ReleaseButtons(self):
269 """Release buttons."""
270 mouse_codes = self.RawMouseCodes(buttons=self.BUTTONS_RELEASED)
271 self.SerialSendReceive(mouse_codes, msg='BluetoothHIDMouse._ReleaseButtons')
272 time.sleep(self.send_delay)
273
274 def PressLeftButton(self):
275 """Press the left button."""
276 self._PressButtons(self.LEFT_BUTTON)
277
278 def ReleaseLeftButton(self):
279 """Release the left button."""
280 self._ReleaseButtons()
281
282 def PressRightButton(self):
283 """Press the right button."""
284 self._PressButtons(self.RIGHT_BUTTON)
285
286 def ReleaseRightButton(self):
287 """Release the right button."""
288 self._ReleaseButtons()
289
290 def LeftClick(self):
291 """Make a left click."""
292 self.PressLeftButton()
293 self.ReleaseLeftButton()
294
295 def RightClick(self):
296 """Make a right click."""
297 self.PressRightButton()
298 self.ReleaseRightButton()
299
300 def ClickAndDrag(self, delta_x=0, delta_y=0):
301 """Click and drag (delta_x, delta_y)
302
303 Args:
304 delta_x: the pixels to move horizontally
305 delta_y: the pixels to move vertically
306 """
307 self.PressLeftButton()
Joseph Hwangb21333a2016-09-01 17:20:45 +0800308 # Keep the left button pressed while moving.
309 self._Move(buttons=self.LEFT_BUTTON, delta_x=delta_x, delta_y=delta_y)
Joseph Hwangd3784cb2016-03-06 21:57:39 +0800310 self.ReleaseLeftButton()
311
312 def Scroll(self, wheel):
313 """Scroll the wheel.
314
315 Args:
316 wheel: the steps to scroll
317 The scroll direction depends on which scroll method is employed,
318 traditional scrolling or Australian scrolling.
319 """
320 if wheel:
321 mouse_codes = self.RawMouseCodes(wheel=wheel)
322 self.SerialSendReceive(mouse_codes, msg='BluetoothHIDMouse.Scroll')
323 time.sleep(self.send_delay)
Joseph Hwang5d0dc642016-01-19 10:21:12 +0800324
325
326def DemoBluetoothHIDKeyboard(remote_address, chars):
327 """A simple demo of acting as a HID keyboard.
328
329 This simple demo works only after the HID device has already paired
330 with the remote device such that a link key has been exchanged. Then
331 the HID device could connect directly to the remote host without
332 pin code and sends the message.
333
334 A full flow would be letting a remote host pair with the HID device
335 with the pin code of the HID device. Thereafter, either the host or
336 the HID device could request to connect. This is out of the scope of
337 this simple demo.
338
339 Args:
340 remote_address: the bluetooth address of the target remote device
341 chars: the characters to send
342 """
343 print 'Creating an emulated bluetooth keyboard...'
Alexander Lent7cbd1592017-08-08 20:12:02 -0700344 keyboard = BluetoothHIDKeyboard(BluetoothHID.PIN_CODE_MODE, RN42)
Joseph Hwangc1edeff2016-06-08 15:38:14 +0800345 keyboard.Init()
Joseph Hwang5d0dc642016-01-19 10:21:12 +0800346
347 print 'Connecting to the remote address %s...' % remote_address
348 try:
349 if keyboard.ConnectToRemoteAddress(remote_address):
Joseph Hwangcb5623b2016-02-17 18:56:34 +0800350 # Send printable ASCII strings a few times.
351 for i in range(1, 4):
Joseph Hwang5d0dc642016-01-19 10:21:12 +0800352 print 'Sending "%s" for the %dth time...' % (chars, i)
353 keyboard.Send(chars + ' ' + str(i))
Joseph Hwangcb5623b2016-02-17 18:56:34 +0800354
355 # Demo special key combinations below.
356 print 'Create a new chrome tab.'
357 keyboard.SendKeyCombination(modifiers=[RN42.LEFT_CTRL],
358 keys=[RN42.SCAN_T])
359
360 print 'Navigate to Google page.'
361 keyboard.Send('www.google.com')
362 time.sleep(1)
363
364 print 'Search hello world.'
365 keyboard.Send('hello world')
366 time.sleep(1)
367
368 print 'Navigate back to the previous page.'
369 keyboard.SendKeyCombination(keys=[RN42.SCAN_F1])
370 time.sleep(1)
371
372 print 'Switch to the previous tab.'
373 keyboard.SendKeyCombination(modifiers=[RN42.LEFT_CTRL, RN42.LEFT_SHIFT],
374 keys=[RN42.SCAN_TAB])
Joseph Hwang5d0dc642016-01-19 10:21:12 +0800375 else:
376 print 'Something is wrong. Not able to connect to the remote address.'
377 print 'Have you already paired RN-42 with the remote host?'
378 finally:
379 print 'Disconnecting...'
380 keyboard.Disconnect()
381
382 print 'Closing the keyboard...'
383 keyboard.Close()
384
385
Joseph Hwangd3784cb2016-03-06 21:57:39 +0800386def DemoBluetoothHIDMouse(remote_address):
387 """A simple demo of acting as a HID mouse.
388
389 Args:
390 remote_address: the bluetooth address of the target remote device
391 """
392 print 'Creating an emulated bluetooth mouse...'
Alexander Lent7cbd1592017-08-08 20:12:02 -0700393 mouse = BluetoothHIDMouse(BluetoothHID.PIN_CODE_MODE, RN42)
Joseph Hwangc1edeff2016-06-08 15:38:14 +0800394 mouse.Init()
Joseph Hwangd3784cb2016-03-06 21:57:39 +0800395
Joseph Hwang24ed76a2016-05-27 13:20:48 +0800396 connected = False
Joseph Hwangd3784cb2016-03-06 21:57:39 +0800397 print 'Connecting to the remote address %s...' % remote_address
398 try:
399 if mouse.ConnectToRemoteAddress(remote_address):
Joseph Hwang24ed76a2016-05-27 13:20:48 +0800400 connected = True
401
Joseph Hwangd3784cb2016-03-06 21:57:39 +0800402 print 'Click and drag horizontally.'
403 mouse.ClickAndDrag(delta_x=100)
404 time.sleep(1)
405
406 print 'Make a right click.'
407 mouse.RightClick()
408 time.sleep(1)
409
410 print 'Move the cursor upper left.'
411 mouse.Move(delta_x=-30, delta_y=-40)
412 time.sleep(1)
413
414 print 'Make a left click.'
415 mouse.LeftClick()
416 time.sleep(1)
417
418 print 'Move the cursor left.'
419 mouse.Move(delta_x=-100)
420 time.sleep(1)
421
422 print 'Move the cursor up.'
423 mouse.Move(delta_y=-90)
424 time.sleep(1)
425
426 print 'Move the cursor down right.'
427 mouse.Move(delta_x=100, delta_y=90)
428 time.sleep(1)
429
430 print 'Scroll in one direction.'
431 mouse.Scroll(-80)
432 time.sleep(1)
433
434 print 'Scroll in the opposite direction.'
435 mouse.Scroll(100)
436 else:
437 print 'Something is wrong. Not able to connect to the remote address.'
438 finally:
Joseph Hwang24ed76a2016-05-27 13:20:48 +0800439 if connected:
440 print 'Disconnecting...'
441 try:
442 mouse.Disconnect()
443 except RN42Exception:
444 # RN-42 may have already disconnected.
445 pass
Joseph Hwangd3784cb2016-03-06 21:57:39 +0800446
447 print 'Closing the mouse...'
448 mouse.Close()
449
450
451def _Parse():
452 """Parse the command line options."""
453 prog = sys.argv[0]
454 example_usage = ('Example:\n' +
455 ' python %s keyboard 00:11:22:33:44:55\n' % prog +
456 ' python %s mouse 00:11:22:33:44:55\n'% prog)
457 parser = argparse.ArgumentParser(
458 description='Emulate a HID device.\n' + example_usage,
459 formatter_class=argparse.RawTextHelpFormatter)
460 parser.add_argument('device',
461 choices=['keyboard', 'mouse'],
462 help='the device type to emulate')
463 parser.add_argument('remote_host_address',
464 help='the remote host address')
465 parser.add_argument('-c', '--chars_to_send',
466 default='echo hello world',
467 help='characters to send to the remote host')
468 args = parser.parse_args()
469
470 if len(args.remote_host_address.replace(':', '')) != 12:
471 print '"%s" is not a valid bluetooth address.' % args.remote_host_address
472 exit(1)
473
474 print ('Emulate a %s and connect to remote host at %s' %
475 (args.device, args.remote_host_address))
476 return args
477
478
479def Demo():
480 """Make demonstrations about how to use the HID emulation classes."""
481 args = _Parse()
482 device = args.device.lower()
483 if device == 'keyboard':
484 DemoBluetoothHIDKeyboard(args.remote_host_address, args.chars_to_send)
485 elif device == 'mouse':
486 DemoBluetoothHIDMouse(args.remote_host_address)
487 else:
488 args.print_help()
489
490
Joseph Hwang5d0dc642016-01-19 10:21:12 +0800491if __name__ == '__main__':
Joseph Hwangd3784cb2016-03-06 21:57:39 +0800492 Demo()