diff --git a/Makefile.include b/Makefile.include index 462c3ab82e..9cf0618a3d 100644 --- a/Makefile.include +++ b/Makefile.include @@ -864,7 +864,7 @@ cleanterm: $(TERMDEPS) $(TERMPROG) $(TERMFLAGS) $(TERMTEE) list-ttys: - $(Q)$(RIOTTOOLS)/usb-serial/list-ttys.sh + $(Q)$(RIOTTOOLS)/usb-serial/ttys.py doc: $(MAKE) -BC $(RIOTBASE) doc diff --git a/dist/tools/usb-serial/ttys.py b/dist/tools/usb-serial/ttys.py new file mode 100755 index 0000000000..722c69116a --- /dev/null +++ b/dist/tools/usb-serial/ttys.py @@ -0,0 +1,220 @@ +#!/usr/bin/python3 +""" +Command line utility to list and filter TTYs +""" +import argparse +import json +import os +import sys +import time + +import pyudev + + +def unescape(string): + """ + Decodes unicode escaping in a string, e.g. "Hallo\\x20World" is decoded as + "Hallo World" + """ + res = bytes(string, "utf8").decode("unicode_escape") + res = res.encode("latin1").decode("utf8", errors="replace") + return res + + +def tty2dict(dev): + """ + Parse the given TTY udev interface into a dict() containing the most + relevant attributes + """ + result = {} + result["path"] = dev.get("DEVNAME") + result["ctime"] = os.stat(result["path"]).st_ctime + result["serial"] = dev.get("ID_SERIAL_SHORT") + result["driver"] = dev.get("ID_USB_DRIVER") + result["model"] = unescape(dev.get("ID_MODEL_ENC")) + result["model_db"] = dev.get("ID_MODEL_FROM_DATABASE") + result["vendor"] = unescape(dev.get("ID_VENDOR_ENC")) + result["vendor_db"] = dev.get("ID_VENDOR_FROM_DATABASE") + + return result + + +def filters_match(filters, tty): + """ + Check if the given TTY interface matches all given filters + """ + if filters.serial is not None: + if filters.serial != tty["serial"]: + return False + + if filters.driver is not None: + if filters.driver != tty["driver"]: + return False + + if filters.model is not None: + if filters.model != tty["model"]: + return False + + if filters.model_db is not None: + if filters.model_db != tty["model_db"]: + return False + + if filters.vendor is not None: + if filters.vendor != tty["vendor"]: + return False + + if filters.vendor_db is not None: + if filters.vendor != tty["vendor_db"]: + return False + + for forbidden_serial in filters.exclude_serial: + if tty["serial"] == forbidden_serial: + return False + + return True + + +def shorten(string, length): + """ + Shorten the given string to the given length, if needed + """ + if len(string) > length: + return string[:length - 3] + "..." + + return string + + +def parse_args(args): + """ + Parse the given command line style arguments with argparse + """ + desc = "List and filter TTY interfaces that might belong to boards" + supported_formats = { + "table", + "json", + "path", + "serial", + "vendor", + "vendor_db", + "model", + "model_db", + "driver", + "ctime", + } + parser = argparse.ArgumentParser(description=desc) + parser.add_argument("--most-recent", action="store_true", + help="Print only the most recently connected matching " + + "TTY") + parser.add_argument("--format", default="table", type=str, + help=f"How to format the TTYs. Supported formats: " + f"{sorted(supported_formats)}") + parser.add_argument("--serial", default=None, type=str, + help="Print only devices matching this serial") + parser.add_argument("--driver", default=None, type=str, + help="Print only devices using this driver") + parser.add_argument("--model", default=None, type=str, + help="Print only devices matching this model " + "(as reported from device)") + parser.add_argument("--model-db", default=None, type=str, + help="Print only devices matching this model " + "(DB entry)") + parser.add_argument("--vendor", default=None, type=str, + help="Print only devices matching this vendor " + "(as reported from device)") + parser.add_argument("--vendor-db", default=None, type=str, + help="Print only devices matching this vendor " + "(DB entry)") + parser.add_argument("--exclude-serial", type=str, nargs='*', default=None, + help="Ignore devices with these serial numbers. " + + "Environment variable EXCLUDE_TTY_SERIAL can " + + "be used alternatively.") + + args = parser.parse_args() + + if args.format not in supported_formats: + sys.exit(f"Format \"{args.format}\" not supported") + + if args.exclude_serial is None: + if "EXCLUDE_TTY_SERIAL" in os.environ: + args.exclude_serial = os.environ["EXCLUDE_TTY_SERIAL"].split() + else: + args.exclude_serial = [] + + return args + + +def print_table(data, headers): + """ + Print the list of dictionaries given in data as table, where headers is + a list of keys to that dict and also servers as table headers. + """ + lengths = [] + for header in headers: + lengths.append(len(header)) + + for item in data: + for i, header in enumerate(headers): + if len(str(item[header])) > lengths[i]: + lengths[i] = len(item[header]) + + sys.stdout.write(f"{headers[0]:{lengths[0]}}") + for i in range(1, len(headers)): + sys.stdout.write(f" | {headers[i]:{lengths[i]}}") + sys.stdout.write("\n" + lengths[0] * "-") + for i in range(1, len(headers)): + sys.stdout.write("-|-" + lengths[i] * "-") + + for item in data: + sys.stdout.write(f"\n{str(item[headers[0]]):{lengths[0]}}") + for header, length in zip(headers[1:], lengths[1:]): + sys.stdout.write(f" | {str(item[header]):{length}}") + + sys.stdout.write("\n") + sys.stdout.flush() + + +def print_results(args, ttys): + """ + Print the given TTY devices according to the given args + """ + if args.format == "json": + print(json.dumps(ttys, indent=2)) + return + + if args.format == "table": + for tty in ttys: + tty["ctime"] = time.strftime("%H:%M:%S", + time.localtime(tty["ctime"])) + headers = ["path", "driver", "vendor", "model", "model_db", "serial", + "ctime"] + print_table(ttys, headers) + return + + for tty in ttys: + print(tty[args.format]) + + +def print_ttys(args): + """ + Print ttys as specified by the given command line arguments + """ + args = parse_args(args) + + ttys = [] + for dev in pyudev.Context().list_devices(subsystem='tty', ID_BUS='usb'): + tty = tty2dict(dev) + if filters_match(args, tty): + ttys.append(tty) + + if args.most_recent: + most_recent = ttys[0] + for tty in ttys: + if tty["ctime"] > most_recent["ctime"]: + most_recent = tty + ttys = [most_recent] + + print_results(args, ttys) + + +if __name__ == "__main__": + print_ttys(sys.argv) diff --git a/doc/doxygen/src/advanced-build-system-tricks.md b/doc/doxygen/src/advanced-build-system-tricks.md index 6b9dc58753..748709af6a 100644 --- a/doc/doxygen/src/advanced-build-system-tricks.md +++ b/doc/doxygen/src/advanced-build-system-tricks.md @@ -210,7 +210,7 @@ The following Make snippet is used: JLINK_SERIAL ?= $(BOARD_SERIAL) # Use the existing script to grab the matching /dev/ttyACM* device - PORT_LINUX ?= $(firstword $(shell $(RIOTTOOLS)/usb-serial/find-tty.sh $(SERIAL))) + PORT ?= $(shell $(RIOTTOOLS)/usb-serial/ttys.py --most-recent --format path --serial $(SERIAL)) endif endif ~~~~~~~~~~~~~~~~~~~ @@ -221,14 +221,32 @@ the debugger hardware. With the `make list-ttys` it is reported as the 'serial': ~~~~~~~~~~~~~~~~~~~ $ make list-ttys -/sys/bus/usb/devices/1-1.4.4: Atmel Corp. EDBG CMSIS-DAP, serial: 'ATML2127031800008360', tty(s): ttyACM1 -/sys/bus/usb/devices/1-1.4.3: SEGGER J-Link, serial: '000683806234', tty(s): ttyACM0 +path | driver | vendor | model | model_db | serial | ctime +-------------|---------|--------------------------|--------------------------------------|-----------------------|--------------------------|--------- +/dev/ttyUSB0 | cp210x | Silicon Labs | CP2102 USB to UART Bridge Controller | CP210x UART Bridge | 0001 | 15:58:13 +/dev/ttyACM1 | cdc_acm | STMicroelectronics | STM32 STLink | ST-LINK/V2.1 | 0671FF535155878281151932 | 15:58:04 +/dev/ttyACM3 | cdc_acm | Arduino (www.arduino.cc) | EOS High Power | Mega ADK R3 (CDC ACM) | 75230313733351110120 | 15:59:57 +/dev/ttyACM2 | cdc_acm | SEGGER | J-Link | J-Link | 000683475134 | 12:41:36 ~~~~~~~~~~~~~~~~~~~ When the above make snippet is included as `RIOT_MAKEFILES_GLOBAL_PRE`, the serial number of the USB device is automatically set if the used board is included in the script. This will then ensure that the board debugger is used -for flashing and the board serial device is used when starting the serial console. +for flashing and the board serial device is used when starting the serial +console. + +It supports command line parameters to filter by vendor name, model name, serial +number, or driver. In addition, the `--most-recent` argument will only print the +most recently added interface (out of those matching the filtering by vendor, +model, etc.). The `--format path` argument will result in only the device path +being printed for convenient use in scripts. + +Handling multiple boards: Simplest approach {#multiple-boards-simple} +=========================================== + +Passing `MOST_RECENT_PORT=1` as environment variable or as parameter to +make will result in the most recently connected board being preferred over the +default PORT for the selected board. Analyze dependency resolution {#analyze-depedency-resolution} ============================= diff --git a/makefiles/tools/serial.inc.mk b/makefiles/tools/serial.inc.mk index ab40d5fee6..ffabec0bb2 100644 --- a/makefiles/tools/serial.inc.mk +++ b/makefiles/tools/serial.inc.mk @@ -1,4 +1,8 @@ -# Use as default the most commonly used ports on Linux and OSX +# Select the most recently attached tty interface +ifeq (1,$(MOST_RECENT_PORT)) + PORT ?= $(shell $(RIOTTOOLS)/usb-serial/ttys.py --most-recent --format path) +endif +# Otherwise, use as default the most commonly used ports on Linux and OSX PORT_LINUX ?= /dev/ttyACM0 PORT_DARWIN ?= $(firstword $(sort $(wildcard /dev/tty.usbmodem*)))