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