65.9K
CodeProject 正在变化。 阅读更多。
Home

将 Davis Instruments Vantage 天气站连接到 Internet

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.67/5 (5投票s)

2007年8月27日

CPOL

7分钟阅读

viewsIcon

66024

downloadIcon

1167

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

Screenshot - weatherlink.jpg

引言

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 类。一旦传递了从气象站检索到的字节数组,该类就会将数组解析成其组成部分,并使其可用。需要进行大量的操作才能将数据转换成正确的形式。该类还包含格式化输出数据以及将一些数字数据转换为更易读的 strings 的过程。为简洁起见,我只在这里包含执行繁重工作的过程。完整的类包含在提供的源代码中。

// 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
© . All rights reserved.