blob: 4901d53a4556ee50fd6dce30e843b26b82e8867e [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
Joseph Hwang5d0dc642016-01-19 10:21:12 +080013
14
15class BluetoothHIDException(Exception):
16 """A dummpy exception class for Bluetooth HID class."""
17 pass
18
19
20class BluetoothHID(RN42):
Joseph Hwang9f0e0242016-06-22 12:50:20 +080021 """A base bluetooth HID emulator class using RN-42 evaluation kit.
22
23 Note: every public member method should
24 return True or a non-None object if successful;
25 return False or Raise an exception otherwise.
26 """
Joseph Hwang5d0dc642016-01-19 10:21:12 +080027
Joseph Hwangd13e5e72016-05-30 18:57:35 +080028 TMP_PIN_CODE = '0000' # A temporary pin code
Joseph Hwang5d0dc642016-01-19 10:21:12 +080029
30 SEND_DELAY_SECS = 0.2 # Need to sleep for a short while otherwise
31 # the bits may get lost during transmission.
Joseph Hwangd13e5e72016-05-30 18:57:35 +080032 INIT_SLEEP_SECS = 5 # Sleep after initialization for stabilization.
Joseph Hwang5d0dc642016-01-19 10:21:12 +080033
34 def __init__(self, device_type, authentication_mode,
35 send_delay=SEND_DELAY_SECS):
36 """Initialization of BluetoothHID
37
38 Args:
39 device_type: the device type for emulation
40 authentication_mode: the authentication mode
41 send_delay: wait a while after sending data
42 """
43 super(BluetoothHID, self).__init__()
44 self.device_type = device_type
Joseph Hwangc1edeff2016-06-08 15:38:14 +080045 self.authentication_mode = authentication_mode
Joseph Hwang5d0dc642016-01-19 10:21:12 +080046 self.send_delay = send_delay
47
Joseph Hwangd13e5e72016-05-30 18:57:35 +080048 def Init(self, factory_reset=True):
49 """Initialize the chip correctly.
Joseph Hwang5d0dc642016-01-19 10:21:12 +080050
Joseph Hwangd13e5e72016-05-30 18:57:35 +080051 Initialize the chip with proper HID register values.
Joseph Hwang5d0dc642016-01-19 10:21:12 +080052
Joseph Hwangd13e5e72016-05-30 18:57:35 +080053 Args:
54 factory_reset: True if a factory reset is needed.
55 False if we only want to reconnect the serial device.
56 """
57 # Create a new serial device every time since the serial driver
58 # on chameleon board is not very stable.
59 self.CreateSerialDevice()
Joseph Hwang5d0dc642016-01-19 10:21:12 +080060
Joseph Hwangd13e5e72016-05-30 18:57:35 +080061 if factory_reset:
62 # Do a factory reset to make sure it is in a known initial state.
63 # Do the factory reset before proceeding to set parameters below.
64 self.FactoryReset()
Joseph Hwang5d0dc642016-01-19 10:21:12 +080065
Joseph Hwangd13e5e72016-05-30 18:57:35 +080066 # Enter command mode to issue commands.
67 self.EnterCommandMode()
Joseph Hwang5d0dc642016-01-19 10:21:12 +080068
Joseph Hwangd13e5e72016-05-30 18:57:35 +080069 # Set HID as the service profile.
70 self.SetServiceProfileHID()
Joseph Hwang5d0dc642016-01-19 10:21:12 +080071
Joseph Hwangd13e5e72016-05-30 18:57:35 +080072 # Set the HID device type.
73 self.SetHIDDevice(self.device_type)
Joseph Hwang5d0dc642016-01-19 10:21:12 +080074
Joseph Hwangd13e5e72016-05-30 18:57:35 +080075 # Set the default class of service.
76 self.SetDefaultClassOfService()
Joseph Hwang5d0dc642016-01-19 10:21:12 +080077
Joseph Hwangd13e5e72016-05-30 18:57:35 +080078 # Set the class of device (CoD) according to the hid device type.
79 self.SetClassOfDevice(self.device_type)
80
81 # Set authentication to the specified mode.
82 self.SetAuthenticationMode(self.authentication_mode)
83
84 # Set RN-42 to work as a slave.
85 self.SetSlaveMode()
86
87 # Enable the connection status message so that we could get the message
88 # of connection/disconnection status.
89 self.EnableConnectionStatusMessage()
90
91 # Set a temporary pin code for testing purpose.
92 self.SetPinCode(self.TMP_PIN_CODE)
93
94 # Reboot so that the configurations above take effect.
95 self.Reboot()
96
97 # Enter command mode again after reboot.
98 self.EnterCommandMode()
99
100 time.sleep(self.INIT_SLEEP_SECS)
101
102 logging.info('A bluetooth HID "%s" device is connected.', self.device_type)
Joseph Hwang9f0e0242016-06-22 12:50:20 +0800103 return True
Joseph Hwang5d0dc642016-01-19 10:21:12 +0800104
105 def __del__(self):
106 self.Close()
107
108 def SetHIDDevice(self, device_type):
109 """Set HID device to the specified device type.
110
111 Args:
112 device_type: the HID device type to emulate
113 """
114 if device_type == self.KEYBOARD:
115 self.SetHIDKeyboard()
116 elif device_type == self.GAMEPAD:
117 self.SetHIDGamepad()
118 elif device_type == self.MOUSE:
119 self.SetHIDMouse()
120 elif device_type == self.COMBO:
121 self.SetHIDCombo()
122 elif device_type == self.JOYSTICK:
123 self.SetHIDJoystick()
124
Joseph Hwang5d0dc642016-01-19 10:21:12 +0800125
126class BluetoothHIDKeyboard(BluetoothHID):
127 """A bluetooth HID keyboard emulator class."""
128
129 def __init__(self, authentication_mode):
130 """Initialization of BluetoothHIDKeyboard
131
132 Args:
133 authentication_mode: the authentication mode
134 """
135 super(BluetoothHIDKeyboard, self).__init__(BluetoothHID.KEYBOARD,
136 authentication_mode)
137
138 def Send(self, data):
139 """Send data to the remote host.
140
141 Args:
142 data: data to send to the remote host
Joseph Hwangcb5623b2016-02-17 18:56:34 +0800143 data could be either a string of printable ASCII characters or
144 a special key combination.
Joseph Hwang5d0dc642016-01-19 10:21:12 +0800145 """
146 # TODO(josephsih): should have a method to check the connection status.
147 # Currently, once RN-42 is connected to a remote host, all characters
148 # except chr(0) transmitted through the serial port are interpreted
149 # as characters to send to the remote host.
Joseph Hwang5d0dc642016-01-19 10:21:12 +0800150 logging.debug('HID device sending %r...', data)
151 self.SerialSendReceive(data, msg='BluetoothHID.Send')
152 time.sleep(self.send_delay)
153
Joseph Hwangcb5623b2016-02-17 18:56:34 +0800154 def SendKeyCombination(self, modifiers=None, keys=None):
155 """Send special key combinations to the remote host.
156
157 Args:
158 modifiers: a list of modifiers
159 keys: a list of scan codes of keys
160 """
161 press_codes = self.PressShorthandCodes(modifiers=modifiers, keys=keys)
162 release_codes = self.ReleaseShorthandCodes()
163 if press_codes and release_codes:
164 self.Send(press_codes)
165 self.Send(release_codes)
166 else:
167 logging.warn('modifers: %s and keys: %s are not valid', modifiers, keys)
168 return None
169
Joseph Hwang5d0dc642016-01-19 10:21:12 +0800170
Joseph Hwangd3784cb2016-03-06 21:57:39 +0800171class BluetoothHIDMouse(BluetoothHID):
172 """A bluetooth HID mouse emulator class."""
173
174 # Definitions of buttons
175 BUTTONS_RELEASED = 0x0
176 LEFT_BUTTON = 0x01
177 RIGHT_BUTTON = 0x02
178
179 def __init__(self, authentication_mode):
180 """Initialization of BluetoothHIDMouse
181
182 Args:
183 authentication_mode: the authentication mode
184 """
185 super(BluetoothHIDMouse, self).__init__(BluetoothHID.MOUSE,
186 authentication_mode)
187
Joseph Hwangb21333a2016-09-01 17:20:45 +0800188 def _Move(self, buttons=0, delta_x=0, delta_y=0):
189 """Move the mouse (delta_x, delta_y) pixels with buttons status.
Joseph Hwangd3784cb2016-03-06 21:57:39 +0800190
191 Args:
Joseph Hwangb21333a2016-09-01 17:20:45 +0800192 buttons: the press/release status of buttons
Joseph Hwangd3784cb2016-03-06 21:57:39 +0800193 delta_x: the pixels to move horizontally
194 positive values: moving right; max value = 127.
195 negative values: moving left; max value = -127.
196 delta_y: the pixels to move vertically
197 positive values: moving down; max value = 127.
198 negative values: moving up; max value = -127.
199 """
200 if delta_x or delta_y:
Joseph Hwangb21333a2016-09-01 17:20:45 +0800201 mouse_codes = self.RawMouseCodes(buttons=buttons,
202 x_stop=delta_x, y_stop=delta_y)
Joseph Hwangd3784cb2016-03-06 21:57:39 +0800203 self.SerialSendReceive(mouse_codes, msg='BluetoothHIDMouse.Move')
204 time.sleep(self.send_delay)
205
Joseph Hwangb21333a2016-09-01 17:20:45 +0800206 def Move(self, delta_x=0, delta_y=0):
207 """Move the mouse (delta_x, delta_y) pixels.
208
209 Pure cursor movement without changing any button status.
210
211 Args:
212 delta_x: the pixels to move horizontally
213 delta_y: the pixels to move vertically
214 """
215 self._Move(delta_x=delta_x, delta_y=delta_y)
216
Joseph Hwangd3784cb2016-03-06 21:57:39 +0800217 def _PressButtons(self, buttons):
218 """Press down the specified buttons
219
220 Args:
221 buttons: the buttons to press
222 """
223 if buttons:
224 mouse_codes = self.RawMouseCodes(buttons=buttons)
225 self.SerialSendReceive(mouse_codes, msg='BluetoothHIDMouse._PressButtons')
226 time.sleep(self.send_delay)
227
228 def _ReleaseButtons(self):
229 """Release buttons."""
230 mouse_codes = self.RawMouseCodes(buttons=self.BUTTONS_RELEASED)
231 self.SerialSendReceive(mouse_codes, msg='BluetoothHIDMouse._ReleaseButtons')
232 time.sleep(self.send_delay)
233
234 def PressLeftButton(self):
235 """Press the left button."""
236 self._PressButtons(self.LEFT_BUTTON)
237
238 def ReleaseLeftButton(self):
239 """Release the left button."""
240 self._ReleaseButtons()
241
242 def PressRightButton(self):
243 """Press the right button."""
244 self._PressButtons(self.RIGHT_BUTTON)
245
246 def ReleaseRightButton(self):
247 """Release the right button."""
248 self._ReleaseButtons()
249
250 def LeftClick(self):
251 """Make a left click."""
252 self.PressLeftButton()
253 self.ReleaseLeftButton()
254
255 def RightClick(self):
256 """Make a right click."""
257 self.PressRightButton()
258 self.ReleaseRightButton()
259
260 def ClickAndDrag(self, delta_x=0, delta_y=0):
261 """Click and drag (delta_x, delta_y)
262
263 Args:
264 delta_x: the pixels to move horizontally
265 delta_y: the pixels to move vertically
266 """
267 self.PressLeftButton()
Joseph Hwangb21333a2016-09-01 17:20:45 +0800268 # Keep the left button pressed while moving.
269 self._Move(buttons=self.LEFT_BUTTON, delta_x=delta_x, delta_y=delta_y)
Joseph Hwangd3784cb2016-03-06 21:57:39 +0800270 self.ReleaseLeftButton()
271
272 def Scroll(self, wheel):
273 """Scroll the wheel.
274
275 Args:
276 wheel: the steps to scroll
277 The scroll direction depends on which scroll method is employed,
278 traditional scrolling or Australian scrolling.
279 """
280 if wheel:
281 mouse_codes = self.RawMouseCodes(wheel=wheel)
282 self.SerialSendReceive(mouse_codes, msg='BluetoothHIDMouse.Scroll')
283 time.sleep(self.send_delay)
Joseph Hwang5d0dc642016-01-19 10:21:12 +0800284
285
286def DemoBluetoothHIDKeyboard(remote_address, chars):
287 """A simple demo of acting as a HID keyboard.
288
289 This simple demo works only after the HID device has already paired
290 with the remote device such that a link key has been exchanged. Then
291 the HID device could connect directly to the remote host without
292 pin code and sends the message.
293
294 A full flow would be letting a remote host pair with the HID device
295 with the pin code of the HID device. Thereafter, either the host or
296 the HID device could request to connect. This is out of the scope of
297 this simple demo.
298
299 Args:
300 remote_address: the bluetooth address of the target remote device
301 chars: the characters to send
302 """
303 print 'Creating an emulated bluetooth keyboard...'
304 keyboard = BluetoothHIDKeyboard(BluetoothHID.PIN_CODE_MODE)
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...'
353 mouse = BluetoothHIDMouse(BluetoothHID.PIN_CODE_MODE)
Joseph Hwangc1edeff2016-06-08 15:38:14 +0800354 mouse.Init()
Joseph Hwangd3784cb2016-03-06 21:57:39 +0800355
Joseph Hwang24ed76a2016-05-27 13:20:48 +0800356 connected = False
Joseph Hwangd3784cb2016-03-06 21:57:39 +0800357 print 'Connecting to the remote address %s...' % remote_address
358 try:
359 if mouse.ConnectToRemoteAddress(remote_address):
Joseph Hwang24ed76a2016-05-27 13:20:48 +0800360 connected = True
361
Joseph Hwangd3784cb2016-03-06 21:57:39 +0800362 print 'Click and drag horizontally.'
363 mouse.ClickAndDrag(delta_x=100)
364 time.sleep(1)
365
366 print 'Make a right click.'
367 mouse.RightClick()
368 time.sleep(1)
369
370 print 'Move the cursor upper left.'
371 mouse.Move(delta_x=-30, delta_y=-40)
372 time.sleep(1)
373
374 print 'Make a left click.'
375 mouse.LeftClick()
376 time.sleep(1)
377
378 print 'Move the cursor left.'
379 mouse.Move(delta_x=-100)
380 time.sleep(1)
381
382 print 'Move the cursor up.'
383 mouse.Move(delta_y=-90)
384 time.sleep(1)
385
386 print 'Move the cursor down right.'
387 mouse.Move(delta_x=100, delta_y=90)
388 time.sleep(1)
389
390 print 'Scroll in one direction.'
391 mouse.Scroll(-80)
392 time.sleep(1)
393
394 print 'Scroll in the opposite direction.'
395 mouse.Scroll(100)
396 else:
397 print 'Something is wrong. Not able to connect to the remote address.'
398 finally:
Joseph Hwang24ed76a2016-05-27 13:20:48 +0800399 if connected:
400 print 'Disconnecting...'
401 try:
402 mouse.Disconnect()
403 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()