Bill Eccles: January 2012 Archives

My Bucket List Just Got Shorter

|

I just removed swimming in the Arctic Ocean from my bucket list.

We have a LaCrosse Technology WS-2010 system which is getting close to being a decade old at this point. Aside from what I don’t like about the system (and there’s a lot), it has performed reliably, sucking weather data out of the atmosphere. Being a Mac-centric family, the fact that the WS-2010-13 PC Interface is a Windows-only, RS-232-based device was maddening. And the fact that the included software didn’t do anything useful for me didn’t help.

So for the last decade, I’ve had a Windows PC hooked up to it. I wrote a Perl script which grabbed the data from the interface and posted it to a MySQL database on a local Mac OS X Server. PHP scripts on that server query the database and generate all kinds of good graphs, some of which I’m particularly proud.

But that PC has become incredibly unreliable, requiring a reset four to five times per week. Sometimes the hard drive required unsticking with a firm whack. (I have a special tool for it, a dead TPV—a solid chunk of brass—from the nearby boiler.) Over the Christmas break, I decided to replace the PC with an mbed microcontroller. Unlike a lot of other microcontrollers out there, this one is very friendly with Ethernet, and that was extremely important in choosing it for the solution to this project. Its price, $59, isn’t too bad, and all I had to do was add a bit of hardware to talk RS-232, rewrite my Perl script as C++, and I’d be done.

As they say, How hard could it be?

I’ve learned a lot in the past two weeks. (Yes, I had two weeks off—vacation plus company-sponsored break.) First, I learned that the resources I relied upon a decade ago to write the code the first time have dried up. Google finds all kinds of references to 2010 (which arrived two years ago, you know), and a lot of them include “WS” in them. Depending on the memory of the Internet for useful things isn’t necessarily a good idea. (Depending on the memory of the Internet for publicly embarrassing things is probably a whole ‘nother story.) Here it is so I at least have a copy, and eventually Google will, too.

Second, I learned a lot about the WS-2010-13 hardware. The manual says specifically that operating it requires “RTS <-9V and DTR >+9V”. I thought that was crap because there are only four wires from the DE-9 connector. I reasoned that one must be TxD, one must be RxD, one must be DTR (because it relies on a transition of DTR to know when to wakeup), and the last must be ground, right? Wrong. Indeed, the last wire is RTS and there is no ground that I can find. How this works exactly, I don’t know, but it does. (I have some guesses, but they’re not worth repeating here.) Anyway, it’s acceptable to connect RTS straight to -12VDC, but RxD, TxD and DTR must use level shifters to connect to your processor.

Third, I re-learned that “high” in RS-232 is -12VDC (actually, between -3VDC and -25VDC) and “low” is +12VDC (+3VDC to +25VDC, of course), so that when you wake up the WS-2010, you should really follow the instructions of the manual and wake it up with a high-low transition on RTS which leaves the RTS at +12VDC. I spent a lot of time waking the interface up with a -12V/+12V/-12V sequence, which woke the interface up, producing the correct response, but which then put it right back to sleep again.

Fourth, I learned the hard way that even after everything works with clipleads, breadboards, and whatnot, you can still royally foul things up when you solder everything in place by reversing VEE and VCC on your MC1488P. The 1488 will let out a little crackle of sorts and will rapidly cease to function. Even after restoring the power in the correct orientation, it will act much like a low-resistance path between VEE and VCC and the 1488 will get pretty toasty. Don’t repeat my mistake.

As to the subtleties of getting the WS-2010-13 to work, it’s not really all that difficult. Most of the info you need is in the manual, the two exceptions being the interpretation of the wind direction spread and rain gauge return. The former is pretty simple, but the latter presents some problems occasionally. The rain gauge, as I’m sure you’re aware, is just a see-saw mechanism. Each tip of the mechanism represents 0.0145636” of rainfall. (I found that somewhere online a decade ago, and now I can’t find it at all.) A 12-bit counter in the rain gauge reports back the total number of tips it’s ever counted. Where things get a bit tricky is that occasionally it reports back a number where some high-order bit has flipped, and then a while later, it’ll flip back. I.e., you’ll have a sudden downpour of 80” of rain and then it’ll dry up just as suddenly, or vice-versa. I choose to report back the raw count coming out of the counter and let the server figure out what happened since it can “look back in time” to see which might be realistic.

If you’d like to see what the data look like, I report the weather here.

The code to do this with an mbed is shown below, and should be available on the mbed website soon. Note that there’s an awful lot of debugging feedback code commented out. These lines are quite useful when things are going wrong, believe you me! But since transmitting the serial data takes a while (even via USB), things run much faster when it’s not needed and is commented out. That I know of, you can uncomment anything and it won’t affect the timing of the conversation with the WS-2010-13. You should also be able to move this to just about any processor as the mbed-specific code is pretty easy to read as pseudo-code and you can substitute your own subroutine calls.

I hope this turns out to be of some use to somebody out there. If it does, please let me know.


/*
WeatherStationInterface.cpp
An interface for the LaCrosse Technology WS-2010-13 PC Interface.

Copyright (C)2012 William N. Eccles

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit
persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be includied in all copies or substiantial portions of the 
Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR
ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECITON WITH
THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/

#include "mbed.h"
#include "EthernetNetIf.h"
#include "HTTPClient.h"
#define WAIT_time 0.05
#define D() wait(WAIT_time)

Serial mac(USBTX, USBRX);
Serial ws2010pc(p28, p27); // tx, rx
DigitalOut DTR(p29);
DigitalOut mled0(LED1);
DigitalOut mled1(LED2);
DigitalOut mled2(LED3);
DigitalOut mled3(LED4);

const char *requestDCFtime = "\x01\x30\xCF\x04";
const char *requestData = "\x01\x31\xCE\x04";
const char *requestNextDataset = "\x01\x32\xCD\x04";
const char *requestStatus = "\x01\x35\xCA\x04";

void blink() {
int m0, m1, m2, m3;

    m0 = mled0;
    m1 = mled1;
    m2 = mled2;
    m3 = mled3;

    mled0 = mled1 = mled2 = mled3 = 0;

    mled0 = 1;D();mled1 = 1;D();mled2 = 1;D();mled3 = 1;D();
    mled0 = 0;D();mled1 = 0;D();mled2 = 0;D();mled3 = 0;D();D();D();
    mled3 = 1;D();mled2 = 1;D();mled1 = 1;D();mled0 = 1;D();        
    mled3 = 0;D();mled2 = 0;D();mled1 = 0;D();mled0 = 0;D();D();D();

    mled0 = m0;
    mled1 = m1;
    mled2 = m2;
    mled3 = m3;

// takes 1s with a delay time of 0.05s

    return;
}

int errorHalt( bool L3, bool L2, bool L1, bool L0 ) {
    while (1) {
        mled3 = L0;
        mled2 = L1;
        mled1 = L2;
        mled0 = L3;
        wait(0.5);
        mled3 = false;
        mled2 = false;
        mled1 = false;
        mled0 = false;
        wait(0.5);
    }
    return -1;
}

void showLEDs( bool L3, bool L2, bool L1, bool L0 ) {
        mled3 = L0;
        mled2 = L1;
        mled1 = L2;
        mled0 = L3;
    return;
}

float H( int byte ) {
    return (float)(byte >> 4);
}

float HS( int byte ) {
    return (float)((byte >> 4) & 0x07);
}

float S( int byte ) {
    byte = (byte & 0x0F) >> 3;
    return ((byte==1) ? -1.0 : 1.0);
}

float L( int byte ) {
    byte = (byte & 0x0F);
    return (float)byte;
}

float LS( int byte ) {
    byte = (byte & 0x07);
    return (float)byte;
}

bool wakeup() {
    char aChar;
    bool wokeup = false;

//    mac.printf("Waking up the WS2010...");
    DTR = 1;
    wait(0.05);
    DTR = 0;
    wait(0.05);

    while (ws2010pc.readable()) {
        aChar = ws2010pc.getc();
        if (aChar==3) {
//            mac.printf(" and it's awake.\n\r");
            wokeup = true;
        } else {
//            mac.printf(" and it didn't want to wake up, returning a %i instead of a 3.\n\r",aChar);
            wokeup = false;
        }        
    }
    wait(0.1);
    return wokeup;
}
int sendString( char *theString ) {
    int index = 0;
    while (theString[index]!='\0') {
        ws2010pc.putc((char)theString[index++]);
//        wait(0.005);
    }
    return index;
}

int receiveString( char *theString, int numChars, int timeout_ms ) {
    int index = 0;
    bool done = false;
    int LSRValue;
    Timer aTimer;

    theString[0]='\0';
    aTimer.start();

    while ((aTimer.read_ms()<=timeout_ms)&&(index LSR;
//    if ((LSRValue!=97)&&(LSRValue!=96)) { mac.printf("Receive error? LSR is %i.\n\r",LSRValue); }
        if (ws2010pc.readable()) {
            theString[index] = ws2010pc.getc();
            if (theString[index]==0x03) {
                done = true;
            }
            index++;
            theString[index]='\0';
        }
    }
    aTimer.stop();
    return index;
}

// MAIN

int main() {
    int i, j, dataLength, strippedLength, LSRValue;
    int db, dt, spreadraw;
    float t1c, t1f, h1, hif, hic;
    float tic, tif, hi;
    float sp, dir, spread, wc;
    float pr;
    float rn;
    string n1, ni, nwin, nr;
    HTTPText txt;
    HTTPResult r;
    bool error, firstTime, haveData, dataPostedOK;
    char theData[1024], strippedData[1024], httpQuery[1024];
    EthernetNetIf eth;
    HTTPClient http;
    EthernetErr ethErr;
    IpAddr myIpAddr;

    firstTime = true;

// Setup Ethernet
    showLEDs( false, false, false, true );

//    mac.printf("Setting up Ethernet...\n\r");
    ethErr = eth.setup();
    if (ethErr) {
//        mac.printf("Error %d in setup.\n\r", ethErr);
        errorHalt( false, false, false, true );                
    } else {
//        mac.printf("Ethernet setup was successful.\n\r");
    }

    while (1) {
        error = false;

// Setup serial port

        showLEDs( false, false, true, false );

//        mac.printf("Opening serial port...\n\r");
        ws2010pc.baud(9600);
        ws2010pc.format(8, Serial::Even, 2);

// Wait six minutes unless this is the first time through this

        showLEDs( false, false, true, true );

        if (!firstTime) {
            DTR = 1;
            mac.printf("Sleeping for six minutes... ");
            i=6;
            while (i>=1) {
                mac.printf("%i...",i--);
                for (j=0; j<12; j++) {
                    blink();
                    wait(4);
                }
            }
            mac.printf("\n\r");
        }

        firstTime = false;

// Wakeup the 2010

        showLEDs( false, true, false, false );

        error = !wakeup();

// Ask the 2010 for data until there is no more to be had.


        haveData = true;
        while (haveData&&(!error)) {
            showLEDs( false, true, false, true );
            mac.printf("Requesting data.... ");
            dataLength = sendString((char *)requestData);
            dataLength = receiveString(theData, 128, 500);
            mac.printf("Received %i characters.\n\r",dataLength);
            if (dataLength<2) {
                mac.printf("No data received.\n\r");
                haveData=false;
            } else if ((theData[0]!=2)||(theData[dataLength-1]!=3)) {
                mac.printf("Bad dataset received.\n\r");
                haveData = false;
            } else if ((theData[1]==1)&&(theData[2]==16)) {
                mac.printf("No data available at the moment. Will retry later.\n\r");
                haveData = false;
            }

            if (haveData) {
/*
                mac.printf("The raw dataset has %i characters: ",dataLength);
                i=0;
                while (i70) {
                    hif = -42.379+2.04901523*t1f+10.14333127*h1-0.22475541*t1f*h1-((6.83783e-3)*t1f*t1f)-((5.481717e-2)*h1*h1)+((1.22874e-3)*t1f*t1f*h1)+((8.5282e-4)*t1f*h1*h1)-((1.99e-6)*t1f*t1f*h1*h1);
                    hic = (hif-32.0)*5.0/9.0;
                } else {
                    hif = t1f;
                    hic = t1c;
                }

//                mac.printf("    T1C:%5.2f T1F:%5.2f H1:%5.2f HIF:%5.2f HIC:%5.2f %s \n\r", t1c, t1f, h1, hif, hic, n1);

// reading: indoor temperature and humidity

                tic = (LS(strippedData[29])*10+H(strippedData[28])+L(strippedData[28])/10)*S(L(strippedData[29]));
                tif = 9.0/5.0*tic+32;
                hi = LS(strippedData[30])*16+H(strippedData[29]);
                ni = (S((int)L(strippedData[30]))==-1) ? "NEW" : "OLD";

//                mac.printf("    TIC:%3.2f TIF:%3.2f HI:%3.2f %s \n\r", tic, tif, hi, ni);

// reading: wind speed, direction and directional spread, wind chill

                sp = (HS(strippedData[24])*100.0+L(strippedData[24])*10.0+H(strippedData[23])+L(strippedData[23])/10.0)*0.6215;
                dir = (float)((int)L(strippedData[26])%4)*100.0+H(strippedData[25])*10.0+L(strippedData[25]);
                spreadraw = (int)(L(strippedData[26])/4);
                switch (spreadraw) {
                case 1:
                    spread = 22.5;
                    break;
                case 2:
                    spread = 45;
                    break;
                case 3:
                    spread = 67.5;
                    break;
                default:
                    spread = 0;
                }
                nwin = (S((int)H(strippedData[24]))==-1) ? "NEW" : "OLD";

//                mac.printf("    SP: %5.2f DIR:%5.2f SPREADRAW:%i SPREAD:%5.2f %s \n\r", sp, dir, spreadraw, spread, nwin);

                if ((t1f<=50)&&(sp>3)) {
                    wc = 35.74+(0.6215*t1f)-(35.75*pow(sp,(float)0.16))+(0.4275*t1f*pow(sp,(float)0.16));
                } else {
                    wc = t1f;
                }

//                mac.printf("    WC: %5.2f\n\r",wc);

// reading: air pressure

                pr = (H(strippedData[27])*100.0+L(strippedData[27])*10.0+H(strippedData[26])+200.0)/33.775;
//                mac.printf("    PR: %6.3f\n\r",pr);

// Ah, rain. The see-saw tips, and each tip represents 0.0145636 inches of rain.

                rn = (strippedData[22]&0x7F)*256+strippedData[21];
                nr = (S((int)H(strippedData[22]))==-1) ? "NEW" : "OLD";

//                mac.printf("    RN: %7.1f NR %s\n\r",rn,nr);

// Generate the query string

                j = sprintf(httpQuery, "http://www.example.com/yourquery.php?datasetnumber=%06d&timedelta=%06d&t1=%.1f&h1=%.1f&n1=%s&hif=%.1f&hic=%.1f&ti=%.1f&hi=%.1f&ni=%s&sp=%.1f&dir=%.1f&spread=%.1f&nwin=%s&wc=%.1f&pr=%.7f&rn=%.1f&nr=%s",db,dt,t1f,h1,n1,hif,hic,tif,hi,ni,sp,dir,spread,nwin,wc,pr,rn,nr);
//                mac.printf("The query is \"%s\"\n\r",httpQuery);

// Now try and post the data. Make sure the response from the server doesn't exceed capacity of the buffer "txt".

                showLEDs( false, true, true, false );

                r = http.get(httpQuery, &txt);
                if(r==HTTP_OK) {
//                    mac.printf("HTTP result:\"%s\"\n\r", txt.gets());
                    dataPostedOK = true; 
                } else {
//                    mac.printf("HTTP error #%d\n\r", r);
                    dataPostedOK = false;
                }

                if (dataPostedOK) {
                    showLEDs( false, true, true, true );
                    error = !wakeup();
                    mac.printf("Requesting next dataset.... ");
                    dataLength = sendString((char *)requestNextDataset);
                    dataLength = receiveString(theData, 128, 300);
                    mac.printf("Received %i characters.\n\r",dataLength);
                    if (dataLength<2) {
                        mac.printf("No data received.\n\r");
                        haveData=false;
                        error = true;
                    } else if ((theData[0]!=2)||(theData[dataLength-1]!=3)) {
                        mac.printf("Bad dataset received.\n\r");
                        haveData = false;
                        error = true;
                    } else if ((theData[1]==1)&&(theData[2]==16)) {
                        mac.printf("No data available at the moment. Will retry later.\n\r");
                        haveData = false;
                    }
                }
            } // end of "if (haveData)"
        }      
    }

    errorHalt( true, true, true, true );
    return 0;

}