Tuesday, December 16, 2014

Acurite Weather Station, Raspberry Pi, USB driver, I messed up, Part 5

Part 4 of this series is here <link>

Yep, I messed up, here's the story:  I was logging data so I could try and decode the barometric sensor and was getting nowhere, so I thought I'd take the console apart to see what kind of pressure sensor it had in it.  So, about an hour later I plugged it back in and it didn't work.

No, I didn't suspect I broke it, everything worked except the actual read.  The reads timed out and then started responding that I was sending the wrong command.  No code had changed, what could be wrong?  After the usual routine of changing cables, checking power, plugging it in and out a bunch of times, it still didn't work.  I moved the console to my other Pi and started taking apart the code.

About a day later, I had tried everything I could think of then it dawned on me that something might be wrong with the initialization.  I found the problem, and it was with the way I set it up.  I removed the attachment of the hidraw driver from the port, and it was providing the initialization to the kernal that allowed the weather station to work.  That was a bad thing, since without this, the kernal couldn't talk to the station.

When I took the command out of the command.txt file and prevented the hidraw driver from connecting, the communication path was fine.  But (there's always one of those), my code couldn't connect a second time.  The first time I plugged it in, everything was fine, but if the device was reset, it started failing again.  This turned out to be caused by my code disconnecting the hidraw driver and not putting it back so it could initialize the device again.

Fortunately, there's fix for that; usblib has a call that will detach the kernal driver automatically and restore it when we're done.  I added that and the station can be plugged in and work, then work again the next time.  While I was in there, I cleaned up some comments, removed the superfluous code that I left in, and generally cleaned up a bit.

It's working fine now and back to everyday use while I look further into the barometric pressure, but that's yet another post on this device.  Meanwhile, here's the updated code for the usb driver portion, and the changes will be in Github in an hour or so.

/*
    Documentation at desert-home.com
    
    This is the actual weather module I run for controlling the house, as such, this 
    may not be what you want.  You may want other-stuff/weatherstation.c

    Experimentation with a USB interface to the Acu-Rite 5 in 1 Weatherstation
    specifically for the Raspberry Pi.  The code may work on other systems, but I 
    don't have one of those.  Contrary to other folk's thinking, I don't care if 
    this ever runs on a Windows PC or an Apple anything.
    
    I specifically used a model 2032 display with the 5 in 1 sensor that I picked
    up at one of those warehouse stores.  The display has a usb plug on it and 
    I thought it might be possible to read the usb port and massage the data myself.
    
    This code represents the result of that effort.
    
    I gathered ideas from all over the web.  I use the latest (for this second)
    libusb and about a thousand examples of various things that other people have 
    done and posted about.  Frankly, I simply can't remember all of them, so please,
    don't be offended if you see your ideas somewhere in here and it isn't attributed.
    
    I simply lost track of where I found what.
    
    This module relies on libusb version 1.0.19 which, at this time, can only be
    compiled from source on the raspberry pi.
    
    Because there's 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 -o weatherstation  weatherstation.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;
} weatherStation;
// These are the sensors the the 5 in 1 weather head provides
struct {
    float   windSpeed;
    time_t  wsTime;
    int     windDirection;
    time_t  wdTime;
    float   temperature;
    time_t  tTime;
    int     humidity;
    time_t  hTime;
    int     rainCounter;
    time_t  rcTime;
} weatherData;

// This is just a function prototype for the compiler
void closeUpAndLeave();

// I want to catch control-C and close down gracefully
void sig_handler(int signo)
{
  if (signo == SIGINT)
    fprintf(stderr,"Shutting down ...\n");
    closeUpAndLeave();
}

/*
This tiny thing simply takes the data and prints it so we can see it
*/
// Array to translate the integer direction provided to text
char *Direction[] = {
    "NNW",
    "NW",
    "WNW",
    "W",
    "WSW",
    "SW",
    "SSW",
    "S",
    "SSE",
    "SE",
    "ESE",
    "E",
    "ENE",
    "NE",
    "NNE",
    "N"  };
// this is a bitmapped byte to tell if the various styles of reports have
// come in.  Bit 0 is R1 first type, bit 2 is R1 type 2 and bit 3 is R2
// even though I don't use R2 yet
uint8_t reportsSeen = 0;

void showit(){

    // make sure enough reports have come in before reporting
    if( reportsSeen >= 3){  // Change this when report 3 is decoded
        fprintf(stdout, "{\"windSpeed\":{\"WS\":\"%.1f\",\"t\":\"%d\"},"
                        "\"windDirection\":{\"WD\":\"%s\",\"t\":\"%d\"},"
                        "\"temperature\":{\"T\":\"%.1f\",\"t\":\"%d\"},"
                        "\"humidity\":{\"H\":\"%d\",\"t\":\"%d\"},"
                        "\"rainCounter\":{\"RC\":\"%d\",\"t\":\"%d\"}}\n",
                weatherData.windSpeed, weatherData.wsTime,
                Direction[weatherData.windDirection],weatherData.wdTime,
                weatherData.temperature, weatherData.tTime,
                weatherData.humidity, weatherData.hTime,
                weatherData.rainCounter, weatherData.rcTime);
        fflush(stdout);
    }
}
/* 
This code translates the data from the 5 in 1 sensors to something 
that can be used by a human.
*/
float getWindSpeed(char *data){
    int leftSide = (data[3] & 0x1f) << 3;
    int rightSide = data[4] & 0x70 >> 4;
    // Yes, I use mph, never got used to kilometers.
    return((float)(leftSide | rightSide) * 0.62);
}
int getWindDirection(char *data){
    return(data[4] & 0x0f);
}
float getTemp(char *data){
    // This item spans bytes, have to reconstruct it
    int leftSide = (data[4] & 0x0f) << 7;
    int rightSide = data[5] & 0x7f;
    float combined = leftSide | rightSide;
    return((combined - 400) / 10.0);
}
int getHumidity(char *data){
    int howWet = data[6] &0x7f;
    return(howWet);
}
int getRainCount(char *data){
    int count = data[6] &0x7f;
    return(count);
}
// Now that I have the data from the station, do something useful with it.

void decode(char *data, int length, int noisy){
    //int i;
    //for(i=0; i<length; i++){
    //    fprintf(stderr,"%0.2X ",data[i]);
    //}
    //fprintf(stderr,"\n"); */
    reportsSeen |= 0x04;  // just pretend I've seen report 2 already
    time_t seconds = time (NULL);
    //There are two varieties of data, both of them have wind speed
    // first variety of the data
    if ((data[2] & 0x0f) == 1){ // this has wind speed, direction and rainfall
        if(noisy)
            fprintf(stderr,"Wind Speed: %.1f ",getWindSpeed(data));
        weatherData.windSpeed = getWindSpeed(data);
        weatherData.wsTime = seconds;
        if(noisy)
            fprintf(stderr,"Wind Direction: %s ",Direction[getWindDirection(data)]);
        weatherData.wdTime = seconds;
        weatherData.windDirection = getWindDirection(data);
        if(noisy){
            fprintf(stderr,"Rain Counter: %d ",getRainCount(data));
            fprintf(stderr,"\n");
        }
        weatherData.rainCounter = getRainCount(data);
        weatherData.rcTime = seconds;
        reportsSeen |= 0x01; //I've seen report 1 now
    }
    // this is the other variety
    if ((data[2] & 0x0f) == 8){ // this has wind speed, temp and relative humidity
        if(noisy)
            fprintf(stderr,"Wind Speed: %.1f ",getWindSpeed(data));
        weatherData.windSpeed = getWindSpeed(data);
        weatherData.wsTime = seconds;
        if(noisy)
            fprintf(stderr,"Temperature: %.1f ",getTemp(data));
        weatherData.temperature = getTemp(data);
        weatherData.tTime = seconds;
        if(noisy){
            fprintf(stderr,"Humidity: %d ", getHumidity(data));
            fprintf(stderr,"\n");
        }
        weatherData.humidity = getHumidity(data);
        weatherData.hTime = seconds;
        reportsSeen |= 0x02;  // I've seen report 2 now

    }
}
/*
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));
        fprintf(stderr,"\n");
        if (desc.idVendor == VENDOR && desc.idProduct == PRODUCT){
            fprintf(stderr,"Found the weather station\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.
void closeUpAndLeave(){
    //OK, done with it, close off and let it go.
    fprintf(stderr,"Done with device, release and close it\n");
    int err = libusb_release_interface(weatherStation.handle, 0); //release the claimed interface
    if(err) {
        fprintf(stderr,"Couldn't release interface, %s\n", libusb_strerror(err));
    }
    fprintf(stderr, " Released interface and restored kernal driver\n");
    libusb_close(weatherStation.handle);
    fprintf(stderr, " Closed Weatherstation device\n");
    libusb_exit(NULL);
    fprintf(stderr, " Closed USB interface\n");
    exit(0);
}

// This is where I read the USB device to get the latest data.
unsigned char data[50]; // where we want the data to go

int getit(int whichOne, int noisy){
    int actual; // how many bytes were actually read
    
    // The second parameter is bmRequestType and is a bitfield
    // See http://www.beyondlogic.org/usbnutshell/usb6.shtml
    // for the definitions of the various bits.  With libusb, the 
    // #defines for these are at:
    // http://libusb.sourceforge.net/api-1.0/group__misc.html#gga0b0933ae70744726cde11254c39fac91a20eca62c34d2d25be7e1776510184209

    actual = libusb_control_transfer(weatherStation.handle, 
        LIBUSB_REQUEST_TYPE_CLASS | LIBUSB_RECIPIENT_INTERFACE | LIBUSB_ENDPOINT_IN,
        //These bytes were stolen with a USB sniffer
        0x01,0x0100+whichOne,0,
        data, sizeof(data), 10000);
    if (actual < 0){
        fprintf(stderr,"Read didn't work for report %d, %s\n", whichOne, libusb_strerror(actual));
    }
    else {
        // If you want both of the reports that the station provides,
        // just allow for it.  Right this second, I've found every thing
        // I need in report 1.  When I look further at report 2, this will
        // change
        //fprintf(stderr,"R%d:%d:", whichOne, actual);
        //int i;
        //for(i=0; i<actual; i++){
        //    fprintf(stderr,"%0.2X ",data[i]);
        //}
        //fprintf(stderr,"\n");
        if (whichOne == 1)
            // The actual data starts after the first byte
            // The first byte is the report number returned by 
            // the usb read.
            decode(&data[1], actual-1, noisy);
    }
}
// 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(int argc, char **argv)
{
    char *usage = {"usage: %s -u -n\n"};
    int libusbDebug = 0; //This will turn on the DEBUG for libusb
    int noisy = 0;       //This will print the packets as they come in
    libusb_device **devs;
    int r, err, c;
    ssize_t cnt;
    
    while ((c = getopt (argc, argv, "unh")) != -1)
        switch (c){
            case 'u':
                libusbDebug = 1;
                break;
            case 'n':
                noisy = 1;
                break;
            case 'h':
                fprintf(stderr, usage, argv[0]);
            case '?':
                exit(1);
            default:
                exit(1);
       }
    fprintf(stderr,"%s Starting ... ",argv[0]);
    fprintf(stderr,"libusbDebug = %d, noisy = %d\n", libusbDebug, noisy);
    // The Pi linker can give you fits.  If you get a linker error on
    // the next line, set LD_LIBRARY_PATH to /usr/local/lib 
    fprintf(stderr,"This is not an error!! Checking linker, %s\n", libusb_strerror(0));

    if (signal(SIGINT, sig_handler) == SIG_ERR)
        fprintf(stderr,"Couldn't set up signal handler\n"); 
    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
    if (libusbDebug)
        libusb_set_debug(NULL, LIBUSB_LOG_LEVEL_DEBUG);
    else
        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);
    }
    // go 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));
        closeUpAndLeave();    }
    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");
    err = libusb_set_auto_detach_kernel_driver(weatherStation.handle, 1);
    if(err){
        fprintf(stderr,"Some problem with detach %s\n", libusb_strerror(err));
        closeUpAndLeave();
    }
    int activeConfig;
    err =libusb_get_configuration(weatherStation.handle, &activeConfig);
    if (err){
        fprintf(stderr,"Can't get current active configuration, %s\n", libusb_strerror(err));;
        closeUpAndLeave();
    }
    fprintf(stderr,"Currently active configuration is %d\n", activeConfig);
    if(activeConfig != 1){
        err = libusb_set_configuration  (weatherStation.handle, 1);
        if (err){
            fprintf(stderr,"Cannot set configuration, %s\n", libusb_strerror(err));;
        closeUpAndLeave();
        }
        fprintf(stderr,"Just did the set configuration\n");
    }
    
    err = libusb_claim_interface(weatherStation.handle, 0); //claim interface 0 (the first) of device (mine had just 1)
    if(err) {
        fprintf(stderr,"Cannot claim interface, %s\n", libusb_strerror(err));
        closeUpAndLeave();
    }
    fprintf(stderr,"Claimed Interface\n");
    // I don't want to just hang up and read the reports as fast as I can, so
    // I'll space them out a bit.  It's weather, and it doesn't change very fast.
    sleep(1);
    fprintf(stderr,"Setting the device 'idle' before starting reads\n");
    err = libusb_control_transfer(weatherStation.handle, 
        0x21, 0x0a, 0, 0, 0, 0, 1000);
    sleep(1);
    int tickcounter= 0;
    fprintf(stderr,"Starting reads\n");
    while(1){
        sleep(1);
        if(tickcounter++ % 10 == 0){
            getit(1, noisy);
        }
        if(tickcounter % 30 == 0){
            getit(2, noisy);
        }
        if (tickcounter % 15 == 0){
            showit();
        }
    }
}

There's more details on what not to do in the post where I described this in detail <link>, yep, I fessed up right in the post where the instructions are.

For the folk that have already grabbed this, sorry, I'll try a bit harder next time.

Part 6 of this series is here <link>

6 comments:

  1. Where did you get the info to extract the data from the data[] string? Did you hack it or get it from some source? I ask partially because the code shows the same bits being extracted from data[6] for getHumidity and getRainCount. Something is not right.

    ReplyDelete
    Replies
    1. I drug it out of the message by trial and error, and a significant boost from some code I ran across somewhere out there showing that various readings spanned the bytes themselves. That means it's actually a bit stream that was shaped into bytes by various methods of reading and saving them.

      However, the bits you mention are from the two DIFFERENT varieties of R1 messages that we can get. Each of then has wind information, but the other things they carry are different. So, there's really three different blobs of data, Two of them are read as R1 and the other is read as R2. I covered this in detail in the various blog posts,

      Look around a bit more and you'll see what I mean.

      Delete
    2. This comment has been removed by the author.

      Delete
    3. Yes, I see now they are from the two different R1 messages. Wow. I'm impressed you were able to figure this out. I'll dig around the posts a bit more. Thanks!

      Delete
  2. Shoot - I had hoped this version of the code would resolve my inconsistent start issue (mentioned in a reply to Part 3).
    Sadly, all I get from this version is this: (at least it's consistent...) ;oD

    ====================================
    ./weatherV2 Starting ... libusbDebug = 1, noisy = 1
    ./weatherV2: symbol lookup error: ./weatherV2: undefined symbol: libusb_strerror

    ReplyDelete
    Replies
    1. This is so common I'm surprised you haven't found the solution yet. The libraries you link with and the libraries you are trying to run with are different. Since you can compile without error, you're doing the compile right, but something is pointing your run-time to the wrong place.

      There's an environment variable LD_LIBRARY_PATH that you can set that will fix this problem, but you have to point it to the right place. Try set LD_LIBRARY_PATH=/usr/local/lib or wherever you installed the version of libusb that you compiled with.

      Then the error should go away since your running with the same libraries you compiled with. This is all part of the shared library implementation on unix based machines and can occasionally drive you nuts.

      The other possibility is that you are trying to run this under Ubuntu, there are other folk that hit this on their machine. They fixed it and left the solution in the comments under part 2.

      Delete