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