package uk.org.smithfamily.mslogger;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.TimeZone;
import uk.org.smithfamily.mslogger.comms.ExtGPSConnectionManager;
import uk.org.smithfamily.mslogger.log.DebugLogManager;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationProvider;
import android.os.Bundle;
import android.os.SystemClock;
import android.text.TextUtils;
import android.text.TextUtils.SimpleStringSplitter;
import android.util.Log;
public enum ExtGPSManager
{
INSTANCE;
Location location;
private String locationTime = null;
private long locationTimestamp;
private int lastNumSatellites = 0;
private String providerName = "ExternalGPS";
private float precision = 10f;
private int locStatus = LocationProvider.OUT_OF_SERVICE;
private boolean hasGGA = false;
private boolean hasRMC = false;
private double readCounter = 0;
private long lastRpsTime = 0;
private volatile ExtGPSThread extGPSThread;
private volatile boolean running;
private static volatile ExtGPSThread watch;
private List<LocationListener> listeners = new ArrayList<LocationListener>();
private Location parseNmeaSentence(String sentence) throws SecurityException
{
SimpleStringSplitter splitter = new TextUtils.SimpleStringSplitter(',');
splitter.setString(sentence);
DebugLogManager.INSTANCE.log(sentence, Log.VERBOSE);
String command = splitter.next();
if (command.equals("$GPGGA"))
{
/*
* $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 number47 the checksum data, always begins
* with *
*/
// UTC time of fix HHmmss.S
String time = splitter.next();
// latitude ddmm.M
String lat = splitter.next();
// direction (N/S)
String latDir = splitter.next();
// longitude dddmm.M
String lon = splitter.next();
// direction (E/W)
String lonDir = splitter.next();
/*
* 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
*/
String quality = splitter.next();
// Number of satellites being tracked
String nbSat = splitter.next();
// Horizontal dilution of position (float)
String hdop = splitter.next();
// Altitude, Meters, above mean sea level
String alt = splitter.next();
// Height of geoid (mean sea level) above WGS84 ellipsoid
// String geoAlt = splitter.next();
// time in seconds since last DGPS update
// DGPS station ID number
if (quality != null && !quality.equals("") && !quality.equals("0"))
{
long updateTime = parseNmeaTime(time);
if (this.locStatus != LocationProvider.AVAILABLE)
{
notifyStatusChanged(LocationProvider.AVAILABLE, null, updateTime);
}
if (!time.equals(locationTime))
{
locationTime = time;
locationTimestamp = parseNmeaTime(time);
location.setTime(locationTimestamp);
}
if (lat != null && !lat.equals(""))
{
location.setLatitude(parseNmeaLatitude(lat, latDir));
}
if (lon != null && !lon.equals(""))
{
location.setLongitude(parseNmeaLongitude(lon, lonDir));
}
if (hdop != null && !hdop.equals(""))
{
location.setAccuracy(Float.parseFloat(hdop) * precision);
}
if (alt != null && !alt.equals(""))
{
location.setAltitude(Double.parseDouble(alt));
}
if (nbSat != null && !nbSat.equals(""))
{
Bundle extras = new Bundle();
int sats = Integer.parseInt(nbSat);
extras.putInt("satellites", sats);
location.setExtras(extras);
if (lastNumSatellites != sats)
{
lastNumSatellites = sats;
notifyStatusChanged(locStatus, extras, updateTime);
}
}
hasGGA = true;
if (hasGGA && hasRMC)
{
notifyLocationChanged(location);
}
}
else if (quality != null && quality.equals("0"))
{
if (locStatus != LocationProvider.TEMPORARILY_UNAVAILABLE)
{
long updateTime = parseNmeaTime(time);
notifyStatusChanged(LocationProvider.TEMPORARILY_UNAVAILABLE, null, updateTime);
}
}
}
else if (command.equals("$GPRMC"))
{
/*
* $GPRMC,123519,A,4807.038,N,01131.000,E,022.4,084.4,230394,003.1,W*6A
*
* Where: RMC Recommended Minimum sentence C 123519 Fix taken at 12:35:19 UTC A Status A=active or V=Void. 4807.038,N Latitude 48 deg
* 07.038' N 01131.000,E Longitude 11 deg 31.000' E 022.4 Speed over the ground in knots 084.4 Track angle in degrees True 230394 Date -
* 23rd of March 1994 003.1,W Magnetic Variation6A The checksum data, always begins with *
*/
// UTC time of fix HHmmss.S
String time = splitter.next();
// fix status (A/V)
String status = splitter.next();
// latitude ddmm.M
String lat = splitter.next();
// direction (N/S)
String latDir = splitter.next();
// longitude dddmm.M
String lon = splitter.next();
// direction (E/W)
String lonDir = splitter.next();
// Speed over the ground in knots
String speed = splitter.next();
// Track angle in degrees True
String bearing = splitter.next();
// UTC date of fix DDMMYY
// String date = splitter.next();
// Magnetic Variation ddd.D
// String magn = splitter.next();
// Magnetic variation direction (E/W)
// String magnDir = splitter.next();
// for NMEA 0183 version 3.00 active the Mode indicator field is added
// Mode indicator, (A=autonomous, D=differential, E=Estimated, N=not valid, S=Simulator )
if (status != null)
{
if (!status.equals("") && status.equals("A"))
{
if (this.locStatus != LocationProvider.AVAILABLE)
{
long updateTime = parseNmeaTime(time);
notifyStatusChanged(LocationProvider.AVAILABLE, null, updateTime);
}
if (!time.equals(locationTime))
{
locationTime = time;
locationTimestamp = parseNmeaTime(time);
location.setTime(locationTimestamp);
}
if (lat != null && !lat.equals(""))
{
location.setLatitude(parseNmeaLatitude(lat, latDir));
}
if (lon != null && !lon.equals(""))
{
location.setLongitude(parseNmeaLongitude(lon, lonDir));
}
if (speed != null && !speed.equals(""))
{
location.setSpeed(parseNmeaSpeed(speed, "N"));
}
if (bearing != null && !bearing.equals(""))
{
location.setBearing(Float.parseFloat(bearing));
}
hasRMC = true;
if (hasGGA && hasRMC)
{
notifyLocationChanged(location);
}
}
else if (status.equals("V"))
{
if (this.locStatus != LocationProvider.TEMPORARILY_UNAVAILABLE)
{
long updateTime = parseNmeaTime(time);
notifyStatusChanged(LocationProvider.TEMPORARILY_UNAVAILABLE, null, updateTime);
}
}
}
}
return location;
}
/**
*
* @param lat
* @param orientation
* @return
*/
private double parseNmeaLatitude(String lat, String orientation)
{
double latitude = 0.0;
if (lat != null && orientation != null && !lat.equals("") && !orientation.equals(""))
{
double temp1 = Double.parseDouble(lat);
double temp2 = Math.floor(temp1 / 100);
double temp3 = (temp1 / 100 - temp2) / 0.6;
if (orientation.equals("S"))
{
latitude = -(temp2 + temp3);
}
else if (orientation.equals("N"))
{
latitude = (temp2 + temp3);
}
}
return latitude;
}
/**
*
* @param lon
* @param orientation
* @return
*/
private double parseNmeaLongitude(String lon, String orientation)
{
double longitude = 0.0;
if (lon != null && orientation != null && !lon.equals("") && !orientation.equals(""))
{
double temp1 = Double.parseDouble(lon);
double temp2 = Math.floor(temp1 / 100);
double temp3 = (temp1 / 100 - temp2) / 0.6;
if (orientation.equals("W"))
{
longitude = -(temp2 + temp3);
}
else if (orientation.equals("E"))
{
longitude = (temp2 + temp3);
}
}
return longitude;
}
/**
*
* @param speed
* @param metric
* @return
*/
private float parseNmeaSpeed(String speed, String metric)
{
float meterSpeed = 0.0f;
if (speed != null && metric != null && !speed.equals("") && !metric.equals(""))
{
float temp1 = Float.parseFloat(speed) / 3.6f;
if (metric.equals("K"))
{
meterSpeed = temp1;
}
else if (metric.equals("N"))
{
meterSpeed = temp1 * 1.852f;
}
}
return meterSpeed;
}
/**
*
* @param time
* @return
*/
private long parseNmeaTime(String time)
{
long timestamp = 0;
SimpleDateFormat fmt = new SimpleDateFormat("HHmmss.SSS");
fmt.setTimeZone(TimeZone.getTimeZone("GMT"));
try
{
if (time != null && time != null)
{
long now = System.currentTimeMillis();
long today = now - (now % 86400000L);
long temp1;
// sometime we don't have millisecond in the time string, so we have to reformat it
temp1 = fmt.parse(String.format((Locale) null, "%010.3f", Double.parseDouble(time))).getTime();
long temp2 = today + temp1;
// if we're around midnight we could have a problem...
if (temp2 - now > 43200000L)
{
timestamp = temp2 - 86400000L;
}
else if (now - temp2 > 43200000L)
{
timestamp = temp2 + 86400000L;
}
else
{
timestamp = temp2;
}
}
}
catch (ParseException e)
{
DebugLogManager.INSTANCE.log("Error while parsing NMEA time: " + e, Log.ERROR);
}
return timestamp;
}
/**
*
* @param s
* @return
*/
private byte computeChecksum(String s)
{
byte checksum = 0;
for (char c : s.toCharArray())
{
checksum ^= (byte) c;
}
return checksum;
}
/**
*
* @param sentence
*/
private void sendNmeaCmd(String sentence)
{
final String command = String.format((Locale) null, "$%s*%X\r\n", sentence, computeChecksum(sentence));
Thread t = new Thread(new Runnable()
{
@Override
public void run()
{
if (running && (extGPSThread != null))
{
extGPSThread.write(command);
DebugLogManager.INSTANCE.log("sendNmeaEnableCommand()", Log.DEBUG);
}
}
});
t.start();
}
/**
*
* @param status
* @param extras
* @param updateTime
*/
private void notifyStatusChanged(int status, Bundle extras, long updateTime)
{
locationTime = null;
hasGGA = false;
hasRMC = false;
notifyStatusChangedNoClear(status, extras, updateTime);
}
/**
*
* @param status
* @param extras
* @param updateTime
*/
private void notifyStatusChangedNoClear(int status, Bundle extras, long updateTime)
{
synchronized (listeners)
{
for (LocationListener ll : listeners)
{
ll.onStatusChanged(providerName, status, extras);
}
}
DebugLogManager.INSTANCE.log("notifyStatusChanged() " + status + " " + extras, Log.DEBUG);
if (this.locStatus != status)
{
this.location = new Location(providerName);
this.locStatus = status;
}
}
/**
*
* @param loc
*/
private void notifyLocationChanged(Location loc)
{
locationTime = null;
hasGGA = false;
hasRMC = false;
if (this.location != null)
{
synchronized (listeners)
{
for (LocationListener ll : listeners)
{
ll.onLocationChanged(loc);
}
}
this.location = new Location(providerName);
DebugLogManager.INSTANCE.log("notifyLocationChanged() " + loc, Log.VERBOSE);
readCounter++;
long delay = System.currentTimeMillis() - lastRpsTime;
if (delay > 1000)
{
double RPS = readCounter / delay * 1000;
lastRpsTime = System.currentTimeMillis();
readCounter = 0;
if (RPS > 0)
{
Bundle extras = new Bundle();
extras.putInt("satellites", lastNumSatellites);
extras.putDouble("rps", RPS);
notifyStatusChangedNoClear(locStatus, extras, locationTimestamp);
}
}
}
}
/**
* Add LocationListener listeners
*/
public void addListener(LocationListener listener)
{
synchronized (listeners)
{
listeners.add(listener);
}
}
/**
* Remove LocationListener listeners
*/
public void removeListener(LocationListener listener)
{
synchronized (listeners)
{
listeners.remove(listener);
}
}
/**
* Notify all listeners of the current status
*/
public void requestStatusUpdate()
{
Bundle extras = new Bundle();
long lastUpdateTime = locationTimestamp;
if (location != null)
{
lastUpdateTime = location.getTime();
extras = location.getExtras();
}
notifyStatusChangedNoClear(locStatus, extras, lastUpdateTime);
}
/**
* Start down the ExtGPS thread
*/
public synchronized void start()
{
if (extGPSThread == null)
{
extGPSThread = new ExtGPSThread();
extGPSThread.setDaemon(true);
extGPSThread.start();
sendNmeaCmd("PSRF100,1,38400,8,1,0"); // NMEA only on
sendNmeaCmd("PSRF103,00,00,01,01"); // GGA on
sendNmeaCmd("PSRF103,01,00,00,01"); // GLL off
sendNmeaCmd("PSRF103,02,00,00,01"); // GSA off
sendNmeaCmd("PSRF103,03,00,00,01"); // GSV off
sendNmeaCmd("PSRF103,04,00,01,01"); // RMC on
sendNmeaCmd("PSRF103,05,00,00,01"); // VTG off
locStatus = LocationProvider.OUT_OF_SERVICE;
locationTime = null;
hasGGA = false;
hasRMC = false;
lastNumSatellites = 0;
location = null;
lastRpsTime = System.currentTimeMillis();
}
synchronized (listeners)
{
for (LocationListener ll : listeners)
{
ll.onProviderEnabled(providerName);
}
}
DebugLogManager.INSTANCE.log("ExtGPSManager.start() " + providerName, Log.DEBUG);
}
/**
* Shut down the ExtGPS thread
*/
public synchronized void stop()
{
if (extGPSThread != null)
{
extGPSThread.halt();
extGPSThread = null;
locStatus = LocationProvider.OUT_OF_SERVICE;
}
running = false;
synchronized (listeners)
{
for (LocationListener ll : listeners)
{
ll.onProviderDisabled(providerName);
}
}
DebugLogManager.INSTANCE.log("ExtGPSManager.stop() " + providerName, Log.DEBUG);
}
/**
* The thread that handles all communications with the External GPS. This must be done in it's own thread as Android gets very picky about
* unresponsive UI threads
*/
private class ExtGPSThread extends Thread
{
private InputStreamReader in;
private PrintStream out2;
private boolean canWrite;
/**
*
*/
public ExtGPSThread()
{
if (watch != null)
{
DebugLogManager.INSTANCE.log("Attempting to create second connection!", Log.ASSERT);
}
watch = this;
setName("ECUThread:" + System.currentTimeMillis());
}
/**
* The main loop of the connection to the ECU
*/
public void run()
{
try
{
ExtGPSConnectionManager.getInstance().init(null, ApplicationSettings.INSTANCE.getExtGPSBluetoothMac());
try
{
Thread.sleep(500);
}
catch (InterruptedException e)
{
DebugLogManager.INSTANCE.logException(e);
}
try
{
ExtGPSConnectionManager.getInstance().connect();
in = new InputStreamReader(ExtGPSConnectionManager.getInstance().getInputStream(), "US-ASCII");
out2 = new PrintStream(ExtGPSConnectionManager.getInstance().getOutputStream(), false, "US-ASCII");
BufferedReader reader = new BufferedReader(in);
String s;
long delay = 100;
running = true;
canWrite = false;
// This is the actual work. Outside influences will toggle 'running' when we want this to stop
while (running)
{
if (reader.ready())
{
s = reader.readLine();
parseNmeaSentence(s);
canWrite = true;
if (locStatus == LocationProvider.AVAILABLE)
{
delay = 50;
}
else
{
// no location available, wait for more satellites
delay = 1000;
}
}
else
{
delay = 100;
}
SystemClock.sleep(delay);
}
}
catch (IOException e)
{
DebugLogManager.INSTANCE.logException(e);
}
catch (RuntimeException t)
{
DebugLogManager.INSTANCE.logException(t);
throw (t);
}
// We're on our way out, so drop the connection
ExtGPSConnectionManager.getInstance().disconnect();
}
finally
{
watch = null;
}
}
/**
* Write to the connected OutStream.
*
* @param buffer The data to write
*/
public void write(String buffer)
{
try
{
do
{
Thread.sleep(100);
} while ((running) && (!canWrite));
if ((running) && (canWrite))
{
out2.print(buffer);
out2.flush();
}
}
catch (InterruptedException e)
{
DebugLogManager.INSTANCE.logException(e);
}
}
/**
* Called by other threads to stop the comms
*/
public void halt()
{
running = false;
}
}
}