blob: c29a069c23d4bd2d0d4d92aa4580018838b41d02 [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):
21 """A base bluetooth HID emulator class using RN-42 evaluation kit."""
22
Joseph Hwangd13e5e72016-05-30 18:57:35 +080023 TMP_PIN_CODE = '0000' # A temporary pin code
Joseph Hwang5d0dc642016-01-19 10:21:12 +080024
25 SEND_DELAY_SECS = 0.2 # Need to sleep for a short while otherwise
26 # the bits may get lost during transmission.
Joseph Hwangd13e5e72016-05-30 18:57:35 +080027 INIT_SLEEP_SECS = 5 # Sleep after initialization for stabilization.
Joseph Hwang5d0dc642016-01-19 10:21:12 +080028
29 def __init__(self, device_type, authentication_mode,
30 send_delay=SEND_DELAY_SECS):
31 """Initialization of BluetoothHID
32
33 Args:
34 device_type: the device type for emulation
35 authentication_mode: the authentication mode
36 send_delay: wait a while after sending data
37 """
38 super(BluetoothHID, self).__init__()
39 self.device_type = device_type
Joseph Hwangc1edeff2016-06-08 15:38:14 +080040 self.authentication_mode = authentication_mode
Joseph Hwang5d0dc642016-01-19 10:21:12 +080041 self.send_delay = send_delay
42
Joseph Hwangd13e5e72016-05-30 18:57:35 +080043 def Init(self, factory_reset=True):
44 """Initialize the chip correctly.
Joseph Hwang5d0dc642016-01-19 10:21:12 +080045
Joseph Hwangd13e5e72016-05-30 18:57:35 +080046 Initialize the chip with proper HID register values.
Joseph Hwang5d0dc642016-01-19 10:21:12 +080047
Joseph Hwangd13e5e72016-05-30 18:57:35 +080048 Args:
49 factory_reset: True if a factory reset is needed.
50 False if we only want to reconnect the serial device.
51 """
52 # Create a new serial device every time since the serial driver
53 # on chameleon board is not very stable.
54 self.CreateSerialDevice()
Joseph Hwang5d0dc642016-01-19 10:21:12 +080055
Joseph Hwangd13e5e72016-05-30 18:57:35 +080056 if factory_reset:
57 # Do a factory reset to make sure it is in a known initial state.
58 # Do the factory reset before proceeding to set parameters below.
59 self.FactoryReset()
Joseph Hwang5d0dc642016-01-19 10:21:12 +080060
Joseph Hwangd13e5e72016-05-30 18:57:35 +080061 # Enter command mode to issue commands.
62 self.EnterCommandMode()
Joseph Hwang5d0dc642016-01-19 10:21:12 +080063
Joseph Hwangd13e5e72016-05-30 18:57:35 +080064 # Set HID as the service profile.
65 self.SetServiceProfileHID()
Joseph Hwang5d0dc642016-01-19 10:21:12 +080066
Joseph Hwangd13e5e72016-05-30 18:57:35 +080067 # Set the HID device type.
68 self.SetHIDDevice(self.device_type)
Joseph Hwang5d0dc642016-01-19 10:21:12 +080069
Joseph Hwangd13e5e72016-05-30 18:57:35 +080070 # Set the default class of service.
71 self.SetDefaultClassOfService()
Joseph Hwang5d0dc642016-01-19 10:21:12 +080072
Joseph Hwangd13e5e72016-05-30 18:57:35 +080073 # Set the class of device (CoD) according to the hid device type.
74 self.SetClassOfDevice(self.device_type)
75
76 # Set authentication to the specified mode.
77 self.SetAuthenticationMode(self.authentication_mode)
78
79 # Set RN-42 to work as a slave.
80 self.SetSlaveMode()
81
82 # Enable the connection status message so that we could get the message
83 # of connection/disconnection status.
84 self.EnableConnectionStatusMessage()
85
86 # Set a temporary pin code for testing purpose.
87 self.SetPinCode(self.TMP_PIN_CODE)
88
89 # Reboot so that the configurations above take effect.
90 self.Reboot()
91
92 # Enter command mode again after reboot.
93 self.EnterCommandMode()
94
95 time.sleep(self.INIT_SLEEP_SECS)
96
97 logging.info('A bluetooth HID "%s" device is connected.', self.device_type)
Joseph Hwang5d0dc642016-01-19 10:21:12 +080098
99 def __del__(self):
100 self.Close()
101
102 def SetHIDDevice(self, device_type):
103 """Set HID device to the specified device type.
104
105 Args:
106 device_type: the HID device type to emulate
107 """
108 if device_type == self.KEYBOARD:
109 self.SetHIDKeyboard()
110 elif device_type == self.GAMEPAD:
111 self.SetHIDGamepad()
112 elif device_type == self.MOUSE:
113 self.SetHIDMouse()
114 elif device_type == self.COMBO:
115 self.SetHIDCombo()
116 elif device_type == self.JOYSTICK:
117 self.SetHIDJoystick()
118
Joseph Hwang5d0dc642016-01-19 10:21:12 +0800119
120class BluetoothHIDKeyboard(BluetoothHID):
121 """A bluetooth HID keyboard emulator class."""
122
123 def __init__(self, authentication_mode):
124 """Initialization of BluetoothHIDKeyboard
125
126 Args:
127 authentication_mode: the authentication mode
128 """
129 super(BluetoothHIDKeyboard, self).__init__(BluetoothHID.KEYBOARD,
130 authentication_mode)
131
132 def Send(self, data):
133 """Send data to the remote host.
134
135 Args:
136 data: data to send to the remote host
Joseph Hwangcb5623b2016-02-17 18:56:34 +0800137 data could be either a string of printable ASCII characters or
138 a special key combination.
Joseph Hwang5d0dc642016-01-19 10:21:12 +0800139 """
140 # TODO(josephsih): should have a method to check the connection status.
141 # Currently, once RN-42 is connected to a remote host, all characters
142 # except chr(0) transmitted through the serial port are interpreted
143 # as characters to send to the remote host.
Joseph Hwang5d0dc642016-01-19 10:21:12 +0800144 logging.debug('HID device sending %r...', data)
145 self.SerialSendReceive(data, msg='BluetoothHID.Send')
146 time.sleep(self.send_delay)
147
Joseph Hwangcb5623b2016-02-17 18:56:34 +0800148 def SendKeyCombination(self, modifiers=None, keys=None):
149 """Send special key combinations to the remote host.
150
151 Args:
152 modifiers: a list of modifiers
153 keys: a list of scan codes of keys
154 """
155 press_codes = self.PressShorthandCodes(modifiers=modifiers, keys=keys)
156 release_codes = self.ReleaseShorthandCodes()
157 if press_codes and release_codes:
158 self.Send(press_codes)
159 self.Send(release_codes)
160 else:
161 logging.warn('modifers: %s and keys: %s are not valid', modifiers, keys)
162 return None
163
Joseph Hwang5d0dc642016-01-19 10:21:12 +0800164
Joseph Hwangd3784cb2016-03-06 21:57:39 +0800165class BluetoothHIDMouse(BluetoothHID):
166 """A bluetooth HID mouse emulator class."""
167
168 # Definitions of buttons
169 BUTTONS_RELEASED = 0x0
170 LEFT_BUTTON = 0x01
171 RIGHT_BUTTON = 0x02
172
173 def __init__(self, authentication_mode):
174 """Initialization of BluetoothHIDMouse
175
176 Args:
177 authentication_mode: the authentication mode
178 """
179 super(BluetoothHIDMouse, self).__init__(BluetoothHID.MOUSE,
180 authentication_mode)
181
182 def Move(self, delta_x=0, delta_y=0):
183 """Move the mouse (delta_x, delta_y) pixels.
184
185 Args:
186 delta_x: the pixels to move horizontally
187 positive values: moving right; max value = 127.
188 negative values: moving left; max value = -127.
189 delta_y: the pixels to move vertically
190 positive values: moving down; max value = 127.
191 negative values: moving up; max value = -127.
192 """
193 if delta_x or delta_y:
194 mouse_codes = self.RawMouseCodes(x_stop=delta_x, y_stop=delta_y)
195 self.SerialSendReceive(mouse_codes, msg='BluetoothHIDMouse.Move')
196 time.sleep(self.send_delay)
197
198 def _PressButtons(self, buttons):
199 """Press down the specified buttons
200
201 Args:
202 buttons: the buttons to press
203 """
204 if buttons:
205 mouse_codes = self.RawMouseCodes(buttons=buttons)
206 self.SerialSendReceive(mouse_codes, msg='BluetoothHIDMouse._PressButtons')
207 time.sleep(self.send_delay)
208
209 def _ReleaseButtons(self):
210 """Release buttons."""
211 mouse_codes = self.RawMouseCodes(buttons=self.BUTTONS_RELEASED)
212 self.SerialSendReceive(mouse_codes, msg='BluetoothHIDMouse._ReleaseButtons')
213 time.sleep(self.send_delay)
214
215 def PressLeftButton(self):
216 """Press the left button."""
217 self._PressButtons(self.LEFT_BUTTON)
218
219 def ReleaseLeftButton(self):
220 """Release the left button."""
221 self._ReleaseButtons()
222
223 def PressRightButton(self):
224 """Press the right button."""
225 self._PressButtons(self.RIGHT_BUTTON)
226
227 def ReleaseRightButton(self):
228 """Release the right button."""
229 self._ReleaseButtons()
230
231 def LeftClick(self):
232 """Make a left click."""
233 self.PressLeftButton()
234 self.ReleaseLeftButton()
235
236 def RightClick(self):
237 """Make a right click."""
238 self.PressRightButton()
239 self.ReleaseRightButton()
240
241 def ClickAndDrag(self, delta_x=0, delta_y=0):
242 """Click and drag (delta_x, delta_y)
243
244 Args:
245 delta_x: the pixels to move horizontally
246 delta_y: the pixels to move vertically
247 """
248 self.PressLeftButton()
249 self.Move(delta_x, delta_y)
250 self.ReleaseLeftButton()
251
252 def Scroll(self, wheel):
253 """Scroll the wheel.
254
255 Args:
256 wheel: the steps to scroll
257 The scroll direction depends on which scroll method is employed,
258 traditional scrolling or Australian scrolling.
259 """
260 if wheel:
261 mouse_codes = self.RawMouseCodes(wheel=wheel)
262 self.SerialSendReceive(mouse_codes, msg='BluetoothHIDMouse.Scroll')
263 time.sleep(self.send_delay)
Joseph Hwang5d0dc642016-01-19 10:21:12 +0800264
265
266def DemoBluetoothHIDKeyboard(remote_address, chars):
267 """A simple demo of acting as a HID keyboard.
268
269 This simple demo works only after the HID device has already paired
270 with the remote device such that a link key has been exchanged. Then
271 the HID device could connect directly to the remote host without
272 pin code and sends the message.
273
274 A full flow would be letting a remote host pair with the HID device
275 with the pin code of the HID device. Thereafter, either the host or
276 the HID device could request to connect. This is out of the scope of
277 this simple demo.
278
279 Args:
280 remote_address: the bluetooth address of the target remote device
281 chars: the characters to send
282 """
283 print 'Creating an emulated bluetooth keyboard...'
284 keyboard = BluetoothHIDKeyboard(BluetoothHID.PIN_CODE_MODE)
Joseph Hwangc1edeff2016-06-08 15:38:14 +0800285 keyboard.Init()
Joseph Hwang5d0dc642016-01-19 10:21:12 +0800286
287 print 'Connecting to the remote address %s...' % remote_address
288 try:
289 if keyboard.ConnectToRemoteAddress(remote_address):
Joseph Hwangcb5623b2016-02-17 18:56:34 +0800290 # Send printable ASCII strings a few times.
291 for i in range(1, 4):
Joseph Hwang5d0dc642016-01-19 10:21:12 +0800292 print 'Sending "%s" for the %dth time...' % (chars, i)
293 keyboard.Send(chars + ' ' + str(i))
Joseph Hwangcb5623b2016-02-17 18:56:34 +0800294
295 # Demo special key combinations below.
296 print 'Create a new chrome tab.'
297 keyboard.SendKeyCombination(modifiers=[RN42.LEFT_CTRL],
298 keys=[RN42.SCAN_T])
299
300 print 'Navigate to Google page.'
301 keyboard.Send('www.google.com')
302 time.sleep(1)
303
304 print 'Search hello world.'
305 keyboard.Send('hello world')
306 time.sleep(1)
307
308 print 'Navigate back to the previous page.'
309 keyboard.SendKeyCombination(keys=[RN42.SCAN_F1])
310 time.sleep(1)
311
312 print 'Switch to the previous tab.'
313 keyboard.SendKeyCombination(modifiers=[RN42.LEFT_CTRL, RN42.LEFT_SHIFT],
314 keys=[RN42.SCAN_TAB])
Joseph Hwang5d0dc642016-01-19 10:21:12 +0800315 else:
316 print 'Something is wrong. Not able to connect to the remote address.'
317 print 'Have you already paired RN-42 with the remote host?'
318 finally:
319 print 'Disconnecting...'
320 keyboard.Disconnect()
321
322 print 'Closing the keyboard...'
323 keyboard.Close()
324
325
Joseph Hwangd3784cb2016-03-06 21:57:39 +0800326def DemoBluetoothHIDMouse(remote_address):
327 """A simple demo of acting as a HID mouse.
328
329 Args:
330 remote_address: the bluetooth address of the target remote device
331 """
332 print 'Creating an emulated bluetooth mouse...'
333 mouse = BluetoothHIDMouse(BluetoothHID.PIN_CODE_MODE)
Joseph Hwangc1edeff2016-06-08 15:38:14 +0800334 mouse.Init()
Joseph Hwangd3784cb2016-03-06 21:57:39 +0800335
Joseph Hwang24ed76a2016-05-27 13:20:48 +0800336 connected = False
Joseph Hwangd3784cb2016-03-06 21:57:39 +0800337 print 'Connecting to the remote address %s...' % remote_address
338 try:
339 if mouse.ConnectToRemoteAddress(remote_address):
Joseph Hwang24ed76a2016-05-27 13:20:48 +0800340 connected = True
341
Joseph Hwangd3784cb2016-03-06 21:57:39 +0800342 print 'Click and drag horizontally.'
343 mouse.ClickAndDrag(delta_x=100)
344 time.sleep(1)
345
346 print 'Make a right click.'
347 mouse.RightClick()
348 time.sleep(1)
349
350 print 'Move the cursor upper left.'
351 mouse.Move(delta_x=-30, delta_y=-40)
352 time.sleep(1)
353
354 print 'Make a left click.'
355 mouse.LeftClick()
356 time.sleep(1)
357
358 print 'Move the cursor left.'
359 mouse.Move(delta_x=-100)
360 time.sleep(1)
361
362 print 'Move the cursor up.'
363 mouse.Move(delta_y=-90)
364 time.sleep(1)
365
366 print 'Move the cursor down right.'
367 mouse.Move(delta_x=100, delta_y=90)
368 time.sleep(1)
369
370 print 'Scroll in one direction.'
371 mouse.Scroll(-80)
372 time.sleep(1)
373
374 print 'Scroll in the opposite direction.'
375 mouse.Scroll(100)
376 else:
377 print 'Something is wrong. Not able to connect to the remote address.'
378 finally:
Joseph Hwang24ed76a2016-05-27 13:20:48 +0800379 if connected:
380 print 'Disconnecting...'
381 try:
382 mouse.Disconnect()
383 except RN42Exception:
384 # RN-42 may have already disconnected.
385 pass
Joseph Hwangd3784cb2016-03-06 21:57:39 +0800386
387 print 'Closing the mouse...'
388 mouse.Close()
389
390
391def _Parse():
392 """Parse the command line options."""
393 prog = sys.argv[0]
394 example_usage = ('Example:\n' +
395 ' python %s keyboard 00:11:22:33:44:55\n' % prog +
396 ' python %s mouse 00:11:22:33:44:55\n'% prog)
397 parser = argparse.ArgumentParser(
398 description='Emulate a HID device.\n' + example_usage,
399 formatter_class=argparse.RawTextHelpFormatter)
400 parser.add_argument('device',
401 choices=['keyboard', 'mouse'],
402 help='the device type to emulate')
403 parser.add_argument('remote_host_address',
404 help='the remote host address')
405 parser.add_argument('-c', '--chars_to_send',
406 default='echo hello world',
407 help='characters to send to the remote host')
408 args = parser.parse_args()
409
410 if len(args.remote_host_address.replace(':', '')) != 12:
411 print '"%s" is not a valid bluetooth address.' % args.remote_host_address
412 exit(1)
413
414 print ('Emulate a %s and connect to remote host at %s' %
415 (args.device, args.remote_host_address))
416 return args
417
418
419def Demo():
420 """Make demonstrations about how to use the HID emulation classes."""
421 args = _Parse()
422 device = args.device.lower()
423 if device == 'keyboard':
424 DemoBluetoothHIDKeyboard(args.remote_host_address, args.chars_to_send)
425 elif device == 'mouse':
426 DemoBluetoothHIDMouse(args.remote_host_address)
427 else:
428 args.print_help()
429
430
Joseph Hwang5d0dc642016-01-19 10:21:12 +0800431if __name__ == '__main__':
Joseph Hwangd3784cb2016-03-06 21:57:39 +0800432 Demo()