blob: 36d625346ebfe13392163e0827c7f458c4b44f77 [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
23 # Suppoerted device types
24 KEYBOARD = 'KEYBOARD'
25 GAMEPAD = 'GAMEPAD'
26 MOUSE = 'MOUSE'
27 COMBO = 'COMBO'
28 JOYSTICK = 'JOYSTICK'
29
30 SEND_DELAY_SECS = 0.2 # Need to sleep for a short while otherwise
31 # the bits may get lost during transmission.
32
33 def __init__(self, device_type, authentication_mode,
34 send_delay=SEND_DELAY_SECS):
35 """Initialization of BluetoothHID
36
37 Args:
38 device_type: the device type for emulation
39 authentication_mode: the authentication mode
40 send_delay: wait a while after sending data
41 """
42 super(BluetoothHID, self).__init__()
43 self.device_type = device_type
44 self.send_delay = send_delay
45
46 # Enter command mode for configuration.
47 self.EnterCommandMode()
48
49 # Set HID as the service profile.
50 self.SetServiceProfileHID()
51
52 # Set the HID device type.
53 self.SetHIDDevice(device_type)
54
55 # Set authentication to the specified mode.
56 self.SetAuthenticationMode(authentication_mode)
57
58 # Set RN-42 to work as a slave.
59 self.SetSlaveMode()
60
61 # Enable the connection status message so that we could get the message
62 # of connection/disconnection status.
63 self.EnableConnectionStatusMessage()
64
65 # Reboot so that the configurations above take in effect.
66 self.Reboot()
67
68 # Should enter command mode again after reboot.
69 self.EnterCommandMode()
70
71 logging.info('A HID "%s" device is created successfully.', device_type)
72
73 def __del__(self):
74 self.Close()
75
76 def SetHIDDevice(self, device_type):
77 """Set HID device to the specified device type.
78
79 Args:
80 device_type: the HID device type to emulate
81 """
82 if device_type == self.KEYBOARD:
83 self.SetHIDKeyboard()
84 elif device_type == self.GAMEPAD:
85 self.SetHIDGamepad()
86 elif device_type == self.MOUSE:
87 self.SetHIDMouse()
88 elif device_type == self.COMBO:
89 self.SetHIDCombo()
90 elif device_type == self.JOYSTICK:
91 self.SetHIDJoystick()
92
Joseph Hwang5d0dc642016-01-19 10:21:12 +080093
94class BluetoothHIDKeyboard(BluetoothHID):
95 """A bluetooth HID keyboard emulator class."""
96
97 def __init__(self, authentication_mode):
98 """Initialization of BluetoothHIDKeyboard
99
100 Args:
101 authentication_mode: the authentication mode
102 """
103 super(BluetoothHIDKeyboard, self).__init__(BluetoothHID.KEYBOARD,
104 authentication_mode)
105
106 def Send(self, data):
107 """Send data to the remote host.
108
109 Args:
110 data: data to send to the remote host
Joseph Hwangcb5623b2016-02-17 18:56:34 +0800111 data could be either a string of printable ASCII characters or
112 a special key combination.
Joseph Hwang5d0dc642016-01-19 10:21:12 +0800113 """
114 # TODO(josephsih): should have a method to check the connection status.
115 # Currently, once RN-42 is connected to a remote host, all characters
116 # except chr(0) transmitted through the serial port are interpreted
117 # as characters to send to the remote host.
Joseph Hwang5d0dc642016-01-19 10:21:12 +0800118 logging.debug('HID device sending %r...', data)
119 self.SerialSendReceive(data, msg='BluetoothHID.Send')
120 time.sleep(self.send_delay)
121
Joseph Hwangcb5623b2016-02-17 18:56:34 +0800122 def SendKeyCombination(self, modifiers=None, keys=None):
123 """Send special key combinations to the remote host.
124
125 Args:
126 modifiers: a list of modifiers
127 keys: a list of scan codes of keys
128 """
129 press_codes = self.PressShorthandCodes(modifiers=modifiers, keys=keys)
130 release_codes = self.ReleaseShorthandCodes()
131 if press_codes and release_codes:
132 self.Send(press_codes)
133 self.Send(release_codes)
134 else:
135 logging.warn('modifers: %s and keys: %s are not valid', modifiers, keys)
136 return None
137
Joseph Hwang5d0dc642016-01-19 10:21:12 +0800138
Joseph Hwangd3784cb2016-03-06 21:57:39 +0800139class BluetoothHIDMouse(BluetoothHID):
140 """A bluetooth HID mouse emulator class."""
141
142 # Definitions of buttons
143 BUTTONS_RELEASED = 0x0
144 LEFT_BUTTON = 0x01
145 RIGHT_BUTTON = 0x02
146
147 def __init__(self, authentication_mode):
148 """Initialization of BluetoothHIDMouse
149
150 Args:
151 authentication_mode: the authentication mode
152 """
153 super(BluetoothHIDMouse, self).__init__(BluetoothHID.MOUSE,
154 authentication_mode)
155
156 def Move(self, delta_x=0, delta_y=0):
157 """Move the mouse (delta_x, delta_y) pixels.
158
159 Args:
160 delta_x: the pixels to move horizontally
161 positive values: moving right; max value = 127.
162 negative values: moving left; max value = -127.
163 delta_y: the pixels to move vertically
164 positive values: moving down; max value = 127.
165 negative values: moving up; max value = -127.
166 """
167 if delta_x or delta_y:
168 mouse_codes = self.RawMouseCodes(x_stop=delta_x, y_stop=delta_y)
169 self.SerialSendReceive(mouse_codes, msg='BluetoothHIDMouse.Move')
170 time.sleep(self.send_delay)
171
172 def _PressButtons(self, buttons):
173 """Press down the specified buttons
174
175 Args:
176 buttons: the buttons to press
177 """
178 if buttons:
179 mouse_codes = self.RawMouseCodes(buttons=buttons)
180 self.SerialSendReceive(mouse_codes, msg='BluetoothHIDMouse._PressButtons')
181 time.sleep(self.send_delay)
182
183 def _ReleaseButtons(self):
184 """Release buttons."""
185 mouse_codes = self.RawMouseCodes(buttons=self.BUTTONS_RELEASED)
186 self.SerialSendReceive(mouse_codes, msg='BluetoothHIDMouse._ReleaseButtons')
187 time.sleep(self.send_delay)
188
189 def PressLeftButton(self):
190 """Press the left button."""
191 self._PressButtons(self.LEFT_BUTTON)
192
193 def ReleaseLeftButton(self):
194 """Release the left button."""
195 self._ReleaseButtons()
196
197 def PressRightButton(self):
198 """Press the right button."""
199 self._PressButtons(self.RIGHT_BUTTON)
200
201 def ReleaseRightButton(self):
202 """Release the right button."""
203 self._ReleaseButtons()
204
205 def LeftClick(self):
206 """Make a left click."""
207 self.PressLeftButton()
208 self.ReleaseLeftButton()
209
210 def RightClick(self):
211 """Make a right click."""
212 self.PressRightButton()
213 self.ReleaseRightButton()
214
215 def ClickAndDrag(self, delta_x=0, delta_y=0):
216 """Click and drag (delta_x, delta_y)
217
218 Args:
219 delta_x: the pixels to move horizontally
220 delta_y: the pixels to move vertically
221 """
222 self.PressLeftButton()
223 self.Move(delta_x, delta_y)
224 self.ReleaseLeftButton()
225
226 def Scroll(self, wheel):
227 """Scroll the wheel.
228
229 Args:
230 wheel: the steps to scroll
231 The scroll direction depends on which scroll method is employed,
232 traditional scrolling or Australian scrolling.
233 """
234 if wheel:
235 mouse_codes = self.RawMouseCodes(wheel=wheel)
236 self.SerialSendReceive(mouse_codes, msg='BluetoothHIDMouse.Scroll')
237 time.sleep(self.send_delay)
Joseph Hwang5d0dc642016-01-19 10:21:12 +0800238
239
240def DemoBluetoothHIDKeyboard(remote_address, chars):
241 """A simple demo of acting as a HID keyboard.
242
243 This simple demo works only after the HID device has already paired
244 with the remote device such that a link key has been exchanged. Then
245 the HID device could connect directly to the remote host without
246 pin code and sends the message.
247
248 A full flow would be letting a remote host pair with the HID device
249 with the pin code of the HID device. Thereafter, either the host or
250 the HID device could request to connect. This is out of the scope of
251 this simple demo.
252
253 Args:
254 remote_address: the bluetooth address of the target remote device
255 chars: the characters to send
256 """
257 print 'Creating an emulated bluetooth keyboard...'
258 keyboard = BluetoothHIDKeyboard(BluetoothHID.PIN_CODE_MODE)
259
260 print 'Connecting to the remote address %s...' % remote_address
261 try:
262 if keyboard.ConnectToRemoteAddress(remote_address):
Joseph Hwangcb5623b2016-02-17 18:56:34 +0800263 # Send printable ASCII strings a few times.
264 for i in range(1, 4):
Joseph Hwang5d0dc642016-01-19 10:21:12 +0800265 print 'Sending "%s" for the %dth time...' % (chars, i)
266 keyboard.Send(chars + ' ' + str(i))
Joseph Hwangcb5623b2016-02-17 18:56:34 +0800267
268 # Demo special key combinations below.
269 print 'Create a new chrome tab.'
270 keyboard.SendKeyCombination(modifiers=[RN42.LEFT_CTRL],
271 keys=[RN42.SCAN_T])
272
273 print 'Navigate to Google page.'
274 keyboard.Send('www.google.com')
275 time.sleep(1)
276
277 print 'Search hello world.'
278 keyboard.Send('hello world')
279 time.sleep(1)
280
281 print 'Navigate back to the previous page.'
282 keyboard.SendKeyCombination(keys=[RN42.SCAN_F1])
283 time.sleep(1)
284
285 print 'Switch to the previous tab.'
286 keyboard.SendKeyCombination(modifiers=[RN42.LEFT_CTRL, RN42.LEFT_SHIFT],
287 keys=[RN42.SCAN_TAB])
Joseph Hwang5d0dc642016-01-19 10:21:12 +0800288 else:
289 print 'Something is wrong. Not able to connect to the remote address.'
290 print 'Have you already paired RN-42 with the remote host?'
291 finally:
292 print 'Disconnecting...'
293 keyboard.Disconnect()
294
295 print 'Closing the keyboard...'
296 keyboard.Close()
297
298
Joseph Hwangd3784cb2016-03-06 21:57:39 +0800299def DemoBluetoothHIDMouse(remote_address):
300 """A simple demo of acting as a HID mouse.
301
302 Args:
303 remote_address: the bluetooth address of the target remote device
304 """
305 print 'Creating an emulated bluetooth mouse...'
306 mouse = BluetoothHIDMouse(BluetoothHID.PIN_CODE_MODE)
307
308 print 'Connecting to the remote address %s...' % remote_address
309 try:
310 if mouse.ConnectToRemoteAddress(remote_address):
311 print 'Click and drag horizontally.'
312 mouse.ClickAndDrag(delta_x=100)
313 time.sleep(1)
314
315 print 'Make a right click.'
316 mouse.RightClick()
317 time.sleep(1)
318
319 print 'Move the cursor upper left.'
320 mouse.Move(delta_x=-30, delta_y=-40)
321 time.sleep(1)
322
323 print 'Make a left click.'
324 mouse.LeftClick()
325 time.sleep(1)
326
327 print 'Move the cursor left.'
328 mouse.Move(delta_x=-100)
329 time.sleep(1)
330
331 print 'Move the cursor up.'
332 mouse.Move(delta_y=-90)
333 time.sleep(1)
334
335 print 'Move the cursor down right.'
336 mouse.Move(delta_x=100, delta_y=90)
337 time.sleep(1)
338
339 print 'Scroll in one direction.'
340 mouse.Scroll(-80)
341 time.sleep(1)
342
343 print 'Scroll in the opposite direction.'
344 mouse.Scroll(100)
345 else:
346 print 'Something is wrong. Not able to connect to the remote address.'
347 finally:
348 print 'Disconnecting...'
349 try:
350 mouse.Disconnect()
351 except RN42Exception:
352 # RN-42 may have already disconnected.
353 pass
354
355 print 'Closing the mouse...'
356 mouse.Close()
357
358
359def _Parse():
360 """Parse the command line options."""
361 prog = sys.argv[0]
362 example_usage = ('Example:\n' +
363 ' python %s keyboard 00:11:22:33:44:55\n' % prog +
364 ' python %s mouse 00:11:22:33:44:55\n'% prog)
365 parser = argparse.ArgumentParser(
366 description='Emulate a HID device.\n' + example_usage,
367 formatter_class=argparse.RawTextHelpFormatter)
368 parser.add_argument('device',
369 choices=['keyboard', 'mouse'],
370 help='the device type to emulate')
371 parser.add_argument('remote_host_address',
372 help='the remote host address')
373 parser.add_argument('-c', '--chars_to_send',
374 default='echo hello world',
375 help='characters to send to the remote host')
376 args = parser.parse_args()
377
378 if len(args.remote_host_address.replace(':', '')) != 12:
379 print '"%s" is not a valid bluetooth address.' % args.remote_host_address
380 exit(1)
381
382 print ('Emulate a %s and connect to remote host at %s' %
383 (args.device, args.remote_host_address))
384 return args
385
386
387def Demo():
388 """Make demonstrations about how to use the HID emulation classes."""
389 args = _Parse()
390 device = args.device.lower()
391 if device == 'keyboard':
392 DemoBluetoothHIDKeyboard(args.remote_host_address, args.chars_to_send)
393 elif device == 'mouse':
394 DemoBluetoothHIDMouse(args.remote_host_address)
395 else:
396 args.print_help()
397
398
Joseph Hwang5d0dc642016-01-19 10:21:12 +0800399if __name__ == '__main__':
Joseph Hwangd3784cb2016-03-06 21:57:39 +0800400 Demo()