/*
* ShareNav - Copyright (c) 2007 Harald Mueller james22 at users dot sourceforge dot net
* Copyright (c) 2008 Kai Krueger apmonkey at users dot sourceforge dot net
* See file COPYING.
*/
package net.sharenav.gps.location;
import java.util.Calendar;
import java.util.Date;
import java.util.TimeZone;
import java.util.Vector;
import net.sharenav.gps.Satellite;
import net.sharenav.sharenav.data.Configuration;
import net.sharenav.sharenav.data.Position;
import net.sharenav.util.Logger;
import net.sharenav.util.StringTokenizer;
import de.enough.polish.util.Locale;
/**
* This class takes NMEA0183 compliant sentences and extracts information from them:
* <pre>
* Geographic Location in Lat/Lon
* field #: 0 1 2 3 4
* sentence: GLL,####.##,N,#####.##,W
*1, Lat (deg, min, hundredths); 2, North or South; 3, Lon; 4, West or East.
*
*Geographic Location in Time Differences
* field #: 0 1 2 3 4 5
* sentence: GTD,#####.#,#####.#,#####.#,#####.#,#####.#
*1-5, TD's for secondaries 1 through 5, respectively.
*
*Bearing to Dest wpt from Origin wpt
* field #: 0 1 2 3 4 5 6
* sentence: BOD,###,T,###,M,####,####
*1-2, brg,True; 3-4, brg, Mag; 5, dest wpt; 6, org wpt.
*
*Vector Track and Speed Over Ground (SOG)
* field #: 0 1 2 3 4 5 6 7 8
* sentence: VTG,###,T,###,M,##.#,N,##.#,K
*1-2, brg, True; 3-4, brg, Mag; 5-6, speed, kNots; 7-8, speed, Kilometers/hr.
*
*Cross Track Error
* field #: 0 1 2 3 4 5
* sentence: XTE,A,A,#.##,L,N
*1, blink/SNR (A=valid, V=invalid); 2, cycle lock (A/V); 3-5, dist
*off, Left or Right, Nautical miles or Kilometers.
*
*Autopilot (format A)
* field #: 0 1 2 3 4 5 6 7 8 9 10
* sentence: APA,A,A,#.##,L,N,A,A,###,M,####
*1, blink/SNR (A/V); 2 cycle lock (A/V); 3-5, dist off, Left or
*Right, Nautical miles or Kilometers; 6-7, arrival circle, arrival
*perpendicular (A/V); 8-9, brg, Magnetic; 10, dest wpt.
*
*Bearing to Waypoint along Great Circle
* fld: 0 1 2 3 4 5 6 7 8 9 10 11 12
* sen: BWC,HHMMSS,####.##,N,#####.##,W,###,T,###,M,###.#,N,####
*1, Hours, Minutes, Seconds of universal time code; 2-3, Lat, N/S;
*4-5, Lon, W/E; 6-7, brg, True; 8-9, brg, Mag; 10-12, range,
*Nautical miles or Kilometers, dest wpt.
*
*BWR: Bearing to Waypoint, Rhumbline, BPI: Bearing to Point of
*Interest, all follow data field format of BWC.
*</pre>
*/
public class NmeaMessage {
/** Logger class for error messages */
protected static final Logger logger = Logger.getInstance(NmeaMessage.class, Logger.TRACE);
/** Buffer holding the current NMEA sentence (without the leading "GP" which
* identifies the source to be a GPS receiver */
public StringBuffer buffer = new StringBuffer(80);
/** The character which separates the fields of the NMEA sentence */
private static String spChar = ",";
/** The current heading in degrees */
private float head;
/** Speed in m/s (converted from knots) */
private float speed;
/** Altitude above mean sea level in meters */
private float alt;
/** Positional dilution of precision */
private float pdop;
/** This will receive the information extracted from NMEA. */
private final LocationMsgReceiver receiver;
/** The number of satellites received (field 6 from message GGA) */
private int mAllSatellites;
/** The number of satellites received (from GSV) */
private int gsvSatellites;
/** Quality of data, 0 = no fix, 1 = GPS, 2 = DGPS */
private int qual;
/** Flag if last received message was GSV */
private boolean lastMsgGSV = false;
/** Array with information about the satellites */
private final Satellite satellites[] = new Satellite[12];
/** The last received GPS time and date */
private Date dateDecode = new Date();
/** The last received position */
private final Position pos = new Position(0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1, 0);
/** Needed to turn GPS time and date (which are in UTC) into timeMillis */
private final Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
/** Count exceptions and stop giving them when there's a bunch */
private int exceptionCount;
public NmeaMessage(LocationMsgReceiver receiver) {
this.receiver = receiver;
}
public StringBuffer getBuffer() {
return buffer;
}
public void decodeMessage() {
decodeMessage(buffer.toString(), true, true);
}
public Position getPosition() {
return pos;
}
public int getMAllSatellites() {
return mAllSatellites;
}
/** This method does the actual decoding work. It puts the data into
* member variables and forwards it to the LocationMsgReceiver.
*
* @param nmea_sentence The NMEA sentence to be decoded
* @param receivePositionIsAllowed Set to true if it is allowed to forward
* a new position from the NMEA sentence to the LocationMsgReceiver.
* @param receiveSatellitesIsAllowed Set to true if it is allowed to forward
* satellite info from the NMEA sentence to the LocationMsgReceiver.
*/
public void decodeMessage(String nmea_sentence, boolean receivePositionIsAllowed,
boolean receiveSatellitesIsAllowed) {
Vector param = StringTokenizer.getVector(nmea_sentence, spChar);
String sentence = (String)param.elementAt(0);
float f = 0.0f;
try {
// receiver.receiveMessage("got " + buffer.toString() );
if (lastMsgGSV && ! "GSV".equals(sentence)) {
if (receiveSatellitesIsAllowed) {
receiver.receiveSatellites(satellites);
}
lastMsgGSV = false;
}
if ("GGA".equals(sentence)) {
// time
// Time of when fix was taken in UTC
f = getFloatToken((String)param.elementAt(1));
if (f != Float.NaN) {
int time_tmp = (int) f;
cal.set(Calendar.SECOND, time_tmp % 100);
cal.set(Calendar.MINUTE, (time_tmp / 100) % 100);
cal.set(Calendar.HOUR_OF_DAY, (time_tmp / 10000) % 100);
}
// // lat
// float lat = getLat((String)param.elementAt(2));
// if ("S".equals(param.elementAt(3))) {
// lat = -lat;
// }
// // lon
// float lon = getLon((String)param.elementAt(4));
// if ("W".equals(param.elementAt(5))) {
// lon = -lon;
// }
// quality
qual = getIntegerToken((String)param.elementAt(6));
// no of Sat;
mAllSatellites = getIntegerToken((String)param.elementAt(7));
// Relative accuracy of horizontal position
f = getFloatToken((String)param.elementAt(8));
if (f != Float.NaN) {
pos.hdop = f;
}
// meters above mean sea level
f = getFloatToken((String)param.elementAt(9));
if (f != Float.NaN) {
alt = f;
}
// Height of geoid above WGS84 ellipsoid
} else if ("RMC".equals(sentence)) {
/* RMC encodes the recomended minimum information */
// Time of when fix was taken in UTC
f = getFloatToken((String)param.elementAt(1));
if (f != Float.NaN) {
int time_tmp = (int) f;
cal.set(Calendar.SECOND, time_tmp % 100);
cal.set(Calendar.MINUTE, (time_tmp / 100) % 100);
cal.set(Calendar.HOUR_OF_DAY, (time_tmp / 10000) % 100);
}
// Status A=active or V=Void.
String valSolution = (String)param.elementAt(2);
if (valSolution.equals("V")) {
this.qual = 0;
receiver.receiveStatus(LocationMsgReceiver.STATUS_NOFIX, mAllSatellites);
return;
}
if (valSolution.equalsIgnoreCase("A") && this.qual == 0) {
this.qual = 1;
}
// Latitude
float lat = getLat((String)param.elementAt(3));
if ("S".equals(param.elementAt(4))) {
lat = -lat;
}
// Longitude
float lon = getLon((String)param.elementAt(5));
if ("W".equals(param.elementAt(6))) {
lon = -lon;
}
f = getFloatToken((String)param.elementAt(7));
if (f != Float.NaN) {
// Speed over the ground in knots, but ShareNav uses m/s
speed = f * 0.5144444f;
}
// Heading in degrees
f = getFloatToken((String)param.elementAt(8));
if (f != Float.NaN) {
head = f;
}
// Date
int date_tmp = getIntegerToken((String)param.elementAt(9));
cal.set(Calendar.YEAR, 2000 + date_tmp % 100);
cal.set(Calendar.MONTH, ((date_tmp / 100) % 100) - 1);
cal.set(Calendar.DAY_OF_MONTH, (date_tmp / 10000) % 100);
// Magnetic Variation is not used
// Copy data to current position
pos.latitude = lat;
pos.longitude = lon;
pos.altitude = alt;
pos.speed = speed;
pos.course = head;
//pos.pdop = pdop;
pos.type = Position.TYPE_GPS;
// Get Date from Calendar
dateDecode = cal.getTime();
// Get milliSecs since 01-Jan-1970 from Date
pos.gpsTimeMillis = dateDecode.getTime();
// FIXME? does this make sense? Not to me, elsewhere pos.timeMillis is from system time, not GPS time.
// in practice, this creates confusion at least in GuiTacho, which can show either system time or GPS
// time on the tacho display
pos.timeMillis = pos.gpsTimeMillis;
if (receivePositionIsAllowed) {
receiver.receivePosition(pos);
}
// TODO: Possible to differentiate 2D and 3D fix?
if (this.qual > 1) {
receiver.receiveStatus(LocationMsgReceiver.STATUS_DGPS, mAllSatellites);
} else {
receiver.receiveStatus(LocationMsgReceiver.STATUS_3D, mAllSatellites);
}
} else if ("VTG".equals(sentence)) {
f = getFloatToken((String)param.elementAt(1));
if (f != Float.NaN) {
head = f;
}
f = getFloatToken((String)param.elementAt(7));
if (f != Float.NaN) {
//Convert from knots to m/s
speed = f * 0.5144444f;
}
} else if ("GSA".equals(sentence)) {
//#debug trace
logger.trace("Decoding GSA");
/**
* Encodes Satellite status
*
* Auto selection of 2D or 3D fix (M = manual)
* 3D fix - values include: 1 = no fix
* 2 = 2D fix
* 3 = 3D fix
*/
/**
* A list of up to 12 PRNs which are used for the fix
*/
for (int j = 0; j < 12; j++) {
/**
* Resetting all the satellites to non locked
*/
if ((satellites[j] != null)) {
satellites[j].isLocked(false);
}
}
for (int i = 0; i < 12; i++) {
int prn = getIntegerToken((String)param.elementAt(i + 3));
if (prn != 0) {
//#debug debug
logger.debug("Satelit " + prn + " is part of fix");
for (int j = 0; j < 12; j++) {
if ((satellites[j] != null) && (satellites[j].id == prn)) {
satellites[j].isLocked(true);
}
}
}
}
/**
* PDOP & HDOP (dilution of precision)
*/
f = getFloatToken((String)param.elementAt(15));
if (f != Float.NaN) {
pos.pdop = f;
}
f = getFloatToken((String)param.elementAt(16));
if (f != Float.NaN) {
pos.hdop = f;
}
f = getFloatToken((String)param.elementAt(17));
if (f != Float.NaN) {
pos.vdop = f;
}
/**
* Horizontal dilution of precision (HDOP)
* Vertical dilution of precision (VDOP)
*/
} else if ("GSV".equals(sentence)) {
/* GSV encodes the satellites that are currently in view
* A maximum of 4 satellites are reported per message,
* if more are visible, then they are split over multiple messages *
*/
int j;
// Calculate which satellites are in this message (message number * 4)
j = (getIntegerToken((String)param.elementAt(2)) - 1) * 4;
if (getIntegerToken((String)param.elementAt(2)) == 1) {
gsvSatellites = 0;
}
int noSatInView =(getIntegerToken((String)param.elementAt(3)));
for (int i = 4; i < param.size() && j < 12; i += 4, j++) {
if (satellites[j] == null) {
satellites[j] = new Satellite();
}
satellites[j].id = getIntegerToken((String)param.elementAt(i));
satellites[j].elev = getIntegerToken((String)param.elementAt(i + 1));
satellites[j].azimut = getIntegerToken((String)param.elementAt(i + 2));
satellites[j].snr = getIntegerToken((String)param.elementAt(i + 3));
/** The number of satellites received (from GSV) */
if (satellites[j].snr != 0) {
gsvSatellites++;
}
}
lastMsgGSV = true;
for (int i = noSatInView; i < 12; i++) {
satellites[i] = null;
}
if (getIntegerToken((String)param.elementAt(2)) == getIntegerToken((String)param.elementAt(1))) {
mAllSatellites = gsvSatellites;
if (receiveSatellitesIsAllowed) {
receiver.receiveSatellites(satellites);
}
lastMsgGSV = false;
}
}
} catch (RuntimeException e) {
if (Configuration.getCfgBitSavedState(Configuration.CFGBIT_SHOW_NMEA_ERRORS) && checkExceptionCount()) {
logger.exception(Locale.get("nmeamessage.ErrorDecoding")/*Error while decoding */ + sentence, e);
}
}
}
private boolean checkExceptionCount() {
exceptionCount++;
if (exceptionCount < 3) {
return true;
} else if (exceptionCount == 3) {
logger.error(Locale.get("nmeamessage.TooManyErrors")/*Too many exceptions, stoppping reporting them*/);
return false;
} else {
return false;
}
}
private int getIntegerToken(String s) {
if (s == null || s.length() == 0 || s.equals("nan")) {
return 0;
}
return Integer.parseInt(s);
}
private float getFloatToken(String s) {
if (s == null || s.length() == 0) {
return 0;
}
if (s.equals("nan")) {
return Float.NaN;
}
return Float.parseFloat(s);
}
private float getLat(String s) {
if (s.length() < 2 || s.equals("nan")) {
return 0.0f;
}
int lat = Integer.parseInt(s.substring(0, 2));
float latf = Float.parseFloat(s.substring(2));
return (lat + (latf / 60));
}
private float getLon(String s) {
if (s.length() < 3 || s.equals("nan")) {
return 0.0f;
}
int lon = Integer.parseInt(s.substring(0, 3));
float lonf = Float.parseFloat(s.substring(3));
return (lon + (lonf / 60));
}
}