Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Connecting a Davis Instruments Vantage Weather Station to the Internet

0.00/5 (No votes)
27 Aug 2007 1  
This article will describe how to connect a Davis Intruments Vantage weather station to the Internet using .NET 2.0 and C# as well as a few pieces of necessary hardware.
Screenshot - weatherlink.jpg

Introduction

Davis Instruments is one of the leading developers of personal weather stations (I'm not affiliated, just a customer). I've used one of their VantagePro wireless weather stations for years now and have really liked it. Its only drawback was that I couldn't get the information from the device when I wasn't looking directly at the console of the unit. Davis sells a piece of hardware that allows a serial connection to the weather station console and an associated Windows program that reads the data from the console. It, however, requires that you have a computer directly connected to the console running at all times to get the data from the weather station. I wanted to be able to get the weather information from anywhere on the Internet without requiring the use of a physically connected computer running at all times.

Doing this required learning a few things and acquiring a few others. Some hardware, aside from the Davis weather station is required. First, I needed to purchase Davis' WeatherLink serial port connector. As mentioned above, this allowed me to get access to the weather station console. Next, I had to find a way to translate serial port signals to TCP packets bi-directionally. There are many products that do this and, if you're handy with some basic hardware development, it's probably not too hard to build such a device yourself. Being lazy, I decided for an off-the-shelf solution called the SitePlayer from NetMedia (about $100).

Then, moving onto the software (that's why you're reading this in the first place, right?), I couldn't find any guidance on interfacing to the weather station through its serial port, let alone over the network, through any of my searches. Davis does provide an SDK, but it only contains some very old C and VB code that had nothing to do with .NET development. I found very little value in them. The documentation is also very weak. As such, I had to figure out how to establish an appropriate serial connection, then how to decode the data that the console returned to me. Once I had established a working serial link between the console and an attached computer, I then had to extend this to sending and receiving the data using a TCP socket. In the end, the solutions are rather simple, although they took me a while to figure out.

Warning: While I used to develop a lot of software (many years ago), I have been away from it for some time and I'm pretty rusty. Also, I'm fairly new to .NET and C# so I'm sure that at the very least, I've missed some basic optimizations of the code. I've tested the code extensively, though, and you should be able to use it as a solid starting point for your project.

Note: Davis has talked about delivering an integrated device to make their VantagePro II weather station available over the network for some time now. As of this writing, it's still not released. Since I'm impatient and want full control anyway, writing my own seemed like the right way to go.

Background

As with any project that communicates with the serial port, there are two basic ways of exchanging data - synchronously and asynchronously. .NET provides callbacks that are easily accessible to deal with data asynchronously, but I found the system confusing for a project like this where the type and size of data being sent and received is fairly well known. So, I chose to do everything synchronously - polling for data. This required that I watch for timing issues - you'll see these in the code. Given that I'm dealing with serial ports and TCP sockets, a lot of error checking needs to happen. It wouldn't be surprising to lose a connection in the middle of a transmission so the code needs to handle that.

There are many commands that result in the Vantage weather station sending data about current and historic weather as well as the status of the station, console and its sensors. I've tried to simplify the code here by showing only what I believe is the most important command - the "LOOP" command - which returns 95 bytes of information about the current weather and status of the station. Processing the LOOP command is also the most complex of the commands so it should be relatively straightforward to implement other commands given the template here.

While I've only tested this code on a VantagePro weather station, I have followed the conventions required to make it work on a newer, VantagePro II station as well.

Using the Code

The code is setup as follows:

  • Open the serial or TCP port
  • Wake up the weather station - it sleeps between commands
  • Send a command
  • Parse the returned data

I initially built the code to talk with the weather station directly through the serial port. This let me debug the communication with the fewest variables and let me get an idea about what had to happen between the SitePlayer and the console. Once I got that working, I extended the program to also get data over a network, including the Internet. To help out as many people as possible, I've included the code for both serial and network connections here.

The cornerstone of the program is the WeatherLoopData class. Once passed the byte array retrieved from the weather station, this class parses the array into its constituent parts and makes them available for use. There's a fair amount of manipulation that has to take place in order to get the data into the correct form. The class also contains procedures to format the data for output and to translate some of the numeric data into easier to read strings. For brevity, I've only included the procedure from the class that does the heavy lifting here. The full class is in the included source code.

// The WeatherLoopData class extracts and stores the weather data from the 
// array of bytes returned from the Vantage weather station
// The array is generated from the return of the LOOP command.
//
public class WeatherLoopData
{
    // Load - disassembles the byte array passed in and loads it into 
    // local data that the accessors can use.
    // Actual data is in the format to the right of the assignments - 
    // I convert it to make it easier to use
    // When bytes have to be assembled into 2-byte, 16-bit numbers, 
    // I convert two bytes from the array into 
    // an Int16 (16-bit integer).  When a single byte is all that's needed, 
    // I just convert it to an Int32.
    // In the end, all integers are cast to Int32 for return.
    public void Load(Byte[] loopByteArray)
    {
        int hours,
            minutes;
        string timeString;
        DateTime currTime;

	// Sbyte - signed byte
	barTrend = Convert.ToInt32((sbyte)loopByteArray[3]);                    
	// Uint16
	barometer = (float)(BitConverter.ToInt16(loopByteArray, 7)) / 1000;     
	// Uint16
	insideTemp = (float)(BitConverter.ToInt16(loopByteArray, 9)) / 10;    
	// Byte - unsigned byte
	insideHumidity = Convert.ToInt32(loopByteArray[11]);                    
	// Uint16
	outsideTemp = (float)(BitConverter.ToInt16(loopByteArray, 12)) / 10;    
	// Byte - unsigned byte
	outsideHumidity = Convert.ToInt32(loopByteArray[33]);                   
	// Uint16
	windDirection = BitConverter.ToInt16(loopByteArray, 16);                
	// Byte - unsigned byte
	currWindSpeed = Convert.ToInt32(loopByteArray[14]);                     
	// Byte - unsigned byte
	avgWindSpeed = Convert.ToInt32(loopByteArray[15]);                      
	// Uint16
	dayRain = (float)(BitConverter.ToInt16(loopByteArray, 50)) / 100;       
        // get the current date and time
        currTime = DateTime.Now;

        // Time from the Vantage is all in 24-hour format.  
        // I move it into a string so I can manipulate it 
        // more easily.
        timeString = BitConverter.ToInt16(loopByteArray, 91).ToString();    // Uint16
        // Extract hours and minutes and convert them to integers - required by Datetime
        hours = Convert.ToInt32(timeString.Substring(0, timeString.Length - 2));
        minutes = Convert.ToInt32(timeString.Substring(timeString.Length - 2, 2));
        // Create a new Datetime instance - use current year, month and day
        sunRise = new DateTime(currTime.Year, currTime.Month, 
		currTime.Day, hours, minutes, 0);

        timeString = BitConverter.ToInt16(loopByteArray, 93).ToString();    // Uint16
        hours = Convert.ToInt32(timeString.Substring(0, timeString.Length - 2));
        minutes = Convert.ToInt32(timeString.Substring(timeString.Length - 2, 2));
        sunSet = new DateTime(currTime.Year, currTime.Month, 
		currTime.Day, hours, minutes, 0); ;
    }
}

With this class in hand, the rest of the job is about establishing a solid serial or network connection, sending a command and getting the data back from the weather station.

Opening the serial port is easy, the trick in communicating with the Vantage, though, is that DTR (Data Terminal Ready) must be set high (true). This little tidbit took me a while to figure out. It seems obvious to me now, but I had assumed that true would be the default.

// Open the serial port for communication
private SerialPort Open_Serial_Port()
{
    try
    {
        SerialPort thePort = new SerialPort("COM1", 19200, Parity.None, 8, StopBits.One);

        // This establishes an event handler for serial comm errors
        thePort.ErrorReceived += new SerialErrorReceivedEventHandler
			(SerialPort_ErrorReceived);

        // Set a timeout just in case there's a big problem and 
        // nothing is being received.  The rest of the code should
        // take care of most problems.  
        // The following line can be used if no timeout is desired:
        // thePort.ReadTimeout = SerialPort.InfiniteTimeout;
        thePort.ReadTimeout = 2500;
        thePort.WriteTimeout = 2500;

        // Set Data Terminal Ready to true - can't transmit without DTR turned on
        thePort.DtrEnable = true;

        thePort.Open();

        return (thePort);
    }
    catch (Exception ex)
    {
        Show_Message(ex.ToString());
        return (null);
    }
}

Opening the TCP socket is shockingly simple once you get the right syntax. I use port 23 because the SitePlayer defaults to talking over a standard telnet port. Below is the try block for dealing with the TCP port opening. The rest of the code in the procedure is the same as for the serial port.

// Open a TCP socket.  Most operations will work on the underlying stream 
// from the port which aren't completely 
// implemented in .NET 2.x
try
{
    // Creating the new TCP socket effectively opens it - 
    // specify IP address or domain name and port
    TcpClient sock = new TcpClient("xxx.xxx.xxx.xxx", 23);
    // Set the timeout of the underlying stream
    // WARNING: several of the methods on the underlying stream object 
    // are not implemented in .NET 2.x
    sock.GetStream().ReadTimeout = 2500;

    return sock;
}

With the port open, we need to go out and get the data. Since the streams for a serial connection and TCP socket need to be handled differently, there are some differences in the way I needed to do this as seen in the code. The weather station returns an array of bytes. As is shown, most of the code is about error checking and timing. We need to make sure that we've received all the data that we are expecting and that our connection remains active during the transmission. Sometimes, the console will respond with a \n\r (new line), acknowledging the command. Other times it won't.

// Retrieve_Command retrieves data from the Vantage weather station 
// using the specified command
private byte[] Retrieve_Serial_Command(SerialPort thePort, 
	string commandString, int returnLength)
{
    bool Found_ACK = false;
    int ACK = 6,        // ASCII 6
        passCount = 1,
        maxPasses = 4;
    int currChar;

    try
    {
        // Clean out the input (receive) buffer just in case something showed up in it
        thePort.DiscardInBuffer();
        // . . . and clean out the output buffer while we're at it for good measure
        thePort.DiscardOutBuffer();

        // Try the command until we get a clean ACKnowledge from the Vantage.  
        // We count the number of passes since
        // a timeout will never occur reading from the sockets buffer.  
        // If we try a bunch of times (maxPasses) and
        // we get nothing back, we assume that the connection is busted
        while (!Found_ACK && passCount < maxPasses)
        {
            thePort.WriteLine(commandString);
            // I'm using the LOOP command as the baseline here because 
	   // many its parameters are a superset of
            // those for other commands.  The most important part of this is that 
	   // the LOOP command is iterative
            // and the station waits 2 seconds between its responses.  
	   // Although it's not clear from the documentation, 
            // I'm assuming that the first packet isn't sent for 2 seconds.  
	   // In any event, the conservative nature
            // of waiting this amount of time probably makes sense to deal with 
	   // serial IO in this manner anyway.
            System.Threading.Thread.Sleep(2000);

            // Wait for the Vantage to acknowledge the receipt of the command - 
	   // sometimes we get a '\n\r'
            // in the buffer first or nor response is given.  
	   // If all else fails, try again.
            while (thePort.BytesToRead > 0 && !Found_ACK)
            {
                // Read the current character
                currChar = thePort.ReadChar();
                if (currChar == ACK)
                    Found_ACK = true;
            }

            passCount += 1;
        }

        // We've tried a bunch of times and have heard nothing back from the port 
        // (nothing's in the buffer).  Let's 
        // bounce outta here
        if (passCount == maxPasses)
            return (null);
        else
        {
            // Allocate a byte array to hold the return data that we care about - 
	   // up to, but not including the '\n'
            // Size the array according to the data passed to the procedure
            byte[] loopString = new byte[returnLength];

            // Wait until the buffer is full - we've received returnLength 
	   // characters from the LOOP response, 
            // including the final '\n' 
            while (thePort.BytesToRead <= loopString.Length)
            {
                // Wait a short period to let more data load into the buffer
                System.Threading.Thread.Sleep(200);
            }

            // Read the first returnLength bytes of the buffer into the array
            thePort.Read(loopString, 0, returnLength);

            return loopString;
        }
    }
    catch (Exception ex)
    {
        Show_Message(ex.ToString());
        return null;
    }
}

Again, because the way we need to deal with the data stream is different between serial and TCP connections, the code for the TCP connection is slightly different. For example, there is no implemented method on the stream for finding out the number of bytes waiting in the buffer. We replace it with a method that tells us simply if data is available.

// Retrieve_Command retrieves data from the Vantage weather station 
// using the specified command
private byte[] Retrieve_Telnet_Command
	(TcpClient thePort, string commandString, int returnLength)
{
    bool Found_ACK = false;
    int currChar,
        ACK = 6,        // ASCII 6
        passCount = 1,
        maxPasses = 4;
    string termCommand;

    try
    {
        // Set a local variable so that it's easier to work with the stream 
        // underlying the TCP socket
        NetworkStream theStream = thePort.GetStream();

        // Try the command until we get a clean ACKnowledge from the Vantage.  
        // We count the number of passes since
        // a timeout will never occur reading from the sockets buffer.  
        // If we try a bunch of times (maxPasses) and
        // we get nothing back, we assume that the connection is busted
        while (!Found_ACK && passCount < maxPasses)
        {
            termCommand = commandString + "\n";
            // Convert the command string to an ASCII byte array - 
	   // required for the .Write method - and send
            theStream.Write(Encoding.ASCII.GetBytes(termCommand), 0, termCommand.Length);
            // According to the Davis documentation, the LOOP command sends 
	   // its response every 2 seconds.  It's
            // not clear if there is a 2-second delay for the first response.  
	   // My trials have show that this can
            // move faster, but still needs some delay.
            System.Threading.Thread.Sleep(500);

            // Wait for the Vantage to acknowledge the receipt of the command - 
	   // sometimes we get a '\r\n'
            // in the buffer first or nor response is given.  
	   // If all else fails, try again.
            while (theStream.DataAvailable && !Found_ACK)
            {
                // Read the current character
                currChar = theStream.ReadByte();
                if (currChar == ACK)
                    Found_ACK = true;
            }

            passCount += 1;
        }

        // We've tried a bunch of times and have heard nothing back from the port 
        // (nothing's in the buffer).  Let's 
        // bounce outta here
        if (passCount == maxPasses)
            return (null);
        else
        {
            // Allocate a byte array to hold the return data that we care about - 
	   // up to, but not including the '\n'
            // Size is determined by LOOP data return - 
	   // this procedure has no way of knowing if it is not passed in.
            byte[] loopString = new byte[returnLength];

            // Wait until the buffer is full - 
	   // we've received returnLength characters from the command response
            while (thePort.Available <= loopString.Length)
            {
                // Wait a short period to let more data load into the buffer
                System.Threading.Thread.Sleep(200);
            }

            // Read the first 95 bytes of the buffer into the array
            theStream.Read(loopString, 0, returnLength);

            return loopString;
        }
    }
    catch (Exception ex)
    {
        Show_Message(ex.ToString());
        return null;
    }
}

That's about all the interesting stuff there is. There is a basic Windows Form for triggering the serial or network connection and displaying the returned data as well as various event handlers for button clicks and serial errors. The code is pretty well documented so should be fairly easy to read through. I have moved this code (the network version) to an ASP.NET Web site (not yet visible) and it's working well. Good luck with your project.

Points of Interest

While I would have loved to treat the processing of the serial and TCP connections with a lot of common code, it turns out that not all of the methods dealing with the stream that underlies the TCP connection are implemented in .NET 2.0. Even the ones that exist are not completely aligned with the methods for the streams underyling a serial connection. As such, I have separate procedures for dealing with serial connections and TCP sockets.

History

  • v1.0 8/22/2007

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here