开发者

How to find all serial devices (ttyS, ttyUSB, ..) on Linux without opening them?

What is the proper way to get a list of all available serial ports/devices on a Linux system?

In other words, when I iterate over all devices in /dev/, how do I tell which ones are serial ports in the classic way, that is, those usually supporting baud rates and RTS/CTS flow control?

The solution would be coded in C.

I ask because I am using a third-party library that does this clearly wrong: It appears to only iterate over /dev/ttyS*. The problem is that there are, for instance, serial ports over USB (provided by USB-RS232 adapters), and those are listed under /dev/ttyUSB*. And reading the Serial-HOWTO at Linux.org, I get the idea that there'll be other name spaces as well, as time comes.

So I need to find the official way to detect serial devices. The problem is that none appears to be documented, or I can't find it.

I imagine one way would be to open all files from /dev/tty* and call a specific ioctl() on them that is only available on serial devices. Would that be a good solution, though?

Update

hrickards suggested to look at the source for "setserial". Its code does exactly what I had in mind:

First, it opens a device with:

fd = open (path, O_RDWR | O_NONBLOCK)开发者_JAVA技巧

Then it invokes:

ioctl (fd, TIOCGSERIAL, &serinfo)

If that call returns no error, then it's a serial device, apparently.

I found similar code in Serial Programming/termios, which suggested to also add the O_NOCTTY option.

There is one problem with this approach, though:

When I tested this code on BSD Unix (that is, Mac OS X), it worked as well. However, serial devices that are provided through Bluetooth cause the system (driver) to try to connect to the Bluetooth device, which takes a while before it'll return with a timeout error. This is caused by just opening the device. And I can imagine that similar things can happen on Linux as well - ideally, I should not need to open the device to figure out its type. I wonder if there's also a way to invoke ioctl functions without an open, or open a device in a way that it does not cause connections to be made?

What should I do?


The /sys filesystem should contain plenty information for your quest. My system (2.6.32-40-generic #87-Ubuntu) suggests:

/sys/class/tty

Which gives you descriptions of all TTY devices known to the system. A trimmed down example:

# ll /sys/class/tty/ttyUSB*
lrwxrwxrwx 1 root root 0 2012-03-28 20:43 /sys/class/tty/ttyUSB0 -> ../../devices/pci0000:00/0000:00:1d.0/usb2/2-1/2-1.4/2-1.4:1.0/ttyUSB0/tty/ttyUSB0/
lrwxrwxrwx 1 root root 0 2012-03-28 20:44 /sys/class/tty/ttyUSB1 -> ../../devices/pci0000:00/0000:00:1d.0/usb2/2-1/2-1.3/2-1.3:1.0/ttyUSB1/tty/ttyUSB1/

Following one of these links:

# ll /sys/class/tty/ttyUSB0/
insgesamt 0
drwxr-xr-x 3 root root    0 2012-03-28 20:43 ./
drwxr-xr-x 3 root root    0 2012-03-28 20:43 ../
-r--r--r-- 1 root root 4096 2012-03-28 20:49 dev
lrwxrwxrwx 1 root root    0 2012-03-28 20:43 device -> ../../../ttyUSB0/
drwxr-xr-x 2 root root    0 2012-03-28 20:49 power/
lrwxrwxrwx 1 root root    0 2012-03-28 20:43 subsystem -> ../../../../../../../../../../class/tty/
-rw-r--r-- 1 root root 4096 2012-03-28 20:43 uevent

Here the dev file contains this information:

# cat /sys/class/tty/ttyUSB0/dev
188:0

This is the major/minor node. These can be searched in the /dev directory to get user-friendly names:

# ll -R /dev |grep "188, *0"
crw-rw----   1 root dialout 188,   0 2012-03-28 20:44 ttyUSB0

The /sys/class/tty dir contains all TTY devices but you might want to exclude those pesky virtual terminals and pseudo terminals. I suggest you examine only those which have a device/driver entry:

# ll /sys/class/tty/*/device/driver
lrwxrwxrwx 1 root root 0 2012-03-28 19:07 /sys/class/tty/ttyS0/device/driver -> ../../../bus/pnp/drivers/serial/
lrwxrwxrwx 1 root root 0 2012-03-28 19:07 /sys/class/tty/ttyS1/device/driver -> ../../../bus/pnp/drivers/serial/
lrwxrwxrwx 1 root root 0 2012-03-28 19:07 /sys/class/tty/ttyS2/device/driver -> ../../../bus/platform/drivers/serial8250/
lrwxrwxrwx 1 root root 0 2012-03-28 19:07 /sys/class/tty/ttyS3/device/driver -> ../../../bus/platform/drivers/serial8250/
lrwxrwxrwx 1 root root 0 2012-03-28 20:43 /sys/class/tty/ttyUSB0/device/driver -> ../../../../../../../../bus/usb-serial/drivers/ftdi_sio/
lrwxrwxrwx 1 root root 0 2012-03-28 21:15 /sys/class/tty/ttyUSB1/device/driver -> ../../../../../../../../bus/usb-serial/drivers/ftdi_sio/


In recent kernels (not sure since when) you can list the contents of /dev/serial to get a list of the serial ports on your system. They are actually symlinks pointing to the correct /dev/ node:

flu0@laptop:~$ ls /dev/serial/
total 0
drwxr-xr-x 2 root root 60 2011-07-20 17:12 by-id/
drwxr-xr-x 2 root root 60 2011-07-20 17:12 by-path/
flu0@laptop:~$ ls /dev/serial/by-id/
total 0
lrwxrwxrwx 1 root root 13 2011-07-20 17:12 usb-Prolific_Technology_Inc._USB-Serial_Controller-if00-port0 -> ../../ttyUSB0
flu0@laptop:~$ ls /dev/serial/by-path/
total 0
lrwxrwxrwx 1 root root 13 2011-07-20 17:12 pci-0000:00:0b.0-usb-0:3:1.0-port0 -> ../../ttyUSB0

This is a USB-Serial adapter, as you can see. Note that when there are no serial ports on the system, the /dev/serial/ directory does not exists. Hope this helps :).


I found

dmesg | grep tty

doing the job.


I'm doing something like the following code. It works for USB-devices and also the stupid serial8250-devuices that we all have 30 of - but only a couple of them realy works.

Basically I use concept from previous answers. First enumerate all tty-devices in /sys/class/tty/. Devices that does not contain a /device subdir is filtered away. /sys/class/tty/console is such a device. Then the devices actually containing a devices in then accepted as valid serial-port depending on the target of the driver-symlink fx.

$ ls -al /sys/class/tty/ttyUSB0//device/driver
lrwxrwxrwx 1 root root 0 sep  6 21:28 /sys/class/tty/ttyUSB0//device/driver -> ../../../bus/platform/drivers/usbserial

and for ttyS0

$ ls -al /sys/class/tty/ttyS0//device/driver
lrwxrwxrwx 1 root root 0 sep  6 21:28 /sys/class/tty/ttyS0//device/driver -> ../../../bus/platform/drivers/serial8250

All drivers driven by serial8250 must be probes using the previously mentioned ioctl.

        if (ioctl(fd, TIOCGSERIAL, &serinfo)==0) {
            // If device type is no PORT_UNKNOWN we accept the port
            if (serinfo.type != PORT_UNKNOWN)
                the_port_is_valid

Only port reporting a valid device-type is valid.

The complete source for enumerating the serialports looks like this. Additions are welcome.

#include <stdlib.h>
#include <dirent.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
#include <termios.h>
#include <sys/ioctl.h>
#include <linux/serial.h>

#include <iostream>
#include <list>

using namespace std;

static string get_driver(const string& tty) {
    struct stat st;
    string devicedir = tty;

    // Append '/device' to the tty-path
    devicedir += "/device";

    // Stat the devicedir and handle it if it is a symlink
    if (lstat(devicedir.c_str(), &st)==0 && S_ISLNK(st.st_mode)) {
        char buffer[1024];
        memset(buffer, 0, sizeof(buffer));

        // Append '/driver' and return basename of the target
        devicedir += "/driver";

        if (readlink(devicedir.c_str(), buffer, sizeof(buffer)) > 0)
            return basename(buffer);
    }
    return "";
}

static void register_comport( list<string>& comList, list<string>& comList8250, const string& dir) {
    // Get the driver the device is using
    string driver = get_driver(dir);

    // Skip devices without a driver
    if (driver.size() > 0) {
        string devfile = string("/dev/") + basename(dir.c_str());

        // Put serial8250-devices in a seperate list
        if (driver == "serial8250") {
            comList8250.push_back(devfile);
        } else
            comList.push_back(devfile); 
    }
}

static void probe_serial8250_comports(list<string>& comList, list<string> comList8250) {
    struct serial_struct serinfo;
    list<string>::iterator it = comList8250.begin();

    // Iterate over all serial8250-devices
    while (it != comList8250.end()) {

        // Try to open the device
        int fd = open((*it).c_str(), O_RDWR | O_NONBLOCK | O_NOCTTY);

        if (fd >= 0) {
            // Get serial_info
            if (ioctl(fd, TIOCGSERIAL, &serinfo)==0) {
                // If device type is no PORT_UNKNOWN we accept the port
                if (serinfo.type != PORT_UNKNOWN)
                    comList.push_back(*it);
            }
            close(fd);
        }
        it ++;
    }
}

list<string> getComList() {
    int n;
    struct dirent **namelist;
    list<string> comList;
    list<string> comList8250;
    const char* sysdir = "/sys/class/tty/";

    // Scan through /sys/class/tty - it contains all tty-devices in the system
    n = scandir(sysdir, &namelist, NULL, NULL);
    if (n < 0)
        perror("scandir");
    else {
        while (n--) {
            if (strcmp(namelist[n]->d_name,"..") && strcmp(namelist[n]->d_name,".")) {

                // Construct full absolute file path
                string devicedir = sysdir;
                devicedir += namelist[n]->d_name;

                // Register the device
                register_comport(comList, comList8250, devicedir);
            }
            free(namelist[n]);
        }
        free(namelist);
    }

    // Only non-serial8250 has been added to comList without any further testing
    // serial8250-devices must be probe to check for validity
    probe_serial8250_comports(comList, comList8250);

    // Return the lsit of detected comports
    return comList;
}


int main() {
    list<string> l = getComList();

    list<string>::iterator it = l.begin();
    while (it != l.end()) {
        cout << *it << endl;
        it++;
    }

    return 0;   
}


I think I found the answer in my kernel source documentation: /usr/src/linux-2.6.37-rc3/Documentation/filesystems/proc.txt

1.7 TTY info in /proc/tty
-------------------------

Information about  the  available  and actually used tty's can be found in the
directory /proc/tty.You'll  find  entries  for drivers and line disciplines in
this directory, as shown in Table 1-11.


Table 1-11: Files in /proc/tty
..............................................................................
 File          Content                                        
 drivers       list of drivers and their usage                
 ldiscs        registered line disciplines                    
 driver/serial usage statistic and status of single tty lines 
..............................................................................

To see  which  tty's  are  currently in use, you can simply look into the file
/proc/tty/drivers:

  > cat /proc/tty/drivers 
  pty_slave            /dev/pts      136   0-255 pty:slave 
  pty_master           /dev/ptm      128   0-255 pty:master 
  pty_slave            /dev/ttyp       3   0-255 pty:slave 
  pty_master           /dev/pty        2   0-255 pty:master 
  serial               /dev/cua        5   64-67 serial:callout 
  serial               /dev/ttyS       4   64-67 serial 
  /dev/tty0            /dev/tty0       4       0 system:vtmaster 
  /dev/ptmx            /dev/ptmx       5       2 system 
  /dev/console         /dev/console    5       1 system:console 
  /dev/tty             /dev/tty        5       0 system:/dev/tty 
  unknown              /dev/tty        4    1-63 console 

Here is a link to this file: http://git.kernel.org/?p=linux/kernel/git/next/linux-next.git;a=blob_plain;f=Documentation/filesystems/proc.txt;hb=e8883f8057c0f7c9950fa9f20568f37bfa62f34a


setserial with the -g option appears to do what you want and the C source is available at http://www.koders.com/c/fid39344DABD14604E70DF1B8FEA7D920A94AF78BF8.aspx.


I have no serial device here to test it, but if you have python and dbus you can try it yourself.

import dbus
bus = dbus.SystemBus()
hwmanager = bus.get_object('org.freedesktop.Hal', '/org/freedesktop/Hal/Manager')
hwmanager_i = dbus.Interface(hwmanager, 'org.freedesktop.Hal.Manager')
print hwmanager_i.FindDeviceByCapability("serial")

If it fails you can search inside hwmanager_i.GetAllDevicesWithProperties() to see if the capability name "serial" that I just guessed has a different name.

HTH


Using /proc/tty/drivers only indicates which tty drivers are loaded. If you're looking for a list of the serial ports check out /dev/serial, it will have two subdirectories: by-id and by-path.

EX:

# find . -type l
./by-path/usb-0:1.1:1.0-port0
./by-id/usb-Prolific_Technology_Inc._USB-Serial_Controller-if00-port0

Thanks to this post: https://superuser.com/questions/131044/how-do-i-know-which-dev-ttys-is-my-serial-port


My approach via group dialout to get every tty with user 'dialout' ls -l /dev/tty* | grep 'dialout' to only get its folder ls -l /dev/tty* | grep 'dialout' | rev | cut -d " " -f1 | rev

easy listen to the tty output e.g. when arduino serial out: head --lines 1 < /dev/ttyUSB0

listen to every tty out for one line only: for i in $(ls -l /dev/tty* | grep 'dialout' | rev | cut -d " " -f1 | rev); do head --lines 1 < $i; done

I really like the approach via looking for drivers: ll /sys/class/tty/*/device/driver

You can pick the tty-Name now: ls /sys/class/tty/*/device/driver | grep 'driver' | cut -d "/" -f 5


I do not have a USB serial device, but there must be a way to find the real ports using the HAL libraries directly:

====================================================================
#! /usr/bin/env bash
#
# Uses HAL to find existing serial hardware
#

for sport in $(hal-find-by-capability --capability serial) ; do
  hal-get-property --udi "${sport}" --key serial.device
done

====================================================================

The posted python-dbus code nor this sh script lists the bluetooth /dev/rfcomm* devices, so it is not the best solution.

Note that on other unix platforms, the serial ports are not named ttyS? and even in linux, some serial cards allow you to name the devices. Assuming a pattern in the serial devices names is wrong.


The serial communication manager library has many API and features targeted for the task you want. If the device is a USB-UART its VID/PID can be used. If the device is BT-SPP than platform specific APIs can be used. Take a look at this project for serial port programming: https://github.com/RishiGupta12/serial-communication-manager


#dmesg | grep tty

This command show you every port


My solution is based on udev library and below code is based on the example2:

#include <string.h>
#include <libudev.h>

bool enumerate_serial_ports(void)
{
    struct udev* udev;
    struct udev_enumerate* enumerate;
    struct udev_list_entry* devices, *dev_list_entry;

    /* create udev object */
    udev = udev_new();
    if (!udev) 
    {
        SPDLOG_ERROR("Cannot create udev context.");
        return false;
    }

    /* create enumerate object */
    enumerate = udev_enumerate_new(udev);
    if (!enumerate) 
    {
        SPDLOG_ERROR("Cannot create enumerate context.");

        udev_unref(udev);
        return false;
    }

    udev_enumerate_add_match_subsystem(enumerate, "tty");
    udev_enumerate_scan_devices(enumerate);

    /* fillup device list */
    devices = udev_enumerate_get_list_entry(enumerate);
    if (!devices) 
    {
        SPDLOG_ERROR("Failed to get device list.");

        udev_enumerate_unref(enumerate);
        udev_unref(udev);
        return false;
    }

    udev_list_entry_foreach(dev_list_entry, devices) 
    {
        struct udev_device* dev = udev_device_new_from_syspath(udev, udev_list_entry_get_name(dev_list_entry));

        // filter out virtual ports
        if((udev_device_get_sysnum(dev) != NULL) && (strstr(udev_device_get_devpath(dev), "/devices/virtual/") == NULL))
        {
            SPDLOG_DEBUG("subsystem={}", udev_device_get_subsystem(dev));
            SPDLOG_DEBUG("syspath={}", udev_device_get_syspath(dev));
            SPDLOG_DEBUG("sysname={}", udev_device_get_sysname(dev));
            SPDLOG_DEBUG("sysnum={}", udev_device_get_sysnum(dev));
            SPDLOG_DEBUG("devnode={}", udev_device_get_devnode(dev));
            SPDLOG_DEBUG("-----------------------------------------");
        }    
        
        /* free dev */
        udev_device_unref(dev);
    }

    /* free enumerate */
    udev_enumerate_unref(enumerate);
    /* free udev */
    udev_unref(udev);

    return true;
}

And the output on a RPI4 with an USB serial adaptor:

[ debug ][11:50:47.645] - subsystem=tty
[ debug ][11:50:47.645] - syspath=/sys/devices/platform/scb/fd500000.pcie/pci0000:00/0000:00:00.0/0000:01:00.0/usb1/1-1/1-1.3/1-1.3:1.0/ttyUSB0/tty/ttyUSB0
[ debug ][11:50:47.645] - sysname=ttyUSB0
[ debug ][11:50:47.645] - sysnum=0
[ debug ][11:50:47.645] - devnode=/dev/ttyUSB0
[ debug ][11:50:47.645] - -----------------------------------------
                           
[ debug ][11:50:47.645] - subsystem=tty
[ debug ][11:50:47.645] - syspath=/sys/devices/platform/soc/fe201000.serial/tty/ttyAMA0
[ debug ][11:50:47.645] - sysname=ttyAMA0
[ debug ][11:50:47.645] - sysnum=0
[ debug ][11:50:47.645] - devnode=/dev/ttyAMA0
[ debug ][11:50:47.645] - -----------------------------------------
                           
[ debug ][11:50:47.646] - subsystem=tty
[ debug ][11:50:47.646] - syspath=/sys/devices/platform/soc/fe215040.serial/tty/ttyS0
[ debug ][11:50:47.646] - sysname=ttyS0
[ debug ][11:50:47.646] - sysnum=0
[ debug ][11:50:47.646] - devnode=/dev/ttyS0
[ debug ][11:50:47.646] - -----------------------------------------


yes, I know, I'm too late (as always). Here is my piece of code (based on the reply of mk2). Maybe this helps someone:

std::vector<std::string> find_serial_ports()
{
 std::vector<std::string> ports;
    std::filesystem::path kdr_path{"/proc/tty/drivers"};
    if (std::filesystem::exists(kdr_path))
    {
        std::ifstream ifile(kdr_path.generic_string());
        std::string line;
        std::vector<std::string> prefixes;
        while (std::getline(ifile, line))
        {
            std::vector<std::string> items;
            auto it = line.find_first_not_of(' ');
            while (it != std::string::npos)
            {

                auto it2 = line.substr(it).find_first_of(' ');
                if (it2 == std::string::npos)
                {
                    items.push_back(line.substr(it));
                    break;
                }
                it2 += it;
                items.push_back(line.substr(it, it2 - it));
                it = it2 + line.substr(it2).find_first_not_of(' ');
            }
            if (items.size() >= 5)
            {
                if (items[4] == "serial" && items[0].find("serial") != std::string::npos)
                {
                    prefixes.emplace_back(items[1]);
                }
            }
        }
        ifile.close();
        for (auto& p: std::filesystem::directory_iterator("/dev"))
        {
            for (const auto& pf : prefixes)
            {
                auto dev_path = p.path().generic_string();
                if (dev_path.size() >= pf.size() && std::equal(dev_path.begin(), dev_path.begin() + pf.size(), pf.begin()))
                {
                    ports.emplace_back(dev_path);
                }
            }
        }
    }
    return ports;
}


Using setserial tool:

setserial -gG /dev/{ttyUSB,ttyS,ttyACM}* 2>/dev/null | grep -Ev "ttyS[0-9]+.*irq\s0\s*"

And if you want only the port device path on the output:

setserial -gG /dev/{ttyUSB,ttyS,ttyACM}* 2>/dev/null | grep -Ev "ttyS[0-9]+.*irq\s0\s*" | cut -d' ' -f1

Possibly this solutions are not applicable to all needs, since some USB devices can be named in another way by UDEV, so more generic but less optimal (NOT RECOMMENDED):

setserial -gG /dev/* 2>/dev/null | grep -Ev "ttyS[0-9]+.*irq\s0\s*" | cut -d' ' -f1
0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜