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

使用 Netduino 的 GPS

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.94/5 (15投票s)

2014年7月14日

CC (ASA 3U)

7分钟阅读

viewsIcon

24005

我最近写了一篇关于使用 Arduino 板作为 GPS 数据记录器的博客文章。我一直使用它为 Truck Tracker 应用程序收集地理位置数据。在这篇文章中,我将探讨为该目的使用 Netduino。

我最近写了一篇关于使用 Arduino 板作为 GPS 数据记录器的博客文章。我一直使用它为 Truck Tracker 应用程序收集地理位置数据。在这篇文章中,我将探讨为该目的使用 Netduino

认识 Netduino

这是 Netduino 板的照片。

Netduino 网站上有更多关于 Netduino 板的技术规格。对我来说最突出的两个特点是

  • 大多数 Arduino 扩展板都与 Netduino 兼容。这意味着许多 Arduino 项目都可以移植到 Netduino。在这篇文章中,我们将探讨将 Arduino 项目中使用的 GPS 扩展板移植到 Netduino。
  • Netduino 的编程环境是 .NET Micro Framework (.NET MF)。这使得您喜欢的 Visual Studio 版本可以作为您的 IDE。

这是 GPS 扩展板安装在 Netduino 上的照片。

连接

如果您按照 ladyada 网站上的说明构建了 GPS 扩展板,那么 Netduino 将使用以下引脚连接与 GPS 扩展板通信:

  • TX (数字 I/O 引脚 0) – 这是传输引脚,来自 GPS 模块的位置数据通过它传输。
  • RX (数字 I/O 引脚 1) – 这是接收引脚,发送到 GPS 模块以对其进行配置的数据通过它传输。
  • PWR (数字 I/O 引脚 2) – 此引脚连接到一个控制 GPS 电源的晶体管。当此引脚设置为 LOW 时,GPS 模块打开;当引脚设置为 HIGH 时,GPS 关闭。

一旦设备通电,TX/RX 引脚就构成一个用于与 GPS 设备通信的串行端口。一旦供电,GPS 数据将开始从 GPS 设备流出。

基本编程

连接和读取 GPS 串行端口数据的最基本的 Netduino 代码如下所示:

using System;
using System.IO.Ports;
using System.Threading;
using Microsoft.SPOT;
using Microsoft.SPOT.Hardware;
using SecretLabs.NETMF.Hardware;
using SecretLabs.NETMF.Hardware.Netduino;

namespace GpsLogger
{
    public class Program
    {
        public static void Main()
        {
            // write your code here
            SerialPort serialPort = new SerialPort("COM1", 4800, Parity.None, 8, StopBits.One);
            serialPort.Open();

            // pin D2 off = gps module turned on
            OutputPort powerPin = new OutputPort(Pins.GPIO_PIN_D2, false);

            while (true)
            {
                int bytesToRead = serialPort.BytesToRead;
                if (bytesToRead > 0)
                {
                    // get the waiting data
                    byte[] buffer = new byte[bytesToRead];
                    serialPort.Read(buffer, 0, buffer.Length);
                    // print out our received data
                    Debug.Print(new String(System.Text.Encoding.UTF8.GetChars(buffer)));
                }

                Thread.Sleep(100); // wait a bit so we get a few bytes at a time...
            }
        }
    }
}

此代码摘自非常活跃的 Netduino 社区论坛上的其中一个帖子。您首先会注意到一些新的 C# using 语句。其中一些来自 Microsoft.SPOT 命名空间。SPOT 是“智能个人对象技术”的缩写,由Microsoft 开发,用于个性化家用电器和其他日常设备。Microsoft 已将此命名空间扩展到包含一系列重要的硬件功能,程序员在开发嵌入式设备固件时可以使用。

代码的前两行开始与 GPS 进行串行通信。Netduino 固件的设置是“COM1”使用数字引脚 0/1 进行串行 TX/RX。下一行代码为 GPS 设备供电。最后,永无止境的 while 循环读取数据并将其打印到调试器。下面是调试器运行的屏幕截图。

我提到过调试器吗?您可以像在 Visual Studio 中一样调试您的 Netduino 嵌入式项目。令人惊讶的是,断点、监视和实时评估都有效。做得好,无论谁想出了这个!

在右下角,您可以看到调试输出,显示来自 GPS 设备串行数据。

改进示例

上面的代码非常适合开始读取 GPS 数据。要获得可重用的 GPS 扩展板库,还需要做更多工作。该 GPS 库的目标是:

  • 将 GPS 代码封装到几个逻辑类中,使其可重用。
  • 利用 Netduino 对事件的支持,创建一个可以供应用程序其他部分订阅的事件。每当有新数据可用时,都会引发该事件。

这是使用该库的新入口点:

public static void Main()
{
    SerialPort serialPort = new SerialPort("COM1", 4800, Parity.None, 8, StopBits.One);

    OutputPort powerPin = new OutputPort(Pins.GPIO_PIN_D2, false);

    Reader gpsShield = new Reader(serialPort, 100, 1.0);
    gpsShield.GpsData += GpsShield_GpsData;
    gpsShield.Start();

    while (true)
    {
        // Do other processing here.
        //
        // Can stop the gps processing by calling...
        //  gpsShield.Stop();
        //
        // Restart by calling...
        //  gpsShield.Start();
        //
        Debug.Print("Main...");
        Thread.Sleep(10000);
    }
}

private static void GpsShield_GpsData(GpsPoint gpsPoint)
{
    Debug.Print("time: " + gpsPoint.Timestamp + "tLat/Lng: " + gpsPoint.Latitude + "/" + gpsPoint.Longitude);
}

此代码的开始部分几乎相同,创建了用于串行通信和供电的“SerialPort”和“OutputPort”对象。它使用构造函数注入创建一个 GPS“Reader”类,该类可以访问“SerialPort”对象。构造函数接受三个参数:一个“SerialPort”实例、一个以毫秒为单位的超时时间和 GPS 数据事件之间的最小距离(以英里为单位)。超时用作读取串行端口的一种限制器。距离参数允许硬件以更高的速率(每秒一次)获取 GPS 观测点,并且仅在点之间的距离大于此最小值时才引发数据可用事件。这允许仅在新的点显着分开时才保存数据,从而更有效地利用 SD 卡内存。

接下来,主应用程序订阅“GpsData”事件,并分配一个事件处理程序。目前,数据仅写入调试控制台。这最终将成为将数据持久化到 SD 存储卡所使用的例程。请注意传递给事件的数据类型为“GpsPoint”。

将数据持久化到 SD 卡的功能目前正在集成到 Netduino 固件中。预计很快会有更新。

public class GpsPoint
{
    public DateTime Timestamp { get; set; }
    public double Latitude { get; set; }
    public double Longitude { get; set; }
    public double SpeedInKnots { get; set; }
    public double BearingInDegrees { get; set; }
}

GpsPoint”的实例包含从 GPS 串行端口解析的位置信息。

reader 类如下所示:

public class Reader
{
    private readonly object _lock = new object();
    private readonly SerialPort _serialPort;
    private readonly int _timeOut;
    private readonly double _minDistanceBetweenPoints;
    private bool _isStarted;
    private Thread _processor;

    public delegate void LineProcessor(string line);

    public delegate void GpsDataProcessor(GpsPoint gpsPoint);

    public event LineProcessor RawLine;
    public event GpsDataProcessor GpsData;

    public bool IsStarted { get { return _isStarted; } }

    public Reader(SerialPort serialPort)
        : this(serialPort, 100, 0.0)
    {

    }
    public Reader(SerialPort serialPort, int timeOutBetweenReadsInMilliseconds, double minDistanceInMilesBetweenPoints)
    {
        _serialPort = serialPort;
        _timeOut = timeOutBetweenReadsInMilliseconds;
        _minDistanceBetweenPoints = minDistanceInMilesBetweenPoints;
    }

    public bool Start()
    {
        lock (_lock)
        {
            if(_isStarted)
            {
                return false;
            }
            _isStarted = true;
            _processor = new Thread(ThreadProc);
            _processor.Start();
        }
        return true;
    }

    public bool Stop()
    {
        lock (_lock)
        {
            if(!_isStarted)
            {
                return false;
            }
            _isStarted = false;
            if(!_processor.Join(5000))
            {
                _processor.Abort();
            }
            return true;
        }
    }

    private void ThreadProc()
    {
        Debug.Print("GPS thread started...");
        if(!_serialPort.IsOpen)
        {
            _serialPort.Open();
        }
        while (_isStarted)
        {
            int bytesToRead = _serialPort.BytesToRead;
            if (bytesToRead > 0)
            {
                byte[] buffer = new byte[bytesToRead];
                _serialPort.Read(buffer, 0, buffer.Length);
                try
                {
                    string temp = new string(System.Text.Encoding.UTF8.GetChars(buffer));
                    ProcessBytes(temp);
                }
                catch (Exception ex)
                {
                    // only process lines we can parse.
                    Debug.Print(ex.ToString());
                }
            }

            Thread.Sleep(_timeOut);
        }
        Debug.Print("GPS thread stopped...");
    }

    private string _data = string.Empty;
    private GpsPoint _lastPoint;
    private void ProcessBytes(string temp)
    {
        while (temp.IndexOf('n') != -1)
        {
            string[] parts = temp.Split('n');
            _data += parts[0];
            _data = _data.Trim();
            if (_data != string.Empty)
            {
                if (_data.IndexOf("$GPRMC") == 0)
                {
                    if(GpsData!=null)
                    {
                        GpsPoint gpsPoint = GprmcParser.Parse(_data);
                        if (gpsPoint != null)
                        {
                            bool isOk = true;
                            if(_lastPoint!=null)
                            {
                                double distance = GeoDistanceCalculator.DistanceInMiles(gpsPoint.Latitude, gpsPoint.Longitude,
                                                                               _lastPoint.Latitude, _lastPoint.Longitude);
                                double distInFeet = distance*5280;
                                Debug.Print("distance = " + distance + " mi (" + distInFeet + " feet)");
                                if(distance<_minDistanceBetweenPoints)
                                {
                                    // Too close to the last point....don't raise the event
                                    isOk = false;
                                }
                            }
                            _lastPoint = gpsPoint;

                            // Raise the event
                            if(isOk)
                            {
                                GpsData(gpsPoint);
                            }
                        }
                    }
                }
                if (RawLine != null)
                {
                    RawLine(_data);
                }
            }
            temp = parts[1];
            _data = string.Empty;
        }
        _data += temp;
    }
}

面向公众的功能包括构造函数、“Start”和“Stop”。我们之前讨论了构造函数参数。“Start”方法启动一个线程来读取和处理 GPS 串行端口。“Stop”方法向线程发出结束信号,并等待线程加入请求停止的线程。

我对 Netduino 上的多线程工作方式不太确定。此代码在我的计算机上似乎有效,但可能存在我不知道的问题。可以轻松地从上述代码中删除额外的线程。我曾想,主线程可能有其他处理(例如响应式 UI),将 GPS 推送到其自己的线程可能是必要的。

ThreadProc”方法是在单独线程中运行的主要处理例程。它是一个循环,直到“Stop”方法将“_isStarted”变量设置为“false”。while 循环的核心是读取和处理 GPS 串行端口的数据。

ProcessBytes”方法处理串行端口数据。GPS 设备提供具有指定格式的 “GPS 语句”流。此方法将传入的字节组合起来,并在形成完整的行(或语句)后对其进行处理。上面的代码仅查找“GPRMC”语句。如果找到一个,则使用以下解析器进行解析:

public class GprmcParser
{
    // Parse the GPRMC line
    //
    public static GpsPoint Parse(string line)
    {
        // $GPRMC,040302.663,A,3939.7,N,10506.6,W,0.27,358.86,200804,,*1A

        if(!IsCheckSumGood(line))
        {
            return null;
        }

        try
        {
            string[] parts = line.Split(',');
            if (parts.Length != 12)
            {
                return null;
            }

            if (parts[2] != "A")
            {
                return null;
            }

            string date = parts[9]; // UTC Date DDMMYY
            if (date.Length != 6)
            {
                return null;
            }
            int year = 2000 + int.Parse(date.Substring(4, 2));
            int month = int.Parse(date.Substring(2, 2));
            int day = int.Parse(date.Substring(0, 2));
            string time = parts[1]; // HHMMSS.XXX
            if (time.Length != 10)
            {
                return null;
            }
            int hour = int.Parse(time.Substring(0, 2));
            int minute = int.Parse(time.Substring(2, 2));
            int second = int.Parse(time.Substring(4, 2));
            int milliseconds = int.Parse(time.Substring(7, 3));
            DateTime utcTime = new DateTime(year, month, day, hour, minute, second, milliseconds);

            string lat = parts[3];  // HHMM.MMMM
            if (lat.Length != 9)
            {
                return null;
            }
            double latHours = double.Parse(lat.Substring(0, 2));
            double latMins = double.Parse(lat.Substring(2));
            double latitude = latHours + latMins / 60.0;
            if (parts[4] == "S")       // N or S
            {
                latitude = -latitude;
            }

            string lng = parts[5];  // HHHMM.M
            if (lng.Length != 10)
            {
                return null;
            }
            double lngHours = double.Parse(lng.Substring(0, 3));
            double lngMins = double.Parse(lng.Substring(3));
            double longitude = lngHours + lngMins / 60.0;
            if (parts[6] == "W")
            {
                longitude = -longitude;
            }

            double speed = double.Parse(parts[7]);
            double bearing = double.Parse(parts[8]);

            // Should probably validate check sum

            GpsPoint gpsPoint = new GpsPoint
                                  {
                                      BearingInDegrees = bearing,
                                      Latitude = latitude,
                                      Longitude = longitude,
                                      SpeedInKnots = speed,
                                      Timestamp = utcTime
                                  };
            return gpsPoint;

        }
        catch (Exception)
        {
            // One of our parses failed...ignore.
        }
        return null;
    }

    private static bool IsCheckSumGood(string sentence)
    {
        int index1 = sentence.IndexOf("$");
        int index2 = sentence.LastIndexOf("*");

        if (index1 != 0 || index2 != sentence.Length - 3 )
        {
            return false;
        }

        string checkSumString = sentence.Substring(index2 + 1, 2);
        int checkSum1 = Convert.ToInt32(checkSumString, 16);

        string valToCheck = sentence.Substring(index1 + 1, index2 - 1);
        char c = valToCheck[0];
        for(int i = 1;i<valToCheck.Length;i++)
        {
            c ^= valToCheck[i];
        }

        return checkSum1 == c;
    }
}

市面上有许多可用的 GPS 语句解析器。上面的解析器提取语句中的编码信息,并返回一个“GpsPoint”对象(如果解析错误,则返回 null)。

然后,处理代码计算新点与前一个点之间的距离。这是使用以下类实现的 Haversine 公式完成的:

public static class GeoDistanceCalculator
{
    private const double _earthRadiusInMiles = 3956.0;
    private const double _earthRadiusInKilometers = 6367.0;
    public static double DistanceInMiles(double lat1, double lng1, double lat2, double lng2)
    {
        return Distance(lat1, lng1, lat2, lng2, _earthRadiusInMiles);
    }
    public static double DistanceInKilometers(double lat1, double lng1, double lat2, double lng2)
    {
        return Distance(lat1, lng1, lat2, lng2, _earthRadiusInKilometers);
    }
    private static double Distance(double lat1, double lng1, double lat2, double lng2, double radius)
    {
        // Implements the Haversine formulat http://en.wikipedia.org/wiki/Haversine_formula
        //
        var lat = MathMF.ToRadians(lat2 - lat1);
        var lng = MathMF.ToRadians(lng2 - lng1);
        var sinLat = MathMF.Sin(0.5*lat);
        var sinLng = MathMF.Sin(0.5*lng);
        var cosLat1 = MathMF.Cos(MathMF.ToRadians(lat1));
        var cosLat2 = MathMF.Cos(MathMF.ToRadians(lat2));
        var h1 = sinLat*sinLat + cosLat1*cosLat2*sinLng*sinLng;
        var h2 = MathMF.Sqrt(h1);
        var h3 = 2 * MathMF.Asin(MathMF.Min(1, h2));
        return radius * h3;
    }
}

此代码严重依赖于由 Elze Kool 开发的数学库(MathMF 命名空间)。我稍微修改了这个库,以便在可能的情况下使用 `System.Math` 函数。我所做的更改如下所示:

public static readonly double PI = System.Math.PI;

public static readonly double E = System.Math.E;


public static double Pow(double x, double y)
{
    return System.Math.Pow(x, y);
}

public static double Sqrt(double x)
{
    return System.Math.Pow(x, 0.5);
}

.NET Micro Framework 中并非所有 .NET 框架都可用。一个例子是“System.Math”命名空间。随着 .NET Micro Framework 的发展,我怀疑其中一些命名空间将变得可用。在此之前,我们必须依赖社区的实现。

如果距离大于指定的(在构造函数中)最小值,则会引发“GpsData”事件。此外,还有一个“RawLine”事件可用于获取每个 GPS 语句。

摘要

使用 Netduino 和 GPS 扩展板非常直接。我特别喜欢可以使用 Visual Studio 作为我的 IDE。当 Netduino 团队发布包含 SD 卡功能的更新固件时,我将继续开发并开始保存数据。我鼓励所有有点好奇心的人去购买一个 Netduino 并开始进行实验。

© . All rights reserved.