Finding the name of your USB device, given a vendor ID, under ubuntu

968 views
Skip to first unread message

Michael Wimble

unread,
Nov 18, 2015, 3:22:29 AM11/18/15
to HomeBrew Robotics Club
My current robot has the following sensors coming in over USB:
  • RoboClaw motor driver
  • RPLidar laser scanner
  • 9 degree of freedom IMU
My robot "Farryn" is running a Ubuntu image on a Raspberry PI 2. Every time the system starts up, or any of the USB devices are plugged in, I get possibly different device names for each of the USB devices. In a previous article, I told you how you could use the udev system to form a symbolic link for each USB device, based upon it's vendor ID. That was true, but the symbolic link formed is not to the character device. It's to a lower level device. And, you cannot perform certain operations, such as some control operations on that device.

My challenge then was to find some way of discovering the name of the character device under the "/dev" directory for a specific USB device. For instance, my RoboClaw gets a name of ttyUSBx, where "x" is some digit, if I plug the device into a USB hub (if it is discovered at all), but gets a similar name or sometimes "ttyACM0" if plugged directory into the Raspberry PI 2 without going though a hub. 

With a lot of digging, I figured out how to traverse the "/sys/bus/usb/devices" tree to discover if there is a known USB device with the required vendor ID. Each of my devices, so far, has a unique vendor ID. If that is not sufficient, you could enhance my code to also test for the product ID.

If you have a USB device plugged in, you can do a "lsusb -v" command and filter through all the lines to discover your device's vendor and product IDs. But that's not the point of this little paper.

The function code is posted below. It returns a pointer to the device name (e.g., "/dev/ttyACM0") for the first USB device found that has the given vendor ID, or NULL if the device is not found. For example, to find the character device name of my RoboClaw USB device, here is some sample code:

int main(void) {
    char *match = findUsbDeviceByVendorId("03eb"); // idVendor value for the RoboClaw device.
    if (match) {
        printf("MATCH: '%s'\n", match);
    } else {
        printf("!!! NO MATCH\n");
    }
}

And "match" might be "/dev/ttyUSB0", "/dev/ttyACM0", "/dev/ttyUSB3", etc. I can use that string to do a file-open and then control the RoboClaw device using my custom robot base driver code.

You own the heap-allocated result returned by findUsbDeviceByVendorId, so be sure to free it if when you are done with the string. Here is the function. My next step will be to turn this into an executable service by itself so you can invoke it in scripts. There are printfs in there for debugging. You should probably remove them as well.

Oh, here is the gist of the code.

Under "/sys/bus/usb/devices/" there will be a subdirectory whose name is based on the USB bus/port chain for a particular device, such as "1-1.2.1". For interesting devices, there is a file within there called "idProduct" and "idVendor" which is a carriage-return delimited string containing the 4-character ASCII representation of the idProduct or idVendor for the device. I use glob expressions to find all the directories that have an idVendor file, then I read the idVendor strings and compare to the desired value. If I find a match, I then need to find the associated device name. Well, within a subdirectory, such as within "/sys/bus/usb/devices/1-1.2.1", there are other subdirectories for various things. One of those subdirectories will have a name that begins with the prefix "tty", and within that subdirectory is yet another subdirectory whose name begins with "tty" and the name of that subdirectory is actually the device name under the "/dev" directory. Simple, huh? Well, I use glob expressions to find the appropriate first of those subdirectories whose name begins with "tty", and then I read the entries in that last subdirectory to find the directory containing the real device name. I then form a string on the heap to point to the real device name and return it.


char* findUsbDeviceByVendorId(const char* vendorId) {
    glob_t vendorGlob;
    int vendorGlobErr = 0;
    
    // Start by looking for all USB devices in the system that have an 'idVendor' file.
    if ((vendorGlobErr = glob("/sys/bus/usb/devices/*/idVendor", 0, NULL, &vendorGlob)) == 0) {

        // Found one or more idVendor files. Find the one wth a value matching the parameter.
        for (unsigned int vendorGlobI = 0; vendorGlobI < vendorGlob.gl_pathc; vendorGlobI++) {
            FILE* vendorFile = fopen(vendorGlob.gl_pathv[vendorGlobI], "r");
            if (vendorFile != NULL) {
                char *vendor = NULL;
                size_t len = 0;
                getline(&vendor, &len, vendorFile);
                if (vendor[strlen(vendor) - 1] == '\n') vendor[strlen(vendor) - 1] = '\0'; // Remove any trailing space.

                printf("idVendor path[%d]: '%s', value: %s\n", vendorGlobI, vendorGlob.gl_pathv[vendorGlobI], vendor);
                
                if (strcmp(vendor, vendorId) != 0) {
                    // Not a match, go on to next one.
                    if (vendor) free(vendor);
                    continue;
                } else {
                    printf("Found matching vendor value: '%s' at path: '%s'\n", vendor, vendorGlob.gl_pathv[vendorGlobI]);
                }
                
                if (vendor) free(vendor);
                
                // Pull out the path of the directory containing the idVendor file.
                regex_t parentRegex;
                regmatch_t parentRegexMatch[2];
                int regexResult;
                if (regcomp(&parentRegex, "^(.*)idVendor[ \t\n]*$", REG_EXTENDED)) {
                    printf("!!! Unable to compile regex\n");
                    exit(EXIT_FAILURE);
                }

                if ((regexResult = regexec(&parentRegex, vendorGlob.gl_pathv[vendorGlobI], 2, parentRegexMatch, 0)) == 0) {
                    // Regex pulled out the parent path prefix from the idVendor full path. Form a path for just the parent directory.
                    char parentPath[strlen(vendorGlob.gl_pathv[vendorGlobI])];
                    strcpy(parentPath, vendorGlob.gl_pathv[vendorGlobI]);
                    parentPath[parentRegexMatch[1].rm_eo] = '\0';
                    printf("... idVendor parent path: '%s'\n", parentPath);
                    
                    // Find subdirectories two levels down that have the name "tty" that are themselves a subdirectory
                    // of a directory that has "tty" as part of it's name prefix. 
                    glob_t ttyGlob;
                    int ttyGlobErr = 0;
                    const char *TTY_GLOB_SUFFIX = "*/tty*/tty";
                    char ttyGlobPath[strlen(parentPath) + strlen(TTY_GLOB_SUFFIX) + 1]; // Reserve enough space for the glab expression.
                    strcpy(ttyGlobPath, parentPath); // Start to form the glob expression.
                    strcpy(&ttyGlobPath[strlen(ttyGlobPath)], TTY_GLOB_SUFFIX); // Finish the glob expresison.
                    printf("... ttyGlobPath: '%s'\n", ttyGlobPath);
                    if ((ttyGlobErr = glob(ttyGlobPath, 0, NULL, &ttyGlob)) == 0) {
                        // We have found the required directory. There will be a directory under here whose name is the
                        // device name under "/dev" for the USB device. E.g., if the directory is "ttyUSB0", then the
                        // corresponding device name is "/dev/ttyUSB0".
                        for (unsigned int ttyGlobI = 0; ttyGlobI < ttyGlob.gl_pathc; ttyGlobI++) {
                            printf("... ... tty path[%d]: '%s'\n", ttyGlobI, ttyGlob.gl_pathv[ttyGlobI]);
                            DIR *ttyDir;
                            if ((ttyDir = opendir(ttyGlob.gl_pathv[ttyGlobI])) != NULL) {
                                struct dirent *ttySubDir;
                                while ((ttySubDir = readdir(ttyDir)) != NULL) {
                                    if (ttySubDir->d_name[0] == '.') continue;
                                    printf("### device name: '%s'\n", ttySubDir->d_name);
                                    const char *RESULT_PREFIX = "/dev/";
                                    char *result = (char*) malloc(strlen(ttySubDir->d_name) + strlen(RESULT_PREFIX) + 1);
                                    strcpy(result, RESULT_PREFIX);
                                    strcpy(&result[strlen(RESULT_PREFIX)], ttySubDir->d_name);
                                    return result;
                                }
                                
                                // Didn't find any directories in the subdirectory.
                                return NULL;
                            } else {
                                // Could not read the subdirectory.
                                return NULL;
                            }
                        } // for
                        
                        // No subdirectory found at all.
                        return NULL;
                    } else {
                        // Didn't find the exprected subdirectory.
                        return NULL;
                    }
                } else {
                    // !!! Regex failure on idVendor glob.
                    return NULL;
                }
            } else {
                // Unable to read the idVendor file.
                return NULL;
            }
        } // for
        
        // No match found for the parameter vendor ID.
        return NULL;
    } else {
        // No vendor globs found.
        return NULL;
    }
}

Dave Curtis

unread,
Nov 18, 2015, 12:16:33 PM11/18/15
to hbrob...@googlegroups.com

> On Nov 18, 2015, at 12:22 AM, Michael Wimble <mwi...@gmail.com> wrote:
>
> My challenge then was to find some way of discovering the name of the character device under the "/dev" directory for a specific USB device. For instance, my RoboClaw gets a name of ttyUSBx, where "x" is some digit, if I plug the device into a USB hub (if it is discovered at all), but gets a similar name or sometimes "ttyACM0" if plugged directory into the Raspberry PI 2 without going though a hub.
>
Sounds like a broken hub. Device enumeration should change just because you are going through a hub.

Dave Hylands

unread,
Nov 18, 2015, 1:54:41 PM11/18/15
to hbrob...@googlegroups.com
I have some python code which uses udev to find the device name (i.e. /dev/ttyUSB0) of a USB device by vendor name, serial number, VID, PID or combinations thereof.

You can take a look here:

This page has a C example which uses libudev:
to print out similar information.

I wouldn't at all be surprised if libudev is using similar techniques (i.e. sysfs) under the covers as what your code sample does, but I would imagine that by using libudev might insulate yourself from future changes in sysfs.


--
You received this message because you are subscribed to the Google Groups "HomeBrew Robotics Club" group.
To unsubscribe from this group and stop receiving emails from it, send an email to hbrobotics+...@googlegroups.com.
To post to this group, send email to hbrob...@googlegroups.com.
Visit this group at http://groups.google.com/group/hbrobotics.
For more options, visit https://groups.google.com/d/optout.



--
Dave Hylands
Shuswap, BC, Canada
http://www.davehylands.com
Reply all
Reply to author
Forward
0 new messages