blob: 8fbb200f901f7007cb315f92827b71bca4773e29 [file] [log] [blame]
Neeraj Poojary59f8b152019-02-01 14:15:57 -08001# Copyright 2019 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"""
6This module provides an abstraction of the Nordic nRF52 bluetooth low energy
7kit.
8"""
9
10from __future__ import print_function
11
12import logging
13import time
14
15import common
16import serial_utils
17from bluetooth_peripheral_kit import PeripheralKit
18from bluetooth_peripheral_kit import PeripheralKitException
19
20
21class nRF52Exception(PeripheralKitException):
22 """A dummy exception class for nRF52 class."""
23 pass
24
25
26class nRF52(PeripheralKit):
27 """This is an abstraction of Nordic's nRF52 Dongle and the C application
28 that implements BLE mouse and keyboard functionality.
29
30 SDK: https://www.nordicsemi.com/Software-and-Tools/Software/nRF5-SDK
31
32 See autotest-private/nRF52/ble_app_hids/README for information about
33 using the SDK to compile the application.
34 """
35
36 # Serial port settings (override)
37 BAUDRATE = 115200
38 DRIVER = 'cdc_acm'
39 # Driver name in udev is 'cdc_acm', but builtin module is 'cdc-acm.ko'
40 # So we need to look for cdc_acm when searching by driver,
41 # but looking in builtins requires searching by 'cdc-acm'.
42 DRIVER_MODULE = 'cdc-acm'
43 BAUDRATE = 115200
44 USB_VID = '1d6b'
45 USB_PID = '0002'
46
47 # A newline can just be a '\n' to denote the end of a command
48 NEWLINE = '\n'
49 CMD_FS = ' ' # Command field separator
50
51 # Supported device types
52 MOUSE = 'MOUSE'
53 KEYBOARD = 'KEYBOARD'
54
55 RESET_SLEEP_SECS = 1
56
57 # Mouse button constants
58 MOUSE_BUTTON_LEFT_BIT = 1
59 MOUSE_BUTTON_RIGHT_BIT = 2
60
61 # Specific Commands
62 # Reboot the nRF52
63 CMD_REBOOT = "RBT"
64 # Reset the nRF52 and erase all previous bonds
65 CMD_FACTORY_RESET = "FRST"
66 # Return the name that is sent in advertisement packets
67 CMD_GET_ADVERTISED_NAME = "GN"
68 # Return the nRF52 firmware version
69 CMD_GET_FIRMWARE_VERSION = "GV"
70 # Return the Bluetooth address of the nRF52
71 CMD_GET_NRF52_MAC = "GM"
72 # Return the address of the device connected (if there exists a connection)
73 CMD_GET_REMOTE_CONNECTION_MAC = "GC"
74 # Return the status of the nRF52's connection with a central device
75 CMD_GET_CONNECTION_STATUS = "GS"
76
77 # Return the type of device the HID service is set
78 CMD_GET_DEVICE_TYPE = "GD"
79 # Set the nRF52 HID service to mouse
80 CMD_SET_MOUSE = "SM"
81 # Set the nRF52 HID service to keyboard
82 CMD_SET_KEYBOARD = "SK"
83 # Start HID service emulation
84 CMD_START_HID_EM = "START"
85 # Start HID service emulation
86 CMD_STOP_HID_EM = "STOP"
87 # Start advertising with the current settings (HID type)
88 CMD_START_ADVERTISING = "ADV"
89
90 # Press (or clear) one or more buttons (left/right)
91 CMD_MOUSE_BUTTON = "B"
92 # Click the left and/or right button of the mouse
93 CMD_MOUSE_CLICK = "C"
94 # Move the mouse along x and/or y axis
95 CMD_MOUSE_MOVE = "M"
96 # Scrolling the mouse wheel up/down
97 CMD_MOUSE_SCROLL = "S"
98
99 def GetCapabilities(self):
100 """What can this kit do/not do that tests need to adjust for?
101
102 Returns:
103 A dictionary from PeripheralKit.CAP_* strings to an appropriate value.
104 See above (CAP_*) for details.
105 """
106 return {PeripheralKit.CAP_TRANSPORTS: [PeripheralKit.TRANSPORT_LE],
107 PeripheralKit.CAP_HAS_PIN: False,
108 PeripheralKit.CAP_INIT_CONNECT: False}
109
110 def EnterCommandMode(self):
111 """Make the kit enter command mode.
112
113 The application on the nRF52 Dongle is always in command mode, so this
114 method will just create a serial connection if necessary
115
116 Returns:
117 True if the kit successfully entered command mode.
118
119 Raises:
120 nRF52Exception if there is an error in creating the serial connection
121 """
122 if self._serial is None:
123 self.CreateSerialDevice()
124 if not self._command_mode:
125 self._command_mode = True
126 return True
127
128 def LeaveCommandMode(self, force=False):
129 """Make the kit leave command mode.
130
131 As above, the nRF52 application is always in command mode.
132
133 Args:
134 force: True if we want to ignore potential errors and leave command mode
135 regardless of those errors
136
137 Returns:
138 True if the kit successfully left command mode.
139 """
140 if self._command_mode or force:
141 self._command_mode = False
142 return True
143
144 def Reboot(self):
145 """Reboot the nRF52 Dongle.
146
147 Does not erase the bond information.
148
149 Returns:
150 True if the kit rebooted successfully.
151 """
152 self.SerialSendReceive(self.CMD_REBOOT,
153 msg='rebooting nRF52')
154 time.sleep(self.RESET_SLEEP_SECS)
155 return True
156
157 def FactoryReset(self):
158 """Factory reset the nRF52 Dongle.
159
160 Erase the bond information and reboot.
161
162 Returns:
163 True if the kit is reset successfully.
164 """
165 self.SerialSendReceive(self.CMD_FACTORY_RESET,
166 msg='factory reset nRF52')
167 time.sleep(self.RESET_SLEEP_SECS)
168 return True
169
170 def GetAdvertisedName(self):
171 """Get the name advertised by the nRF52.
172
173 Returns:
174 The device name that the application uses in advertising
175 """
176 return self.SerialSendReceive(self.CMD_GET_ADVERTISED_NAME,
177 msg='getting advertised name')
178
179 def GetFirmwareVersion(self):
180 """Get the firmware version of the kit.
181
182 This is useful for checking what features are supported if we want to
183 support muliple versions of some kit.
184
185 For nRF52, returns the Link Layer Version (8 corresponds to BT 4.2),
186 Nordic Company ID (89), and Firmware ID (135).
187
188 Returns:
189 The firmware version of the kit.
190 """
191 return self.SerialSendReceive(self.CMD_GET_FIRMWARE_VERSION,
192 msg='getting firmware version')
193
194 def GetOperationMode(self):
195 """Get the operation mode.
196
197 This is master/slave in Bluetooth BR/EDR; the Bluetooth LE equivalent is
198 central/peripheral. For legacy reasons, we call it MASTER or SLAVE only.
199 Not all kits may support all modes.
200
201 nRF52 only supports peripheral role
202
203 Returns:
204 The operation mode of the kit.
205 """
206 logging.debug('GetOperationMode is a NOP on nRF52')
207 return "SLAVE"
208
209 def SetMasterMode(self):
210 """Set the kit to master/central mode.
211
212 nRF52 application only acts as a peripheral
213
214 Returns:
215 True if master/central mode was set successfully.
216
217 Raises:
218 A kit-specific exception if master/central mode is unsupported.
219 """
220 error_msg = 'Failed to set master/central mode'
221 logging.error(error_msg)
222 raise nRF52Exception(error_msg)
223
224 def SetSlaveMode(self):
225 """Set the kit to slave/peripheral mode.
226
227 Silently succeeds, because the nRF52 application is always a peripheral
228
229 Returns:
230 True if slave/peripheral mode was set successfully.
231
232 Raises:
233 A kit-specific exception if slave/peripheral mode is unsupported.
234 """
235 logging.debug('SetSlaveMode is a NOP on nRF52')
236 return True
237
238 def GetAuthenticationMode(self):
239 """Get the authentication mode.
240
241 This specifies how the device will authenticate with the DUT, for example,
242 a PIN code may be used.
243
244 Not supported on nRF52 application.
245
246 Returns:
247 None as the nRF52 does not support an Authentication mode.
248 """
249 logging.debug('GetAuthenticationMode is a NOP on nRF52')
250 return None
251
252 def SetAuthenticationMode(self, mode):
253 """Set the authentication mode to the specified mode.
254
255 If mode is PIN_CODE_MODE, implementations must ensure the default PIN
256 is set by calling _SetDefaultPinCode() as appropriate.
257
258 Not supported on nRF52 application.
259
260 Args:
261 mode: the desired authentication mode (specified in PeripheralKit)
262
263 Returns:
264 True if the mode was set successfully,
265
266 Raises:
267 A kit-specific exception if given mode is not supported.
268 """
269 error_msg = 'nRF52 does not support authentication mode'
270 logging.error(error_msg)
271 raise nRF52Exception(error_msg)
272
273 def GetPinCode(self):
274 """Get the pin code.
275
276 Returns:
277 A string representing the pin code,
278 None if there is no pin code stored.
279 """
280 warn_msg = 'nRF52 does not support PIN code mode, no PIN exists'
281 logging.warn(warn_msg)
282 return None
283
284 def SetPinCode(self, pin):
285 """Set the pin code.
286
287 Not support on nRF52 application.
288
289 Returns:
290 True if the pin code is set successfully,
291
292 Raises:
293 A kit-specifc exception if the pin code is invalid.
294 """
295 error_msg = 'nRF52 does not support PIN code mode'
296 logging.error(error_msg)
297 raise nRF52Exception(error_msg)
298
299 def GetServiceProfile(self):
300 """Get the service profile.
301
302 Unrelated to HID for the nRF52 application, so ignore for now
303
304 Returns:
305 The service profile currently in use (as per constant in PeripheralKit)
306 """
307 logging.debug('GetServiceProfile is a NOP on nRF52')
308 return "HID"
309
310 def SetServiceProfileSPP(self):
311 """Set SPP as the service profile.
312
313 Unrelated to HID for the nRF52 application, so ignore for now
314
315 Returns:
316 True if the service profile was set to SPP successfully.
317
318 Raises:
319 A kit-specifc exception if unsuppported.
320 """
321 error_msg = 'Failed to set SPP service profile'
322 logging.error(error_msg)
323 raise nRF52Exception(error_msg)
324
325 def SetServiceProfileHID(self):
326 """Set HID as the service profile.
327
328 nRF52 application only does HID at the moment. Silently succeeds
329
330 Returns:
331 True if the service profile was set to HID successfully.
332 """
333 logging.debug('SetServiceProfileHID is a NOP on nRF52')
334 return True
335
336 def GetLocalBluetoothAddress(self):
337 """Get the address advertised by the nRF52, which is the MAC address.
338
339 Address is returned as XX:XX:XX:XX:XX:XX
340
341 Returns:
342 The address of the nRF52 if successful or None if it fails
343 """
344 address = self.SerialSendReceive(self.CMD_GET_NRF52_MAC,
345 msg='getting local MAC address')
346 return address
347
348 def GetRemoteConnectedBluetoothAddress(self):
349 """Get the address of the device that is connected to the nRF52.
350
351 Address is returned as XX:XX:XX:XX:XX:XX
352 If not connected, nRF52 will return 00:00:00:00:00:00
353
354 Returns:
355 The address of the connected device or a null address if successful.
356 None if the serial receiving fails
357 """
358 address = self.SerialSendReceive(self.CMD_GET_REMOTE_CONNECTION_MAC,
359 msg='getting remote MAC address')
360 if len(address) == 17:
361 return address
362 else:
363 logging.error('remote connection address is invalid: %s', raw_address)
364 return None
365
366 def GetConnectionStatus(self):
367 """Get whether the nRF52 is connected to another device.
368
369 nRF52 returns a string 'INVALID' or 'CONNECTED'
370
371 Returns:
372 True if the nRF52 is connected to another device
373 """
374 result = self.SerialSendReceive(self.CMD_GET_CONNECTION_STATUS,
375 msg = 'getting connection status')
376 return result == 'CONNECTED'
377
378 def EnableConnectionStatusMessage(self):
379 """Enable the connection status message.
380
381 On some kits, this is required to use connection-related methods.
382
383 Not supported by the nRF52 application for now. This could be
384 changed so that Connection Status Messages are sent by nRF52.
385
386 Returns:
387 True if enabling the connection status message successfully.
388 """
389 logging.debug('EnableConnectionStatusMessage is a NOP on nRF52')
390 return True
391
392 def DisableConnectionStatusMessage(self):
393 """Disable the connection status message.
394
395 Not supported by the nRF52 application for now. This could be
396 changed so that Connection Status Messages are sent by nRF52.
397
398 Returns:
399 True if disabling the connection status message successfully.
400 """
401 logging.debug('DisableConnectionStatusMessage is a NOP on nRF52')
402 return True
403
404 def GetHIDDeviceType(self):
405 """Get the HID device type.
406
407 Returns:
408 A string representing the HID device type
409 """
410 return self.SerialSendReceive(self.CMD_GET_DEVICE_TYPE,
411 msg='getting HID device type')
412
413 def SetHIDType(self, device_type):
414 """Set HID type to the specified device type.
415
416 Args:
417 device_type: the HID type to emulate, from PeripheralKit
418 (MOUSE, KEYBOARD)
419
420 Returns:
421 True if successful
422
423 Raises:
424 A kit-specific exception if that device type is not supported.
425 """
426 if device_type == self.MOUSE:
427 result = self.SerialSendReceive(self.CMD_SET_MOUSE,
428 msg='setting mouse as HID type')
429 print(result)
430 elif device_type == self.KEYBOARD:
431 self.SerialSendReceive(self.CMD_SET_KEYBOARD,
432 msg='setting keyboard as HID type')
433 else:
434 msg = "Failed to set HID type, not supported: %s" % device_type
435 logging.error(msg)
436 raise nRF52Exception(msg)
437 return True
438
439 def GetClassOfService(self):
440 """Get the class of service, if supported.
441
442 Not supported on nRF52
443
444 Returns:
445 None, the only reasonable value for BLE-only devices
446 """
447 logging.debug('GetClassOfService is a NOP on nRF52')
448 return None
449
450 def SetClassOfService(self, class_of_service):
451 """Set the class of service, if supported.
452
453 The class of service is a number usually assigned by the Bluetooth SIG.
454 Usually supported only on BR/EDR kits.
455
456 Not supported on nRF52, but fake it
457
458 Args:
459 class_of_service: A decimal integer representing the class of service.
460
461 Returns:
462 True as this action is not supported.
463 """
464 logging.debug('SetClassOfService is a NOP on nRF52')
465 return True
466
467 def GetClassOfDevice(self):
468 """Get the class of device, if supported.
469
470 The kit uses a hexadeciaml string to represent the class of device.
471 It is converted to a decimal number as the return value.
472 The class of device is a number usually assigned by the Bluetooth SIG.
473 Usually supported only on BR/EDR kits.
474
475 Not supported on nRF52, so None
476
477 Returns:
478 None, the only reasonable value for BLE-only devices.
479 """
480 logging.debug('GetClassOfDevice is a NOP on nRF52')
481 return None
482
483 def SetClassOfDevice(self, device_type):
484 """Set the class of device, if supported.
485
486 The class of device is a number usually assigned by the Bluetooth SIG.
487 Usually supported only on BR/EDR kits.
488
489 Not supported on nRF52, but fake it.
490
491 Args:
492 device_type: A decimal integer representing the class of device.
493
494 Returns:
495 True as this action is not supported.
496 """
497 logging.debug('SetClassOfDevice is a NOP on nRF52')
498 return True
499
500 def SetRemoteAddress(self, remote_address):
501 """Set the remote Bluetooth address.
502
503 (Usually this will be the device under test that we want to connect with,
504 where the kit starts the connection.)
505
506 Not supported on nRF52 HID application.
507
508 Args:
509 remote_address: the remote Bluetooth MAC address, which must be given as
510 12 hex digits with colons between each pair.
511 For reference: '00:29:95:1A:D4:6F'
512
513 Returns:
514 True if the remote address was set successfully.
515
516 Raises:
517 PeripheralKitException if the given address was malformed.
518 """
519 error_msg = 'Failed to set remote address'
520 logging.error(error_msg)
521 raise nRF52Exception(error_msg)
522
523 def Connect(self):
524 """Connect to the stored remote bluetooth address.
525
526 In the case of a timeout (or a failure causing an exception), the caller
527 is responsible for retrying when appropriate.
528
529 Not supported on nRF52 HID application.
530
531 Returns:
532 True if connecting to the stored remote address succeeded, or
533 False if a timeout occurs.
534 """
535 error_msg = 'Failed to connect to remote device'
536 logging.error(error_msg)
537 raise nRF52Exception(error_msg)
538
539 def Disconnect(self):
540 """Disconnect from the remote device.
541
542 Specifically, this causes the peripheral emulation kit to disconnect from
543 the remote connected device, usually the DUT.
544
545 Returns:
546 True if disconnecting from the remote device succeeded.
547 """
548 self.SerialSendReceive(self.CMD_DISCONNECT,
549 msg='disconnect')
550 return True
551
552 def StartAdvertising(self):
553 """Command the nRF52 to begin advertising with its current settings.
554
555 Returns:
556 True if successful.
557 """
558 self.SerialSendReceive(self.CMD_START_ADVERTISING,
559 msg='start advertising')
560 return True
561
562 def MouseMove(self, delta_x, delta_y):
563 """Move the mouse (delta_x, delta_y) steps.
564
565 Buttons currently pressed will stay pressed during this operation.
566 This move is relative to the current position by the HID standard.
567 Valid step values must be in the range [-127,127].
568
569 Args:
570 delta_x: The number of steps to move horizontally.
571 Negative values move left, positive values move right.
572 delta_y: The number of steps to move vertically.
573 Negative values move up, positive values move down.
574
575 Returns:
576 True if successful.
577 """
578 command = self.CMD_MOUSE_MOVE + self.CMD_FS
579 command += str(delta_x) + self.CMD_FS + str(delta_y)
580 message = 'moving BLE mouse ' + str(delta_x) + " " + str(delta_y)
581 result = self.SerialSendReceive(command, msg=message)
582 return True
583
584 def MouseScroll(self, steps):
585 """Scroll the mouse wheel steps number of steps.
586
587 Buttons currently pressed will stay pressed during this operation.
588 Valid step values must be in the range [-127,127].
589
590 Args:
591 steps: The number of steps to scroll the wheel.
592 With traditional scrolling:
593 Negative values scroll down, positive values scroll up.
594 With reversed (formerly "Australian") scrolling this is reversed.
595
596 Returns:
597 True if successful.
598 """
599 command = self.CMD_MOUSE_SCROLL + self.CMD_FS
600 command += self.CMD_FS
601 command += str(steps) + self.CMD_FS
602 message = 'scrolling BLE mouse'
603 result = self.SerialSendReceive(command, msg=message)
604 return True
605
606 def MouseHorizontalScroll(self, steps):
607 """Horizontally scroll the mouse wheel steps number of steps.
608
609 Buttons currently pressed will stay pressed during this operation.
610 Valid step values must be in the range [-127,127].
611
612 There is no nRF52 limitation for implementation. If we can program
613 the correct HID event report to emulate horizontal scrolling, this
614 can be supported.
615 **** Not implemented ****
616 Args:
617 steps: The number of steps to scroll the wheel.
618 With traditional scrolling:
619 Negative values scroll left, positive values scroll right.
620 With reversed (formerly "Australian") scrolling this is reversed.
621
622 Returns:
623 True if successful.
624 """
625 return True
626
627 def _MouseButtonCodes(self):
628 """Gives the letter codes for whatever buttons are pressed.
629
630 Returns:
631 A int w/ bits representing pressed buttons.
632 """
633 currently_pressed = 0
634 for button in self._buttons_pressed:
635 if button == PeripheralKit.MOUSE_BUTTON_LEFT:
636 currently_pressed += self.MOUSE_BUTTON_LEFT_BIT
637 elif button == PeripheralKit.MOUSE_BUTTON_RIGHT:
638 currently_pressed += self.MOUSE_BUTTON_RIGHT_BIT
639 else:
640 error = "Unknown mouse button in state: %s" % button
641 logging.error(error)
642 raise nRF52Exception(error)
643 return currently_pressed
644
645 def MousePressButtons(self, buttons):
646 """Press the specified mouse buttons.
647
648 The kit will continue to press these buttons until otherwise instructed, or
649 until its state has been reset.
650
651 Args:
652 buttons: A set of buttons, as PeripheralKit MOUSE_BUTTON_* values, that
653 will be pressed (and held down).
654
655 Returns:
656 True if successful.
657 """
658 self._MouseButtonStateUnion(buttons)
659 button_codes = self._MouseButtonCodes()
660 command = self.CMD_MOUSE_BUTTON + self.CMD_FS
661 command += str(button_codes)
662 message = 'pressing BLE mouse buttons'
663 result = self.SerialSendReceive(command, msg=message)
664 return True
665
666 def MouseReleaseAllButtons(self):
667 """Release all mouse buttons.
668
669 Returns:
670 True if successful.
671 """
672 self._MouseButtonStateClear()
673 command = self.CMD_MOUSE_BUTTON + self.CMD_FS
674 command += '0'
675 message = 'releasing all BLE HOG mouse buttons'
676 result = self.SerialSendReceive(command, msg=message)
677 return True
678
679 def Reset(self):
680 result = self.SerialSendReceive(nRF52.CMD_REBOOT, msg='reset nRF52')
681 return True
682
683 def SetModeMouse(self):
684 self.EnterCommandMode()
685 result = self.SerialSendReceive(nRF52.CMD_SET_MOUSE, msg='set nRF52 mouse')
686 return True
687
688 def GetKitInfo(self, connect_separately=False, test_reset=False):
689 """A simple demo of getting kit information."""
690 if connect_separately:
691 print('create serial device: %s' % self.CreateSerialDevice())
692 if test_reset:
693 print('factory reset: %s' % self.FactoryReset())
694 self.EnterCommandMode()
695 print('advertised name: %s' % self.GetAdvertisedName())
696 print('firmware version: %s' % self.GetFirmwareVersion())
697 print('local bluetooth address: %s' % self.GetLocalBluetoothAddress())
698 print('connection status: %s' % self.GetConnectionStatus())
699 # The class of service/device is None for LE kits (it is BR/EDR-only)
700
701
702if __name__ == '__main__':
703 kit_instance = nRF52()
704 kit_instance.GetKitInfo()