Note: This is a reprint from the ISUG Tech Journal. It must indicate the following: “This article was first published in the ISUG Technical Journal, Jan.-Feb. 2007.”
Cutout: In this project we will write two components. One will be a server to listen for requests on port 1968. The other will be a client that will read information from the GPS receiver and send it to the server. The message the server will receive will be the client’s current latitude and longitude. It will then use this information to plot the current location on a map using MS MapPoint
A Location Tracking System Using PowerBuilder, a GPS Receiver, and Microsoft MapPoint
Finding the location of any individual in real-time
by Deanne Chance
Did you ever wonder where your kids are driving around on a Saturday night? Perhaps you are a business owner and need to know where your workforce is located on different job sites. With the help of PowerBuilder, a wireless connection, a GPS receiver, and Microsoft MapPoint, you can track the location of any individual in real-time. This article will show you how.
To get things started you will need a few things:
· PowerBuilder
· A serial-based GPS receiver (Bluetooth will do)
· MS MapPoint 2004
· MS Visual C# (for sockets implementation)
· A static IP address or a router capable of port forwarding
· A wireless connection for the client
· Some time to learn!
Introduction to Client/Server Programming
First, an introduction to socket-based client/server programming: In this project we will write two components. One will be a server to listen for requests on port 1968. The other will be a client that will read information from the GPS receiver and send it to the server. The message the server will receive will be the client’s current latitude and longitude. It will then use this information to plot the current location on a map using MS MapPoint.
You may be wondering why it is necessary to use port 1968. The answer is simple: It is not a well-known port, so we are free to use any number we like provided it is less than 2^8. In this case, I choose 1968 because it was the year I was born, but you can choose any number you like.
Determining Your IP Address
The next part of the equation is to get the IP address of the server. One way you can do this is to visit a site called http://whatsmyipaddress.com. This will give you the IP address the outside world uses to communicate with your computer. However, unless you have a static IP address, your server machine will have been assigned a local address by your router that is not accessible to the outside world. To get around this, you will need to set up port forwarding on the router. For example, if I login to my Linksys router, which is usually at 192.168.1.1, I can choose the applications and gaming tab to specify that I would like all requests to port 1968 to go to the machine my server is listening on. And how to do I know that address? Just use ipconfig at a command line. Here is an example so you get the idea.
Figure 1
Figure 2
Figure 3
Writing the Server
The server component is responsible for listening for requests from the client. In order to “listen” for requests, you will need two pieces of information: the IP address of the server machine and the port you wish to listen on. The IP address can be obtained by running ipconfig at a command line, while the port can be anything you want as long as it is not well known, and is less than 2^8. Note, in the example, I have hard-coded in the IP address and port number. However, in a production implementation, you probably want these values to be user-configurable.
To implement our TCP client/server sockets, we have a couple of choices. One is to use the Win32 API; the other is to create a .NET assembly. In this case, I chose the latter. In essence, what we are doing is calling a .NET assembly from PowerBuilder using COM wrappers. What this allows us to do is to create a class using C#, for example, and create a DLL that we can call from PowerBuilder. Wondering how to do that? See Bruce Armstrong’s article at http://pbdj.sys-con.com/read/258395.htm for details. However, in terms of the assembly, I will share my C# code with you in Listing 1 but defer the details of creating the assembly to his article.
You may wonder at this point, now that you have created a .NET assembly, how you use it in PowerBuilder. The following code listing shows how I am calling my assembly from PowerBuilder from a ue_postopen event. Notice that I have hard-coded in the IP address and port number. In a production version you would want these to be user-configurable options.
Integer li_Return
ole_map.object.Units = 1
ioo_map = ole_map.object.NewMap(1)
ioo_listner = CREATE OleObject
li_Return = ioo_Listner.ConnectToNewObject( "DotNetListner.DotNetSocketListner" )
ioo_listner.ipAddr = "192.168.1.108";
ioo_listner.portNumber = 1968;
ioo_listner.Initialize();
Once we have initialized our “socket listener,” we will need to retrieve data from the client. We will also need some agreement as to the protocol for the messages it accepts from the client. In this case, I am having the client pass the following information (each separated by a comma): latitude, longitude, user, and current time. For example, here is a sample message that will be received by the server:
42.43608, -89.022857, Chance, 1:22 a.m..
The code for retrieving data from the client is shown below. There are some important points to consider here. One, this is not a multi-threaded application, thus we have the infinite loop. However, I have placed some calls to Yield() to allow for any queued messages to be processed in between data retrievals from the client. Messy, but it works.
Double ldb_lat, ldb_long
do while 1=1
Yield()
ioo_Listner.GetData()
ldb_lat = Double(ioo_Listner.Latitude);
ldb_long = Double(ioo_Listner.Longitude);
Yield()
event post ue_plotdata(ldb_lat, ldb_long)
loop
Finally, once the client has retrieved the current latitude and longitude from the GPS receiver and sent it to the server, we need to plot the current position on the map. There are many mapping solutions out there, from Yahoo! Maps to Google Maps to MS MapPoint. I choose the latter, primarily because — well, what would happen if your Internet connection went down? There would be no way to make the mapping calls. Given that, the MS MapPoint code for plotting a given latitude and longitude is in Listing 2.
Writing the Client
The client component is responsible for reading information from the GPS receiver and sending it to the server. You will need to decide how often this occurs by programming the timer event. In this case, I chose every five minutes. As far as reading GPS information, this can be done using Win32 API calls that “talk” to the comm port for which the receiver is configured.
The protocol the receiver uses is called NMEA (for more, see www.gpsinformation.org/dale/nmea.htm). In general, the device sends sentences that describe the current position. Of interest to us is the GPGAA message that includes the current location. NMEA sentences are comma-delimited, so they are prime candidates for easy parsing using an external data window. For this example, I used Ian Thain’s NMEA parser, which can be found on Code-Exchange. So, the sequence of events is:
· Read an NMEA sentence from the GPS receiver
· Determine if it is GPGAA
· Convert the data to decimal degrees
· Send the message described above to the server
First, we need a way to communicate with the GPS server. To do that, we can use some Win32 API calls. Following are their prototypes. Why not just use the built-in PowerBuilder I/O functions such as FileOpen and FileRead? The underlying reason is that PowerBuilder will buffer the data for you (on top of the native buffering that the OS provides). This can be problematic for serial I/O devices.
· Function Long CreateFile(ref string lpszName, long fdwAccess, long fdwShareMode, long lpsa, long fdwCreate, long fdwAttrsAndFlags, long hTemplateFile) Library "Kernel32.dll"
· Function Long CloseHandle(Long hObject) Library "Kernel32.dll"
· Function Long ReadFile(Long hFile, ref string lpBuffer, long nBytesToRead, ref Long & nBytesRead, Long lNull) Library "Kernel32.dll"
· Function Long WriteFile(Long hFile, ref string lpBuffer, long nBytesToWrite, ref Long & nBytesWritten, LONG lNull) Library "Kernel32.dll"
The next order of business is to see how Ian Thain wrote his object to go about parsing a particular NMEA stream. For that, all we have to do is take a look at the format of an example sentence. Since our code picks off GGA; essential fix data that provide 3D location and accuracy data, we will look at that. A sample is in Listing 3.
What you can see from this example is that the data is comma delimited. This makes it a prime candidate for easy parsing by storing it in an external data window using the ImportString function. To get at the individual elements, we only need to index them by their position in the data window.
To extract each individual sentence out of the stream to be passed for parsing to the NMEA object, I wrote the wf_get_token function. This will extract a string based on a delimiter. For the NMEA stream, that is the newline character.
Finally, we need a way to send our message to our server. You can write a .NET assembly by following the example that acts as a client in order to send the GPS information to the client.
One final note: If you plan on connecting to a Bluetooth serial port greater than COM9, you will need to use the syntax "\\.\COMx" as the port name. Just substitute the port number you wish to use for x. The cause of the problem is that CreateFile accepts strings "COM1" - "COM9" as names of devices, but rejects those with two or more digits.
That’s it! You have now rolled your own location tracker. If you want to improve it, you might consider tracking more than one device at a time. This could easily be managed by an external data window that contains the user’s name and last position. When a new position comes in, simply do a lookup on the user name, update the data window, and plot the new location on the map. Additionally, all the hard-coded references to ports and IP addresses would well be served by user configuration files.
Happy coding!
Listing 1
using System;
using System.Collections.Generic;
using System.Text;
using System.Net.Sockets;
using System.Net;
using Microsoft.Win32;
namespace DotNetListner
{
public class DotNetSocketListner
{
TcpListener server = null;
int i = 0;
public int portNumber;
public int PortNumber
{
get {return portNumber;}
set { portNumber = value; }
}
public string ipAddr;
public string IPAddr
{
get { return ipAddr; }
set { ipAddr = value; }
}
public string latitude;
public string Latitude
{
get { return latitude; }
}
public string longitude;
public string Longitude
{
get { return longitude; }
}
public void Initialize()
{
//parse our I.P. addres
IPAddress localAddr = IPAddress.Parse(ipAddr);
// TcpListener server = new TcpListener(port);
server = new TcpListener(localAddr, portNumber);
// Start listening for client requests.
server.Start();
}
public void GetData()
{
// Perform a blocking call to accept requests.
TcpClient client = server.AcceptTcpClient();
// Get a stream object for reading and writing
NetworkStream stream = client.GetStream();
System.IO.StreamReader streamReader = new System.IO.StreamReader(stream);
byte[] bytes = new byte[client.ReceiveBufferSize];
// Read can return anything from 0 to numBytesToRead.
// This method blocks until at least one byte is read.
stream.Read(bytes, 0, (int)client.ReceiveBufferSize);
// Returns the data received from the host to the console.
string returndata = Encoding.UTF8.GetString(bytes);
//break up the string
string[] parts = returndata.Split(new char[] { ',' });
//set our properties
latitude = parts[0];
longitude = parts[1];
}
}
}
Listing 2
OleObject loo_Location, loo_PinCurrentFix
OleObject findresults
OleObject Map
if (IsNull(pushpin)) Then
pushpin.Delete();
End If
//parse out current lat/long
map = ole_map.object.ActiveMap;
loo_location = map.GetLocation(ab_lat, ab_long, 1);
loo_location.GoTo();
map.ZoomOut();
pushpin = ole_map.object.ActiveMap.AddPushpin(loo_location, "You are here");
pushpin.Select();
//Now use the Map.ObjectsFromPoint method to get points from the above location
findresults = ole_map.Object.ActiveMap.ObjectsFromPoint(ole_map.object.ActiveMap.LocationToX(loo_location), ole_map.object.ActiveMap.LocationToY(loo_location));
Listing 3
$GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,*47
Where:
GGA Global Positioning System Fix Data
123519 Fix taken at 12:35:19 UTC
4807.038,N Latitude 48 deg 07.038' N
01131.000,E Longitude 11 deg 31.000' E
1 Fix quality: 0 = invalid
1 = GPS fix (SPS)
2 = DGPS fix
3 = PPS fix
4 = Real Time Kinematic
5 = Float RTK
6 = estimated (dead reckoning) (2.3 feature)
7 = Manual input mode
8 = Simulation mode
08 Number of satellites being tracked
0.9 Horizontal dilution of position
545.4,M Altitude, Meters, above mean sea level
46.9,M Height of geoid (mean sea level) above WGS84
ellipsoid
(empty field) time in seconds since last DGPS update
(empty field) DGPS station ID number