xref: /aosp_15_r20/external/pigweed/pw_system/py/pw_system/find_serial_port.py (revision 61c4878ac05f98d0ceed94b57d316916de578985)
1# Copyright 2024 The Pigweed Authors
2#
3# Licensed under the Apache License, Version 2.0 (the "License"); you may not
4# use this file except in compliance with the License. You may obtain a copy of
5# the License at
6#
7#     https://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12# License for the specific language governing permissions and limitations under
13# the License.
14"""Find serial ports."""
15
16import argparse
17import logging
18import operator
19import sys
20from typing import Optional, Sequence
21
22from serial.tools.list_ports import comports
23from serial.tools.list_ports_common import ListPortInfo
24
25from pw_cli.interactive_prompts import interactive_index_select
26
27_LOG = logging.getLogger(__package__)
28
29
30def _parse_args():
31    parser = argparse.ArgumentParser(description=__doc__)
32    parser.add_argument(
33        '-i',
34        '--interactive',
35        action='store_true',
36        help='Show an interactive prompt to select a port.',
37    )
38    parser.add_argument(
39        '-l',
40        '--list-ports',
41        action='store_true',
42        help='List all port info.',
43    )
44    parser.add_argument(
45        '-p',
46        '--product',
47        help='Print ports matching this product name.',
48    )
49    parser.add_argument(
50        '-m',
51        '--manufacturer',
52        help='Print ports matching this manufacturer name.',
53    )
54    parser.add_argument(
55        '-s',
56        '--serial-number',
57        help='Print ports matching this serial number.',
58    )
59    parser.add_argument(
60        '-1',
61        '--print-first-match',
62        action='store_true',
63        help='Print the first port found sorted by device path.',
64    )
65    return parser.parse_args()
66
67
68def _print_ports(ports: Sequence[ListPortInfo]):
69    for cp in ports:
70        for line in [
71            f"device        = {cp.device}",
72            f"name          = {cp.name}",
73            f"description   = {cp.description}",
74            f"vid           = {cp.vid}",
75            f"pid           = {cp.pid}",
76            f"serial_number = {cp.serial_number}",
77            f"location      = {cp.location}",
78            f"manufacturer  = {cp.manufacturer}",
79            f"product       = {cp.product}",
80            f"interface     = {cp.interface}",
81        ]:
82            print(line)
83        print()
84
85
86def main(
87    list_ports: bool = False,
88    product: Optional[str] = None,
89    manufacturer: Optional[str] = None,
90    serial_number: Optional[str] = None,
91    print_first_match: bool = False,
92    interactive: bool = False,
93) -> int:
94    """List device info or print matches."""
95    ports = sorted(comports(), key=operator.attrgetter('device'))
96
97    if list_ports:
98        _print_ports(ports)
99        return 0
100
101    if interactive:
102        selected_port = interactive_serial_port_select()
103        if selected_port:
104            print(selected_port)
105            return 0
106        return 1
107
108    any_match_found = False
109
110    # Print matching devices
111    for port in ports:
112        if (
113            product is not None
114            and port.product is not None
115            and product in port.product
116        ):
117            any_match_found = True
118            print(port.device)
119
120        if (
121            manufacturer is not None
122            and port.manufacturer is not None
123            and manufacturer in port.manufacturer
124        ):
125            any_match_found = True
126            print(port.device)
127
128        if (
129            serial_number is not None
130            and port.serial_number is not None
131            and serial_number in port.serial_number
132        ):
133            any_match_found = True
134            print(port.device)
135
136        if any_match_found and print_first_match:
137            return 0
138
139    if not any_match_found:
140        return 1
141
142    return 0
143
144
145def interactive_serial_port_select(auto_select_only_one=True) -> str:
146    """Prompt the user to select a detected serial port.
147
148    Returns: String containing the path to the tty device.
149    """
150    ports = sorted(comports(), key=operator.attrgetter('device'))
151
152    if not ports:
153        print()
154        print(
155            '\033[31mERROR:\033[0m No serial ports detected.',
156            file=sys.stderr,
157        )
158        sys.exit(1)
159
160    if auto_select_only_one and len(ports) == 1:
161        # Auto select the first port.
162        return ports[0].device
163
164    # Create valid entry list
165    port_lines = list(
166        f'{port.device} - {port.manufacturer} - {port.description}'
167        for i, port in enumerate(ports)
168    )
169
170    print()
171    print('Please select a serial port device.')
172    print('Available ports:')
173    selected_index, _selected_text = interactive_index_select(
174        selection_lines=port_lines,
175        prompt_text=(
176            'Enter a port index or press up/down (Ctrl-C to cancel)\n> '
177        ),
178    )
179
180    selected_port = ports[selected_index]
181
182    return selected_port.device
183
184
185if __name__ == '__main__':
186    sys.exit(main(**vars(_parse_args())))
187