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 string
s. 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.
public class WeatherLoopData
{
public void Load(Byte[] loopByteArray)
{
int hours,
minutes;
string timeString;
DateTime currTime;
barTrend = Convert.ToInt32((sbyte)loopByteArray[3]);
barometer = (float)(BitConverter.ToInt16(loopByteArray, 7)) / 1000;
insideTemp = (float)(BitConverter.ToInt16(loopByteArray, 9)) / 10;
insideHumidity = Convert.ToInt32(loopByteArray[11]);
outsideTemp = (float)(BitConverter.ToInt16(loopByteArray, 12)) / 10;
outsideHumidity = Convert.ToInt32(loopByteArray[33]);
windDirection = BitConverter.ToInt16(loopByteArray, 16);
currWindSpeed = Convert.ToInt32(loopByteArray[14]);
avgWindSpeed = Convert.ToInt32(loopByteArray[15]);
dayRain = (float)(BitConverter.ToInt16(loopByteArray, 50)) / 100;
currTime = DateTime.Now;
timeString = BitConverter.ToInt16(loopByteArray, 91).ToString();
hours = Convert.ToInt32(timeString.Substring(0, timeString.Length - 2));
minutes = Convert.ToInt32(timeString.Substring(timeString.Length - 2, 2));
sunRise = new DateTime(currTime.Year, currTime.Month,
currTime.Day, hours, minutes, 0);
timeString = BitConverter.ToInt16(loopByteArray, 93).ToString();
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.
private SerialPort Open_Serial_Port()
{
try
{
SerialPort thePort = new SerialPort("COM1", 19200, Parity.None, 8, StopBits.One);
thePort.ErrorReceived += new SerialErrorReceivedEventHandler
(SerialPort_ErrorReceived);
thePort.ReadTimeout = 2500;
thePort.WriteTimeout = 2500;
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.
try
{
TcpClient sock = new TcpClient("xxx.xxx.xxx.xxx", 23);
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.
private byte[] Retrieve_Serial_Command(SerialPort thePort,
string commandString, int returnLength)
{
bool Found_ACK = false;
int ACK = 6,
passCount = 1,
maxPasses = 4;
int currChar;
try
{
thePort.DiscardInBuffer();
thePort.DiscardOutBuffer();
while (!Found_ACK && passCount < maxPasses)
{
thePort.WriteLine(commandString);
System.Threading.Thread.Sleep(2000);
while (thePort.BytesToRead > 0 && !Found_ACK)
{
currChar = thePort.ReadChar();
if (currChar == ACK)
Found_ACK = true;
}
passCount += 1;
}
if (passCount == maxPasses)
return (null);
else
{
byte[] loopString = new byte[returnLength];
while (thePort.BytesToRead <= loopString.Length)
{
System.Threading.Thread.Sleep(200);
}
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.
private byte[] Retrieve_Telnet_Command
(TcpClient thePort, string commandString, int returnLength)
{
bool Found_ACK = false;
int currChar,
ACK = 6,
passCount = 1,
maxPasses = 4;
string termCommand;
try
{
NetworkStream theStream = thePort.GetStream();
while (!Found_ACK && passCount < maxPasses)
{
termCommand = commandString + "\n";
theStream.Write(Encoding.ASCII.GetBytes(termCommand), 0, termCommand.Length);
System.Threading.Thread.Sleep(500);
while (theStream.DataAvailable && !Found_ACK)
{
currChar = theStream.ReadByte();
if (currChar == ACK)
Found_ACK = true;
}
passCount += 1;
}
if (passCount == maxPasses)
return (null);
else
{
byte[] loopString = new byte[returnLength];
while (thePort.Available <= loopString.Length)
{
System.Threading.Thread.Sleep(200);
}
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 stream
s underyling a serial connection. As such, I have separate procedures for dealing with serial connections and TCP sockets.
History