Search code examples
c++nmea

How should I parse and check through a string?


Okay so I have a C++ project I want to complete which involves receiving NMEA sentences and parsing through them to check if they conform to the "grammar rules" of NMEA.

The current method I have been doing this is using if statements but this is obviously not good coding practice.

What are other methods I could use to attempt to check string sentences for specific characters and groups of characters?


Solution

  • Let us assume that you have NMEA data like this

    $GPGGA,124613.90,5543.3221231,N,03739.1368442,E,1,15,0.69,147.0851,M,14.4298,M,,*54 $GPGSV,3,1,10,27,12,078,41,05,31,308,49,16,25,043,44,02,11,268,44*7E $GPGSV,3,2,10,26,03,031,39,07,74,216,52,09,58,121,52,30,39,234,48*71 $GPGSV,3,3,10,23,30,116,46,04,37,114,47*79 $GLGSV,2,1,07,84,17,338,43,78,15,212,48,85,12,032,46,67,84,223,53*67 $GLGSV,2,2,07,77,67,195,47,76,50,047,54,66,32,144,52*5C $GPGGA,124614.00,5543.3221239,N,03739.1368445,E,1,15,0.69,147.0864,M,14.4298,M,,*53 $GPGSV,3,1,10,27,12,078,41,05,31,308,49,16,25,043,43,02,11,268,44*79 $GPGSV,3,2,10,26,03,031,39,07,74,216,52,09,58,121,52,30,39,234,48*71 $GPGSV,3,3,10,23,30,116,46,04,37,114,47*79 $GLGSV,2,1,07,84,17,338,43,78,15,212,48,85,12,032,46,67,84,223,53*67 $GLGSV,2,2,07,77,67,195,47,76,50,047,54,66,32,144,52*5C

    And if we want to extract GGA and GSV data, you may want to use the following code:

    #include <string>
    #include <ctime>
    #include <cstring>
    #include <iostream>
    #include <fstream>
    #include <iomanip>
    
    constexpr size_t NumberOfFixQualityStrings = 9;
    constexpr size_t NumberOfSatellitesPerGSVSentencePart = 4;
    constexpr size_t MaxNumberOfPartsInSentence = 10;
    constexpr size_t MaxTokensInSentence = 64;
    constexpr size_t NumberOfFieldsInGGA = 12;
    
    std::string fixQualityString[NumberOfFixQualityStrings]{ 
        "invalid", "GPS fix (SPS)", "DGPS fix", "PPS fix", "Real Time Kinematic", "Float RTK", 
        "estimated (dead reckoning", "Manual input mode", "Simulation mode" };
    
    //  essential fix data which provide 3D location and accuracy data
    struct GGA { 
        // Time of last satellite fix
        unsigned int fixTimeInUtcHours{};
        unsigned int fixTimeInUtcMinutes{};
        unsigned int fixTimeInUtcSeconds{};
        unsigned int fixTimeInUtcMilliSeconds{};
        // Position: Lattitude
        unsigned int lattitudeInDegree{};
        double lattitudeInMinutes{};
        std::string lattitideDirection{};
        // Position: Longitude
        unsigned int longitudeInDegree{};
        double longitudeInMinutes{};
        std::string longitudeDirection{};
        // FixQuality // see dteails as string above
        unsigned int fixQuality{};
        std::string fixQualityString{};
        // Number of satellites being tracked (can be more than shown in GSV, not all are beeing used for calculation)
        unsigned int numberOfTrackedSatellites{};
        //  Horizontal dilution of position
        double horizontalDilution{};
        // Altitude, Meters, above mean sea level
        double altitude{};
        std::string altitudeDimension{};
        // Height of geoid (mean sea level) above WGS84 ellipsoid
        double goidHight{};
        std::string goidHightDimension{};
    };
    
    // Detail information for satellites in satellit view (GSV)
    struct SatelliteData {
        std::string satellitePRNnumber{};
        double elevationInDegress{};
        double azimuthInDegrees{};
        double snr{};  // signal noise ratio
    };
    
    // Part of a GSV sentence
    struct GSVSentencePart {
        size_t numberOfSentencesForFullData{};
        size_t sentencePartNumber{};
        size_t numberOfSatellitesInView{};
        size_t numberOfSatellitesInThisPart{};
        SatelliteData satelliteData[NumberOfSatellitesPerGSVSentencePart];
    };
    struct GSV
    {
        GSVSentencePart gsvSentencePart[MaxNumberOfPartsInSentence];
        size_t numberOfParts{};
    };
    
    bool checksumTest(std::string& line) {
        bool result{ false };
        // Check, if there is a 2 digt checksum at the end and convert it to decimal
        if (size_t pos{}, checkSumGiven{ std::stoul(line.substr(line.size() - 2), &pos, 16) }; pos == 2)
        {
            // Strip off checksum part
            line = line.substr(1,line.size() - 4);
            // Calculate checksum
            unsigned char calculatedChecksum{ 0U }; for (const unsigned char c : line)  calculatedChecksum ^= c;
            // Get result
            result = (calculatedChecksum == checkSumGiven);
        }
        return result;
    }
    
    // Split all strings into a tokens
    size_t splitIntoTokens(std::string& s, std::string (&tokens)[MaxTokensInSentence]) {
        // Number of converted tokens
        size_t numberOfTokens{ 0 };
        // First check checksum
        if (checksumTest(s)) {
            // Now split along each comma
            for (size_t i{ 0U }, startpos{ 0U }; i < s.size(); ++i) {
                // So, if there is a comma or the end of the string
                if ((s[i] == ',') || (i == (s.size() - 1))) {
                    // Copy substring
                    tokens[numberOfTokens++] = s.substr(startpos, i - startpos);
                    startpos = i + 1;
                }
            }
        }
        return numberOfTokens;
    }
    
    
    GGA convertStringToGGA(std::string& s) {
        GGA gga; 
        // Split string into tokens and check, if it worked
        if (std::string tokens[MaxTokensInSentence]; splitIntoTokens(s, tokens) > NumberOfFieldsInGGA && tokens[0] == "GPGGA") {
            gga.fixTimeInUtcHours = std::stoul(tokens[1].substr(0, 2));
            gga.fixTimeInUtcMinutes = std::stoul(tokens[1].substr(2, 2));
            gga.fixTimeInUtcSeconds = std::stoul(tokens[1].substr(4, 2));
            gga.fixTimeInUtcMilliSeconds = std::stod(tokens[1].substr(6, 2))*1000.0;
            gga.lattitudeInDegree = std::stoul(tokens[2].substr(0, 2));
            gga.lattitudeInMinutes = std::stod(tokens[2].substr(2));
            gga.lattitideDirection = tokens[3];
            gga.longitudeInDegree = std::stoul(tokens[4].substr(0, 2));
            gga.longitudeInMinutes = std::stod(tokens[4].substr(2));
            gga.longitudeDirection = tokens[5];
            gga.fixQuality = std::stoul(tokens[6]);
            gga.fixQualityString = (gga.fixQuality < NumberOfFixQualityStrings) ? fixQualityString[gga.fixQuality] : fixQualityString[0];
            gga.numberOfTrackedSatellites = std::stoul(tokens[7]);
            gga.horizontalDilution = std::stod(tokens[8]);
            gga.altitude = std::stod(tokens[9]);
            gga.altitudeDimension = tokens[10];
            gga.goidHight = std::stod(tokens[11]);
            gga.goidHightDimension = tokens[12];
        }
        return gga;
    }
    
    GSVSentencePart convertToGSVSentencePart(std::string& s) {
        GSVSentencePart gsvsp;
        // Split string into tokens and check, if it worked
        std::string tokens[MaxTokensInSentence];
        if (size_t numberOfCOnvertedTokens = splitIntoTokens(s, tokens); numberOfCOnvertedTokens > 0 && tokens[0] == "GPGSV") {
            gsvsp.numberOfSentencesForFullData = std::stoul(tokens[1]);
            gsvsp.sentencePartNumber = std::stoul(tokens[2]);
            gsvsp.numberOfSatellitesInView = std::stoul(tokens[3]);
            gsvsp.numberOfSatellitesInThisPart = 0;
            for (size_t currentToken = 4; currentToken < numberOfCOnvertedTokens; currentToken += 4) {
                gsvsp.satelliteData[gsvsp.numberOfSatellitesInThisPart].satellitePRNnumber = tokens[currentToken];
                gsvsp.satelliteData[gsvsp.numberOfSatellitesInThisPart].elevationInDegress = stod(tokens[currentToken + 1]);
                gsvsp.satelliteData[gsvsp.numberOfSatellitesInThisPart].azimuthInDegrees= stod(tokens[currentToken + 2]);
                gsvsp.satelliteData[gsvsp.numberOfSatellitesInThisPart].snr = stod(tokens[currentToken + 3]);
                ++gsvsp.numberOfSatellitesInThisPart;
            }
        }
        return gsvsp;
    }
    std::string calculateElapsedTime(const GGA& previousGGA, const GGA& nextGGA) {
        std::tm tmPrevious{}, tmNext{};
        tmPrevious.tm_year = 100; tmPrevious.tm_mon = 1; tmPrevious.tm_mday = 1;
        tmNext.tm_year = 100; tmNext.tm_mon = 1; tmNext.tm_mday = 1;
        tmPrevious.tm_hour = previousGGA.fixTimeInUtcHours;
        tmPrevious.tm_min = previousGGA.fixTimeInUtcMinutes;
        tmPrevious.tm_sec = previousGGA.fixTimeInUtcSeconds;
        std::time_t previousTime = std::mktime(&tmPrevious);
        tmNext.tm_hour = nextGGA.fixTimeInUtcHours;
        tmNext.tm_min = nextGGA.fixTimeInUtcMinutes;
        tmNext.tm_sec = nextGGA.fixTimeInUtcSeconds;
        std::time_t nextTime = std::mktime(&tmNext);
        double diff = std::difftime(nextTime, previousTime);
        diff  = diff + 1.0*nextGGA.fixTimeInUtcMilliSeconds/1000.0- 1.0*previousGGA.fixTimeInUtcMilliSeconds/1000.0;
        return std::to_string(diff);
    }
    
    int main() {
        // Open file and check, if it is open
        if (std::ifstream nmeaFile("r:\\log.txt"); nmeaFile) {
    
            GGA previousGGA;
            GGA nextGGA;
            GSV gsv;
            size_t state{ 0 };
            for (std::string line{}; std::getline(nmeaFile, line); ) {
                switch  ( state) {
    
                case 0:     // wait for first GGA data
                    if (line.substr(0, 6) == "$GPGGA") {
    
                        previousGGA = nextGGA;
                        nextGGA = convertStringToGGA(line);
    
                        state = 1;
                        gsv = {};
                    }
                    break;
                case 1: // wait for GSV
                    if (line.substr(0, 6) == "$GPGSV") {
                        gsv.gsvSentencePart[gsv.numberOfParts] = convertToGSVSentencePart(line);
                        if (gsv.gsvSentencePart[gsv.numberOfParts].numberOfSentencesForFullData ==
                            gsv.gsvSentencePart[gsv.numberOfParts].sentencePartNumber) {
                            state = 0;
                            ++gsv.numberOfParts;
                            // Now all data are available in reable and structed format.
                            // You can do, what you want with them
                            // For example, we can print all Satellite Data:
                            size_t counter{ 0 };
                            for (size_t i = 0; i < gsv.numberOfParts; ++i) {
                                for (size_t j = 0; j < gsv.gsvSentencePart[i].numberOfSatellitesInThisPart; j++) {
                                    std::cout << "Satellite: " << std::setw(2) << ++counter << "  Satellite name: " <<
                                        std::setw(3) << gsv.gsvSentencePart[i].satelliteData[j].satellitePRNnumber <<
                                        "   SNR: " << std::setw(8) << gsv.gsvSentencePart[i].satelliteData[j].snr << 
                                        "  Elapsed time: "<< calculateElapsedTime(previousGGA, nextGGA)<< " s\n";
                                }
                            }
                            --gsv.numberOfParts;
                        }
                        ++gsv.numberOfParts;
                    }
                    break;
                } 
            }
        }
        return 0;
    }
    

    Coding style is "beginner"-level for easier understanding.

    Modern C++ approach would be totally different, but not so easy to understand.