1# 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
94    def soft_reset(self):
95        """Initates a PD soft reset sequence
96
97        """
98        raise NotImplementedError(
99                'soft_reset should be implemented in derived class')
100
101    def hard_reset(self):
102        """Initates a PD hard reset sequence
103
104        """
105        raise NotImplementedError(
106                'hard_reset should be implemented in derived class')
107
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            swap_state = self.utils.SNK_CONNECT
221        else:
222            drp_mode = 'src'
223            swap_state = self.utils.SRC_CONNECT
224        # Force disconnect
225        self.drp_set(drp_mode)
226        # Wait for disconnect time
227        time.sleep(disc_time_sec)
228        # Verify that the device is disconnected
229        disconnect = self.is_disconnected()
230
231        # If the other device is dualrole, then forcing dualrole mode will
232        # only cause the disconnect to appear momentarily and reconnect
233        # in the power role forced by the drp_set() call. For this case,
234        # the role swap verifies that a disconnect/connect sequence occurred.
235        if disconnect == False:
236            time.sleep(self.utils.CONNECT_TIME)
237            # Connected, verify if power role swap has ocurred
238            if swap_state == self.utils.get_pd_state(self.port):
239                # Restore default dualrole mode
240                self.drp_set('on')
241                # Restore orignal power role
242                connect = self.pr_swap()
243                if connect == False:
244                    logging.warn('DRP on both devices, 2nd power swap failed')
245                return connect
246
247        # Restore default dualrole mode
248        self.drp_set('on')
249        # Allow enough time for protocol state machine
250        time.sleep(self.utils.CONNECT_TIME)
251        # Check if connected
252        connect = self.is_connected()
253        logging.info('Disconnect = %r, Connect = %r', disconnect, connect)
254        return bool(disconnect and connect)
255
256    def drp_set(self, mode):
257        """Sets dualrole mode
258
259        @param mode: desired dual role setting (on, off, snk, src)
260
261        @returns True is set was successful, False otherwise
262        """
263        # Set desired dualrole mode
264        self.utils.set_pd_dualrole(mode)
265        # Get the expected output
266        resp = self.utils.dualrole_resp[self.utils.dual_index[mode]]
267        # Get current setting
268        current = self.utils.get_pd_dualrole()
269        # Verify that setting is correct
270        return bool(resp == current)
271
272    def try_src(self, enable):
273        """Enables/Disables Try.SRC PD protocol setting
274
275        @param enable: True to enable, False to disable
276
277        @returns True is setting was successful, False if feature not
278        supported by the device, or not set as desired.
279        """
280        # Create Try.SRC pd command
281        cmd = 'pd trysrc %d' % int(enable)
282        # Try.SRC on/off is output, if supported feature
283        regex = ['Try\.SRC\s([\w]+)|(Parameter)']
284        m = self.utils.send_pd_command_get_output(cmd, regex)
285        # Determine if Try.SRC feature is supported
286        trysrc = re.search('Try\.SRC\s([\w]+)', m[0][0])
287        if not trysrc:
288            logging.warn('Try.SRC not supported on this PD device')
289            return False
290        # TrySRC is supported on this PD device, verify setting.
291        logging.info('Try.SRC mode = %s', trysrc.group(1))
292        if enable:
293            val = 'on'
294        else:
295            val = 'off'
296        return bool(val == m[0][1])
297
298    def soft_reset(self):
299        """Initates a PD soft reset sequence
300
301        To verify that a soft reset sequence was initiated, the
302        reply message is checked to verify that the reset command
303        was acknowledged by its port pair. The connect state should
304        be same as it was prior to issuing the reset command.
305
306        @returns True if the port pair acknowledges the the reset message
307        and if following the command, the device returns to the same
308        connected state. False otherwise.
309        """
310        RESET_DELAY = 0.5
311        cmd = 'pd %d soft' % self.port
312        state_before = self.utils.get_pd_state(self.port)
313        reply = self.utils.send_pd_command_get_reply_msg(cmd)
314        if reply != self.utils.PD_CONTROL_MSG_DICT['Accept']:
315            return False
316        time.sleep(RESET_DELAY)
317        state_after = self.utils.get_pd_state(self.port)
318        return state_before == state_after
319
320    def hard_reset(self):
321        """Initates a PD hard reset sequence
322
323        To verify that a hard reset sequence was initiated, the
324        console ouput is scanned for HARD RST TX. In addition, the connect
325        state should be same as it was prior to issuing the reset command.
326
327        @returns True if the port pair acknowledges that hard reset was
328        initiated and if following the command, the device returns to the same
329        connected state. False otherwise.
330        """
331        RESET_DELAY = 1.0
332        cmd = 'pd %d hard' % self.port
333        state_before = self.utils.get_pd_state(self.port)
334        self.utils.enable_pd_console_debug()
335        try:
336            self.utils.send_pd_command_get_output(cmd, ['.*(HARD\sRST\sTX)'])
337        except error.TestFail:
338            logging.warn('HARD RST TX not found')
339            return False
340        finally:
341            self.utils.disable_pd_console_debug()
342
343        time.sleep(RESET_DELAY)
344        state_after = self.utils.get_pd_state(self.port)
345        return state_before == state_after
346
347    def pr_swap(self):
348        """Attempts a power role swap
349
350        In order to attempt a power role swap the device must be
351        connected and support dualrole mode. Once these two criteria
352        are checked a power role command is issued. Following a delay
353        to allow for a reconnection the new power role is checked
354        against the power role prior to issuing the command.
355
356        @returns True if the device has swapped power roles, False otherwise.
357        """
358        # Get starting state
359        if not self.is_drp() and not self.drp_set('on'):
360            logging.warn('Dualrole Mode not enabled!')
361            return False
362        if self.is_connected() == False:
363            logging.warn('PD contract not established!')
364            return False
365        current_pr = self.utils.get_pd_state(self.port)
366        swap_cmd = 'pd %d swap power' % self.port
367        self.utils.send_pd_command(swap_cmd)
368        time.sleep(self.utils.CONNECT_TIME)
369        new_pr = self.utils.get_pd_state(self.port)
370        logging.info('Power swap: %s -> %s', current_pr, new_pr)
371        if self.is_connected() == False:
372            logging.warn('Device not connected following PR swap attempt.')
373            return False
374        return current_pr != new_pr
375
376
377class PDPlanktonDevice(PDConsoleDevice):
378    """Class for PD Plankton devices
379
380    This class contains methods for PD funtions which are unique to the
381    Plankton Type C factory testing board. It inherits all the methods
382    for PD console devices.
383    """
384
385    def __init__(self, console, port):
386        """Initialization method
387
388        @param console: UART console for this device
389        @param port: USB PD port number
390        """
391        # Instantiate the PD console object
392        super(PDPlanktonDevice, self).__init__(console, 0)
393        # Indicate this is Plankton device
394        self.is_plankton = True
395
396    def _toggle_plankton_drp(self):
397        """Issue 'usbc_action drp' Plankton command
398
399        @returns value of drp_enable in Plankton FW
400        """
401        drp_cmd = 'usbc_action drp'
402        drp_re = ['DRP\s=\s(\d)']
403        # Send DRP toggle command to Plankton and get value of 'drp_enable'
404        m = self.utils.send_pd_command_get_output(drp_cmd, drp_re)
405        return int(m[0][1])
406
407    def _enable_plankton_drp(self):
408        """Enable DRP mode on Plankton
409
410        DRP mode can only be toggled and is not able to be explicitly
411        enabled/disabled via the console. Therefore, this method will
412        toggle DRP mode until the console reply indicates that this
413        mode is enabled. The toggle happens a maximum of two times
414        in case this is called when it's already enabled.
415
416        @returns True when DRP mode is enabled, False if not successful
417        """
418        for attempt in xrange(2):
419            if self._toggle_plankton_drp() == True:
420                logging.info('Plankton DRP mode enabled')
421                return True
422        logging.error('Plankton DRP mode set failure')
423        return False
424
425    def _verify_state_sequence(self, states_list, console_log):
426        """Compare PD state transitions to expected values
427
428        @param states_list: list of expected PD state transitions
429        @param console_log: console output which contains state names
430
431        @returns True if the sequence matches, False otherwise
432        """
433        # For each state in the expected state transiton table, build
434        # the regexp and search for it in the state transition log.
435        for state in states_list:
436            state_regx = r'C{0}\s+[\w]+:\s({1})'.format(self.port,
437                                                        state)
438            if re.search(state_regx, console_log) is None:
439                return False
440        return True
441
442    def cc_disconnect_connect(self, disc_time_sec):
443        """Disconnect/reconnect using Plankton
444
445        Plankton supports a feature which simulates a USB Type C disconnect
446        and reconnect.
447
448        @param disc_time_sec: Time in seconds for disconnect period.
449        """
450        DISC_DELAY = 100
451        disc_cmd = 'fake_disconnect %d  %d' % (DISC_DELAY,
452                                               disc_time_sec * 1000)
453        self.utils.send_pd_command(disc_cmd)
454
455    def drp_disconnect_connect(self, disc_time_sec):
456        """Disconnect/reconnect using Plankton
457
458        Utilize Plankton disconnect/connect utility and verify
459        that both disconnect and reconnect actions were successful.
460
461        @param disc_time_sec: Time in seconds for disconnect period.
462
463        @returns True if device disconnects, then returns to a connected
464        state. False if either step fails.
465        """
466        self.cc_disconnect_connect(disc_time_sec)
467        time.sleep(disc_time_sec / 2)
468        disconnect = self.is_disconnected()
469        time.sleep(disc_time_sec / 2 + self.utils.CONNECT_TIME)
470        connect = self.is_connected()
471        return disconnect and connect
472
473    def drp_set(self, mode):
474        """Sets dualrole mode
475
476        @param mode: desired dual role setting (on, off, snk, src)
477
478        @returns True if dualrole mode matches the requested value or
479        is successfully set to that value. False, otherwise.
480        """
481        # Get correct dualrole console response
482        resp = self.utils.dualrole_resp[self.utils.dual_index[mode]]
483        # Get current value of dualrole
484        drp = self.utils.get_pd_dualrole()
485        if drp == resp:
486            return True
487
488        if mode == 'on':
489            # Setting dpr_enable on Plankton will set dualrole mode to on
490            return self._enable_plankton_drp()
491        else:
492            # If desired setting is other than 'on', need to ensure that
493            # drp mode on Plankton is disabled.
494            if resp == 'on':
495                # This will turn off drp_enable flag and set dualmode to 'off'
496                return self._toggle_plankton_drp()
497            # With drp_enable flag off, can set to desired setting
498            return self.utils.set_pd_dualrole(mode)
499
500    def _reset(self, cmd, states_list):
501        """Initates a PD reset sequence
502
503        Plankton device has state names available on the console. When
504        a soft reset is issued the console log is extracted and then
505        compared against the expected state transisitons.
506
507        @param cmd: reset type (soft or hard)
508        @param states_list: list of expected PD state transitions
509
510        @returns True if state transitions match, False otherwise
511        """
512        # Want to grab all output until either SRC_READY or SNK_READY
513        reply_exp = ['(.*)(C\d)\s+[\w]+:\s([\w]+_READY)']
514        m = self.utils.send_pd_command_get_output(cmd, reply_exp)
515        return self._verify_state_sequence(states_list, m[0][0])
516
517    def soft_reset(self):
518        """Initates a PD soft reset sequence
519
520        @returns True if state transitions match, False otherwise
521        """
522        snk_reset_states = [
523            'SOFT_RESET',
524            'SNK_DISCOVERY',
525            'SNK_REQUESTED',
526            'SNK_TRANSITION',
527            'SNK_READY'
528        ]
529
530        src_reset_states = [
531            'SOFT_RESET',
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 soft' % self.port
548        return self._reset(cmd, states_list)
549
550    def hard_reset(self):
551        """Initates a PD hard reset sequence
552
553        @returns True if state transitions match, False otherwise
554        """
555        snk_reset_states = [
556            'HARD_RESET_SEND',
557            'HARD_RESET_EXECUTE',
558            'SNK_HARD_RESET_RECOVER',
559            'SNK_DISCOVERY',
560            'SNK_REQUESTED',
561            'SNK_TRANSITION',
562            'SNK_READY'
563        ]
564
565        src_reset_states = [
566            'HARD_RESET_SEND',
567            'HARD_RESET_EXECUTE',
568            'SRC_HARD_RESET_RECOVER',
569            'SRC_DISCOVERY',
570            'SRC_NEGOCIATE',
571            'SRC_ACCEPTED',
572            'SRC_POWERED',
573            'SRC_TRANSITION',
574            'SRC_READY'
575        ]
576
577        if self.is_src():
578            states_list = src_reset_states
579        elif self.is_snk():
580            states_list = snk_reset_states
581        else:
582            raise error.TestFail('Port Pair not in a connected state')
583
584        cmd = 'pd %d hard' % self.port
585        return self._reset(cmd, states_list)
586
587
588class PDPortPartner(object):
589    """Methods used to instantiate PD device objects
590
591    This class is initalized with a list of servo consoles. It
592    contains methods to determine if USB PD devices are accessible
593    via the consoles and attempts to determine USB PD port partners.
594    A PD device is USB PD port specific, a single console may access
595    multiple PD devices.
596
597    """
598
599    def __init__(self, consoles):
600        """Initialization method
601
602        @param consoles: list of servo consoles
603        """
604        self.consoles = consoles
605
606    def _send_pd_state(self, port, console):
607        """Tests if PD device exists on a given port number
608
609        @param port: USB PD port number to try
610        @param console: servo UART console
611
612        @returns True if 'pd <port> state' command gives a valid
613        response, False otherwise
614        """
615        cmd = 'pd %d state' % port
616        regex = r'(Port C\d)|(Parameter)'
617        m = console.send_command_get_output(cmd, [regex])
618        # If PD port exists, then output will be Port C0 or C1
619        regex = r'Port C{0}'.format(port)
620        if re.search(regex, m[0][0]):
621            return True
622        return False
623
624    def _find_num_pd_ports(self, console):
625        """Determine number of PD ports for a given console
626
627        @param console: uart console accssed via servo
628
629        @returns: number of PD ports accessible via console
630        """
631        MAX_PORTS = 2
632        num_ports = 0
633        for port in xrange(MAX_PORTS):
634            if self._send_pd_state(port, console):
635                num_ports += 1
636        return num_ports
637
638    def _is_pd_console(self, console):
639        """Check if pd option exists in console
640
641        @param console: uart console accssed via servo
642
643        @returns: True if 'pd' is found, False otherwise
644        """
645        try:
646            m = console.send_command_get_output('help', [r'(pd)\s+'])
647            return True
648        except error.TestFail:
649            return False
650
651    def _is_plankton_console(self, console):
652        """Check for Plankton console
653
654        This method looks for a console command option 'usbc_action' which
655        is unique to Plankton PD devices.
656
657        @param console: uart console accssed via servo
658
659        @returns True if usbc_action command is present, False otherwise
660        """
661        try:
662            m = console.send_command_get_output('help', [r'(usbc_action)'])
663            return True
664        except error.TestFail:
665            return False
666
667    def _check_port_pair(self, dev_pair):
668        """Check if two PD devices could be connected
669
670        If two USB PD devices are connected, then they should be in
671        either the SRC_READY or SNK_READY states and have opposite
672        power roles. In addition, they must be on different servo
673        consoles.
674
675        @param: list of two possible PD port parters
676
677        @returns True if not the same console and both PD devices
678        are a plausible pair based only on their PD states.
679        """
680        # Don't test if on the same servo console
681        if dev_pair[0].console == dev_pair[1].console:
682            logging.info('PD Devices are on same platform -> cant be a pair')
683            return False
684        # Must be SRC <--> SNK or SNK <--> SRC
685        return bool((dev_pair[0].is_src() and dev_pair[1].is_snk()) or
686                    (dev_pair[0].is_snk() and dev_pair[1].is_src()))
687
688    def _verify_plankton_connection(self, dev_pair):
689        """Verify DUT to Plankton PD connection
690
691        This method checks for a Plankton PD connection for the
692        given port by first verifying if a PD connection is present.
693        If found, then it uses a Plankton feature to force a PD disconnect.
694        If the port is no longer in the connected state, and following
695        a delay, is found to be back in the connected state, then
696        a DUT pd to Plankton connection is verified.
697
698        @param dev_pair: list of two PD devices
699
700        @returns True if DUT to Plankton pd connection is verified
701        """
702        DISC_CHECK_TIME = .5
703        DISC_WAIT_TIME = 2
704        CONNECT_TIME = 4
705
706        if not self._check_port_pair(dev_pair):
707            return False
708
709        for index in xrange(len(dev_pair)):
710            try:
711                # Force PD disconnect
712                dev_pair[index].cc_disconnect_connect(DISC_WAIT_TIME)
713                time.sleep(DISC_CHECK_TIME)
714                # Verify that both devices are now disconnected
715                if (dev_pair[0].is_disconnected() and
716                    dev_pair[1].is_disconnected()):
717                    # Allow enough time for reconnection
718                    time.sleep(DISC_WAIT_TIME + CONNECT_TIME)
719                    if self._check_port_pair(dev_pair):
720                        # Have verifed a pd disconnect/reconnect sequence
721                        logging.info('Plankton <-> DUT pair found')
722                        return True
723                else:
724                    # Delay to allow
725                    time.sleep(DISC_WAIT_TIME + CONNECT_TIME)
726            except NotImplementedError:
727                logging.info('dev %d is not Plankton', index)
728        return False
729
730    def identify_pd_devices(self):
731        """Instantiate PD devices present in test setup
732
733        @returns list of 2 PD devices if a DUT <-> Plankton found. If
734        not found, then returns an empty list.
735        """
736        devices = []
737        # For each possible uart console, check to see if a PD console
738        # is present and determine the number of PD ports.
739        for console in self.consoles:
740            if self._is_pd_console(console):
741                is_plank = self._is_plankton_console(console)
742                num_ports = self._find_num_pd_ports(console)
743                # For each PD port that can be accessed via the console,
744                # instantiate either PDConsole or PDPlankton device.
745                for port in xrange(num_ports):
746                    if is_plank:
747                        logging.info('Plankton PD Device on port %d', port)
748                        devices.append(PDPlanktonDevice(console, port))
749                    else:
750                        devices.append(PDConsoleDevice(console, port))
751                        logging.info('Console PD Device on port %d', port)
752
753        # Determine PD port partners in the list of PD devices. Note, that
754        # there can be PD devices which are not accessible via a uart console,
755        # but are connected to a PD port which is accessible.
756        test_pair = []
757        for deva in devices:
758            for dev_idx in range(devices.index(deva) + 1, len(devices)):
759                devb = devices[dev_idx]
760                pair = [deva, devb]
761                if self._verify_plankton_connection(pair):
762                    test_pair = pair
763                    devices.remove(deva)
764                    devices.remove(devb)
765        return test_pair
766
767