/*********************************************************************************
* TotalCross Software Development Kit *
* Copyright (C) 2000-2012 SuperWaba Ltda. *
* All Rights Reserved *
* *
* This library and virtual machine is distributed in the hope that it will *
* be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. *
* *
* This file is covered by the GNU LESSER GENERAL PUBLIC LICENSE VERSION 3.0 *
* A copy of this license is located in file license.txt at the root of this *
* SDK or can be downloaded here: *
* http://www.gnu.org/licenses/lgpl-3.0.txt *
* *
*********************************************************************************/
package totalcross.io.device.gps;
import totalcross.io.*;
import totalcross.io.device.*;
import totalcross.sys.*;
import totalcross.util.*;
/**
* Class that retrieves GPS coordinates read from the COM (or Bluetooth, or IR) port.
* Windows Mobile, Android and iOS use the native API instead of reading from the COM port.
*
* This class only retrieves data updating the internal fields. If you want to display that data,
* you may use the GPSView class.
*
* For example:
*
* <pre>
* new Thread()
* {
* public void run()
* {
* gps = new GPS();
* for (int i = 0; i < 60*2 && gps.location[0] == 0; i++) // wait 60s
* {
* Vm.safeSleep(500);
* try
* {
* gps.retrieveGPSData();
* }
* catch (Exception eee)
* {
* eee.printStackTrace();
* break;
* }
* }
* }
* }.start();
* </pre>
*
* If the GPS fails connecting to the satellites, and the phone has signal, you can use the cell tower location as a
* rough location. The precision is vary between 50m to 3km, depending where the phone is. You can get the
* latitude and longitude using CellInfo.toCoordinates.
*
* See the tc.samples.maps.GoogleMaps sample.
*
* @see totalcross.io.device.gps.GPS
* @see totalcross.phone.CellInfo#toCoordinates()
*
* @since TotalCross 1.38
*/
public class GPS
{
/** Stores the location - latitude on first index (0) and longitude on second index (1).
* On low signal, it contains the value <CODE>INVALID</code>. */
public double[] location = {INVALID,INVALID};
/** Stores the direction in degrees from the North.
* On low signal, it contains the value <CODE>INVALID</code>. */
public double direction = INVALID;
/** Stores the speed in knots.
* On low signal, it contains the value <CODE>INVALID</code>. */
public double velocity = INVALID;
/**
* Number of satellites in view. Not supported on WP8.
*
* @since TotalCross 1.20
*/
public int satellites;
/** Stores the time of the last updated. */
public Time lastFix = new Time();
/** The retrieved message, or null in Windows Mobile and Android. */
public String messageReceived;
/** The last PDOP, if any.
* On low signal, it contains the value <CODE>INVALID</code>.
* @since TotalCross 1.27
*/
public double pdop = INVALID;
/** The reason for the low signal, if retrieveGPSData returned false.
* @since TotalCross 1.38
*/
public String lowSignalReason;
/** A value that indicates that invalid data was retrieved.
* Declared as the minimum double value.
*/
public static final double INVALID = Convert.MIN_DOUBLE_VALUE;
PortConnector sp;
private byte[] buf = new byte[1];
private StringBuffer sb = new StringBuffer(512);
private static boolean nativeAPI = Settings.isWindowsDevice() || Settings.platform.equals(Settings.ANDROID) || Settings.isIOS();
private static boolean isOpen;
boolean dontFinalize;
/**
* Returns the Windows CE GPS COM port, which can be used to open a PortConnector. Sample:
*
* <pre>
* String com;
* if (Settings.isWindowsDevice() && (com = GPS.getWinCEGPSCom()) != null)
* sp = new PortConnector(Convert.chars2int(com), 9600, 7, PortConnector.PARITY_EVEN, 1);
* </pre>
*
* @return A string like "COM3", or null if no keys with GPS was found under HKLM\Drivers\BuildIn.
*/
public static String getWinCEGPSCom() // guich@tc100b5_38
{
try
{
try // guich@tc120_51
{
String key = "System\\CurrentControlSet\\GPS Intermediate Driver\\Multiplexer\\ActiveDevice";
String prefix = Registry.getString(Registry.HKEY_LOCAL_MACHINE, key, "Prefix");
int index = Registry.getInt(Registry.HKEY_LOCAL_MACHINE, key, "Index");
if (prefix.equals("COM"))
return "COM" + index;
}
catch (ElementNotFoundException enfe)
{
// ignore if a key was not found
}
String[] keys = Registry.list(Registry.HKEY_LOCAL_MACHINE, "Drivers\\BuiltIn");
for (int i = 0; i < keys.length; i++)
{
String k = keys[i].toLowerCase();
if (k.indexOf("serial") >= 0)
{
String key = "Drivers\\BuiltIn\\" + keys[i], prefix, name;
try
{
prefix = Registry.getString(Registry.HKEY_LOCAL_MACHINE, key, "Prefix");
name = Registry.getString(Registry.HKEY_LOCAL_MACHINE, key, "FriendlyName");
if (prefix.equals("COM") && name.toLowerCase().indexOf("gps") >= 0)
return "COM" + Registry.getInt(Registry.HKEY_LOCAL_MACHINE, key, "Index");
}
catch (ElementNotFoundException enfe)
{
// ignore if a key was not found
}
}
}
}
catch (Exception e)
{
}
return null;
}
/**
* Constructs a GPS control, opening a default port at 9600 bps. Already prepared for PIDION scanners. It
* automatically scans the Windows CE registry searching for the correct GPS COM port.
*
* Under Windows Mobile and Android, uses the internal GPS api.
*
* @throws IOException If something goes wrong or if there's already an open instance of the GPS class
*/
public GPS() throws IOException
{
checkOpen();
if (!nativeAPI || !startGPS())
{
String com;
if ("PIDION".equals(Settings.deviceId)) // guich@586_7
sp = new PortConnector(Convert.chars2int("COM4"), 9600, 7, PortConnector.PARITY_EVEN, 1);
else if (Settings.isWindowsDevice() && (com = getWinCEGPSCom()) != null)
sp = new PortConnector(Convert.chars2int(com), 9600, 7, PortConnector.PARITY_EVEN, 1);
else
sp = new PortConnector(0, 9600);
}
if (sp != null)
{
sp.readTimeout = 1500;
sp.setFlowControl(false);
}
}
/**
* Constructs a GPS control with the given serial port. For example:
*
* <pre>
* PortConnector sp = new PortConnector(PortConnector.BLUETOOTH, 9600);
* sp.setReadTimeout(500);
* gps = new GPS(sp);
* </pre>
* Don't use this constructor under Android nor Windows Mobile.
* @see #GPS()
* @throws IOException If something goes wrong or if there's already an open instance of the GPS class
*/
public GPS(PortConnector sp) throws IOException
{
checkOpen();
if (sp != null)
this.sp = sp;
else
if (nativeAPI)
startGPS();
}
private boolean startGPS() throws IOException {return false;}
native boolean startGPS4D() throws IOException;
private void checkOpen() throws IOException
{
if (isOpen) throw new IOException("There's already an open instance of the GPS class");
isOpen = true;
}
/**
* Closes the underlying PortConnector or native api.
*/
public void stop()
{
dontFinalize = true;
isOpen = false;
if (sp == null)
stopGPS();
else
{
try {sp.close();} catch (Exception e) {}
sp = null;
}
}
private void stopGPS()
{
}
native void stopGPS4D();
/**
* Returns the latitude (<code>location[0]</code>).
*/
public double getLatitude()
{
return location[0];
}
/**
* Returns the longitude (<code>location[1]</code>).
*/
public double getLongitude()
{
return location[1];
}
/**
* Returns the coordinate of the given string and direction.
*
* @param s
* the string in coordinate format.
* @param dir
* the direction: SWEN
*/
public static double toCoordinate(String s, char dir)
{
double deg = 0;
int i = s.indexOf('.'); // guich@421_58
if (i >= 0)
{
int divider = 1;
int size = s.length();
for (int d = size - i - 1; d > 0; d--)
divider *= 10;
try
{
deg = (double) Convert.toInt(s.substring(0, i - 2)) +
((double) Convert.toInt(s.substring(i - 2, i)) +
(double) Convert.toInt(s.substring(i + 1, size)) / divider) / 60;
}
catch (InvalidNumberException ine)
{
return 0;
}
if (dir == 'S' || dir == 'W')
deg = -deg;
}
return deg;
}
public static String toCoordinateGGMMSS(String coord, char dir) throws InvalidNumberException
{
try
{
String g, m, s;
if (dir == 'E' || dir == 'W') // LONGITUDE
{
int gint = Convert.toInt(coord.substring(0, 3));
g = gint + " ";
m = coord.substring(3, 5) + " ";
double temp = Convert.toDouble("0" + coord.substring(5, 10)) * 60;
s = Convert.toString(temp, 4);
}
else
{
g = coord.substring(0, 2) + " "; // LATITUDE
m = coord.substring(2, 4) + " ";
double temp = Convert.toDouble("0" + coord.substring(4, 9)) * 60;
s = Convert.toString(temp, 4);
}
return g + m + s + " " + dir;
}
catch (IndexOutOfBoundsException ioobe)
{
return "Err: " + coord + " - " + dir;
}
}
/** Call this method to retrieve the data from the GPS.
* @returns true if the data was retrieved, false if low signal.
* @see #lowSignalReason
*/
public boolean retrieveGPSData()
{
try
{
lowSignalReason = null;
location[0] = location[1] = direction = velocity = INVALID;
satellites = 0;
return sp != null ? processSerial() : processNative();
}
catch (Exception e1)
{
lowSignalReason = e1.getMessage()+" ("+(e1.getClass())+")";
return false;
}
}
private int updateLocation()
{
return 0;
}
native int updateLocation4D();
// private methods
private boolean processNative()
{
int result = updateLocation();
if ((result & 8) == 0) velocity = INVALID;
if ((result & 4) == 0) direction = INVALID;
return (result & 3) != 0; // latitude and longitude: one doesn't make sense without the other
}
private boolean processSerial() throws Exception
{
String message;
boolean ok = false;
while ((message = nextMessage()) != null)
{
messageReceived = message;
String[] sp = Convert.tokenizeString(message, ',');
if (sp[0].equals("$GPGGA")) // guich@tc115_71
{
if ("".equals(sp[2]) || "".equals(sp[4]))
continue;
location[0] = toCoordinate(sp[2], sp[3].charAt(0));
location[1] = toCoordinate(sp[4], sp[5].charAt(0));
if (sp.length > 7 && sp[7].length() > 0)
satellites = Convert.toInt(sp[7]); //flsobral@tc120_66: new field, satellites.
}
else if (sp[0].equals("$GPGLL"))
{
if (!"A".equals(sp[6]))
continue;
location[0] = toCoordinate(sp[1], sp[2].charAt(0));
location[1] = toCoordinate(sp[3], sp[4].charAt(0));
if (sp[5].length() > 0)
{
lastFix.hour = Convert.toInt(sp[5].substring(0, 2));
lastFix.minute = Convert.toInt(sp[5].substring(2, 4));
lastFix.second = Convert.toInt(sp[5].substring(4, 6));
lastFix.millis = 0;
}
ok = true;
}
// fleite@421_57: Adding position and time message's decode routine
else if (sp[0].equals("$GPRMC") && sp.length >= 8 && "A".equals(sp[2]))
{
if (!"A".equals(sp[2]))
continue;
location[0] = toCoordinate(sp[3], sp[4].charAt(0));
location[1] = toCoordinate(sp[5], sp[6].charAt(0));
if (sp[1].length() >= 6)
{
lastFix.hour = Convert.toInt(sp[1].substring(0, 2));
lastFix.minute = Convert.toInt(sp[1].substring(2, 4));
lastFix.second = Convert.toInt(sp[1].substring(4, 6));
lastFix.millis = 0;
}
if (sp[7].length() > 0)
velocity = Convert.toDouble(sp[7]); //knots
if (sp[8].length() > 0)
direction = Convert.toDouble(sp[8]); //degrees
ok = true;
}
else if (sp[0].equals("$GPGSA")) // guich@tc126_66
try
{
pdop = sp.length > 15 ? Convert.toDouble(sp[15]) : 0;
} catch (Exception e) {}
}
return ok;
}
/**
* Reads the next message from the port connector.
*
* guich@tc115_71: completely changed
* flsobral@tc120_66: messages are now validated.
*
* @return
* @throws IOException
*/
private String nextMessage() throws IOException
{
StringBuffer sb = this.sb;
byte[] buf = this.buf;
String ret = null;
int len = 0;
int retry = 5;
int read;
while ((read = sp.readBytes(buf, 0, 1)) != -1)
{
if (read == 0 && --retry <= 0)
throw new IOException("Invalid port: nothing to read.");
int c = buf[0] & 0xFF;
if (c == '$')
sb.setLength(0);
sb.append((char) c);
if (c == '\n')
break;
else if (c > 128) // just trash?
throw new IOException("Invalid port: only trash was retrieved.");
else if (++len > 512)
{
int idx = sb.toString().indexOf('$');
if (idx == -1)
sb.setLength(0);
else
sb.delete(0, idx);
len = sb.length();
}
}
if (len > 0)
{
ret = sb.toString();
if (ret.length() <= 3)
return null;
sb.setLength(0);
ret = validateMessage(ret);
}
return ret;
}
/**
* Checks if the received message is valid.
*
* @param message
* @return the given message without the ending CRLF if valid, otherwise null is returned.
*
* @since TotalCross 1.20
*/
private String validateMessage(String message)
{
char[] msgChars = message.toCharArray();
int checksum = 0;
if (msgChars[0] != '$' || msgChars[msgChars.length - 2] != '\r' || msgChars[msgChars.length - 1] != '\n')
return null;
for (int i = msgChars.length - 6; i > 0; i--)
{
if (msgChars[i] == '\r' || msgChars[i] == '\n' || msgChars[i] == '$' || msgChars[i] == '*')
return null;
checksum ^= msgChars[i];
}
if (msgChars[msgChars.length - 5] == '*')
{
int checksum2 = Convert.hex2signed(new String(msgChars, msgChars.length - 4, 2));
if (checksum != checksum2)
return null;
}
return new String(msgChars, 0, msgChars.length - 2);
}
protected void finalize()
{
this.stop();
}
}