使用 Netduino 的 GPS






4.94/5 (15投票s)
我最近写了一篇关于使用 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 并开始进行实验。