diff options
| author | ckgt <ckarlsson25@gmail.com> | 2015-03-28 19:08:43 -0400 |
|---|---|---|
| committer | ckgt <ckarlsson25@gmail.com> | 2015-03-28 19:08:43 -0400 |
| commit | 09a3c9c3485a023b5ae98652c46129941ca7fd41 (patch) | |
| tree | fb3b89f819d54d3efdef291079ddd061c40a4eb5 /src | |
| parent | 78627e9bc711b012d38855daeb004fe55b3d4d26 (diff) | |
Initial commit.
Diffstat (limited to 'src')
| -rw-r--r-- | src/GPSFix.cpp | 412 | ||||
| -rw-r--r-- | src/GPSService.cpp | 509 | ||||
| -rw-r--r-- | src/NMEACommand.cpp | 108 | ||||
| -rw-r--r-- | src/NMEAParser.cpp | 466 | ||||
| -rw-r--r-- | src/NumberConversion.cpp | 144 |
5 files changed, 1639 insertions, 0 deletions
diff --git a/src/GPSFix.cpp b/src/GPSFix.cpp new file mode 100644 index 0000000..818b232 --- /dev/null +++ b/src/GPSFix.cpp @@ -0,0 +1,412 @@ +/* + * GPSFix.cpp + * + * Created on: Jul 23, 2014 + * Author: Cameron Karlsson + */ + +#include <nmeaparse/GPSFix.h> +#include <cmath> +#include <string> +#include <sstream> +#include <iomanip> + +using namespace std; +using namespace std::chrono; + +using namespace nmea; + + + +// =========================================================== +// ======================== GPS SATELLITE ==================== +// =========================================================== + +string GPSSatellite::toString(){ + stringstream ss; + + ss << "[PRN: " << setw(3) << setfill(' ') << prn << " " + << " SNR: " << setw(3) << setfill(' ') << snr << " dB " + << " Azimuth: " << setw(3) << setfill(' ') << azimuth << " deg " + << " Elevation: " << setw(3) << setfill(' ') << elevation << " deg " + << "]"; + + return ss.str(); +} +GPSSatellite::operator std::string(){ + return toString(); +} + + + +// ========================================================= +// ======================== GPS ALMANAC ==================== +// ========================================================= + +void GPSAlmanac::clear(){ + lastPage = 0; + totalPages = 0; + processedPages = 0; + visibleSize = 0; + satellites.clear(); +} +void GPSAlmanac::updateSatellite(GPSSatellite sat){ + if (satellites.size() > visibleSize) + { //we missed the new almanac start page, start over. + clear(); + } + + satellites.push_back(sat); +} +double GPSAlmanac::percentComplete(){ + if (totalPages == 0){ + return 0.0; + } + + return ((double)processedPages) / ((double)totalPages) * 100.0; +} +double GPSAlmanac::averageSNR(){ + + double avg = 0; + double relevant = 0; + for (size_t i = 0; i < satellites.size(); i++){ + if (satellites[i].snr > 0){ + relevant += 1.0; + } + } + + for (size_t i = 0; i < satellites.size(); i++){ + if (satellites[i].snr > 0){ + avg += satellites[i].snr / relevant; + } + } + + return avg; +} +double GPSAlmanac::minSNR(){ + double min = 9999999; + if (satellites.size() == 0){ + return 0; + } + int32_t num_over_zero = 0; + for (size_t i = 0; i < satellites.size(); i++){ + if (satellites[i].snr > 0){ + num_over_zero++; + if (satellites[i].snr < min){ + min = satellites[i].snr; + } + } + } + if (num_over_zero == 0){ + return 0; + } + return min; +} + +double GPSAlmanac::maxSNR(){ + double max = 0; + for (size_t i = 0; i < satellites.size(); i++){ + if (satellites[i].snr > 0){ + if (satellites[i].snr > max){ + max = satellites[i].snr; + } + } + } + return max; +} + + + + +// =========================================================== +// ======================== GPS TIMESTAMP ==================== +// =========================================================== + + +GPSTimestamp::GPSTimestamp(){ + hour = 0; + min = 0; + sec = 0; + + month = 1; + day = 1; + year = 1970; + + rawTime = 0; + rawDate = 0; +}; + +// indexed from 1! +std::string GPSTimestamp::monthName(uint32_t index){ + if (index < 1 || index > 12){ + std::stringstream ss; + ss << "[month:" << index << "]"; + return ss.str(); + } + + std::string names[] = { + "January", + "February", + "March", + "April", + "May", + "June", + "July", + "August", + "September", + "October", + "November", + "December" + }; + return names[index - 1]; +}; + +// Returns seconds since Jan 1, 1970. Classic Epoch time. +time_t GPSTimestamp::getTime() { + struct tm t = { 0 }; + t.tm_year = year - 1900; // This is year-1900, so 112 = 2012 + t.tm_mon = month; // month from 0:Jan + t.tm_mday = day; + t.tm_hour = hour; + t.tm_min = min; + t.tm_sec = (int)sec; + return mktime(&t); +} + +void GPSTimestamp::setTime(double raw_ts){ + rawTime = raw_ts; + + hour = (int32_t)trunc(raw_ts / 10000.0); + min = (int32_t)trunc((raw_ts - hour * 10000) / 100.0); + sec = raw_ts - min * 100 - hour * 10000; +} + +//ddmmyy +void GPSTimestamp::setDate(int32_t raw_date){ + rawDate = raw_date; + // If uninitialized, use posix time. + if(rawDate == 0) { + month = 1; + day = 1; + year = 1970; + } + else { + day = (int32_t)trunc(raw_date / 10000.0); + month = (int32_t)trunc((raw_date - 10000 * day) / 100.0); + year = raw_date - 10000 * day - 100 * month + 2000; + } +} + +std::string GPSTimestamp::toString(){ + std::stringstream ss; + ss << hour << "h " << min << "m " << sec << "s" << " " << monthName(month) << " " << day << " " << year; + return ss.str(); +}; + + + + + + + +// ===================================================== +// ======================== GPS FIX ==================== +// ===================================================== + +GPSFix::GPSFix() { + + quality = 0; // Searching... + status = 'V'; // Void + type = 1; // 1=none, 2=2d, 3=3d + + haslock = 0; + + dilution = 0; + horizontalDilution = 0; // Horizontal - Best is 1, >20 is terrible, so 0 means uninitialized + verticalDilution = 0; + latitude = 0; + longitude = 0; + speed = 0; + travelAngle = 0; + altitude = 0; + trackingSatellites = 0; + visibleSatellites = 0; + + +} + +GPSFix::~GPSFix() { + // TODO Auto-generated destructor stub +} + +// Returns the duration since the Host has received information +seconds GPSFix::timeSinceLastUpdate(){ + time_t now = time(NULL); + struct tm stamp = { 0 }; + + stamp.tm_hour = timestamp.hour; + stamp.tm_min = timestamp.min; + stamp.tm_sec = (int)timestamp.sec; + stamp.tm_year = timestamp.year-1900; + stamp.tm_mon = timestamp.month-1; + stamp.tm_mday = timestamp.day; + + time_t then = mktime(&stamp); + uint64_t secs = (uint64_t)difftime(now,then); + return seconds((uint64_t)secs); +} + +bool GPSFix::hasEstimate(){ + return (latitude != 0 && longitude != 0) || (quality == 6); +} + +bool GPSFix::setlock(bool locked){ + if (haslock != locked){ + haslock = locked; + return true; + } + return false; +} + +bool GPSFix::locked(){ + return haslock; +} + +// Returns meters +double GPSFix::horizontalAccuracy(){ + // horizontal 2drms 95% = 4.0 -- from GPS CHIP datasheets + return 4.0 * horizontalDilution; +} + +// Returns meters +double GPSFix::verticalAccuracy(){ + // Vertical 2drms 95% = 6.0 -- from GPS CHIP datasheets + return 6.0 * verticalDilution; +} + +// Takes a degree travel heading (0-360') and returns the name +std::string GPSFix::travelAngleToCompassDirection(double deg, bool abbrev){ + + //normalize, just in case + int32_t c = (int32_t)round(deg / 360.0 * 8.0); + int32_t r = c % 8; + if (r < 0){ + r = 8 + r; + } + + if (abbrev){ + std::string dirs[] = { + "N", + "NE", + "E", + "SE", + "S", + "SW", + "W", + "NW", + "N" + }; + return dirs[r]; + } + else { + std::string dirs[] = { + "North", + "North East", + "East", + "South East", + "South", + "South West", + "West", + "North West", + "North" + }; + return dirs[r]; + } + +}; + + +std::string fixStatusToString(char status){ + switch (status){ + case 'A': + return "Active"; + case 'V': + return "Void"; + default: + return "Unknown"; + } +} +std::string fixTypeToString(uint8_t type){ + switch (type){ + case 1: + return "None"; + case 2: + return "2D"; + case 3: + return "3D"; + default: + return "Unknown"; + } +} +std::string fixQualityToString(uint8_t quality){ + switch (quality){ + case 0: + return "Invalid"; + case 1: + return "Standard"; + case 2: + return "DGPS"; + case 3: + return "PPS fix"; + case 4: + return "Real Time Kinetic"; + case 5: + return "Real Time Kinetic (float)"; + case 6: + return "Estimate"; + default: + return "Unknown"; + } +} + +std::string GPSFix::toString(){ + stringstream ss; + ios_base::fmtflags oldflags = ss.flags(); + + ss << "========================== GPS FIX ================================" << endl + << " Status: \t\t" << ((haslock) ? "LOCK!" : "SEARCHING...") << endl + << " Satellites: \t\t" << trackingSatellites << " (tracking) of " << visibleSatellites << " (visible)" << endl + << " < Fix Details >" << endl + << " Age: " << timeSinceLastUpdate().count() << " s" << endl + << " Timestamp: " << timestamp.toString() << " UTC \n\t\t\t(raw: " << timestamp.rawTime << " time, " << timestamp.rawDate << " date)" << endl + << " Raw Status: " << status << " (" << fixStatusToString(status) << ")" << endl + << " Type: " << (int)type << " (" << fixTypeToString(type) << ")" << endl + << " Quality: " << (int)quality << " (" << fixQualityToString(quality) << ")" << endl + << " Lat/Lon (N,E): " << setprecision(6) << fixed << latitude << "' N, " << longitude << "' E" << endl; + + ss.flags(oldflags); //reset + + ss << " DOP (P,H,V): " << dilution << ", " << horizontalDilution << ", " << verticalDilution << endl + << " Accuracy(H,V): " << horizontalAccuracy() << " m, " << verticalAccuracy() << " m" << endl; + + ss << " Altitude: " << altitude << " m" << endl + << " Speed: " << speed << " km/h" << endl + << " Travel Dir: " << travelAngle << " deg [" << travelAngleToCompassDirection(travelAngle) << "]" << endl + << " SNR: avg: " << almanac.averageSNR() << " dB [min: " << almanac.minSNR() << " dB, max:" << almanac.maxSNR() << " dB]" << endl; + + ss << " < Almanac (" << almanac.percentComplete() << "%) >" << endl; + if (almanac.satellites.size() == 0){ + ss << " > No satellite info in almanac." << endl; + } + for (size_t i = 0; i < almanac.satellites.size(); i++){ + ss << " [" << setw(2) << setfill(' ') << (i + 1) << "] " << almanac.satellites[i].toString() << endl; + } + + return ss.str(); +} +GPSFix::operator std::string(){ + return toString(); +} + + + diff --git a/src/GPSService.cpp b/src/GPSService.cpp new file mode 100644 index 0000000..4f90a0f --- /dev/null +++ b/src/GPSService.cpp @@ -0,0 +1,509 @@ +/* + * GPSService.cpp + * + * Created on: Aug 14, 2014 + * Author: Cameron Karlsson + */ + + +#include <nmeaparse/GPSService.h> +#include <nmeaparse/NumberConversion.h> + +#include <iostream> +#include <cmath> + +using namespace std; +using namespace std::chrono; + +using namespace nmea; + + +// ------ Some helpers ---------- +// Takes the NMEA lat/long format (dddmm.mmmm, [N/S,E/W]) and converts to degrees N,E only +double convertLatLongToDeg(string llstr, string dir){ + + double pd = parseDouble(llstr); + double deg = trunc(pd / 100); //get ddd from dddmm.mmmm + double mins = pd - deg * 100; + + deg = deg + mins / 60.0; + + char hdg = 'x'; + if (dir.size() > 0){ + hdg = dir[0]; + } + + //everything should be N/E, so flip S,W + if (hdg == 'S' || hdg == 'W'){ + deg *= -1.0; + } + + return deg; +} +double convertKnotsToKilometersPerHour(double knots){ + return knots * 1.852; +} + + + +// ------------- GPSSERVICE CLASS ------------- + + + + +GPSService::GPSService(NMEAParser& parser) { + attachToParser(parser); // attach to parser in the GPS object +} + +GPSService::~GPSService() { + // TODO Auto-generated destructor stub +} + +void GPSService::attachToParser(NMEAParser& _parser){ + + // http://www.gpsinformation.org/dale/nmea.htm + /* used sentences... + $GPGGA - time,position,fix data + $GPGSA - gps receiver operating mode, satellites used in position and DOP values + $GPGSV - number of gps satellites in view, satellite ID, elevation,azimuth, and SNR + $GPRMC - time,date, position,course, and speed data + $GPVTG - course and speed information relative to the ground + $GPZDA - 1pps timing message + $PSRF150 - gps module "ok to send" + */ + _parser.setSentenceHandler("PSRF150", [this](const NMEASentence& nmea){ + this->read_PSRF150(nmea); + }); + _parser.setSentenceHandler("GPGGA", [this](const NMEASentence& nmea){ + this->read_GPGGA(nmea); + }); + _parser.setSentenceHandler("GPGSA", [this](const NMEASentence& nmea){ + this->read_GPGSA(nmea); + }); + _parser.setSentenceHandler("GPGSV", [this](const NMEASentence& nmea){ + this->read_GPGSV(nmea); + }); + _parser.setSentenceHandler("GPRMC", [this](const NMEASentence& nmea){ + this->read_GPRMC(nmea); + }); + _parser.setSentenceHandler("GPVTG", [this](const NMEASentence& nmea){ + this->read_GPVTG(nmea); + }); + +} + + + + +void GPSService::read_PSRF150(const NMEASentence& nmea){ + // nothing right now... + // Called with checksum 3E (valid) for GPS turning ON + // Called with checksum 3F (invalid) for GPS turning OFF +} + +void GPSService::read_GPGGA(const NMEASentence& nmea){ + /* -- EXAMPLE -- + $GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,*47 + + $GPGGA,205630.945,3346.1070,N,08423.6687,W,0,03,,30.8,M,-30.8,M,,0000*73 // ATLANTA!!!! + + Where: + GGA Global Positioning System Fix Data + index: + [0] 123519 Fix taken at 12:35:19 UTC + [1-2] 4807.038,N Latitude 48 deg 07.038' N + [3-4] 1131.000,E Longitude 11 deg 31.000' E + [5] 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 + [6] 08 Number of satellites being tracked + [7] 0.9 Horizontal dilution of position + [8-9] 545.4,M Altitude, Meters, above mean sea level + [10-11] 46.9,M Height of geoid (mean sea level) above WGS84 + ellipsoid + [12] (empty field) time in seconds since last DGPS update + [13] (empty field) DGPS station ID number + [13] *47 the checksum data, always begins with * + */ + try + { + if (!nmea.checksumOK()){ + throw NMEAParseError("Checksum is invalid!"); + } + + if (nmea.parameters.size() < 14){ + throw NMEAParseError("GPS data is missing parameters."); + } + + + // TIMESTAMP + this->fix.timestamp.setTime(parseDouble(nmea.parameters[0])); + + string sll; + string dir; + // LAT + sll = nmea.parameters[1]; + dir = nmea.parameters[2]; + if (sll.size() > 0){ + this->fix.latitude = convertLatLongToDeg(sll, dir); + } + + // LONG + sll = nmea.parameters[3]; + dir = nmea.parameters[4]; + if (sll.size() > 0){ + this->fix.longitude = convertLatLongToDeg(sll, dir); + } + + + // FIX QUALITY + bool lockupdate = false; + this->fix.quality = (uint8_t)parseInt(nmea.parameters[5]); + if (this->fix.quality == 0){ + lockupdate = this->fix.setlock(false); + } + else if (this->fix.quality == 1){ + lockupdate = this->fix.setlock(true); + } + else {} + + + // TRACKING SATELLITES + this->fix.trackingSatellites = (int32_t)parseInt(nmea.parameters[6]); + if (this->fix.visibleSatellites < this->fix.trackingSatellites){ + this->fix.visibleSatellites = this->fix.trackingSatellites; // the visible count is in another sentence. + } + + // ALTITUDE + if (nmea.parameters[8].size() > 0){ + this->fix.altitude = parseDouble(nmea.parameters[8]); + } + else { + // leave old value + } + + //calling handlers + if (lockupdate){ + this->onLockStateChanged(this->fix.haslock); + } + this->onUpdate(); + } + catch (NumberConversionError& ex) + { + NMEAParseError pe("GPS Number Bad Format [$GPGGA] :: " + ex.message, nmea); + throw pe; + } + catch (NMEAParseError& ex) + { + NMEAParseError pe("GPS Data Bad Format [$GPGGA] :: " + ex.message, nmea); + throw pe; + } +} + +void GPSService::read_GPGSA(const NMEASentence& nmea){ + /* -- EXAMPLE -- + $GPGSA,A,3,04,05,,09,12,,,24,,,,,2.5,1.3,2.1*39 + + $GPGSA,A,3,18,21,22,14,27,19,,,,,,,4.4,2.7,3.4*32 + + Where: + GSA Satellite status + [0] A Auto selection of 2D or 3D fix (M = manual) + [1] 3 3D fix - values include: 1 = no fix + 2 = 2D fix + 3 = 3D fix + [2-13] 04,05... PRNs of satellites used for fix (space for 12) + [14] 2.5 PDOP (dilution of precision) + [15] 1.3 Horizontal dilution of precision (HDOP) + [16] 2.1 Vertical dilution of precision (VDOP) + [16] *39 the checksum data, always begins with * + */ + + + try + { + if (!nmea.checksumOK()){ + throw NMEAParseError("Checksum is invalid!"); + } + + if (nmea.parameters.size() < 17){ + throw NMEAParseError("GPS data is missing parameters."); + } + + + // FIX TYPE + bool lockupdate = false; + uint64_t fixtype = parseInt(nmea.parameters[1]); + this->fix.type = (int8_t)fixtype; + if (fixtype == 1){ + lockupdate = this->fix.setlock(false); + } + else if (fixtype == 3) { + lockupdate = this->fix.setlock(true); + } + else {} + + + // DILUTION OF PRECISION -- PDOP + double dop = parseDouble(nmea.parameters[14]); + this->fix.dilution = dop; + + // HORIZONTAL DILUTION OF PRECISION -- HDOP + double hdop = parseDouble(nmea.parameters[15]); + this->fix.horizontalDilution = hdop; + + // VERTICAL DILUTION OF PRECISION -- VDOP + double vdop = parseDouble(nmea.parameters[16]); + this->fix.verticalDilution = vdop; + + //calling handlers + if (lockupdate){ + this->onLockStateChanged(this->fix.haslock); + } + this->onUpdate(); + } + catch (NumberConversionError& ex) + { + NMEAParseError pe("GPS Number Bad Format [$GPGSA] :: " + ex.message, nmea); + throw pe; + } + catch (NMEAParseError& ex) + { + NMEAParseError pe("GPS Data Bad Format [$GPGSA] :: " + ex.message, nmea); + throw pe; + } +} + +void GPSService::read_GPGSV(const NMEASentence& nmea){ + /* -- EXAMPLE -- + $GPGSV,2,1,08,01,40,083,46,02,17,308,41,12,07,344,39,14,22,228,45*75 + + + $GPGSV,3,1,12,01,00,000,,02,00,000,,03,00,000,,04,00,000,*7C + $GPGSV,3,2,12,05,00,000,,06,00,000,,07,00,000,,08,00,000,*77 + $GPGSV,3,3,12,09,00,000,,10,00,000,,11,00,000,,12,00,000,*71 + + Where: + GSV Satellites in view + [0] 2 Number of sentences for full data + [1] 1 sentence 1 of 2 + [2] 08 Number of satellites in view + + [3] 01 Satellite PRN number + [4] 40 Elevation, degrees + [5] 083 Azimuth, degrees + [6] 46 SNR - higher is better + [...] for up to 4 satellites per sentence + [17] *75 the checksum data, always begins with * + */ + + try + { + if (!nmea.checksumOK()){ + throw NMEAParseError("Checksum is invalid!"); + } + + // can't do this check because the length varies depending on satallites... + //if(nmea.parameters.size() < 18){ + // throw NMEAParseError("GPS data is missing parameters."); + //} + + // VISIBLE SATELLITES + this->fix.visibleSatellites = (int32_t)parseInt(nmea.parameters[2]); + if (this->fix.trackingSatellites == 0){ + this->fix.visibleSatellites = 0; // if no satellites are tracking, then none are visible! + } // Also NMEA defaults to 12 visible when chip powers on. Obviously not right. + + uint32_t totalPages = (uint32_t)parseInt(nmea.parameters[0]); + uint32_t currentPage = (uint32_t)parseInt(nmea.parameters[1]); + + + //if this is the first page, then reset the almanac + if (currentPage == 1){ + this->fix.almanac.clear(); + //cout << "CLEARING ALMANAC" << endl; + } + + this->fix.almanac.lastPage = currentPage; + this->fix.almanac.totalPages = totalPages; + this->fix.almanac.visibleSize = this->fix.visibleSatellites; + + int entriesInPage = (nmea.parameters.size() - 3) >> 2; //first 3 are not satellite info + //- entries come in 4-ples, and truncate, so used shift + GPSSatellite sat; + for (int i = 0; i < entriesInPage; i++){ + int prop = 3 + i * 4; + + // PRN, ELEVATION, AZIMUTH, SNR + sat.prn = (uint32_t)parseInt(nmea.parameters[prop]); + sat.elevation = (uint32_t)parseInt(nmea.parameters[prop + 1]); + sat.azimuth = (uint32_t)parseInt(nmea.parameters[prop + 2]); + sat.snr = (uint32_t)parseInt(nmea.parameters[prop + 3]); + + //cout << "ADDING SATELLITE ::" << sat.toString() << endl; + this->fix.almanac.updateSatellite(sat); + } + + this->fix.almanac.processedPages++; + + // + if (this->fix.visibleSatellites == 0){ + this->fix.almanac.clear(); + } + + + //cout << "ALMANAC FINISHED page " << this->fix.almanac.processedPages << " of " << this->fix.almanac.totalPages << endl; + this->onUpdate(); + + } + catch (NumberConversionError& ex) + { + NMEAParseError pe("GPS Number Bad Format [$GPGSV] :: " + ex.message, nmea); + throw pe; + } + catch (NMEAParseError& ex) + { + NMEAParseError pe("GPS Data Bad Format [$GPGSV] :: " + ex.message, nmea); + throw pe; + } +} + +void GPSService::read_GPRMC(const NMEASentence& nmea){ + /* -- EXAMPLE --- + $GPRMC,123519,A,4807.038,N,01131.000,E,022.4,084.4,230394,003.1,W*6A + $GPRMC,235957.025,V,,,,,,,070810,,,N*4B + $GPRMC,061425.000,A,3346.2243,N,08423.4706,W,0.45,18.77,060914,,,A*47 + + Where: + RMC Recommended Minimum sentence C + [0] 123519 Fix taken at 12:35:19 UTC + [1] A Status A=active or V=Void. + [2-3] 4807.038,N Latitude 48 deg 07.038' N + [4-5] 01131.000,E Longitude 11 deg 31.000' E + [6] 022.4 Speed over the ground in knots + [7] 084.4 Track angle in degrees True + [8] 230394 Date - 23rd of March 1994 + [9-10] 003.1,W Magnetic Variation + [10] *6A The checksum data, always begins with * + // NMEA 2.3 includes another field after + */ + + try + { + if (!nmea.checksumOK()){ + throw NMEAParseError("Checksum is invalid!"); + } + + if (nmea.parameters.size() < 11){ + throw NMEAParseError("GPS data is missing parameters."); + } + + // TIMESTAMP + this->fix.timestamp.setTime(parseDouble(nmea.parameters[0])); + + string sll; + string dir; + // LAT + sll = nmea.parameters[2]; + dir = nmea.parameters[3]; + if (sll.size() > 0){ + this->fix.latitude = convertLatLongToDeg(sll, dir); + } + + // LONG + sll = nmea.parameters[4]; + dir = nmea.parameters[5]; + if (sll.size() > 0){ + this->fix.longitude = convertLatLongToDeg(sll, dir); + } + + + // ACTIVE + bool lockupdate = false; + char status = 'V'; + if (nmea.parameters[1].size() > 0){ + status = nmea.parameters[1][0]; + } + this->fix.status = status; + if (status == 'V'){ + lockupdate = this->fix.setlock(false); + } + else if (status == 'A') { + lockupdate = this->fix.setlock(true); + } + else { + lockupdate = this->fix.setlock(false); //not A or V, so must be wrong... no lock + } + + + this->fix.speed = convertKnotsToKilometersPerHour(parseDouble(nmea.parameters[6])); // received as knots, convert to km/h + this->fix.travelAngle = parseDouble(nmea.parameters[7]); + this->fix.timestamp.setDate((int32_t)parseInt(nmea.parameters[8])); + + + //calling handlers + if (lockupdate){ + this->onLockStateChanged(this->fix.haslock); + } + this->onUpdate(); + } + catch (NumberConversionError& ex) + { + NMEAParseError pe("GPS Number Bad Format [$GPRMC] :: " + ex.message, nmea); + throw pe; + } + catch (NMEAParseError& ex) + { + NMEAParseError pe("GPS Data Bad Format [$GPRMC] :: " + ex.message, nmea); + throw pe; + } +} + +void GPSService::read_GPVTG(const NMEASentence& nmea){ + /* + $GPVTG,054.7,T,034.4,M,005.5,N,010.2,K*48 + + where: + VTG Track made good and ground speed + [0-1] 054.7,T True track made good (degrees) + [2-3] 034.4,M Magnetic track made good + [4-5] 005.5,N Ground speed, knots + [6-7] 010.2,K Ground speed, Kilometers per hour + [7] *48 Checksum + */ + + try + { + if (!nmea.checksumOK()){ + throw NMEAParseError("Checksum is invalid!"); + } + + if (nmea.parameters.size() < 8){ + throw NMEAParseError("GPS data is missing parameters."); + } + + // SPEED + // if empty, is converted to 0 + this->fix.speed = parseDouble(nmea.parameters[6]); //km/h + + + this->onUpdate(); + } + catch (NumberConversionError& ex) + { + NMEAParseError pe("GPS Number Bad Format [$GPVTG] :: " + ex.message, nmea); + throw pe; + } + catch (NMEAParseError& ex) + { + NMEAParseError pe("GPS Data Bad Format [$GPVTG] :: " + ex.message, nmea); + throw pe; + } +} + diff --git a/src/NMEACommand.cpp b/src/NMEACommand.cpp new file mode 100644 index 0000000..a432767 --- /dev/null +++ b/src/NMEACommand.cpp @@ -0,0 +1,108 @@ +/* + * NMEACommand.cpp + * + * Created on: Sep 8, 2014 + * Author: Cameron Karlsson + */ + +#include <nmeaparse/NMEACommand.h> +#include <iomanip> +#include <sstream> + +using namespace std; +using namespace nmea; + + +NMEACommand::NMEACommand(){}; + +NMEACommand::~NMEACommand(){}; + +string NMEACommand::toString(){ + return addChecksum(message); +} + +string NMEACommand::addChecksum(std::string s){ + stringstream zz; + zz << name << "," << s; + checksum = NMEAParser::calculateChecksum(zz.str()); + + stringstream ss; + ios_base::fmtflags oldflags = ss.flags(); + ss << "$" << zz.str() << "*" << hex << uppercase << internal << setfill('0') << setw(2) << (int)checksum << "\r\n"; + ss.flags(oldflags); //reset + + return ss.str(); +}; + + + + /* + // $PSRF100,0,9600,8,1,0*0C + + Table 2-4 Set Serial Port Data Format + Name Example Unit Description + Message ID $PSRF100 PSRF100 protocol header + Protocol 0 0=SiRF binary, 1=NMEA + Baud 9600 1200, 2400, 4800, 9600, 19200, 38400, 57600, and 115200 + DataBits 8 8,71 + + StopBits 1 0,1 1. SiRF protocol is only valid for 8 data bits, 1stop bit, and no parity. + Parity 0 0=None, 1=Odd, 2=Even + Checksum *0C + <CR> <LF> End of message termination + */ +std::string NMEACommandSerialConfiguration::toString(){ + stringstream ss; + + ss << "1," << baud << "," << databits << "," << stopbits << "," << parity; + message = ss.str(); + + return NMEACommand::addChecksum(message); +} + + + + // $PSRF103,00,01,00,01*25 + /* + * Table 2-9 Query/Rate Control Data Format + Name Example Unit Description + Message ID $PSRF103 PSRF103 protocol header + Msg 00 See Table 2-10 + Mode 01 0=SetRate, 1=Query + Rate 00 sec Output—off=0, max=255 + CksumEnable 01 0=Disable Checksum, 1=Enable Checksum + Checksum *25 + <CR> <LF> End of message termination + */ + /* + * Table 2-10 Messages + Value Description + 0 GGA + 1 GLL + 2 GSA + 3 GSV + 4 RMC + 5 VTG + 6 MSS (If internal beacon is supported) + 7 Not defined + 8 ZDA (if 1PPS output is supported) + 9 Not defined + */ +// Data Members: +// int messageID; +// int mode; +// int rate; +// int checksumEnable; +// Creates a valid NMEA $PSRF103 command sentence. +std::string NMEACommandQueryRate::toString(){ + stringstream ss; + + ss << setfill('0') << setw(2) << messageID << "," + << setfill('0') << setw(2) << mode << "," + << setfill('0') << setw(2) << rate << "," + << setfill('0') << setw(2) << checksumEnable; + message = ss.str(); + + return NMEACommand::addChecksum(message); +} + diff --git a/src/NMEAParser.cpp b/src/NMEAParser.cpp new file mode 100644 index 0000000..f5d9653 --- /dev/null +++ b/src/NMEAParser.cpp @@ -0,0 +1,466 @@ +/* + * NMEAParser.cpp + * + * Created on: Aug 12, 2014 + * Author: Cameron Karlsson + */ + +#include <nmeaparse/NMEAParser.h> +#include <nmeaparse/NumberConversion.h> +#include <sstream> +#include <iostream> +#include <algorithm> +#include <cctype> + +using namespace std; +using namespace nmea; + + + +// --------- NMEA PARSE ERROR-------------- + +NMEAParseError::NMEAParseError(std::string msg) + : message(msg) +{} +NMEAParseError::NMEAParseError(std::string msg, NMEASentence n) + : message(msg), nmea(n) +{} + +NMEAParseError::~NMEAParseError() +{} + +std::string NMEAParseError::what(){ + return message; +} + + + + + // --------- NMEA SENTENCE -------------- + +NMEASentence::NMEASentence() : isvalid(false), calculatedChecksum(0), parsedChecksum(0) +{} +NMEASentence::~NMEASentence() +{} +bool NMEASentence::valid() const { + return isvalid; +} +bool NMEASentence::checksumOK() const { + return (parsedChecksum != 0 && calculatedChecksum != 0) + && + (parsedChecksum == calculatedChecksum); +} + + + +// true if the text contains a non-alpha numeric value +bool hasNonAlphaNum(string txt){ + for (size_t i = 0; i < txt.size(); i++){ + if ( !isalnum(txt[i]) ){ + return true; + } + } + return false; +} + +// true if alphanumeric or '-' +bool validParamChars(string txt){ + for (size_t i = 0; i < txt.size(); i++){ + if (!isalnum(txt[i])){ + if (txt[i] != '-'){ + return false; + } + } + } + return true; +} + +// remove all whitespace +void squish(string& str){ + + char chars[] = {'\t',' '}; + for (size_t i = 0; i < sizeof(chars); ++i) + { + // needs include <algorithm> + str.erase(std::remove(str.begin(), str.end(), chars[i]), str.end()); + } +} + +// remove side whitespace +void trim(string& str){ + stringstream trimmer; + trimmer << str; + str.clear(); + trimmer >> str; +} + + + // --------- NMEA PARSER -------------- + + + +NMEAParser::NMEAParser() + : log(false), maxbuffersize(NMEA_PARSER_MAX_BUFFER_SIZE), fillingbuffer(false) +{ + + +} + +NMEAParser::~NMEAParser() { + // TODO Auto-generated destructor stub +} + + +void NMEAParser::setSentenceHandler(std::string cmdKey, std::function<void(const NMEASentence&)> handler){ + eventTable.erase(cmdKey); + + //std::pair<string, function<void(NMEASentence)>> entry(cmdKey, handler); + //eventTable.insert(entry); + eventTable.insert({ cmdKey, handler }); +} + +void NMEAParser::readByte(uint8_t b){ + uint8_t startbyte = '$'; + + if (fillingbuffer){ + if (b == '\n'){ + buffer.push_back(b); + try { + readSentence(buffer); + buffer.clear(); + fillingbuffer = false; + } + catch (exception&){ + // If anything happens, let it pass through, but reset the buffer first. + buffer.clear(); + fillingbuffer = false; + throw; + } + } + else{ + if (buffer.size() < maxbuffersize){ + buffer.push_back(b); + } + else { + buffer.clear(); //clear the host buffer so it won't overflow. + fillingbuffer = false; + } + } + } + else { + if (b == startbyte){ // only start filling when we see the start byte. + fillingbuffer = true; + buffer.push_back(b); + } + } +} + +void NMEAParser::readBuffer(uint8_t* b, uint32_t size){ + for (uint32_t i = 0; i < size; ++i){ + readByte(b[i]); + } +} + +void NMEAParser::readLine(string cmd){ + cmd += "\r\n"; + for (size_t i = 0; i < cmd.size(); ++i){ + readByte(cmd[i]); + } +} + +// Loggers +void NMEAParser::onInfo(NMEASentence& nmea, string txt){ + if (log){ + cout << "[Info] " << txt << endl; + } +} +void NMEAParser::onWarning(NMEASentence& nmea, string txt){ + if (log){ + cout << "[Warning] " << txt << endl; + } +} +void NMEAParser::onError(NMEASentence& nmea, string txt){ + throw NMEAParseError("[ERROR] " + txt); +} + +// takes a complete NMEA string and gets the data bits from it, +// calls the corresponding handler in eventTable, based on the 5 letter sentence code +void NMEAParser::readSentence(std::string cmd){ + + NMEASentence nmea; + + onInfo(nmea, "Processing NEW string..."); + + if (cmd.size() == 0){ + onWarning(nmea, "Blank string -- Skipped processing."); + return; + } + + // If there is a newline at the end (we are coming from the byte reader + if ( *(cmd.end()-1) == '\n'){ + if (*(cmd.end() - 2) == '\r'){ // if there is a \r before the newline, remove it. + cmd = cmd.substr(0, cmd.size() - 2); + } + else + { + onWarning(nmea, "Malformed newline, missing carriage return (\\r) "); + cmd = cmd.substr(0, cmd.size()-1); + } + } + + ios_base::fmtflags oldflags = cout.flags(); + + // Remove all whitespace characters. + size_t beginsize = cmd.size(); + squish(cmd); + if (cmd.size() != beginsize){ + stringstream ss; + ss << "New NMEA string was full of " << (beginsize - cmd.size()) << " whitespaces!"; + onWarning(nmea, ss.str()); + } + + + onInfo(nmea, string("NMEA string: (\"") + cmd + "\")"); + + + // Seperates the data now that everything is formatted + try{ + parseText(nmea, cmd); + } + catch (NMEAParseError&){ + throw; + } + catch (std::exception& e){ + string s = " >> NMEA Parser Internal Error: Indexing error?... "; + throw std::runtime_error(s + e.what()); + } + cout.flags(oldflags); //reset + + // Handle/Throw parse errors + if (!nmea.valid()){ + + size_t linewidth = 35; + stringstream ss; + if (nmea.text.size() > linewidth){ + ss << "Invalid text. (\"" << nmea.text.substr(0, linewidth) << "...\")"; + } + else{ + ss << "Invalid text. (\"" << nmea.text << "\")"; + } + + onError(nmea, ss.str()); + return; + } + + + // Call the "any sentence" event handler, even if invalid checksum, for possible logging elsewhere. + onInfo(nmea, "Calling generic onSentence()."); + onSentence(nmea); + + + // Call event handlers based on map entries + function<void(const NMEASentence&)> handler = eventTable[nmea.name]; + if (handler){ + onInfo(nmea, string("Calling specific handler for sentence named \"") + nmea.name + "\""); + handler(nmea); + } + else + { + onWarning(nmea, string("Null event handler for type (name: \"") + nmea.name + "\")"); + } + + + + + + cout.flags(oldflags); //reset + +} + +// takes the string *between* the '$' and '*' in nmea sentence, +// then calculates a rolling XOR on the bytes +uint8_t NMEAParser::calculateChecksum(string s){ + uint8_t checksum = 0; + for (size_t i = 0; i < s.size(); i++){ + checksum = checksum ^ s[i]; + } + + // will display the calculated checksum in hex + //if(log) + //{ + // ios_base::fmtflags oldflags = cout.flags(); + // cout << "NMEA parser Info: calculated CHECKSUM for \"" << s << "\": 0x" << std::hex << (int)checksum << endl; + // cout.flags(oldflags); //reset + //} + return checksum; +} + + +void NMEAParser::parseText(NMEASentence& nmea, string txt){ + + if (txt.size() == 0){ + nmea.isvalid = false; + return; + } + + nmea.isvalid = false; // assume it's invalid first + nmea.text = txt; // save the received text of the sentence + + // Looking for index of last '$' + size_t startbyte = 0; + size_t dollar = txt.find_last_of('$'); + if (dollar == string::npos){ + // No dollar sign... INVALID! + return; + } + else + { + startbyte = dollar; + } + + + // Get rid of data up to last'$' + txt = txt.substr(startbyte + 1); + + + // Look for checksum + size_t checkstri = txt.find_last_of('*'); + bool haschecksum = checkstri != string::npos; + if (haschecksum){ + // A checksum was passed in the message, so calculate what we expect to see + nmea.calculatedChecksum = calculateChecksum(txt.substr(0, checkstri)); + } + else + { + // No checksum is only a warning because some devices allow sending data with no checksum. + onWarning(nmea, "No checksum information provided. Could not find '*'."); + } + + // Handle comma edge cases + size_t comma = txt.find(','); + if (comma == string::npos){ //comma not found, but there is a name... + if (txt.size() > 0) + { // the received data must just be the name + if ( hasNonAlphaNum(txt) ){ + nmea.isvalid = false; + return; + } + nmea.name = txt; + nmea.isvalid = true; + return; + } + else + { //it is a '$' with no information + nmea.isvalid = false; + return; + } + } + + //"$," case - no name + if (comma == 0){ + nmea.isvalid = false; + return; + } + + + //name should not include first comma + nmea.name = txt.substr(0, comma); + if ( hasNonAlphaNum(nmea.name) ){ + nmea.isvalid = false; + return; + } + + + //comma is the last character/only comma + if (comma + 1 == txt.size()){ + nmea.parameters.push_back(""); + nmea.isvalid = true; + return; + } + + + //move to data after first comma + txt = txt.substr(comma + 1, txt.size() - (comma + 1)); + + //parse parameters according to csv + istringstream f(txt); + string s; + while (getline(f, s, ',')) { + //cout << s << endl; + nmea.parameters.push_back(s); + } + + + //above line parsing does not add a blank parameter if there is a comma at the end... + // so do it here. + if (*(txt.end() - 1) == ','){ + + // supposed to have checksum but there is a comma at the end... invalid + if (haschecksum){ + nmea.isvalid = false; + return; + } + + //cout << "NMEA parser Warning: extra comma at end of sentence, but no information...?" << endl; // it's actually standard, if checksum is disabled + nmea.parameters.push_back(""); + + stringstream sz; + sz << "Found " << nmea.parameters.size() << " parameters."; + onInfo(nmea, sz.str()); + + } + else + { + stringstream sz; + sz << "Found " << nmea.parameters.size() << " parameters."; + onInfo(nmea, sz.str()); + + //possible checksum at end... + size_t endi = nmea.parameters.size() - 1; + size_t checki = nmea.parameters[endi].find_last_of('*'); + if (checki != string::npos){ + string last = nmea.parameters[endi]; + nmea.parameters[endi] = last.substr(0, checki); + if (checki == last.size() - 1){ + onError(nmea, "Checksum '*' character at end, but no data."); + } + else{ + nmea.checksum = last.substr(checki + 1, last.size() - checki); //extract checksum without '*' + + onInfo(nmea, string("Found checksum. (\"*") + nmea.checksum + "\")"); + + try + { + nmea.parsedChecksum = (uint8_t)parseInt(nmea.checksum, 16); + } + catch( NumberConversionError& ) + { + onError(nmea, string("parseInt() error. Parsed checksum string was not readable as hex. (\"") + nmea.checksum + "\")"); + } + + onInfo(nmea, string("Checksum ok? ") + (nmea.checksumOK() ? "YES" : "NO") + "!"); + + + } + } + } + + + for (size_t i = 0; i < nmea.parameters.size(); i++){ + if (!validParamChars(nmea.parameters[i])){ + nmea.isvalid = false; + stringstream ss; + ss << "Invalid character in parameter (from 0) " << i << "."; + onError(nmea, ss.str() ); + break; + } + } + + + nmea.isvalid = true; + + return; + +} + + diff --git a/src/NumberConversion.cpp b/src/NumberConversion.cpp new file mode 100644 index 0000000..a5d429d --- /dev/null +++ b/src/NumberConversion.cpp @@ -0,0 +1,144 @@ +/* + * NumberConversion.cpp + * + * Created on: Sep 2, 2014 + * Author: Cameron Karlsson + */ + +#include <nmeaparse/NumberConversion.h> + +using namespace std; + +namespace nmea { +// Note: both parseDouble and parseInt return 0 with "" input. + + double parseDouble(std::string s){ + + char* p; + double d = ::strtod(s.c_str(), &p); + if (*p != 0){ + std::stringstream ss; + ss << "NumberConversionError: parseDouble() error in argument \"" << s << "\", '" + << *p << "' is not a number."; + throw NumberConversionError(ss.str()); + } + return d; + } + int64_t parseInt(std::string s, int radix){ + char* p; + + int64_t d = ::strtoll(s.c_str(), &p, radix); + + if (*p != 0) { + std::stringstream ss; + ss << "NumberConversionError: parseInt() error in argument \"" << s << "\", '" + << *p << "' is not a number."; + throw NumberConversionError(ss.str()); + } + return d; + } + +} + +/* +#include <iostream> +void NumberConversion_test(){ + string s; + float f; + long long k; + + try{ + s = "-1.345"; + f = parseDouble(s); + cout << s << ": " << f << endl; + } + catch(NumberConversionError& ex){ + cout << ex.message << endl; + } + + + try{ + s = "-1.23e-2"; + f = parseDouble(s); + cout << s << ": " << f << endl; + } + catch(NumberConversionError& ex){ + cout << ex.message << endl; + } + + + try{ + s = ""; + f = parseDouble(s); + cout << s << ": " << f << endl; + } + catch(NumberConversionError& ex){ + cout << ex.message << endl; + } + + + + try{ + // -- fails, ok + s = "asd"; + f = parseDouble(s); + cout << s << ": " << f << endl; + } + catch(NumberConversionError& ex){ + cout << ex.message << endl; + } + + try{ + s = "-1234.123"; + k = parseInt(s); + cout << s << ": " << k << endl; + } + catch(NumberConversionError& ex){ + cout << ex.message << endl; + } + + + + try{ + s = "01234"; + k = parseInt(s); + cout << s << ": " << k << endl; + } + catch(NumberConversionError& ex){ + cout << ex.message << endl; + } + + + try{ + // -- converts to 0 + s = ""; + k = parseInt(s); + cout << s << ": " << k << endl; + } + catch(NumberConversionError& ex){ + cout << ex.message << endl; + } + + + try{ + // -- fails, ok + s = "asd"; + k = parseInt(s); + cout << s << ": " << k << endl; + } + catch(NumberConversionError& ex){ + cout << ex.message << endl; + } + + try{ + // -- fails, ok + s = "-16"; + k = parseInt(s); + cout << s << ": " << k << endl; + } + catch(NumberConversionError& ex){ + cout << ex.message << endl; + } + + } + */
\ No newline at end of file |
