1
0
mirror of https://github.com/RIOT-OS/RIOT.git synced 2024-12-29 04:50:03 +01:00

dist/tools/usb-serial: Add ttys.py

- Provide a new tool to list and filter TTYs
- Change `Makefile.include` to use `$(RIOTTOOLS)/usb-serial/ttys.py`
  instead of `$(Q)$(RIOTTOOLS)/usb-serial/list-ttys.sh` to implement
  `make list-ttys`
- Extend `makefiles/tools/serial.inc.mk` to allow using the most recent
  port by passing `MOST_RECENT_PORT=1` as environment variable or
  parameter to make

Co-authored-by: chrysn <chrysn@fsfe.org>
Co-authored-by: Koen Zandberg <koen@bergzand.net>
This commit is contained in:
Marian Buschsieweke 2022-03-03 13:49:47 +01:00
parent 644f32fb9c
commit b296ade6b2
No known key found for this signature in database
GPG Key ID: CB8E3238CE715A94
4 changed files with 248 additions and 6 deletions

View File

@ -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

220
dist/tools/usb-serial/ttys.py vendored Executable file
View File

@ -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)

View File

@ -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}
=============================

View File

@ -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*)))