/* GNU General Public License CacheWolf is a software for PocketPC, Win and Linux that enables paperless caching. It supports the sites geocaching.com and opencaching.de Copyright (C) 2006 CacheWolf development team See http://www.cachewolf.de/ for more information. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; version 2 of the License. This program 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. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */ package CacheWolf.navi; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import CacheWolf.Preferences; import CacheWolf.database.CWPoint; import CacheWolf.utils.Common; import CacheWolf.utils.Extractor; import ewe.io.FileWriter; import ewe.io.IOException; import ewe.sys.Convert; import ewe.sys.Time; import ewe.sys.TimerProc; import ewe.sys.Vm; /** * @author Kalle * Class for decoding NMEA sentences */ public class CWGPSPoint extends CWPoint implements TimerProc { public static final int LOGNMEA = 0x01; public static final int LOGRAW = 0x02; public static final int LOGALL = LOGNMEA | LOGRAW; public double Speed; // Speed: km/h public double Bear; // Bearing public String Time; // Time: HHmmss.SS public String Date; // Date: ddMMyy public int Fix; // Fix (0: none, 1: GPS, 2: differential GPS). See getFix() for more possible values. public int numSat; // Satellites in use, -1 indicates no data, -2 that data could not be interpreted public int numSatsInView; // Satellites in view public double HDOP; // Horizontal dilution of precision public double Alt; // Altitude in meters // Logging int logTimer = 0; int logFlag = 0; boolean writeLog = false; boolean doLogging = false; FileWriter logFile; String lastStrExamined = ""; // Regex numberMatcher = new Regex("\\-?\\d+"); public CWGPSPoint() { super(); this.Speed = 0; this.Bear = 0; this.Time = ""; this.Date = ""; this.Fix = 0; this.numSat = 0; this.numSatsInView = 0; this.Alt = 0; this.HDOP = 0; } public double getSpeed() { return this.Speed; } public double getBear() { return this.Bear; } public String getTime() { return this.Time; } /** * @return > 0: fixed <br> * 0: not fixed <br> * -1: no data from serial port <br> * -2 data from serial port could not be interpreted */ public int getFix() { return this.Fix; } /** * this method should be called, if COM-Port is closed */ public void noData() { this.Fix = 0; this.numSat = 0; this.HDOP = 0; } /** * this method should be called, if not data is coming from COM-Port but is expected to come */ public void noDataError() { this.Fix = -1; this.numSat = -1; this.HDOP = -1; } /** * this method should be called, if examine returns for several calls that it couldn't interprete the data */ public void noInterpretableData() { this.Fix = -2; this.numSat = -2; this.HDOP = -2; } public void ticked(int timerId, int elapsed) { if (timerId == logTimer) { writeLog = true; } } /** * * @param logFileDir * directory for logfile * @param seconds * intervall for writing to logfile * @param flag * level of logging * @return 0 success, -1 failure */ public int startLog(String logFileDir, int seconds, int flag) { Time currTime = new Time(); currTime.getTime(); currTime.setFormat("yyyyMMdd'_'HHmm"); String logFileName = logFileDir + currTime.toString() + ".log"; // create Logfile try { logFile = new FileWriter(logFileName); } catch (IOException e) { Preferences.itself().log("Error creating LogFile " + logFileName, e, true); return -1; } // start timer logTimer = Vm.requestTimer(this, 1000 * seconds); logFlag = flag; doLogging = true; return 0; } public void stopLog() { writeLog = false; if (doLogging) { try { logFile.close(); } catch (IOException e) {/* Too lazy to do something */ } if (logTimer > 0) { Vm.cancelTimer(logTimer); logTimer = 0; } } doLogging = false; } public int getSats() { return this.numSat; } public int getSatsInView() { return this.numSatsInView; } public double getAlt() { return this.Alt; } public double getHDOP() { return this.HDOP; } /** * Sets the attributes from a NMEA String * * @param NMEA * string with data to examine * @return true if some data could be interpreted false otherwise */ public boolean examine(String NMEA) { boolean interpreted = false; boolean logWritten = false; try { int i, start, end; String latDeg = "0", latMin = "0", latNS = "N"; String lonDeg = "0", lonMin = "0", lonEW = "E"; String currToken; end = 0; lastStrExamined = NMEA; while (true) { start = NMEA.indexOf("$GP", end); if (start == -1) break; end = NMEA.indexOf("*", start); if ((end == -1) || (end + 3 > NMEA.length())) break; if ((end - start) < 15 || !checkSumOK(NMEA.substring(start, end + 3))) { continue; } // Write log after finding valid NMEA sequence if (writeLog && (logFlag & LOGRAW) > 0) { try { logFile.write(NMEA.substring(start, end + 3) + "\n"); logWritten = true; } catch (IOException e) { // Preferences.itself().log("Ignored Exception", e, true); } } Extractor ex = new Extractor("," + NMEA.substring(start, end), ",", ",", 0, true); currToken = ex.findNext(); if (currToken.equals("$GPGGA")) { i = 0; while ((currToken = ex.findNext()).length() > 0) { // indicate that some error occured in the data -> in this case frace fix to non-fixed in order to avoid invalid coordinates when a fix is indicated to the higher level API boolean latlonerror = false; i++; switch (i) { case 1: this.Time = currToken; break; case 2: try { latDeg = currToken.substring(0, 2); interpreted = true; } catch (IndexOutOfBoundsException e) { latlonerror = true; } try { latMin = currToken.substring(2, currToken.length()); interpreted = true; } catch (IndexOutOfBoundsException e) { latlonerror = true; } break; case 3: latNS = currToken; break; case 4: try { lonDeg = currToken.substring(0, 3); interpreted = true; } catch (IndexOutOfBoundsException e) { latlonerror = true; } try { lonMin = currToken.substring(3, currToken.length()); interpreted = true; } catch (IndexOutOfBoundsException e) { latlonerror = true; } break; case 5: lonEW = currToken; break; case 6: if (!latlonerror) { this.Fix = Convert.toInt(currToken); interpreted = true; break; } else { this.Fix = 0; break; } case 7: this.numSat = Convert.toInt(currToken); interpreted = true; break; case 8: try { this.HDOP = Common.parseDouble(currToken); interpreted = true; } catch (NumberFormatException e) { // Preferences.itself().log("Ignored Exception", e, true); } break; case 9: try { this.Alt = Common.parseDouble(currToken); interpreted = true; } catch (NumberFormatException e) { // Preferences.itself().log("Ignored Exception", e, true); } break; } // switch } // while if (Fix > 0) this.set(latNS, latDeg, latMin, "0", lonEW, lonDeg, lonMin, "0", TransformCoordinates.DMM); } // if if (currToken.equals("$GPVTG")) { i = 0; while ((currToken = ex.findNext()).length() > 0) { i++; switch (i) { case 1: try { this.Bear = Common.parseDouble(currToken); interpreted = true; } catch (NumberFormatException e) { // Preferences.itself().log("Ignored Exception", e, true); } if (this.Bear > 360) Preferences.itself().log("Error bear VTG", null); break; case 7: try { this.Speed = Common.parseDouble(currToken); interpreted = true; } catch (NumberFormatException e) { // Preferences.itself().log("Ignored Exception", e, true); } break; } // switch } // while } // if if (currToken.equals("$GPRMC")) { i = 0; String status = "V"; boolean latlonerror = false; while ((currToken = ex.findNext()).length() > 0) { i++; switch (i) { case 1: this.Time = currToken; interpreted = true; break; case 2: status = currToken; if (status.equals("A")) this.Fix = 1; else this.Fix = 0; interpreted = true; break; case 3: try { latDeg = currToken.substring(0, 2); interpreted = true; } catch (IndexOutOfBoundsException e) { latlonerror = true; } try { latMin = currToken.substring(2, currToken.length()); interpreted = true; } catch (IndexOutOfBoundsException e) { latlonerror = true; } break; case 4: latNS = currToken; interpreted = true; break; case 5: try { lonDeg = currToken.substring(0, 3); interpreted = true; } catch (IndexOutOfBoundsException e) { // Preferences.itself().log("Ignored Exception", e, true); } try { lonMin = currToken.substring(3, currToken.length()); interpreted = true; } catch (IndexOutOfBoundsException e) { // Preferences.itself().log("Ignored Exception", e, true); } break; case 6: lonEW = currToken; interpreted = true; break; case 7: if (status.equals("A")) { try { this.Speed = Common.parseDouble(currToken) * 1.854; interpreted = true; } catch (NumberFormatException e) { // Preferences.itself().log("Ignored Exception", e, true); } } break; case 8: if (status.equals("A") && currToken.length() > 0) { try { this.Bear = Common.parseDouble(currToken); interpreted = true; } catch (NumberFormatException e) { // Preferences.itself().log("Ignored Exception", e, true); } } break; case 9: if (status.equals("A") && currToken.length() > 0) { try { this.Date = currToken; interpreted = true; } catch (NumberFormatException e) { // Preferences.itself().log("Ignored Exception", e, true); } } break; } // switch } // while if (latlonerror) this.Fix = 0; else { if (status.equals("A")) { this.set(latNS, latDeg, latMin, "0", lonEW, lonDeg, lonMin, "0", TransformCoordinates.DMM); } } } // if if (currToken.equals("$GPGSV")) { i = 0; while ((currToken = ex.findNext()).length() > 0) { i++; switch (i) { case 3: this.numSatsInView = Convert.toInt(currToken); interpreted = true; break; } // switch } // while } // if } // while } catch (Exception e) { Preferences.itself().log("Exception in examine in CWGPSPoint", e, true); } if (logWritten) writeLog = false; return interpreted; } /** * Sets the attributes from a GPSD <code>POLL</code> object * * @param gps * {@link JSONObject} containing GPS <code>POLL</code> data. * @return true if some data could be interpreted false otherwise * Tblue> For now, this always returns true. Any ideas what * should be treated as not interpretable? * @throws JSONException * When trying to access a not existing key (should not happen!). */ public boolean examineGpsd(JSONObject gps) throws JSONException { JSONArray fixes = gps.getJSONArray("fixes"); JSONArray skyviews = gps.getJSONArray("skyviews"); JSONArray sats; JSONObject a_fix, a_skyview; int fix_mode, i; double my_lat, my_lon; Time TimeObj = new Time(); lastStrExamined = gps.toString(); TimeObj.setTime((long) (gps.getDouble("timestamp") * 1000)); this.Time = TimeObj.format("HHmmss.SS"); this.Date = TimeObj.format("ddMMyy"); if (fixes.length() > 0) { // We will only use the first fix. // TODO: Randomize? a_fix = fixes.getJSONObject(0); // 0: no mode seen yet, 1: none, 2: 2D, 3: 3D. // Tblue> Does 3D mean differential here? this.Fix = (fix_mode = a_fix.getInt("mode")) > 0 ? fix_mode - 1 : 0; // Speed is in m/s. if (a_fix.has("speed")) { this.Speed = (a_fix.getDouble("speed") / 1000) * 60 * 60; } if (a_fix.has("track")) { this.Bear = a_fix.getDouble("track"); } if (a_fix.has("alt")) { this.Alt = a_fix.getDouble("alt"); } if (a_fix.has("lat") && a_fix.has("lon")) { my_lat = a_fix.getDouble("lat"); my_lon = a_fix.getDouble("lon"); set(my_lat > 0 ? "N" : "S", String.valueOf(my_lat), "0", "0", my_lon > 0 ? "E" : "W", String.valueOf(my_lon), "0", "0", TransformCoordinates.DD); } } if (skyviews.length() > 0) { // We will only use the first skyview. // TODO: Randomize? a_skyview = skyviews.getJSONObject(0); if (a_skyview.has("hdop")) { this.HDOP = a_skyview.getDouble("hdop"); } sats = a_skyview.getJSONArray("satellites"); this.numSatsInView = sats.length(); if (this.numSatsInView > 0) { for (this.numSat = 0, i = 0; i < this.numSatsInView; i++) { if (sats.getJSONObject(i).getBoolean("used")) { this.numSat++; } } } } return true; } /** * Sets the attributes from an old-style GPSD string. * * @param gps * GPSD string with data to examine * Format: GPSD,key=value,... * @return true if some data could be interpreted false otherwise */ public boolean examineOldGpsd(String gps) { boolean valid = false; if (!gps.startsWith("GPSD,")) return false; Extractor ex = new Extractor(gps, ",", ",", 4, true); String part; while ((part = ex.findNext()).length() > 0) { if (part.startsWith("A=") && part.indexOf('?') < 0) { // The current altitude as "A=%f", meters above mean sea level. this.Alt = Common.parseDouble(part.substring(2)); valid = true; } else if (part.startsWith("D=") && part.indexOf('?') < 0) { // Returns the UTC time in the ISO 8601 format, "D=yyyy-mm-ddThh:mm:ss.ssZ" // 0000000000111111111122 // 0123456789012345678901 String year = part.substring(2, 6); String month = part.substring(7, 9); String day = part.substring(10, 12); String hour = part.substring(13, 15); String min = part.substring(16, 18); String sec = part.substring(19, 21); this.Date = year + month + day; this.Time = hour + min + sec; valid = true; } else if (part.startsWith("P=")) { // Returns the current position in the form "P=%f %f"; numbers are in degrees, latitude first. if (part.indexOf('?') < 0) { this.Fix = 1; int spacepos = part.indexOf(' '); if (spacepos >= 3) { String lat = part.substring(2, spacepos); String lon = part.substring(spacepos + 1); this.latDec = Common.parseDouble(lat); this.lonDec = Common.parseDouble(lon); } else this.set(part.substring(2)); } else { this.Fix = 0; } valid = true; } else if (part.startsWith("Q=")) { // Returns "Q=%d %f %f %f %f %f": a count of satellites used in the last fix, // and five dimensionless dilution-of-precision (DOP) numbers -- // spherical, horizontal, vertical, time, and total geometric. int spacepos = part.indexOf(' '); if (part.indexOf('?') < 0 && spacepos >= 3) { this.numSat = Common.parseInt(part.substring(2, spacepos)); valid = true; } else { this.numSat = 0; } this.numSatsInView = 0; // Not supported by GPSD // TODO parse DOP values } else if (part.startsWith("T=") && part.indexOf('?') < 0) { // Track made good; course "T=%f" in degrees from true north. this.Bear = Common.parseDouble(part.substring(2)); valid = true; } else if (part.startsWith("V=") && part.indexOf('?') < 0) { // The current speed over ground as "V=%f" in knots. this.Speed = Common.parseDouble(part.substring(2)); valid = true; } } return valid; } private boolean checkSumOK(String nmea) { int startPos = 1; // begin after $ int endPos = nmea.length() - 3;// without * an two checksum chars byte checkSum = 0; for (int i = startPos; i < endPos; i++) { checkSum ^= nmea.charAt(i); } try { return (checkSum == Byte.parseByte(nmea.substring(endPos + 1), 16)); } catch (IndexOutOfBoundsException e) { return false; } catch (NumberFormatException e) { return false; } } public void printAll() { Preferences.itself().log("Latitude: " + this.getLatDeg(TransformCoordinates.DD)); Preferences.itself().log("Longitude: " + this.getLonDeg(TransformCoordinates.DD)); Preferences.itself().log("Speed: " + this.Speed); Preferences.itself().log("Bearing: " + this.Bear); Preferences.itself().log("Time: " + this.Time); Preferences.itself().log("Fix: " + this.Fix); Preferences.itself().log("Sats: " + this.numSat); Preferences.itself().log("Sats in view: " + this.numSatsInView); Preferences.itself().log("HDOP: " + this.HDOP); Preferences.itself().log("Alt: " + this.Alt); Preferences.itself().log("----------------"); } }