Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

dist/tools/usb_serial: Add tool for listing and filtering TTY interfaces #17737

Merged
merged 1 commit into from
Mar 10, 2022

Conversation

maribu
Copy link
Member

@maribu maribu commented Mar 3, 2022

Contribution description

This provides dist/tools/usb-serial/ttys.py to list and filter TTY interfaces. I just give some example invocations to show the features:

$ ./dist/tools/usb-serial/ttys.py 
path         | driver  | vendor              | model                                | model_db                                                      | serial                   | ctime   
-------------|---------|---------------------|--------------------------------------|---------------------------------------------------------------|--------------------------|---------
/dev/ttyUSB0 | cp210x  | Silicon Labs        | CP2102 USB to UART Bridge Controller | CP210x UART Bridge                                            | 0001                     | 12:41:35
/dev/ttyACM1 | cdc_acm | STMicroelectronics  | STM32 STLink                         | ST-LINK/V2.1                                                  | 0671FF535155878281151932 | 12:48:09
/dev/ttyACM0 | cdc_acm | LG Electronics Inc. | USB Controls                         | 100 Series/C230 Series Chipset Family USB 3.0 xHCI Controller | ABC123456789             | 09:13:08
/dev/ttyACM2 | cdc_acm | SEGGER              | J-Link                               | J-Link                                                        | 000683475134             | 12:41:36
$ ./dist/tools/usb-serial/ttys.py  --most-recent --format path
/dev/ttyACM2
$ ./dist/tools/usb-serial/ttys.py  --format json
[
  {
    "path": "/dev/ttyUSB0",
    "ctime": 1646739695.2475922,
    "serial": "0001",
    "driver": "cp210x",
    "model": "CP2102 USB to UART Bridge Controller",
    "vendor": "Silicon Labs"
  },
  {
    "path": "/dev/ttyACM1",
    "ctime": 1646739673.1676824,
    "serial": "0671FF535155878281151932",
    "driver": "cdc_acm",
    "model": "STM32 STLink",
    "vendor": "STMicroelectronics"
  },
  {
    "path": "/dev/ttyACM0",
    "ctime": 1646727188.9166667,
    "serial": "ABC123456789",
    "driver": "cdc_acm",
    "model": "USB Controls",
    "vendor": "LG Electronics Inc."
  },
  {
    "path": "/dev/ttyACM2",
    "ctime": 1646739696.894252,
    "serial": "000683475134",
    "driver": "cdc_acm",
    "model": "J-Link",
    "vendor": "SEGGER"
  }
]
$ ./dist/tools/usb-serial/ttys.py --help
usage: ttys.py [-h] [--most-recent] [--format FORMAT] [--serial SERIAL] [--driver DRIVER] [--model MODEL] [--model-db MODEL_DB] [--vendor VENDOR]
               [--vendor-db VENDOR_DB] [--exclude-serial [EXCLUDE_SERIAL ...]]

List and filter TTY interfaces that might belong to boards

options:
  -h, --help            show this help message and exit
  --most-recent         Print only the most recently connected matching TTY
  --format FORMAT       How to format the TTYs. Supported formats: ['ctime', 'driver', 'json', 'model', 'model_db', 'path', 'serial', 'table', 'vendor',
                        'vendor_db']
  --serial SERIAL       Print only devices matching this serial
  --driver DRIVER       Print only devices using this driver
  --model MODEL         Print only devices matching this model (as reported from device)
  --model-db MODEL_DB   Print only devices matching this model (DB entry)
  --vendor VENDOR       Print only devices matching this vendor (as reported from device)
  --vendor-db VENDOR_DB
                        Print only devices matching this vendor (DB entry)
  --exclude-serial [EXCLUDE_SERIAL ...]
                        Ignore devices with these serial numbers. Environment variable EXCLUDE_TTY_SERIAL can be used alternatively.

This is wired up to make list-ttys.

Additionally, make MOST_RECENT_PORT=1 term should now connect to the most recently connected board.

Testing procedure

Run the script for different parameters and check if it works as advertised.

Issues/PRs references

None

@maribu maribu requested review from aabadie and jia200x as code owners March 3, 2022 13:00
@github-actions github-actions bot added Area: build system Area: Build system Area: doc Area: Documentation Area: tools Area: Supplementary tools labels Mar 3, 2022
@chrysn
Copy link
Member

chrysn commented Mar 3, 2022

Nice, I like.

This depends on pyudev (which is not common, as found by my survey with N=1). How do other scripts we provide deal with that? (It is in Debian as python3-pyudev, and has been for ages, in case that matters).

It provides a bit less information than list-ttys.sh when it comes to the model; maybe that can be enhanced? For comparison:

Path          | Driver     | Vendor               | Model                          | Serial                         | ctime   
--------------|------------|----------------------|--------------------------------|--------------------------------|---------
/dev/ttyACM0  | cdc_acm    | NXP                  | ARM mbed                       | 9904360259004e450035101900000058000000009796990b | 14:12:50

vs.

/sys/bus/usb/devices/1-1: ARM "BBC micro:bit CMSIS-DAP", serial: '9904360259004e450035101900000058000000009796990b', tty(s): ttyACM0

where "ARM" and "BBC micro:bit CMSIS-DAP" come from /sys/bus/usb/devices/1-1/manufacturer and product, respectively. Seeing the "micro:bit" part would be good because that would pave the way for setting them per board, and making MOST_RECENT_PORT the default.

To avoid having too many tools I'd suggest we ensure this has feature parity with find-tty.sh and list-ttys.sh, and to deprecate them (ideally already when merging this).

@maribu
Copy link
Member Author

maribu commented Mar 3, 2022

I added --format as option, which now can be json, table, or any key in the tty dict() (such as path to only print the path).

I also no preferred ID_MODEL over ID_MODEL_FROM_DATABASE (and same for vendor) as a wild guess that this might spill out the micro:bit. I will test later at home, if that guess was correct.

It might be sensible to check for a bunch of boards if there are more attributes available via udev that contain insightful information. I guess I found an excuse to undust a bunch of boards for research :)

@chrysn
Copy link
Member

chrysn commented Mar 3, 2022 via email

@maribu
Copy link
Member Author

maribu commented Mar 3, 2022

I added the decoding of the unicode escape stuff udev provides. The json module does a fine job in sanitizing any strange strings (look at the non-UTF-8 Umlaut in "Universität"):

$ make list-ttys
path         | driver   | vendor                    | model                                | serial                                           | ctime   
-------------|----------|---------------------------|--------------------------------------|--------------------------------------------------|---------
/dev/ttyUSB1 | ftdi_sio | Freie Universität Berlin | MSB-A2                               | ARR6DKEE                                         | 22:54:36
/dev/ttyACM0 | cdc_acm  | ARM                       | DAPLink CMSIS-DAP                    | 9900000036424e45003930160000006d0000000097969901 | 22:37:11
/dev/ttyUSB0 | cp210x   | Silicon Labs              | CP2102 USB to UART Bridge Controller | 0001                                             | 22:38:17
$ ./dist/tools/usb-serial/ttys.py --format json
[
  {
    "path": "/dev/ttyUSB1",
    "ctime": 1646344476.4937167,
    "serial": "ARR6DKEE",
    "driver": "ftdi_sio",
    "model": "MSB-A2",
    "vendor": "Freie Universit\u00c3\u00a4t Berlin"
  },
  {
    "path": "/dev/ttyACM0",
    "ctime": 1646343431.6859725,
    "serial": "9900000036424e45003930160000006d0000000097969901",
    "driver": "cdc_acm",
    "model": "DAPLink CMSIS-DAP",
    "vendor": "ARM"
  },
  {
    "path": "/dev/ttyUSB0",
    "ctime": 1646343497.3789763,
    "serial": "0001",
    "driver": "cp210x",
    "model": "CP2102 USB to UART Bridge Controller",
    "vendor": "Silicon Labs"
  }
]

I also added a bit of logic to just detect the smallest column size fitting each column, rather than shortening. That way the table will never break, but also fit small terminals if the data to show fits.

@maribu
Copy link
Member Author

maribu commented Mar 3, 2022

Btw: I flashed the J-Link firmware on all of my micro:bits, so that I see a DAPLink CMSIS-DAP were you see the micro:bit :/

Decodes unicode escaping in a string, e.g. "Hallo\\x20World" is decoded as
"Hallo World"
"""
return bytes(string, "utf-8").decode("unicode_escape")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return bytes(string, "utf-8").decode("unicode_escape")
return bytes(string, "ascii").decode("unicode_escape").encode('latin1').decode('utf8', error='replace')

The 'utf-8' in the original version is a red herring: the input is ASCII per the (presumed) escaping rules. Also the "unicode_escape" is misleading (but Python has no equivalent "c_escape"): It would properly unescape any \u1234' strings, but what's actually there is \x89`, and that's part of Python's still weird admittance of latin1's special position in the encoding world that got grandfathered into Unicode by the assignment of code points 128-255.

That all result is the mojibake you see on Universit*t.

The proposed changes are still lossy (a bit like the underscores), but in a better defined way (precisely those characters that are not valid UTF-8 are replaced), and still easy to enter (you can enter regular text, or copy-paste the output around).

This loses bytes that are not UTF-8, which could in theory be salvaged through surrogate escaping, but if someone's device is named "foo\xffbar", that's not because their whole system is using a codec where \xff is valid, but just because someone screwed up in firmware development. (Not that there'd be a rule that these things are UTF-8, but anything else is just so 20th century).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This gives me

$ make list-ttys
Traceback (most recent call last):
  File "/home/maribu/Repos/software/RIOT/dist/tools/usb-serial/ttys.py", line 220, in <module>
    print_ttys(sys.argv)
  File "/home/maribu/Repos/software/RIOT/dist/tools/usb-serial/ttys.py", line 205, in print_ttys
    tty = tty2dict(dev)
  File "/home/maribu/Repos/software/RIOT/dist/tools/usb-serial/ttys.py", line 36, in tty2dict
    result["vendor"] = unescape(dev.get("ID_VENDOR_ENC"))
  File "/home/maribu/Repos/software/RIOT/dist/tools/usb-serial/ttys.py", line 19, in unescape
    res = bytes(string, "ascii").decode("unicode_escape")
UnicodeEncodeError: 'ascii' codec can't encode character '\xe4' in position 18: ordinal not in range(128)
make: *** [/home/maribu/Repos/software/RIOT/tests/periph_timer_periodic/../../Makefile.include:867: list-ttys] Error 1

Using bytes(string, "utf8") instead of bytes(string, "ascii") gives me:

$ make list-ttys
path         | driver   | vendor                   | model  | model_db               | serial   | ctime   
-------------|----------|--------------------------|--------|------------------------|----------|---------
/dev/ttyUSB0 | ftdi_sio | Freie Universität Berlin | MSB-A2 | FT232 Serial (UART) IC | ARR6DKEE | 20:21:22

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's

  • funny because it means that udev is letting high bytes go through unescaped (while escaping even whitespace with \x20)
  • weird because I've just tested it and when I enter UTF-8 in a RIOT USB device's name that conversion makes it break ...

... except that I just found that USB identifiers are UTF-16 (or UCS-2?); reading up a little to ensure that we don't nail down a conversion here that turns out to be broken just because it happens to work at the Universität device.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Umlaut actually looks pretty UTF-8 like to me:

$ udevadm info /dev/ttyUSB0 
P: /devices/pci0000:00/0000:00:14.0/usb1/1-2/1-2:1.0/ttyUSB0/tty/ttyUSB0
N: ttyUSB0
S: serial/by-id/usb-Freie_Universität_Berlin_MSB-A2_ARR6DKEE-if00-port0
S: serial/by-path/pci-0000:00:14.0-usb-0:2:1.0-port0
E: DEVLINKS=/dev/serial/by-id/usb-Freie_Universität_Berlin_MSB-A2_ARR6DKEE-if00-port0 /dev/serial/by-path/pci-0000:00:14.0-usb-0:2:1.0-port0
E: DEVNAME=/dev/ttyUSB0
E: DEVPATH=/devices/pci0000:00/0000:00:14.0/usb1/1-2/1-2:1.0/ttyUSB0/tty/ttyUSB0
E: ID_BUS=usb
E: ID_MODEL=MSB-A2
E: ID_MODEL_ENC=MSB-A2
E: ID_MODEL_FROM_DATABASE=FT232 Serial (UART) IC
E: ID_MODEL_ID=6001
E: ID_PATH=pci-0000:00:14.0-usb-0:2:1.0
E: ID_PATH_TAG=pci-0000_00_14_0-usb-0_2_1_0
E: ID_PCI_CLASS_FROM_DATABASE=Serial bus controller
E: ID_PCI_INTERFACE_FROM_DATABASE=XHCI
E: ID_PCI_SUBCLASS_FROM_DATABASE=USB controller
E: ID_REVISION=0600
E: ID_SERIAL=Freie_Universität_Berlin_MSB-A2_ARR6DKEE
E: ID_SERIAL_SHORT=ARR6DKEE
E: ID_TYPE=generic
E: ID_USB_DRIVER=ftdi_sio
E: ID_USB_INTERFACES=:ffffff:
E: ID_USB_INTERFACE_NUM=00
E: ID_VENDOR=Freie_Universität_Berlin
E: ID_VENDOR_ENC=Freie\x20Universität\x20Berlin
E: ID_VENDOR_FROM_DATABASE=Future Technology Devices International, Ltd
E: ID_VENDOR_ID=0403
E: MAJOR=188
E: MINOR=0
E: SUBSYSTEM=tty
E: USEC_INITIALIZED=7836843200

Using

def unescape(string):
    """
    Decodes unicode escaping in a string, e.g. "Hallo\\x20World" is decoded as
    "Hallo World"
    """
    return string.replace("\\x20", " ")

Yields:

path         | driver   | vendor                   | model  | model_db               | serial   | ctime   
-------------|----------|--------------------------|--------|------------------------|----------|---------
/dev/ttyUSB0 | ftdi_sio | Freie Universität Berlin | MSB-A2 | FT232 Serial (UART) IC | ARR6DKEE | 21:17:33

If udev really only replaces spaces and invalid stuff, restoring spaces would be the only thing needed. Maybe I should find some documentation on this :)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've verified that the current conversion produces correct output even when non-UCS2 model names are used; furthermore, the output is consistent with the output of lsusb.

The incantation looks weird in terms of locales, but that's just because Python's decode('unicode_escape') makes a hardcoded choice of latin1 for bytes that are present in the string in unescaped form, and that gets reverted in the latin1 encode step.

I think this is fine (but will need to make up my mind about whether I want to clean up the crude UTF-16 support I hacked into _cpy_str_to_utf16 for inclusion in RIOT :-) ).

@chrysn
Copy link
Member

chrysn commented Mar 4, 2022

I flashed the J-Link firmware on all of my micro:bits, so that I see a DAPLink CMSIS-DAP were you see the micro:bit :/

If we at any point happen to develop (or port) a JTAG probe on (to) RIOT, let's make sure both its over-the-network and USB identifiers are such that they can indicate when they're hardwired to any given board and indicate that :-)

@maribu
Copy link
Member Author

maribu commented Mar 8, 2022

OK, I think I addressed all comments. I also exposed the DB entry for vendor and model, as they can be vastly different from the reported model and vendor name. I could imagine that a debugger would report the board it is hardwired to in the model string, while the DB entry would also give the type of the debugger. There might be use cases to filter for either of them.

Copy link
Member

@chrysn chrysn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO it could replace list-ttys.sh (and find-tty-.sh) in a few places, but that can also be done after the tool is established. (Easy replacements are various READMEs that offer list-ttys.sh to find serial numbers -- given it's not used anywhere else, and its most common mention is as a known bug in the release notes and one more bug from non-GNU find use, I think it could be deprecated immediately).

A few more comments, but other than that I think it's good.

dist/tools/usb-serial/ttys.py Outdated Show resolved Hide resolved
dist/tools/usb-serial/ttys.py Outdated Show resolved Hide resolved
if args.most_recent:
most_recent = ttys[0]
for tty in ttys:
if tty["ctime"] > most_recent["ctime"]:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a curious observation: Dual devices like hifive1 the below have exactly the same timestamp (down to the nanosecond, so probably created atomically). The current mechanism takes the first of them in the sequence returned from pyudev, which I think will be fine.

path         | driver   | vendor | model         | model_db                      | serial | ctime   
-------------|----------|--------|---------------|-------------------------------|--------|---------
/dev/ttyUSB0 | ftdi_sio | FTDI   | Dual RS232-HS | FT2232C/D/H Dual UART/FIFO IC | None   | 13:19:40
/dev/ttyUSB1 | ftdi_sio | FTDI   | Dual RS232-HS | FT2232C/D/H Dual UART/FIFO IC | None   | 13:19:40

If at some point we encounter devices where any other TTY than the first is used, a filter for that can still be added (I wouldn't do that now when there is no use case yet).

Alternative Tool: ttys.py
-------------------------

As an alternative to above shell script, `$(RIOTTOOLS)/usb-serial/ttys.py` can
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The above is already ttys.py, I think this can be unified.

board:

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~{Makefile}
PORT ?= $(shell $(RIOTTOOLS)/usb-serial/ttys.py --most-recent --format path)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does it still make sense to have that snippet around now that MOST_RECENT_PORT is available?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will try to come up with a more sensible example.

@maribu maribu force-pushed the dist/tools/usb-serial branch 2 times, most recently from fde8de4 to 360382a Compare March 8, 2022 19:26
Copy link
Member

@chrysn chrysn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ACK: this is correct for all I can tell, and an improvement to the workflows.

@chrysn chrysn added the CI: ready for build If set, CI server will compile all applications for all available boards for the labeled PR label Mar 9, 2022
- 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 <[email protected]>
Co-authored-by: Koen Zandberg <[email protected]>
@maribu maribu force-pushed the dist/tools/usb-serial branch from 360382a to b296ade Compare March 9, 2022 14:40
@maribu maribu added CI: skip compile test If set, CI server will run only non-compile jobs, but no compile jobs or their dependent jobs CI: ready for build If set, CI server will compile all applications for all available boards for the labeled PR and removed CI: ready for build If set, CI server will compile all applications for all available boards for the labeled PR labels Mar 9, 2022
@maribu
Copy link
Member Author

maribu commented Mar 9, 2022

Due to the change of the backend for make list-ttys in Makefile.include this PR will not be detected as being eligible for skipping compilation test. I disabled compilation tests by hand, as I'm extremely confident that a Murdock run won't uncover any bugs this PR might contain - Murdock will never call make list-ttys anyway.

@maribu
Copy link
Member Author

maribu commented Mar 10, 2022

All green :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Area: build system Area: Build system Area: doc Area: Documentation Area: tools Area: Supplementary tools CI: ready for build If set, CI server will compile all applications for all available boards for the labeled PR CI: skip compile test If set, CI server will run only non-compile jobs, but no compile jobs or their dependent jobs
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants