I recently blogged about using the Arduino board as a GPS data logger. I have been using this to collect geo-location data for the Truck Tracker application. In this post, I will explore using the Netduino for that purpose.
Meet the Netduino
Here is a photo of the Netduino board.
There are more technical specs for the Netduino board on the Netduino site. The two features that stand out to me are:
- Most of the Arduino shields are compatible with the Netduino. This means that many of the Arduino projects can be ported to the Netduino. In this post, we will look at porting the GPS shield used in the Arduino project to work with the Netduino.
- The programming environment of the Netduino is the .NET Micro Framework (.NET MF). This allows your favorite flavor of Visual Studio to be used as your IDE.
Here is a photo of the GPS shield mounted to the Netduino.
Connections
If you built the GPS shield following the instructions on the ladyada site, then the following are the pin out connections that the Netduino will use to communicate with the GPS shield:
- TX (Digital I/O Pin 0) – This is the transmit pin, data that comes from the GPS module with location data.
- RX (Digital I/O Pin 1) – This is the receive pin, data that goes to the GPS module to configure it.
- PWR (Digital I/O Pin 2) – This pin is connected to a transistor that controls power to the GPS. When this pin is set to LOW the GPS module turns on and when the pin is set to HIGH the GPS turns off.
Once the unit has power, the TX/RX pins are a serial port that is used to communicate with the GPS unit. Once power is applied, GPS data will begin being streamed from the GPS unit.
Basic Programming
The most basic Netduino code to connect and read data from the GPS serial port is shown below:
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()
{
SerialPort serialPort = new SerialPort("COM1", 4800, Parity.None, 8, StopBits.One);
serialPort.Open();
OutputPort powerPin = new OutputPort(Pins.GPIO_PIN_D2, false);
while (true)
{
int bytesToRead = serialPort.BytesToRead;
if (bytesToRead > 0)
{
byte[] buffer = new byte[bytesToRead];
serialPort.Read(buffer, 0, buffer.Length);
Debug.Print(new String(System.Text.Encoding.UTF8.GetChars(buffer)));
}
Thread.Sleep(100);
}
}
}
}
This code is taken from the one of the posts on the very active Netduino community forum. The first thing you will notice is a few new C# using statements. There are a few from the Microsoft.SPOT namespace. SPOT stands for "smart personal object technology" and was developed by Microsoft to personalize household electronics and other everyday devices. Microsoft has extended this namespace to include a range of essential hardware features that programmers can use when developing embedded device firmware. There are also a couple of using statements with a Secretlabs.NETMF.Hardware
namespace. Secret Labs is the company that is developing the Netduino hardware and manages the developer community.
The first two lines of the code begins serial communications with the GPS. The Netduino firmware is setup so that "COM1" uses digital pins 0 / 1 for serial TX / RX. The next line of code applies power to the GPS unit. Finally the never ending while loop, reads and prints data to the debugger. Below is a screen shot of the debugger in action.
Did I mention the debugger? You can debug your Netduino embedded project just like you are used to in Visual Studio. Amazingly, break points, watches and live evaluations all work. Great job to who ever figured that out!
In the bottom right, you can see the debug output showing the serial data coming from the GPS unit.
A Refined Example
The above code is great to get you started reading the GPS data. There is a bit more work to be done to get a reusable GPS shield library. Here are the goals of this GPS library:
- Encapsulate the GPS code into a few logical classes in a form that is reusable.
- Take advantage of the Netduino’s support of events by creating an event that other parts of the application can subscribe. The event will be raised when ever new data is available.
Here is the new entry point that is using the library:
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)
{
Debug.Print("Main...");
Thread.Sleep(10000);
}
}
private static void GpsShield_GpsData(GpsPoint gpsPoint)
{
Debug.Print("time: " + gpsPoint.Timestamp + "tLat/Lng: " + gpsPoint.Latitude + "/" + gpsPoint.Longitude);
}
This code starts nearly the same by creating a ‘SerialPort
’ and ‘OutputPort
’ objects for serial communication and power. It uses constructor injection to create a GPS ‘Reader
’ class with access to the ‘SerialPort
’ object. The constructor takes three parameters: an instance of ‘SerialPort
’, a timeout in milliseconds, and the minimum distance (in miles) between GPS data events. The timeout is used as a bit of a throttle for reading the serial port. The distance parameter allows the hardware take GPS observation points at a higher rate (one per second) and only raise data available events if the distance between the points is larger than this minimum. This allows data to be saved only when the new points are significantly apart thus allowing the SD card memory to be used more efficiently.
Next the "GpsData
" event is subscribed to by the main application and an event handler is assigned. Currently, the data is only written to the debug console. This will eventually be the routine used to persist the data to the SD memory card. Notice the data being passed to the event is of type "GpsPoint
".
The ability to persist data to SD cards is currently being baked into the Netduino firmware. There is an update expected soon.
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; }
}
Instances of "GpsPoint
" contain the location information parsed from the GPS serial port.
The reader class is shown below:
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)
{
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)
{
isOk = false;
}
}
_lastPoint = gpsPoint;
if(isOk)
{
GpsData(gpsPoint);
}
}
}
}
if (RawLine != null)
{
RawLine(_data);
}
}
temp = parts[1];
_data = string.Empty;
}
_data += temp;
}
}
The public facing features include the CTOR, ‘Start’, and ‘Stop’. We previously discussed the CTOR parameters. The ‘Start’ method spins up a thread to read and process the GPS serial port. The ‘Stop’ method signals the thread to end and waits for it to join the thread requesting the stop.
I am not as sure of how the threading is working on the Netduino. This code seems to work on my computer, but may have issues that I am not aware of. The additional thread could easily be removed from the above code. I was thinking the main thread may have other processing (think a responsive UI) and pushing the GPS to its own thread may be necessary.
The ‘ThreadProc
’ method is the main processing routine running in the separate thread. It is a loop that continues until the ‘_isStarted’ variable is set to ‘false
’ by the ‘Stop
’ method. The guts of the while loop reads and processes data from the GPS serial port.
The ‘ProcessBytes
’ method process the serial port data. The GPS unit provides a stream of ‘GPS sentences’ that have a specified format. This method puts together the incoming bytes and processes completed lines (or sentences) once they are formed. The code above looks for only the "GPRMC" sentence. If one is found it is parsed using the following parser:
public class GprmcParser
{
public static GpsPoint Parse(string line)
{
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];
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];
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];
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")
{
latitude = -latitude;
}
string lng = parts[5];
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]);
GpsPoint gpsPoint = new GpsPoint
{
BearingInDegrees = bearing,
Latitude = latitude,
Longitude = longitude,
SpeedInKnots = speed,
Timestamp = utcTime
};
return gpsPoint;
}
catch (Exception)
{
}
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;
}
}
There are many available GPS sentence parsers out there. The above parser extracts the encoded information from the sentence and returns a ‘GpsPoint
’ object (or null if there was a parsing error).
The processing code then calculates the distance between the new point and the previous point. This is done using the Haversine formula implemented by the following class:
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)
{
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;
}
}
This code relies heavily on a math library (MathMF namespace) developed by Elze Kool. I slightly modified this library to use the System.Math
functions where ever they were available. The changes that I made are shown below:
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);
}
Not all the .NET framework is available in the .NET micro framework. One example is the ‘System.Math’ namespace. As the .NET micro framework grows up, I suspect that some of these namespaces will become available. Until then, we must rely on implementation made by the community.
If the distance is larger than the specified (in the CTOR) minimum, then the ‘GpsData
’ event is raised. In addition a ‘RawLine
’ event is available for every GPS sentence to be available.
Summary
Using the Netduino with the GPS shield was very straight-forward. I especially enjoy that I can use Visual Studio as my IDE. When the Netduino folks post an updated firmware that includes SD card features, I will continue this development and begin saving the data. I encourage anyone with a bit of curiosity to go get your self a Netduino and begin experimenting.