blob: 17ccbb37656b2e65c78dbf8bcd4c90c6a4a5f1df [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
Alexander Lentedd8d562017-08-17 17:23:58 -0700113 # Set a temporary pin code for testing purpose.
114 # Only do this when we want to use a pin code.
115 if self.authentication_mode == PeripheralKit.PIN_CODE_MODE:
116 result = self.SetPinCode(self.TMP_PIN_CODE) and result
117
Joseph Hwangd13e5e72016-05-30 18:57:35 +0800118 # Enable the connection status message so that we could get the message
119 # of connection/disconnection status.
Alexander Lentce006df2017-08-17 17:22:24 -0700120 result = self.EnableConnectionStatusMessage() and result
Joseph Hwangd13e5e72016-05-30 18:57:35 +0800121
Joseph Hwangd13e5e72016-05-30 18:57:35 +0800122 # Reboot so that the configurations above take effect.
Alexander Lentce006df2017-08-17 17:22:24 -0700123 result = self.Reboot() and result
Joseph Hwangd13e5e72016-05-30 18:57:35 +0800124
125 # Enter command mode again after reboot.
Alexander Lentce006df2017-08-17 17:22:24 -0700126 result = self.EnterCommandMode() and result
Joseph Hwangd13e5e72016-05-30 18:57:35 +0800127
128 time.sleep(self.INIT_SLEEP_SECS)
129
130 logging.info('A bluetooth HID "%s" device is connected.', self.device_type)
Alexander Lentce006df2017-08-17 17:22:24 -0700131 return result
Joseph Hwang5d0dc642016-01-19 10:21:12 +0800132
Joseph Hwang5d0dc642016-01-19 10:21:12 +0800133
134class BluetoothHIDKeyboard(BluetoothHID):
135 """A bluetooth HID keyboard emulator class."""
136
Alexander Lent7cbd1592017-08-08 20:12:02 -0700137 def __init__(self, authentication_mode, kit_impl):
Joseph Hwang5d0dc642016-01-19 10:21:12 +0800138 """Initialization of BluetoothHIDKeyboard
139
140 Args:
141 authentication_mode: the authentication mode
Alexander Lent7cbd1592017-08-08 20:12:02 -0700142 kit_impl: the implementation of a Bluetooth HID peripheral kit to use
Joseph Hwang5d0dc642016-01-19 10:21:12 +0800143 """
Alexander Lent7cbd1592017-08-08 20:12:02 -0700144 super(BluetoothHIDKeyboard, self).__init__(
Alexander Lentce006df2017-08-17 17:22:24 -0700145 PeripheralKit.KEYBOARD, authentication_mode, kit_impl)
Joseph Hwang5d0dc642016-01-19 10:21:12 +0800146
147 def Send(self, data):
148 """Send data to the remote host.
149
150 Args:
151 data: data to send to the remote host
Joseph Hwangcb5623b2016-02-17 18:56:34 +0800152 data could be either a string of printable ASCII characters or
153 a special key combination.
Joseph Hwang5d0dc642016-01-19 10:21:12 +0800154 """
155 # TODO(josephsih): should have a method to check the connection status.
156 # Currently, once RN-42 is connected to a remote host, all characters
157 # except chr(0) transmitted through the serial port are interpreted
158 # as characters to send to the remote host.
Joseph Hwang5d0dc642016-01-19 10:21:12 +0800159 logging.debug('HID device sending %r...', data)
160 self.SerialSendReceive(data, msg='BluetoothHID.Send')
161 time.sleep(self.send_delay)
162
Joseph Hwangcb5623b2016-02-17 18:56:34 +0800163 def SendKeyCombination(self, modifiers=None, keys=None):
164 """Send special key combinations to the remote host.
165
166 Args:
167 modifiers: a list of modifiers
168 keys: a list of scan codes of keys
169 """
170 press_codes = self.PressShorthandCodes(modifiers=modifiers, keys=keys)
171 release_codes = self.ReleaseShorthandCodes()
172 if press_codes and release_codes:
173 self.Send(press_codes)
174 self.Send(release_codes)
175 else:
176 logging.warn('modifers: %s and keys: %s are not valid', modifiers, keys)
177 return None
178
Joseph Hwang5d0dc642016-01-19 10:21:12 +0800179
Joseph Hwangd3784cb2016-03-06 21:57:39 +0800180class BluetoothHIDMouse(BluetoothHID):
181 """A bluetooth HID mouse emulator class."""
182
183 # Definitions of buttons
184 BUTTONS_RELEASED = 0x0
185 LEFT_BUTTON = 0x01
186 RIGHT_BUTTON = 0x02
187
Alexander Lent7cbd1592017-08-08 20:12:02 -0700188 def __init__(self, authentication_mode, kit_impl):
Joseph Hwangd3784cb2016-03-06 21:57:39 +0800189 """Initialization of BluetoothHIDMouse
190
191 Args:
192 authentication_mode: the authentication mode
Alexander Lent7cbd1592017-08-08 20:12:02 -0700193 kit_impl: the implementation of a Bluetooth HID peripheral kit to use
Joseph Hwangd3784cb2016-03-06 21:57:39 +0800194 """
Alexander Lent7cbd1592017-08-08 20:12:02 -0700195 super(BluetoothHIDMouse, self).__init__(
Alexander Lentce006df2017-08-17 17:22:24 -0700196 PeripheralKit.MOUSE, authentication_mode, kit_impl)
Joseph Hwangd3784cb2016-03-06 21:57:39 +0800197
Alexander Lentce006df2017-08-17 17:22:24 -0700198 def Move(self, delta_x=0, delta_y=0, buttons=0):
Joseph Hwangb21333a2016-09-01 17:20:45 +0800199 """Move the mouse (delta_x, delta_y) pixels with buttons status.
Joseph Hwangd3784cb2016-03-06 21:57:39 +0800200
201 Args:
202 delta_x: the pixels to move horizontally
203 positive values: moving right; max value = 127.
204 negative values: moving left; max value = -127.
205 delta_y: the pixels to move vertically
206 positive values: moving down; max value = 127.
207 negative values: moving up; max value = -127.
Alexander Lentce006df2017-08-17 17:22:24 -0700208 buttons: the press/release status of buttons
Joseph Hwangd3784cb2016-03-06 21:57:39 +0800209 """
210 if delta_x or delta_y:
Joseph Hwangb21333a2016-09-01 17:20:45 +0800211 mouse_codes = self.RawMouseCodes(buttons=buttons,
212 x_stop=delta_x, y_stop=delta_y)
Joseph Hwangd3784cb2016-03-06 21:57:39 +0800213 self.SerialSendReceive(mouse_codes, msg='BluetoothHIDMouse.Move')
214 time.sleep(self.send_delay)
215
216 def _PressButtons(self, buttons):
217 """Press down the specified buttons
218
219 Args:
220 buttons: the buttons to press
221 """
222 if buttons:
223 mouse_codes = self.RawMouseCodes(buttons=buttons)
224 self.SerialSendReceive(mouse_codes, msg='BluetoothHIDMouse._PressButtons')
225 time.sleep(self.send_delay)
226
227 def _ReleaseButtons(self):
228 """Release buttons."""
229 mouse_codes = self.RawMouseCodes(buttons=self.BUTTONS_RELEASED)
230 self.SerialSendReceive(mouse_codes, msg='BluetoothHIDMouse._ReleaseButtons')
231 time.sleep(self.send_delay)
232
233 def PressLeftButton(self):
234 """Press the left button."""
235 self._PressButtons(self.LEFT_BUTTON)
236
237 def ReleaseLeftButton(self):
238 """Release the left button."""
239 self._ReleaseButtons()
240
241 def PressRightButton(self):
242 """Press the right button."""
243 self._PressButtons(self.RIGHT_BUTTON)
244
245 def ReleaseRightButton(self):
246 """Release the right button."""
247 self._ReleaseButtons()
248
249 def LeftClick(self):
250 """Make a left click."""
251 self.PressLeftButton()
252 self.ReleaseLeftButton()
253
254 def RightClick(self):
255 """Make a right click."""
256 self.PressRightButton()
257 self.ReleaseRightButton()
258
259 def ClickAndDrag(self, delta_x=0, delta_y=0):
260 """Click and drag (delta_x, delta_y)
261
262 Args:
263 delta_x: the pixels to move horizontally
264 delta_y: the pixels to move vertically
265 """
266 self.PressLeftButton()
Joseph Hwangb21333a2016-09-01 17:20:45 +0800267 # Keep the left button pressed while moving.
Alexander Lentce006df2017-08-17 17:22:24 -0700268 self.Move(delta_x=delta_x, delta_y=delta_y, buttons=self.LEFT_BUTTON)
Joseph Hwangd3784cb2016-03-06 21:57:39 +0800269 self.ReleaseLeftButton()
270
271 def Scroll(self, wheel):
272 """Scroll the wheel.
273
274 Args:
275 wheel: the steps to scroll
276 The scroll direction depends on which scroll method is employed,
277 traditional scrolling or Australian scrolling.
278 """
279 if wheel:
280 mouse_codes = self.RawMouseCodes(wheel=wheel)
281 self.SerialSendReceive(mouse_codes, msg='BluetoothHIDMouse.Scroll')
282 time.sleep(self.send_delay)
Joseph Hwang5d0dc642016-01-19 10:21:12 +0800283
284
285def DemoBluetoothHIDKeyboard(remote_address, chars):
286 """A simple demo of acting as a HID keyboard.
287
288 This simple demo works only after the HID device has already paired
289 with the remote device such that a link key has been exchanged. Then
290 the HID device could connect directly to the remote host without
291 pin code and sends the message.
292
293 A full flow would be letting a remote host pair with the HID device
294 with the pin code of the HID device. Thereafter, either the host or
295 the HID device could request to connect. This is out of the scope of
296 this simple demo.
297
298 Args:
299 remote_address: the bluetooth address of the target remote device
300 chars: the characters to send
301 """
302 print 'Creating an emulated bluetooth keyboard...'
Alexander Lentce006df2017-08-17 17:22:24 -0700303 # TODO(josephsih): Refactor test code to remove need for RN42 import
304 keyboard = BluetoothHIDKeyboard(PeripheralKit.PIN_CODE_MODE, RN42)
Joseph Hwangc1edeff2016-06-08 15:38:14 +0800305 keyboard.Init()
Joseph Hwang5d0dc642016-01-19 10:21:12 +0800306
307 print 'Connecting to the remote address %s...' % remote_address
308 try:
309 if keyboard.ConnectToRemoteAddress(remote_address):
Joseph Hwangcb5623b2016-02-17 18:56:34 +0800310 # Send printable ASCII strings a few times.
311 for i in range(1, 4):
Joseph Hwang5d0dc642016-01-19 10:21:12 +0800312 print 'Sending "%s" for the %dth time...' % (chars, i)
313 keyboard.Send(chars + ' ' + str(i))
Joseph Hwangcb5623b2016-02-17 18:56:34 +0800314
315 # Demo special key combinations below.
316 print 'Create a new chrome tab.'
317 keyboard.SendKeyCombination(modifiers=[RN42.LEFT_CTRL],
318 keys=[RN42.SCAN_T])
319
320 print 'Navigate to Google page.'
321 keyboard.Send('www.google.com')
322 time.sleep(1)
323
324 print 'Search hello world.'
325 keyboard.Send('hello world')
326 time.sleep(1)
327
328 print 'Navigate back to the previous page.'
329 keyboard.SendKeyCombination(keys=[RN42.SCAN_F1])
330 time.sleep(1)
331
332 print 'Switch to the previous tab.'
333 keyboard.SendKeyCombination(modifiers=[RN42.LEFT_CTRL, RN42.LEFT_SHIFT],
334 keys=[RN42.SCAN_TAB])
Joseph Hwang5d0dc642016-01-19 10:21:12 +0800335 else:
336 print 'Something is wrong. Not able to connect to the remote address.'
337 print 'Have you already paired RN-42 with the remote host?'
338 finally:
339 print 'Disconnecting...'
340 keyboard.Disconnect()
341
342 print 'Closing the keyboard...'
343 keyboard.Close()
344
345
Joseph Hwangd3784cb2016-03-06 21:57:39 +0800346def DemoBluetoothHIDMouse(remote_address):
347 """A simple demo of acting as a HID mouse.
348
349 Args:
350 remote_address: the bluetooth address of the target remote device
351 """
352 print 'Creating an emulated bluetooth mouse...'
Alexander Lentce006df2017-08-17 17:22:24 -0700353 # TODO(josephsih): Refactor test code to remove need for RN42 import
354 mouse = BluetoothHIDMouse(PeripheralKit.PIN_CODE_MODE, RN42)
Joseph Hwangc1edeff2016-06-08 15:38:14 +0800355 mouse.Init()
Joseph Hwangd3784cb2016-03-06 21:57:39 +0800356
Joseph Hwang24ed76a2016-05-27 13:20:48 +0800357 connected = False
Joseph Hwangd3784cb2016-03-06 21:57:39 +0800358 print 'Connecting to the remote address %s...' % remote_address
359 try:
360 if mouse.ConnectToRemoteAddress(remote_address):
Joseph Hwang24ed76a2016-05-27 13:20:48 +0800361 connected = True
362
Joseph Hwangd3784cb2016-03-06 21:57:39 +0800363 print 'Click and drag horizontally.'
364 mouse.ClickAndDrag(delta_x=100)
365 time.sleep(1)
366
367 print 'Make a right click.'
368 mouse.RightClick()
369 time.sleep(1)
370
371 print 'Move the cursor upper left.'
372 mouse.Move(delta_x=-30, delta_y=-40)
373 time.sleep(1)
374
375 print 'Make a left click.'
376 mouse.LeftClick()
377 time.sleep(1)
378
379 print 'Move the cursor left.'
380 mouse.Move(delta_x=-100)
381 time.sleep(1)
382
383 print 'Move the cursor up.'
384 mouse.Move(delta_y=-90)
385 time.sleep(1)
386
387 print 'Move the cursor down right.'
388 mouse.Move(delta_x=100, delta_y=90)
389 time.sleep(1)
390
391 print 'Scroll in one direction.'
392 mouse.Scroll(-80)
393 time.sleep(1)
394
395 print 'Scroll in the opposite direction.'
396 mouse.Scroll(100)
397 else:
398 print 'Something is wrong. Not able to connect to the remote address.'
399 finally:
Joseph Hwang24ed76a2016-05-27 13:20:48 +0800400 if connected:
401 print 'Disconnecting...'
402 try:
403 mouse.Disconnect()
Alexander Lentce006df2017-08-17 17:22:24 -0700404 # TODO(josephsih): Refactor test code to remove need for RN42 import
Joseph Hwang24ed76a2016-05-27 13:20:48 +0800405 except RN42Exception:
406 # RN-42 may have already disconnected.
407 pass
Joseph Hwangd3784cb2016-03-06 21:57:39 +0800408
409 print 'Closing the mouse...'
410 mouse.Close()
411
412
413def _Parse():
414 """Parse the command line options."""
415 prog = sys.argv[0]
416 example_usage = ('Example:\n' +
417 ' python %s keyboard 00:11:22:33:44:55\n' % prog +
418 ' python %s mouse 00:11:22:33:44:55\n'% prog)
419 parser = argparse.ArgumentParser(
420 description='Emulate a HID device.\n' + example_usage,
421 formatter_class=argparse.RawTextHelpFormatter)
422 parser.add_argument('device',
423 choices=['keyboard', 'mouse'],
424 help='the device type to emulate')
425 parser.add_argument('remote_host_address',
426 help='the remote host address')
427 parser.add_argument('-c', '--chars_to_send',
428 default='echo hello world',
429 help='characters to send to the remote host')
430 args = parser.parse_args()
431
432 if len(args.remote_host_address.replace(':', '')) != 12:
433 print '"%s" is not a valid bluetooth address.' % args.remote_host_address
434 exit(1)
435
436 print ('Emulate a %s and connect to remote host at %s' %
437 (args.device, args.remote_host_address))
438 return args
439
440
441def Demo():
442 """Make demonstrations about how to use the HID emulation classes."""
443 args = _Parse()
444 device = args.device.lower()
445 if device == 'keyboard':
446 DemoBluetoothHIDKeyboard(args.remote_host_address, args.chars_to_send)
447 elif device == 'mouse':
448 DemoBluetoothHIDMouse(args.remote_host_address)
449 else:
450 args.print_help()
451
452
Joseph Hwang5d0dc642016-01-19 10:21:12 +0800453if __name__ == '__main__':
Joseph Hwangd3784cb2016-03-06 21:57:39 +0800454 Demo()