xref: /aosp_15_r20/external/autotest/client/common_lib/cros/cfm/usb/usb_port_manager.py (revision 9c5db1993ded3edbeafc8092d69fe5de2ee02df7)
1import collections
2import logging
3import os.path
4
5
6PortId = collections.namedtuple('PortId', ['bus', 'port_number'])
7
8GPIO_PATH = '/sys/class/gpio'
9GUADO_CONTROLLER = 'INT3437:00'
10
11# Mapping from bus ID and port number to the GPIO index.
12_PORT_ID_TO_GPIO_INDEX_DICT = {
13    # On Guado, there are three gpios that control usb port power.
14    # These are offsets used to calculate GPIO index.
15    'guado': {
16        # Front ports
17        PortId(bus=1, port_number=2): 56,  # Front left USB 2
18        PortId(bus=2, port_number=1): 56,  # Front left USB 3
19        PortId(bus=1, port_number=3): 57,  # Front right USB 2
20        PortId(bus=2, port_number=2): 57,  # Front right USB 3
21        # Back ports (same GPIO is used for both ports)
22        PortId(bus=1, port_number=5): 47,  # Back upper USB 2
23        PortId(bus=2, port_number=3): 47,  # Back upper USB 3
24        PortId(bus=1, port_number=6): 47,  # Back lower USB 2
25        PortId(bus=2, port_number=4): 47,  # Back lower USB 3
26    },
27    'guado-cfm': {
28        # Front ports
29        PortId(bus=1, port_number=2): 56,  # Front left USB 2
30        PortId(bus=2, port_number=1): 56,  # Front left USB 3
31        PortId(bus=1, port_number=3): 57,  # Front right USB 2
32        PortId(bus=2, port_number=2): 57,  # Front right USB 3
33        # Back ports (same GPIO is used for both ports)
34        PortId(bus=1, port_number=5): 47,  # Back upper USB 2
35        PortId(bus=2, port_number=3): 47,  # Back upper USB 3
36        PortId(bus=1, port_number=6): 47,  # Back lower USB 2
37        PortId(bus=2, port_number=4): 47,  # Back lower USB 3
38    },
39    # On Fizz, there are in total 5 usb ports and per port usb power
40    # is controlled by EC with user space command:
41    # ectool gpioset USBx_ENABLE 0/1 (x from 1 to 5).
42    'fizz': {
43        # USB 2 bus.
44        PortId(bus=1, port_number=3): 4,    # Front right USB 2
45        PortId(bus=1, port_number=4): 5,    # Front left USB 2
46        PortId(bus=1, port_number=5): 1,    # Back left USB 2
47        PortId(bus=1, port_number=6): 2,    # Back middle USB 2
48        PortId(bus=1, port_number=2): 3,    # Back right USB 2
49        # USB 3 bus.
50        PortId(bus=2, port_number=3): 4,    # Front right USB 3
51        PortId(bus=2, port_number=4): 5,    # Front left USB 3
52        PortId(bus=2, port_number=5): 1,    # Back left USB 3
53        PortId(bus=2, port_number=6): 2,    # Back middle USB 3
54        PortId(bus=2, port_number=2): 3,    # Back right USB 3
55    },
56    'fizz-cfm': {
57        # USB 2 bus.
58        PortId(bus=1, port_number=3): 4,    # Front right USB 2
59        PortId(bus=1, port_number=4): 5,    # Front left USB 2
60        PortId(bus=1, port_number=5): 1,    # Back left USB 2
61        PortId(bus=1, port_number=6): 2,    # Back middle USB 2
62        PortId(bus=1, port_number=2): 3,    # Back right USB 2
63        # USB 3 bus.
64        PortId(bus=2, port_number=3): 4,    # Front right USB 3
65        PortId(bus=2, port_number=4): 5,    # Front left USB 3
66        PortId(bus=2, port_number=5): 1,    # Back left USB 3
67        PortId(bus=2, port_number=6): 2,    # Back middle USB 3
68        PortId(bus=2, port_number=2): 3,    # Back right USB 3
69    }
70}
71
72
73def _get_gpio_index(board, port_id):
74    return _PORT_ID_TO_GPIO_INDEX_DICT[board][port_id]
75
76
77class UsbPortManager(object):
78    """
79    Manages USB ports.
80
81    Can for example power cycle them.
82    """
83    def __init__(self, host):
84        """
85        Initializes with a host.
86
87        @param host a Host object.
88        """
89        self._host = host
90
91    def set_port_power(self, port_ids, power_on):
92        """
93        Turns on or off power to the USB port for peripheral devices.
94
95        @param port_ids Iterable of PortId instances (i.e. bus, port_number
96            tuples) to set power for.
97        @param power_on If true, turns power on. If false, turns power off.
98        """
99        for port_id in port_ids:
100            gpio_index = _get_gpio_index(self._get_board(), port_id)
101            self._set_gpio_power(self._get_board(), gpio_index, power_on)
102
103    def _find_gpio_base_index(self, expected_controller):
104        """
105        Finds the gpiochip* base index using the expected controller.
106
107        If `cat /sys/class/gpio/gpiochip<N>/label` has the expected controller, return <N>
108
109        @param expected_controller The controller to match to return gpiochip<N>/base
110        """
111        gpiochips = self._run(
112            'ls -d /sys/class/gpio/gpiochip*').stdout.strip().split('\n')
113        if not gpiochips:
114            raise ValueError('No gpiochips found')
115
116        for gpiochip in gpiochips:
117            logging.debug('Checking gpiochip path "%s" for controller %s',
118                gpiochip, expected_controller)
119            gpiochip_label = os.path.join(gpiochip, 'label')
120            gpiochip_controller = self._run(
121                'cat {}'.format(gpiochip_label)).stdout.strip()
122
123            if gpiochip_controller == expected_controller:
124                gpiochip_base = os.path.join(gpiochip, 'base')
125                gpiochip_base_index = self._run(
126                    'cat {}'.format(gpiochip_base)).stdout.strip()
127                return int(gpiochip_base_index)
128
129        raise ValueError('Expected controller not found')
130
131    def _get_board(self):
132        # host.get_board() adds 'board: ' in front of the board name
133        return self._host.get_board().split(':')[1].strip()
134
135    def _set_gpio_power_guado(self, gpio_idx, power_on):
136        """
137        Turns on or off the power for a specific GPIO on board Guado.
138
139        @param gpio_idx The *offset* of the gpio to set the power for, added to the base.
140        @param power_on If True, powers on the GPIO. If False, powers it off.
141        """
142
143        # First, we need to find the gpio base
144        gpio_base_index = self._find_gpio_base_index(GUADO_CONTROLLER)
145
146        # Once base is found, calculate index
147        gpio_index = gpio_base_index + gpio_idx
148        logging.debug('Using gpio index: "%s"', gpio_index)
149
150        gpio_path = '/sys/class/gpio/gpio{}'.format(gpio_index)
151        did_export = False
152        if not self._host.path_exists(gpio_path):
153            did_export = True
154            self._run('echo {} > /sys/class/gpio/export'.format(
155                    gpio_index))
156        try:
157            self._run('echo out > {}/direction'.format(gpio_path))
158            value_string = '1' if power_on else '0'
159            self._run('echo {} > {}/value'.format(
160                    value_string, gpio_path))
161        finally:
162            if did_export:
163                self._run('echo {} > /sys/class/gpio/unexport'.format(
164                        gpio_index))
165
166    def _set_gpio_power_fizz(self, gpio_idx, power_on):
167        """
168        Turns on or off the power for a specific GPIO on board Fizz.
169
170        @param gpio_idx The index of the gpio to set the power for.
171        @param power_on If True, powers on the GPIO. If False, powers it off.
172        """
173        value_string = '1' if power_on else '0'
174        cmd = 'ectool gpioset USB{}_ENABLE {}'.format(gpio_idx,
175              value_string)
176        self._run(cmd)
177
178    def _set_gpio_power(self, board, gpio_index, power_on):
179        """
180        Turns on or off the power for a specific GPIO.
181
182        @param board Board type. Currently support: Guado, Fizz.
183        @param gpio_idx The index of the gpio to set the power for.
184        @param power_on If True, powers on the GPIO. If False, powers it off.
185        """
186        if board == 'guado' or board == 'guado-cfm':
187            self._set_gpio_power_guado(gpio_index, power_on)
188        elif board == 'fizz' or board == 'fizz-cfm':
189            self._set_gpio_power_fizz(gpio_index, power_on)
190        else:
191            raise ValueError('Unsupported board type {}.'.format(board))
192
193    def _run(self, command):
194        logging.debug('Running: "%s"', command)
195        res = self._host.run(command)
196        logging.debug('Result: "%s"', res)
197        return res
198