将 Davis Instruments Vantage 天气站连接到 Internet






4.67/5 (5投票s)
本文将介绍如何使用 .NET 2.0 和 C# 以及一些必要的硬件,将 Davis Instruments Vantage 天气站连接到互联网。

引言
Davis Instruments 是个人气象站的领先开发商之一(我不是其附属,只是一个客户)。我多年来一直使用他们的一款 VantagePro 无线气象站,并且非常喜欢它。它唯一的缺点是,当我不直接看设备控制台时,我无法获取设备信息。Davis 公司销售一种硬件,可以实现与气象站控制台的串行连接,以及一个读取控制台数据的相关 Windows 程序。但是,这需要你始终连接一台计算机到控制台运行,才能从气象站获取数据。我想在任何地方通过互联网获取天气信息,而无需使用一台始终运行的物理连接的计算机。
要做到这一点,需要学习一些知识并获得一些其他东西。除了 Davis 气象站外,还需要一些硬件。首先,我需要购买 Davis 的 WeatherLink 串行端口连接器。如前所述,这使我能够访问气象站控制台。接下来,我必须找到一种双向将串行端口信号转换为 TCP 数据包的方法。有很多产品可以做到这一点,如果你擅长一些基本的硬件开发,自己构建这样的设备可能并不难。我比较懒,所以选择了一个现成的解决方案,叫做 NetMedia 的 SitePlayer(约 100 美元)。
然后,转向软件(这才是你们阅读本文的原因,对吧?),在我所有的搜索中,我都找不到任何关于通过串行端口与气象站接口的指导,更不用说通过网络了。Davis 确实提供了一个 SDK,但它只包含一些非常老的 C 和 VB 代码,与 .NET 开发无关。我从中几乎找不到价值。文档也非常薄弱。因此,我必须弄清楚如何建立适当的串行连接,然后如何解码控制台返回给我的数据。一旦我建立了控制台与连接计算机之间的有效串行连接,我就必须将其扩展到使用 TCP 套接字发送和接收数据。最终,解决方案相当简单,尽管我花了一些时间才弄清楚。
警告:虽然我以前开发了很多软件(很多年前),但我已经离开了很长一段时间,而且我现在有点生疏。另外,我对 .NET 和 C# 比较陌生,所以我确定至少我错过了一些代码的基本优化。不过,我已经对代码进行了广泛的测试,你应该可以将它作为你项目的坚实起点。
注意:Davis 已经谈论了很长一段时间要提供一个集成设备,使他们的 VantagePro II 气象站可以通过网络访问。截至本文撰写之时,它仍未发布。由于我没有耐心,而且无论如何都想拥有完全的控制权,所以我认为自己编写是正确的做法。
背景
与任何与串行端口通信的项目一样,有两种基本的数据交换方式——同步和异步。.NET 提供了易于访问的回调来异步处理数据,但我发现对于像我这样的项目,发送和接收的数据类型和大小是相当确定的,因此该系统令人困惑。所以,我选择全部同步进行——轮询数据。这要求我注意时序问题——你会在代码中看到它们。考虑到我正在处理串行端口和 TCP 套接字,需要进行大量的错误检查。在传输过程中丢失连接并不奇怪,所以代码需要处理这种情况。
有很多命令可以使 Vantage 气象站发送有关当前和历史天气以及站点、控制台及其传感器状态的数据。我试图通过仅显示我认为最重要的命令——“LOOP
”命令——来简化代码,该命令返回 95 字节的当前天气和站点状态信息。处理 LOOP
命令也是最复杂的命令之一,所以给定这里的模板,实现其他命令应该相对简单。
虽然我只在 VantagePro 气象站上测试过这段代码,但我已经遵循了所需的约定,使其也能在较新的 VantagePro II 站上工作。
Using the Code
代码的设置如下:
- 打开串行或 TCP 端口
- 唤醒气象站 - 它在命令之间休眠
- 发送命令
- 解析返回的数据
我最初构建代码是为了直接通过串行端口与气象站通信。这使我能够用最少的变量调试通信,并让我了解 SitePlayer 和控制台之间需要发生什么。一旦我解决了这个问题,我就扩展了程序,以便通过网络(包括互联网)获取数据。为了尽可能多地帮助大家,我在这里包含了串行连接和网络连接的代码。
程序的核心是 WeatherLoopData
类。一旦传递了从气象站检索到的字节数组,该类就会将数组解析成其组成部分,并使其可用。需要进行大量的操作才能将数据转换成正确的形式。该类还包含格式化输出数据以及将一些数字数据转换为更易读的 string
s 的过程。为简洁起见,我只在这里包含执行繁重工作的过程。完整的类包含在提供的源代码中。
// 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); ;
}
}
有了这个类,剩下的工作就是建立一个稳定的串行或网络连接,发送一个命令并从气象站获取数据。
打开串行端口很容易,但与 Vantage 通信的关键是 DTR(Data Terminal Ready)必须设置为高(true
)。这个小细节花了我一段时间才弄清楚。现在对我来说很明显,但我以为 true
是默认值。
// 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);
}
}
一旦你获得了正确的语法,打开 TCP 套接字就非常简单了。我使用端口 23,因为 SitePlayer 默认通过标准的 telnet 端口进行通信。下面是处理 TCP 端口打开的 try
块。该过程中其余的代码与串行端口相同。
// 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;
}
端口打开后,我们需要出去获取数据。由于串行连接和 TCP 套接字的数据流需要以不同的方式处理,因此我在代码中看到的方法存在一些差异。气象站返回一个字节数组。如所示,大部分代码是关于错误检查和时序的。我们需要确保我们已经收到了所有预期的数据,并且在传输过程中连接保持活动状态。有时,控制台会响应 \n\r
(换行符)来确认命令。有时则不会。
// 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;
}
}
同样,由于我们处理数据流的方式在串行连接和 TCP 连接之间不同,所以 TCP 连接的代码略有不同。例如,在 stream
上没有实现检测缓冲区中等待字节数的方法。我们用一个简单地告诉我们数据是否可用的方法来代替它。
// 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;
}
}
这就是所有有趣的东西了。有一个基本的 Windows 窗体用于触发串行或网络连接以及显示返回的数据,还有各种用于按钮点击和串行错误的事件处理程序。代码的文档相当齐全,应该很容易阅读。我已经将此代码(网络版本)迁移到一个 ASP.NET 网站(尚未上线),并且运行良好。祝你的项目好运。
关注点
虽然我很想用大量的公共代码来处理串行连接和 TCP 连接,但事实证明,.NET 2.0 中并非所有处理 TCP 连接底层 stream
的方法都已实现。即使存在的方法,也与串行连接底层 stream
的方法不完全一致。因此,我为处理串行连接和 TCP 套接字分别编写了过程。
历史
- v1.0 2007/8/22