blob: 0d0d51ac4e9b6c35da05ca0be5b941e829402f56 [file] [log] [blame]
Scottc98bfd72016-02-22 16:18:53 -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
5import re
6import logging
7import time
8
9from autotest_lib.client.common_lib import error
10from autotest_lib.server.cros.servo import pd_console
11
12
13class PDDevice(object):
14 """Base clase for all PD devices
15
16 This class provides a set of APIs for expected Type C PD required actions
17 in TypeC FAFT tests. The base class is specific for Type C devices that
18 do not have any console access.
19
20 """
21
22 def is_src(self):
23 """Checks if the port is connected as a source
24
25 """
26 raise NotImplementedError(
27 'is_src should be implemented in derived class')
28
29 def is_snk(self):
30 """Checks if the port is connected as a sink
31
32 @returns None
33 """
34 raise NotImplementedError(
35 'is_snk should be implemented in derived class')
36
37 def is_connected(self):
38 """Checks if the port is connected
39
40 @returns True if in a connected state, False otherwise
41 """
42 return self.is_src() or self.is_snk()
43
44 def is_disconnected(self):
45 """Checks if the port is disconnected
46
47 """
48 raise NotImplementedError(
49 'is_disconnected should be implemented in derived class')
50
51 def is_ufp(self):
52 """Checks if data role is UFP
53
54 """
55 raise NotImplementedError(
56 'is_ufp should be implemented in derived class')
57
58 def is_dfp(self):
59 """Checks if data role is DFP
60
61 """
62 raise NotImplementedError(
63 'is_dfp should be implemented in derived class')
64
65 def is_drp(self):
66 """Checks if dual role mode is supported
67
68 """
69 raise NotImplementedError(
70 'is_drp should be implemented in derived class')
71
72 def dr_swap(self):
73 """Attempts a data role swap
74
75 """
76 raise NotImplementedError(
77 'dr_swap should be implemented in derived class')
78
79 def pr_swap(self):
80 """Attempts a power role swap
81
82 """
83 raise NotImplementedError(
84 'pr_swap should be implemented in derived class')
85
86 def vbus_request(self, voltage):
87 """Requests a specific VBUS voltage from SRC
88
89 @param voltage: requested voltage level (5, 12, 20) in volts
90 """
91 raise NotImplementedError(
92 'vbus_request should be implemented in derived class')
93
Scottf58d0b12016-04-22 10:23:26 -070094 def soft_reset(self):
Scottc98bfd72016-02-22 16:18:53 -080095 """Initates a PD soft reset sequence
96
97 """
98 raise NotImplementedError(
Scott4e194872016-02-08 16:26:17 -080099 'soft_reset should be implemented in derived class')
Scottc98bfd72016-02-22 16:18:53 -0800100
101 def hard_reset(self):
102 """Initates a PD hard reset sequence
103
104 """
105 raise NotImplementedError(
Scott4e194872016-02-08 16:26:17 -0800106 'hard_reset should be implemented in derived class')
Scottc98bfd72016-02-22 16:18:53 -0800107
108 def drp_set(self, mode):
109 """Sets dualrole mode
110
111 @param mode: desired dual role setting (on, off, snk, src)
112 """
113 raise NotImplementedError(
114 'drp_set should be implemented in derived class')
115
116 def drp_disconnect_connect(self, disc_time_sec):
117 """Force PD disconnect/connect via drp settings
118
119 @param disc_time_sec: Time in seconds between disconnect and reconnect
120 """
121 raise NotImplementedError(
122 'drp_disconnect_reconnect should be implemented in \
123 derived class')
124
125 def cc_disconnect_connect(self, disc_time_sec):
126 """Force PD disconnect/connect using Plankton fw command
127
128 @param disc_time_sec: Time in seconds between disconnect and reconnect
129 """
130 raise NotImplementedError(
131 'cc_disconnect_reconnect should be implemented in \
132 derived class')
133
134
135class PDConsoleDevice(PDDevice):
136 """Class for PD devices that have console access
137
138 This class contains methods for common PD actions for any PD device which
139 has UART console access. It inherits the PD device base class. In addition,
140 it stores both the UART console and port for the PD device.
141 """
142
143 def __init__(self, console, port):
144 """Initialization method
145
146 @param console: UART console object
147 @param port: USB PD port number
148 """
149 # Save UART console
150 self.console = console
151 # Instantiate PD utilities used by methods in this class
152 self.utils = pd_console.PDConsoleUtils(console)
153 # Save the PD port number for this device
154 self.port = port
155 # Not a Plankton device
156 self.is_plankton = False
157
158 def is_src(self):
159 """Checks if the port is connected as a source
160
161 @returns True if connected as SRC, False otherwise
162 """
163 state = self.utils.get_pd_state(self.port)
164 return bool(state == self.utils.SRC_CONNECT)
165
166 def is_snk(self):
167 """Checks if the port is connected as a sink
168
169 @returns True if connected as SNK, False otherwise
170 """
171 state = self.utils.get_pd_state(self.port)
172 return bool(state == self.utils.SNK_CONNECT)
173
174 def is_connected(self):
175 """Checks if the port is connected
176
177 @returns True if in a connected state, False otherwise
178 """
179 state = self.utils.get_pd_state(self.port)
180 return bool(state == self.utils.SNK_CONNECT or
181 state == self.utils.SRC_CONNECT)
182
183 def is_disconnected(self):
184 """Checks if the port is disconnected
185
186 @returns True if in a disconnected state, False otherwise
187 """
188 state = self.utils.get_pd_state(self.port)
189 return bool(state == self.utils.SRC_DISC or
190 state == self.utils.SNK_DISC)
191
192 def is_drp(self):
193 """Checks if dual role mode is supported
194
195 @returns True if dual role mode is 'on', False otherwise
196 """
197 return self.utils.is_pd_dual_role_enabled()
198
199 def drp_disconnect_connect(self, disc_time_sec):
200 """Disconnect/reconnect using drp mode settings
201
202 A PD console device doesn't have an explicit connect/disconnect
203 command. Instead, the dualrole mode setting is used to force
204 disconnects in devices which support this feature. To disconnect,
205 force the dualrole mode to be the opposite role of the current
206 connected state.
207
208 @param disc_time_sec: time in seconds to wait to reconnect
209
210 @returns True if device disconnects, then returns to a connected
211 state. False if either step fails.
212 """
213 # Dualrole mode must be supported
214 if self.is_drp() is False:
215 logging.warn('Device not DRP capable, unabled to force disconnect')
216 return False
217 # Force state will be the opposite of current connect state
218 if self.is_src():
219 drp_mode = 'snk'
220 else:
221 drp_mode = 'src'
222 # Force disconnect
223 self.drp_set(drp_mode)
224 # Wait for disconnect time
225 time.sleep(disc_time_sec)
226 # Verify that the device is disconnected
227 disconnect = self.is_disconnected()
228 # Restore default dualrole mode
229 self.drp_set('on')
230 # Allow enough time for protocol state machine
231 time.sleep(self.utils.CONNECT_TIME)
232 # Check if connected
233 connect = self.is_connected()
234 logging.info('Disconnect = %r, Connect = %r', disconnect, connect)
235 return bool(disconnect and connect)
236
237 def drp_set(self, mode):
238 """Sets dualrole mode
239
240 @param mode: desired dual role setting (on, off, snk, src)
241
242 @returns True is set was successful, False otherwise
243 """
244 # Set desired dualrole mode
245 self.utils.set_pd_dualrole(mode)
246 # Get the expected output
247 resp = self.utils.dualrole_resp[self.utils.dual_index[mode]]
248 # Get current setting
249 current = self.utils.get_pd_dualrole()
250 # Verify that setting is correct
251 return bool(resp == current)
252
253 def try_src(self, enable):
254 """Enables/Disables Try.SRC PD protocol setting
255
256 @param enable: True to enable, False to disable
257
258 @returns True is setting was successful, False if feature not
259 supported by the device, or not set as desired.
260 """
261 # Create Try.SRC pd command
262 cmd = 'pd trysrc %d' % int(enable)
263 # Try.SRC on/off is output, if supported feature
264 regex = ['Try\.SRC\s([\w]+)|(Parameter)']
265 m = self.utils.send_pd_command_get_output(cmd, regex)
266 # Determine if Try.SRC feature is supported
267 trysrc = re.search('Try\.SRC\s([\w]+)', m[0][0])
268 if not trysrc:
269 logging.warn('Try.SRC not supported on this PD device')
270 return False
271 # TrySRC is supported on this PD device, verify setting.
272 logging.info('Try.SRC mode = %s', trysrc.group(1))
273 if enable:
274 val = 'on'
275 else:
276 val = 'off'
277 return bool(val == m[0][1])
278
Scottf58d0b12016-04-22 10:23:26 -0700279 def soft_reset(self):
Scottcae6fe42016-02-08 16:26:17 -0800280 """Initates a PD soft reset sequence
281
282 To verify that a soft reset sequence was initiated, the
283 reply message is checked to verify that the reset command
284 was acknowledged by its port pair. The connect state should
285 be same as it was prior to issuing the reset command.
286
Scottcae6fe42016-02-08 16:26:17 -0800287 @returns True if the port pair acknowledges the the reset message
288 and if following the command, the device returns to the same
289 connected state. False otherwise.
290 """
291 RESET_DELAY = 0.5
292 cmd = 'pd %d soft' % self.port
293 state_before = self.utils.get_pd_state(self.port)
294 reply = self.utils.send_pd_command_get_reply_msg(cmd)
295 if reply != self.utils.PD_CONTROL_MSG_DICT['Accept']:
296 return False
297 time.sleep(RESET_DELAY)
298 state_after = self.utils.get_pd_state(self.port)
299 return state_before == state_after
300
Scott4e194872016-02-08 16:26:17 -0800301 def hard_reset(self):
302 """Initates a PD hard reset sequence
303
304 To verify that a hard reset sequence was initiated, the
305 console ouput is scanned for HARD RST TX. In addition, the connect
306 state should be same as it was prior to issuing the reset command.
307
308 @returns True if the port pair acknowledges that hard reset was
309 initiated and if following the command, the device returns to the same
310 connected state. False otherwise.
311 """
312 RESET_DELAY = 1.0
313 cmd = 'pd %d hard' % self.port
314 state_before = self.utils.get_pd_state(self.port)
315 self.utils.enable_pd_console_debug()
316 try:
317 self.utils.send_pd_command_get_output(cmd, ['.*(HARD\sRST\sTX)'])
318 except error.TestFail:
319 logging.warn('HARD RST TX not found')
320 return False
321 finally:
322 self.utils.disable_pd_console_debug()
323
324 time.sleep(RESET_DELAY)
325 state_after = self.utils.get_pd_state(self.port)
326 return state_before == state_after
327
Scottcae6fe42016-02-08 16:26:17 -0800328 def pr_swap(self):
329 """Attempts a power role swap
330
331 In order to attempt a power role swap the device must be
332 connected and support dualrole mode. Once these two criteria
333 are checked a power role command is issued. Following a delay
334 to allow for a reconnection the new power role is checked
335 against the power role prior to issuing the command.
336
337 @returns True if the device has swapped power roles, False otherwise.
338 """
339 # Get starting state
340 if not self.is_drp() and not self.drp_set('on'):
341 logging.warn('Dualrole Mode not enabled!')
342 return False
343 if self.is_connected() == False:
344 logging.warn('PD contract not established!')
345 return False
346 current_pr = self.utils.get_pd_state(self.port)
347 swap_cmd = 'pd %d swap power' % self.port
348 self.utils.send_pd_command(swap_cmd)
349 time.sleep(self.utils.CONNECT_TIME)
350 new_pr = self.utils.get_pd_state(self.port)
351 logging.info('Power swap: %s -> %s', current_pr, new_pr)
352 if self.is_connected() == False:
353 logging.warn('Device not connected following PR swap attempt.')
354 return False
355 return current_pr != new_pr
356
Scottc98bfd72016-02-22 16:18:53 -0800357
358class PDPlanktonDevice(PDConsoleDevice):
359 """Class for PD Plankton devices
360
361 This class contains methods for PD funtions which are unique to the
362 Plankton Type C factory testing board. It inherits all the methods
363 for PD console devices.
364 """
365
366 def __init__(self, console, port):
367 """Initialization method
368
369 @param console: UART console for this device
370 @param port: USB PD port number
371 """
372 # Instantiate the PD console object
373 super(PDPlanktonDevice, self).__init__(console, 0)
374 # Indicate this is Plankton device
375 self.is_plankton = True
376
377 def _toggle_plankton_drp(self):
378 """Issue 'usbc_action drp' Plankton command
379
380 @returns value of drp_enable in Plankton FW
381 """
382 drp_cmd = 'usbc_action drp'
383 drp_re = ['DRP\s=\s(\d)']
384 # Send DRP toggle command to Plankton and get value of 'drp_enable'
385 m = self.utils.send_pd_command_get_output(drp_cmd, drp_re)
386 return int(m[0][1])
387
388 def _enable_plankton_drp(self):
389 """Enable DRP mode on Plankton
390
391 DRP mode can only be toggled and is not able to be explicitly
392 enabled/disabled via the console. Therefore, this method will
393 toggle DRP mode until the console reply indicates that this
394 mode is enabled. The toggle happens a maximum of two times
395 in case this is called when it's already enabled.
396
397 @returns True when DRP mode is enabled, False if not successful
398 """
399 for attempt in xrange(2):
400 if self._toggle_plankton_drp() == True:
401 logging.info('Plankton DRP mode enabled')
402 return True
403 logging.error('Plankton DRP mode set failure')
404 return False
405
Scottcae6fe42016-02-08 16:26:17 -0800406 def _verify_state_sequence(self, states_list, console_log):
407 """Compare PD state transitions to expected values
408
409 @param states_list: list of expected PD state transitions
410 @param console_log: console output which contains state names
Scott4e194872016-02-08 16:26:17 -0800411
Scottcae6fe42016-02-08 16:26:17 -0800412 @returns True if the sequence matches, False otherwise
413 """
414 # For each state in the expected state transiton table, build
415 # the regexp and search for it in the state transition log.
416 for state in states_list:
417 state_regx = r'C{0}\s+[\w]+:\s({1})'.format(self.port,
418 state)
419 if re.search(state_regx, console_log) is None:
420 return False
421 return True
422
Scottc98bfd72016-02-22 16:18:53 -0800423 def cc_disconnect_connect(self, disc_time_sec):
424 """Disconnect/reconnect using Plankton
425
426 Plankton supports a feature which simulates a USB Type C disconnect
427 and reconnect.
428
429 @param disc_time_sec: Time in seconds for disconnect period.
430 """
431 DISC_DELAY = 100
432 disc_cmd = 'fake_disconnect %d %d' % (DISC_DELAY,
433 disc_time_sec * 1000)
434 self.utils.send_pd_command(disc_cmd)
435
436 def drp_set(self, mode):
437 """Sets dualrole mode
438
439 @param mode: desired dual role setting (on, off, snk, src)
440
441 @returns True if dualrole mode matches the requested value or
442 is successfully set to that value. False, otherwise.
443 """
444 # Get correct dualrole console response
445 resp = self.utils.dualrole_resp[self.utils.dual_index[mode]]
446 # Get current value of dualrole
447 drp = self.utils.get_pd_dualrole()
448 if drp == resp:
449 return True
450
451 if mode == 'on':
452 # Setting dpr_enable on Plankton will set dualrole mode to on
453 return self._enable_plankton_drp()
454 else:
455 # If desired setting is other than 'on', need to ensure that
456 # drp mode on Plankton is disabled.
457 if resp == 'on':
458 # This will turn off drp_enable flag and set dualmode to 'off'
459 return self._toggle_plankton_drp()
460 # With drp_enable flag off, can set to desired setting
461 return self.utils.set_pd_dualrole(mode)
462
Scott4e194872016-02-08 16:26:17 -0800463 def _reset(self, cmd, states_list):
464 """Initates a PD reset sequence
Scottcae6fe42016-02-08 16:26:17 -0800465
466 Plankton device has state names available on the console. When
467 a soft reset is issued the console log is extracted and then
468 compared against the expected state transisitons.
469
Scott4e194872016-02-08 16:26:17 -0800470 @param cmd: reset type (soft or hard)
471 @param states_list: list of expected PD state transitions
472
473 @returns True if state transitions match, False otherwise
474 """
475 # Want to grab all output until either SRC_READY or SNK_READY
476 reply_exp = ['(.*)(C\d)\s+[\w]+:\s([\w]+_READY)']
477 m = self.utils.send_pd_command_get_output(cmd, reply_exp)
478 return self._verify_state_sequence(states_list, m[0][0])
479
Scottf58d0b12016-04-22 10:23:26 -0700480 def soft_reset(self):
Scott4e194872016-02-08 16:26:17 -0800481 """Initates a PD soft reset sequence
482
Scottcae6fe42016-02-08 16:26:17 -0800483 @returns True if state transitions match, False otherwise
484 """
Scottf58d0b12016-04-22 10:23:26 -0700485 snk_reset_states = [
486 'SOFT_RESET',
487 'SNK_DISCOVERY',
488 'SNK_REQUESTED',
489 'SNK_TRANSITION',
490 'SNK_READY'
491 ]
492
493 src_reset_states = [
494 'SOFT_RESET',
495 'SRC_DISCOVERY',
496 'SRC_NEGOCIATE',
497 'SRC_ACCEPTED',
498 'SRC_POWERED',
499 'SRC_TRANSITION',
500 'SRC_READY'
501 ]
502
503 if self.is_src():
504 states_list = src_reset_states
505 elif self.is_snk():
506 states_list = snk_reset_states
507 else:
508 raise error.TestFail('Port Pair not in a connected state')
509
Scottcae6fe42016-02-08 16:26:17 -0800510 cmd = 'pd %d soft' % self.port
Scott4e194872016-02-08 16:26:17 -0800511 return self._reset(cmd, states_list)
512
513 def hard_reset(self):
514 """Initates a PD hard reset sequence
515
516 @returns True if state transitions match, False otherwise
517 """
518 snk_reset_states = [
519 'HARD_RESET_SEND',
520 'HARD_RESET_EXECUTE',
521 'SNK_HARD_RESET_RECOVER',
522 'SNK_DISCOVERY',
523 'SNK_REQUESTED',
524 'SNK_TRANSITION',
525 'SNK_READY'
526 ]
527
528 src_reset_states = [
529 'HARD_RESET_SEND',
530 'HARD_RESET_EXECUTE',
531 'SRC_HARD_RESET_RECOVER',
532 'SRC_DISCOVERY',
533 'SRC_NEGOCIATE',
534 'SRC_ACCEPTED',
535 'SRC_POWERED',
536 'SRC_TRANSITION',
537 'SRC_READY'
538 ]
539
540 if self.is_src():
541 states_list = src_reset_states
542 elif self.is_snk():
543 states_list = snk_reset_states
544 else:
545 raise error.TestFail('Port Pair not in a connected state')
546
547 cmd = 'pd %d hard' % self.port
548 return self._reset(cmd, states_list)
Scottcae6fe42016-02-08 16:26:17 -0800549
Scottc98bfd72016-02-22 16:18:53 -0800550
551class PDPortPartner(object):
552 """Methods used to instantiate PD device objects
553
554 This class is initalized with a list of servo consoles. It
555 contains methods to determine if USB PD devices are accessible
556 via the consoles and attempts to determine USB PD port partners.
557 A PD device is USB PD port specific, a single console may access
558 multiple PD devices.
559
560 """
561
562 def __init__(self, consoles):
563 """Initialization method
564
565 @param consoles: list of servo consoles
566 """
567 self.consoles = consoles
568
569 def _send_pd_state(self, port, console):
570 """Tests if PD device exists on a given port number
571
572 @param port: USB PD port number to try
573 @param console: servo UART console
574
575 @returns True if 'pd <port> state' command gives a valid
576 response, False otherwise
577 """
578 cmd = 'pd %d state' % port
579 regex = r'(Port C\d)|(Parameter)'
580 m = console.send_command_get_output(cmd, [regex])
581 # If PD port exists, then output will be Port C0 or C1
582 regex = r'Port C{0}'.format(port)
583 if re.search(regex, m[0][0]):
584 return True
585 return False
586
587 def _find_num_pd_ports(self, console):
588 """Determine number of PD ports for a given console
589
590 @param console: uart console accssed via servo
591
592 @returns: number of PD ports accessible via console
593 """
594 MAX_PORTS = 2
595 num_ports = 0
596 for port in xrange(MAX_PORTS):
597 if self._send_pd_state(port, console):
598 num_ports += 1
599 return num_ports
600
601 def _is_pd_console(self, console):
602 """Check if pd option exists in console
603
604 @param console: uart console accssed via servo
605
606 @returns: True if 'pd' is found, False otherwise
607 """
608 try:
609 m = console.send_command_get_output('help', [r'(pd)\s+'])
610 return True
611 except error.TestFail:
612 return False
613
614 def _is_plankton_console(self, console):
615 """Check for Plankton console
616
617 This method looks for a console command option 'usbc_action' which
618 is unique to Plankton PD devices.
619
620 @param console: uart console accssed via servo
621
622 @returns True if usbc_action command is present, False otherwise
623 """
624 try:
625 m = console.send_command_get_output('help', [r'(usbc_action)'])
626 return True
627 except error.TestFail:
628 return False
629
630 def _check_port_pair(self, dev_pair):
631 """Check if two PD devices could be connected
632
633 If two USB PD devices are connected, then they should be in
634 either the SRC_READY or SNK_READY states and have opposite
635 power roles. In addition, they must be on different servo
636 consoles.
637
638 @param: list of two possible PD port parters
639
640 @returns True if not the same console and both PD devices
641 are a plausible pair based only on their PD states.
642 """
643 # Don't test if on the same servo console
644 if dev_pair[0].console == dev_pair[1].console:
645 logging.info('PD Devices are on same platform -> cant be a pair')
646 return False
647 # Must be SRC <--> SNK or SNK <--> SRC
648 return bool((dev_pair[0].is_src() and dev_pair[1].is_snk()) or
649 (dev_pair[0].is_snk() and dev_pair[1].is_src()))
650
651 def _verify_plankton_connection(self, dev_pair):
652 """Verify DUT to Plankton PD connection
653
654 This method checks for a Plankton PD connection for the
655 given port by first verifying if a PD connection is present.
656 If found, then it uses a Plankton feature to force a PD disconnect.
657 If the port is no longer in the connected state, and following
658 a delay, is found to be back in the connected state, then
659 a DUT pd to Plankton connection is verified.
660
661 @param dev_pair: list of two PD devices
662
663 @returns True if DUT to Plankton pd connection is verified
664 """
665 DISC_CHECK_TIME = .5
666 DISC_WAIT_TIME = 2
667 CONNECT_TIME = 4
668
669 if not self._check_port_pair(dev_pair):
670 return False
671
672 for index in xrange(len(dev_pair)):
673 try:
674 # Force PD disconnect
675 dev_pair[index].cc_disconnect_connect(DISC_WAIT_TIME)
676 time.sleep(DISC_CHECK_TIME)
677 # Verify that both devices are now disconnected
678 if (dev_pair[0].is_disconnected() and
679 dev_pair[1].is_disconnected()):
680 # Allow enough time for reconnection
681 time.sleep(DISC_WAIT_TIME + CONNECT_TIME)
682 if self._check_port_pair(dev_pair):
683 # Have verifed a pd disconnect/reconnect sequence
684 logging.info('Plankton <-> DUT pair found')
685 return True
686 else:
687 # Delay to allow
688 time.sleep(DISC_WAIT_TIME + CONNECT_TIME)
689 except NotImplementedError:
690 logging.info('dev %d is not Plankton', index)
691 return False
692
693 def identify_pd_devices(self):
694 """Instantiate PD devices present in test setup
695
696 @returns list of 2 PD devices if a DUT <-> Plankton found. If
697 not found, then returns an empty list.
698 """
699 devices = []
700 # For each possible uart console, check to see if a PD console
701 # is present and determine the number of PD ports.
702 for console in self.consoles:
703 if self._is_pd_console(console):
704 is_plank = self._is_plankton_console(console)
705 num_ports = self._find_num_pd_ports(console)
706 # For each PD port that can be accessed via the console,
707 # instantiate either PDConsole or PDPlankton device.
708 for port in xrange(num_ports):
709 if is_plank:
710 logging.info('Plankton PD Device on port %d', port)
711 devices.append(PDPlanktonDevice(console, port))
712 else:
713 devices.append(PDConsoleDevice(console, port))
714 logging.info('Console PD Device on port %d', port)
715
716 # Determine PD port partners in the list of PD devices. Note, that
717 # there can be PD devices which are not accessible via a uart console,
718 # but are connected to a PD port which is accessible.
719 test_pair = []
720 for deva in devices:
721 for dev_idx in range(devices.index(deva) + 1, len(devices)):
722 devb = devices[dev_idx]
723 pair = [deva, devb]
724 if self._verify_plankton_connection(pair):
725 test_pair = pair
726 devices.remove(deva)
727 devices.remove(devb)
728 return test_pair
729