Monday, December 1, 2014

AcuRite Weather Station, Raspberry Pi and a USB interface; Part 2

In part 1 <link> I described the AcuRite weather station I have and how I wanted to hook into the USB interface so I could have the data and do anything I wanted to with it.  I also gave instructions on how to modify certain files such that you have a device that a user can read.  Now, we have to hook some code to the USB port and actually deal with the device.  But first, we have to get a library that will help us along.

From my research, I only found one source that had the things I needed and enough documentation to actually stand a chance of implementing it, libusb.  Libusb is a multiplatform library to interface to a USB device from user space.  Let's discuss this a tiny bit since there are better sites out there to get this information from, but you need a tiny bit of information to understand what comes next.

On unix, there's users and the kernal.  The kernal is the ultimate controller of everything; it has access to all the facilities and takes care of portioning them out and keeping track of what goes on.  Users can't mess with the things the kernal controls, and that's a good thing.  We can't kill ourselves and have to restore the system from backup if we stay away from the things the kernal should be dealing with.  Unfortunately, the USB buss belongs to the kernal.  So, we need to use kernal interface routines to control the USB devices and those are really hard to understand.  Libusb gives us access to those interfaces in a reasonably simpler (not simple) way.  USB is very complex and nothing can make it easy, but it can, at least, be possible.

So where do we get libusb?  It's already on the machine, but the version there is several generations back and hard to link to.  I searched around and found out a few things that can help you keep from being as confused as I was when I first started this.

1. Almost all the web sites out there deal with a version of libusb that is outdated.  Libusb has gone through several changes and massive improvement this year (2014) and most sites don't work with the newest stuff.

2. As always, the newest stuff is more stable and has greater capabilities.

3.  In your searching, you'll run across libusb and libusbx.  These are two different things; libusbx is a fork of libusb that extended certain things.  The two were merged back together to form a new libusb that serves both projects.  So, web sites that talk about one of them, probably won't work with the latest.

4. You can't automatically install the latest libusb.  The packages on the Raspberry Pi are behind several versions.

5. Using the latest version, lots of the examples on the web won't even compile.

6. You WANT the latest version.

This all means you will have to get the latest version and compile it yourself.  There are two ways of doing this: get a download from the libusb website and install it, or get the source from github and install that.  I did both and they both worked, but I decided the best way for me is to grab it from github, because I know that represents what I want right now.  This may not be true in a couple of months, so consider a download from the libusb home site.

I used libusb-1.0.19 as my base.  You can get it from their (new) home site <link> , just click on 'download' and choose latest.  In a few weeks, this might not get you this version, so decide if you want the latest (I would) or if you want this one.  If you chose to go with 1.0.19, it'll be under the 'Previous Releases' area.  Download it and put it on the Pi somewhere.  Now you have to compile it.

You'll have to extract the sources; just use tar xvf filename and watch it fly by.  Then go into the new directory and read the file 'INSTALL'.  This is where they keep the build instructions, and for me, it was a simple configure, make, make install sequence and everything was done.  However, there's two caveats to this.  Make sure your Pi is up to date and install libudev-dev.

I had restored my Pi from backup first and the backup was from a while ago.  This meant that the software on the machine was months behind.  This gave me no end of trouble which I fixed by simply updating the machine:

sudo apt-get update
sudo apt-get upgrade

The trick to this is to do the update about 30 minutes before bedtime, then do the upgrade just before going to bed.  I don't know how long the upgrade took, I was asleep when it finished, but it took a long time.  You don't want to watch it, it's boring.  There is a confirmation prompt though, so wait for it to ask if you really want to do this, answer yes, and go to bed.  This works by the update going out and reading the various pacakages to see what the very latest is; it doesn't actually change anything except that.  The upgrade actually gets the new stuff and installs it.  So, the upgrade may have a whole lot to do and could take a while.

So, update, upgrade, get libusb but don't compile it.  You're still not ready.  Now you need to get libudev-dev.  This is an interface to the support functions that udev provides (remember udev from the last post?), and libusb needs them.  Fortunately, you can use apt-get to grab these:

sudo apt-get install libudev-dev

So, are you finally ready to compile libusb?  After reviewing the instruction in INSTALL in the directory you got with the release, go ahead and make the library.  However, be sure to sudo each step of the process.  If you don't errors happen and with the huge amount of stuff printed, it's real easy to miss something.  For me, it was:

sudo ./configure
sudo make
sudo make install

Now you're finally ready to actually do something.  Libusb is a 'c' library, so I used c code to deal with it.  I know, python is my usual preference, but when in Rome ...  Remember, most of the examples on the web won't work with the latest libusb, so it was a bit tough getting things to work.  I finally had to just step through some of them a line at a time and adapt them to work with the latest.  It was a royal pain, but it did help me to learn a lot about what I was doing.

So, here's the first code to deal with the USB device that we're going to eventually use:

usbexample1.c
/*
    Experimentation with a USB interface to the Acu-Rite 5 in 1 Weatherstation
    specifically for the Raspberry Pi.
    
    Because there likely to be a version of libusb and the associated header file
    on a Pi, use the command line below to build it since the build of libusb-1.0.19
    places things in /usr/local
    
    cc usbexample1.c -L/usr/local/lib -lusb-1.0    
    use ldd weatherstation to check which libraries are linked in.
    If you still have trouble with compilation, remember that cc has a -v
    parameter that can help you unwind what is happening.
*/

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <time.h>
#include <unistd.h>
#include <sys/time.h>
#include <libusb-1.0/libusb.h>

// The vendor id and product number for the AcuRite 5 in 1 weather head.
#define VENDOR 0x24c0
#define PRODUCT 0x0003

// I store things about the weather device USB connection here.
struct {
    libusb_device *device;
    libusb_device_handle *handle;
    int verbose;
} weatherStation;

/*
This code is related to dealing with the USB device
*/
// This searches the USB bus tree to find the device
int findDevice(libusb_device **devs)
{
    libusb_device *dev;
    int err = 0, i = 0, j = 0;
    uint8_t path[8]; 
    
    while ((dev = devs[i++]) != NULL) {
        struct libusb_device_descriptor desc;
        int r = libusb_get_device_descriptor(dev, &desc);
        if (r < 0) {
            fprintf(stderr,"Couldn't get device descriptor, %s\n", libusb_strerror(err));
            return(1);
        }

        fprintf(stderr,"%04x:%04x (bus %d, device %d)",
            desc.idVendor, desc.idProduct,
            libusb_get_bus_number(dev), libusb_get_device_address(dev));

        //r = libusb_get_port_numbers(dev, path, sizeof(path));
        //if (r > 0) {
        //  fprintf(stderr," path: %d", path[0]);
        //  for (j = 1; j < r; j++)
        //      fprintf(stderr,".%d", path[j]);
        //}
        fprintf(stderr,"\n");
        
        if (desc.idVendor == VENDOR && desc.idProduct == PRODUCT){
            fprintf(stderr,"Found the one I want\n");
            weatherStation.device = dev;
            return (1);
        }
    }
    return(0);
}

// to handle testing and try to be clean about closing the USB device,
// I'll catch the signal and close off.

// I do several things here that aren't strictly necessary.  As I learned about
// libusb, I tried things and also used various techniques to learn about the 
// weatherstation's implementation.  I left a lot of it in here in case I needed to
// use it later.  Someone may find it useful to hack into some other device.
int main(void)
{
    libusb_device **devs;
    int r, err;
    ssize_t cnt;

    err = libusb_init(NULL);
    if (err < 0){
        fprintf(stderr,"Couldn't init usblib, %s\n", libusb_strerror(err));
        exit(1);
    }
    // This is where you can get debug output from libusb.
    // just set it to LIBUSB_LOG_LEVEL_DEBUG
    libusb_set_debug(NULL, LIBUSB_LOG_LEVEL_INFO);
    
    cnt = libusb_get_device_list(NULL, &devs);
    if (cnt < 0){
        fprintf(stderr,"Couldn't get device list, %s\n", libusb_strerror(err));
        exit(1);
    }
    // got get the device; the device handle is saved in weatherStation struct.
    if (!findDevice(devs)){
        fprintf(stderr,"Couldn't find the device\n");
        exit(1);
    }
    // Now I've found the weather station and can start to try stuff
    // So, I'll get the device descriptor
    struct libusb_device_descriptor deviceDesc;
    err = libusb_get_device_descriptor(weatherStation.device, &deviceDesc);
    if (err){
        fprintf(stderr,"Couldn't get device descriptor, %s\n", libusb_strerror(err));
        exit(1);
    }
    fprintf(stderr,"got the device descriptor back\n");
    
    // Open the device and save the handle in the weatherStation struct
    err = libusb_open(weatherStation.device, &weatherStation.handle);
    if (err){
        fprintf(stderr,"Open failed, %s\n", libusb_strerror(err));
        exit(1);
    }
    fprintf(stderr,"I was able to open it\n");
    // Now that it's opened, I can free the list of all devices
    libusb_free_device_list(devs, 1); // Documentation says to get rid of the list
                                      // Once I have the device I need
    fprintf(stderr,"Released the device list\n");
    // Now I have to check to see if the kernal using udev has attached
    // a driver to the device.  If it has, it has to be detached so I can
    // use the device.
    if(libusb_kernel_driver_active(weatherStation.handle, 0) == 1) { //find out if kernel driver is attached
        fprintf(stderr,"Kernal driver active\n");
        if(libusb_detach_kernel_driver(weatherStation.handle, 0) == 0) //detach it
            fprintf(stderr,"Kernel Driver Detached!\n");
    }
    int activeConfig;
    err =libusb_get_configuration   (weatherStation.handle, &activeConfig);
    if (err){
        fprintf(stderr,"Can't get current active configuration, %s\n", libusb_strerror(err));;
        exit(1);
    }
    fprintf(stderr,"Currently active configuration is %d\n", activeConfig);

    err = libusb_set_configuration  (weatherStation.handle, 1);
    if (err){
        fprintf(stderr,"Cannot set configuration, %s\n", libusb_strerror(err));;
        exit(1);
    }
    fprintf(stderr,"Just did the set configuration\n");

    
    err = libusb_claim_interface(weatherStation.handle, 0); //claim interface 0 (the first) of device (mine had jsut 1)
    if(err) {
        fprintf(stderr,"Cannot claim interface, %s\n", libusb_strerror(err));
        exit(1);
    }
    fprintf(stderr,"Claimed Interface\n");
    fprintf(stderr,"Number of configurations: %d\n",deviceDesc.bNumConfigurations);
    struct libusb_config_descriptor *config;
    libusb_get_config_descriptor(weatherStation.device, 0, &config);
    fprintf(stderr,"Number of Interfaces: %d\n",(int)config->bNumInterfaces);
    // I know, the device only has one interface, but I wanted this code
    // to serve as a reference for some future hack into some other device,
    // so I put this loop to show the other interfaces that may
    // be there.  And, like most of this module, I stole the ideas from
    // somewhere, but I can't remember where (I guess it's google overload)
    const struct libusb_interface *inter;
    const struct libusb_interface_descriptor *interdesc;
    const struct libusb_endpoint_descriptor *epdesc;
    int i, j, k;
    for(i=0; i<(int)config->bNumInterfaces; i++) {
        inter = &config->interface[i];
        fprintf(stderr,"Number of alternate settings: %d\n", inter->num_altsetting);
        for(j=0; j < inter->num_altsetting; j++) {
            interdesc = &inter->altsetting[j];
            fprintf(stderr,"Interface Number: %d\n", (int)interdesc->bInterfaceNumber);
            fprintf(stderr,"Number of endpoints: %d\n", (int)interdesc->bNumEndpoints);
            for(k=0; k < (int)interdesc->bNumEndpoints; k++) {
                epdesc = &interdesc->endpoint[k];
                fprintf(stderr,"Descriptor Type: %d\n",(int)epdesc->bDescriptorType);
                fprintf(stderr,"Endpoint Address: 0x%0.2X\n",(int)epdesc->bEndpointAddress);
                // Below is how to tell which direction the 
                // endpoint is supposed to work.  It's the high order bit
                // in the endpoint address.  I guess they wanted to hide it.
                fprintf(stderr," Direction is ");
                if ((int)epdesc->bEndpointAddress & LIBUSB_ENDPOINT_IN != 0)
                    fprintf(stderr," In (device to host)");
                else
                    fprintf(stderr," Out (host to device)");
                fprintf(stderr,"\n");
            }
        }
    }
    //OK, done with it, close off and let it go.
    fprintf(stderr,"Done with device, release and close it\n");
    err = libusb_release_interface(weatherStation.handle, 0); //release the claimed interface
    if(err) {
        fprintf(stderr,"Couldn't release interface, %s\n", libusb_strerror(err));
        exit(1);
    }
    libusb_close(weatherStation.handle);
    libusb_exit(NULL);
    exit(0);
}

Remember I told you that the installation process of libusb puts it in /usr/local?  Well, it's a little bit worse than that.  The actual directory it puts the header file libusb.h in is /usr/local/include/libusb-1.0 which makes it a bit more complicated to deal with.  In the source file above I used the line:

#include <libusb-1.0/libusb.h>

They also put the libraries we need in the directory /usr/local/lib, which is pretty normal.  These things make the command line to the compiler:

cc  usbexample1.c -L/usr/local/lib -lusb-1.0

because the compiler defaults to look in usr/local/include for the libusb.h file, but you have to get specific for the library.  Pay attention here, I fought this for a while because I was picking up the wrong library and some symbols weren't defined.    If you get into trouble and just can't figure out what is going wrong, the compiler has a -v flag that will print what it's doing step by step.  Carefully read this output and you should see where you're having a problem.  It outputs a lot of data though, so it isn't easy.

cc  -v usbexample1.c -L/usr/local/lib -lusb-1.0

Once you get it to compile, you'll be left with an a.out file that you can run to see what happens.  Just type in ./a.out and look at the output:

pi@deserthome:~/src$ ./a.out
24c0:0003 (bus 1, device 4)
Found the one I want
got the device descriptor back
I was able to open it
Released the device list
Currently active configuration is 1
Just did the set configuration
Claimed Interface
Number of configurations: 1
Number of Interfaces: 1
Number of alternate settings: 1
Interface Number: 0
Number of endpoints: 1
Descriptor Type: 5
Endpoint Address: 0x81
 Direction is  In (device to host)
Done with device, release and close it

This tells you that the code was able to find the device based on the vendor and id number, then through various arcane machinations, it was able to open it.  Then it just released it and closed it.

I put a lot of comments in the file, so you should be able to follow along and use the libusb api documentation <link> to understand it more fully.  Also notice that I sent all output to stderr.  For those of you that didn't understand that, Linux has both stderr and stdout.  Stdout is the normal place you send text you want to read and stderr is for errors.  There's a good reason for this, and I'll tell you all about it in the next post.

I will have the code above available on github soon, I just haven't done it yet.  So, if you're in a hurry, do a little copy and paste to create the file.  This code can also serve as a current (sort of, since things change pretty quickly) example of how to get to and reserve a USB device.  In the next installment I get more specific to the weather station.

So now we have the Pi set up with a USB device that is hooked to the weather station and some code that can attach to it and open it.  Next we have to read the data and decode it.

Have fun.

Part three of this series is here <link>.

9 comments:

  1. Hi Dave,

    This is awesome! Unfortunately, I am stuck with an error in the compilation of usbexample1.c (see below). I followed all your tips and tricks. Any idea what is going on?

    Your input would be much appreciated!

    <<-- cc -v output (shortened)-->>

    cc -v usbexample1.c -L/usr/local/lib -lusb-1.0
    Using built-in specs.
    COLLECT_GCC=cc
    COLLECT_LTO_WRAPPER=/usr/lib/gcc/x86_64-linux-gnu/4.8/lto-wrapper
    Target: x86_64-linux-gnu
    Configured with: ../src/configure -v --with-pkgversion='Ubuntu 4.8.2-19ubuntu1' --with-
    ...
    Thread model: posix
    gcc version 4.8.2 (Ubuntu 4.8.2-19ubuntu1)
    COLLECT_GCC_OPTIONS='-v' '-L/usr/local/lib' '-mtune=generic' '-march=x86-64'
    /usr/lib/gcc/x86_64-linux-gnu/4.8/cc1 -quiet -v -imultiarch x86_64-linux-gnu usbexample1.c -quiet -dumpbase usbexample1.c -mtune=generic -march=x86-64 -auxbase usbexample1 -version -fstack-protector -Wformat -Wformat-security -o /tmp/cc4rRTtK.s
    GNU C (Ubuntu 4.8.2-19ubuntu1) version 4.8.2 (x86_64-linux-gnu)
    compiled by GNU C version 4.8.2, GMP version 5.1.3, MPFR version 3.1.2-p3, MPC version 1.0.1
    GGC heuristics: --param ggc-min-expand=100 --param ggc-min-heapsize=131072
    ignoring nonexistent directory "/usr/local/include/x86_64-linux-gnu"
    ignoring nonexistent directory "/usr/lib/gcc/x86_64-linux-gnu/4.8/../../../../x86_64-linux-gnu/include"
    #include "..." search starts here:
    #include <...> search starts here:
    /usr/lib/gcc/x86_64-linux-gnu/4.8/include
    /usr/local/include
    /usr/lib/gcc/x86_64-linux-gnu/4.8/include-fixed
    /usr/include/x86_64-linux-gnu
    /usr/include
    End of search list.
    GNU C (Ubuntu 4.8.2-19ubuntu1) version 4.8.2 (x86_64-linux-gnu)
    compiled by GNU C version 4.8.2, GMP version 5.1.3, MPFR version 3.1.2-p3, MPC version 1.0.1
    GGC heuristics: --param ggc-min-expand=100 --param ggc-min-heapsize=131072
    Compiler executable checksum: dc75e0628c9356affcec059d0c81cc01
    usbexample1.c: In function ‘main’:
    usbexample1.c:179:17: warning: '0' flag ignored with precision and ‘%X’ gnu_printf format [-Wformat=]
    fprintf(stderr,"Endpoint Address: 0x%0.2X\n",(int)epdesc->bEndpointAddress);
    ^
    COLLECT_GCC_OPTIONS='-v' '-L/usr/local/lib' '-mtune=generic' '-march=x86-64'
    as -v --64 -o /tmp/ccZdHHPn.o /tmp/cc4rRTtK.s
    GNU assembler version 2.24 (x86_64-linux-gnu) using BFD version (GNU Binutils for Ubuntu) 2.24
    COMPILER_PATH=/usr/lib/gcc/x86_64-linux-gnu/4.8/:/usr/lib/gcc/x86_64-linux-
    ...
    COLLECT_GCC_OPTIONS='-v' '-L/usr/local/lib' '-mtune=generic' '-march=x86-64'
    ...

    <<-- a.out output -->
    24c0:0003 (bus 1, device 6)
    Found the one I want
    got the device descriptor back
    I was able to open it
    Released the device list
    Currently active configuration is 1
    Just did the set configuration
    Claimed Interface
    Number of configurations: 1
    libusb: warning [seek_to_next_config] config length mismatch wTotalLength 34 real 27
    libusb: warning [seek_to_next_config] config length mismatch wTotalLength 34 real 27
    libusb: error [parse_endpoint] short endpoint descriptor read 0/2
    libusb: error [raw_desc_to_config] parse_configuration failed with error -1

    ReplyDelete
    Replies
    1. I don't have a clue what is going on. I never tried this on an X86; my work has all been done on a Raspberry Pi. But id died when it was trying step through the iterfaces. Maybe take a look at the libusb repository and step through the issues on X64 builds. You may turn up something there.

      Delete
    2. Hi Dave,

      Thanks for pointing me in the builds direction. As it turns out the standard libusb that shipped with the latest Ubuntu 14.04 is what I need as opposed to the one you suggested to download for the Pi. Compiling it with those did the trick!

      cc -v usbexample1.c -lusb-1.0

      Now on with the project ...

      Delete
  2. Hi Dave, So far I have tried to execute the usbexample1.c from the user but it does not have the right permissions to do that. I was able to run it successfully from the root. Should I continue to the next step using the root or how can I make it so that the usbexample1.c will work from the user account? We went to the root and changed the permissions for the user, but nothing seemed to work so far. Do you have any advice?

    ReplyDelete
    Replies
    1. Just run it as root. You've got something, somewhere that depends on root permission and it can be a real pain to chase down and fix. I'd just use sudo to run it and get on with testing things. Later, when you get it doing what you want, look into the permission problem more deeply.

      Delete
  3. [August 2017]
    Platform: Pi W Zero

    Building libusb is a little different now and there are a couple of pre-reqs that, if not unix savvy, can send you on a google hunt. Latest versions is now 1.0.21
    * You need the auto* tools since there is no longer a simple executable 'configure' file in the root of the tarball
    * sudo apt-get install autoconf
    * This will bring in autoconf, automake, etc
    * sudo apt-get install libtool
    * This installs the libtools needed by the bootstrap scripts
    * Now we can configure!
    * sudo ./autogen.sh (creates the configure script)
    * sudo make
    * sudo make install

    :-D I'll Update more if I run into further deltas.

    ReplyDelete
    Replies
    1. PS: verified that weatherstation.c works with libusb-1.0.21. No issues.

      Delete
  4. Hi, I also have the 5-in-1 weather station and Smarthub device, but there is no USB connection anywhere. What am I missing. I would like to capture data from the weather station directly (or from the Smarthub). Any advice on how to proceed?

    ReplyDelete
    Replies
    1. I have blog posts on using the weatherhead directly on this site, and the code for it is available on github. I didn't work with the Smarthub, so I am no help at all on that device. Others have though, look around a bit.

      Delete