Swift NMEA parser











up vote
1
down vote

favorite












I'm quite new to Swift, and although I've been programming as a hobby for many years, I'm still not very confident about my code.



I'm currently rewriting an old library of mine, an NMEA parser. NMEA is a standard for GPS sentences containing position, time, satellites signal power and so on.



Basically this code receives a string and extracts values from it. For example:



$GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,*47


Where "123519" it current time, "4807.038,N,01131.000,E" are latitude/longitude and so on. There are many, many types of NMEA sentences, reference here.



import Foundation

typealias Byte = UInt8

extension UInt8 {
var hexValue:String {
return (self < 16 ? "0":"") + String(self, radix:16, uppercase:true)
}
}

/*
Allows to test against nil multiple variables aton once.
*/
extension Collection where Element == Optional<Any> {
func allNotNil() -> Bool {
return self.compactMap { $0 }.count > self.count
}

func atleastOneNotNil() -> Bool {
return self.compactMap { $0 }.count > 0
}

func allNil() -> Bool {
return self.compactMap { $0 }.count == 0
}

func atleastOneIsNil() -> Bool {
return self.contains { $0 == nil }
}
}

class NMEASentenceParser {
static let shared = NMEASentenceParser()

private init() {}

func parse(_ nmeaSentence:String) -> Any? {
if nmeaSentence.isEmpty {
return nil
}

if nmeaSentence.hasPrefix("$GPGGA") {
return GPGGA(nmeaSentence)
} else if nmeaSentence.hasPrefix("$GPGSA") {
return GPGSA(nmeaSentence)
}

return nil
}

//Generic class
class NMEASentence {
private var sentence:String
var trimmedSentence:String

var isValid:Bool {
get {
return sentence.suffix(2) == checksum().hexValue
}
}

func checksum() -> UInt8 {
var xor:UInt8 = 0
for i in 0..<trimmedSentence.utf8.count {
xor = xor ^ Array(trimmedSentence.utf8)[i]
}
return xor
}

init?(_ nmeaSentence:String) {
sentence = nmeaSentence

//duplicate sentence trimmed from its "$" and checksum
let start = sentence.index(sentence.startIndex, offsetBy: 1)
let end = sentence.index(sentence.endIndex, offsetBy: -3)
trimmedSentence = String(sentence[start..<end])

if isValid == false {
return nil
}

}

}

/*
Source: https://www.gpsinformation.org/dale/nmea.htm
GGA - essential fix data which provide 3D location and accuracy data.

$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 number
*47 the checksum data, always begins with *
*/
class GPGGA:NMEASentence {
override init?(_ nmeaSentence:String) {
//init will fail if
super.init(nmeaSentence)

let splittedSentence = trimmedSentence.split(separator: ",", maxSplits: Int.max, omittingEmptySubsequences: false)

utcTime = Float(splittedSentence[1])
latitude = Coordinate(splittedSentence[2], splittedSentence[3])
longitude = Coordinate(splittedSentence[4], splittedSentence[5])
fixQuality = FixQuality(rawValue: Int(splittedSentence[6]) ?? -1)
numberOfSatellites = Int(splittedSentence[7])
horizontalDilutionOfPosition = Float(splittedSentence[8])
mslAltitude = Float(splittedSentence[9])
mslAltitudeUnit = String(splittedSentence[10])
heightOfGeoid = Float(splittedSentence[11])
heightOfGeoidUnit = String(splittedSentence[10])

if [utcTime,latitude,longitude,fixQuality,numberOfSatellites,horizontalDilutionOfPosition,mslAltitude,mslAltitudeUnit,heightOfGeoid,heightOfGeoidUnit].atleastOneIsNil() == true {
return nil
}
}
var utcTime:Float?
var latitude:Coordinate?
var longitude:Coordinate?
var fixQuality:FixQuality?
var numberOfSatellites:Int?
var horizontalDilutionOfPosition:Float?
var mslAltitude:Float?
var mslAltitudeUnit:String?
var heightOfGeoid:Float?
var heightOfGeoidUnit:String?
}

/*
$GPGSA,A,3,04,05,,09,12,,,24,,,,,2.5,1.3,2.1*39

Where:
GSA Satellite status
A Auto selection of 2D or 3D fix (M = manual)
3 3D fix - values include: 1 = no fix
2 = 2D fix
3 = 3D fix
04,05... PRNs of satellites used for fix (space for 12)
2.5 PDOP (dilution of precision)
1.3 Horizontal dilution of precision (HDOP)
2.1 Vertical dilution of precision (VDOP)
*39 the checksum data, always begins with *
*/
class GPGSA:NMEASentence {
override init?(_ nmeaSentence:String) {
super.init(nmeaSentence)

let splittedSentence = trimmedSentence.split(separator: ",", maxSplits: Int.max, omittingEmptySubsequences: false)

fixSelectionMode = FixSelectionMode(rawValue: Character(String(splittedSentence[1])))
threeDFixMode = FixMode(rawValue: Int(splittedSentence[2]) ?? 0)
for i in 0..<12 {
prn.append(Int(splittedSentence[3+i]))
}
pdop = Float(splittedSentence[15])
hdop = Float(splittedSentence[16])
vdop = Float(splittedSentence[17])

//prn can definitely contail nil values, as less than 12 GPS can be in sight
if [fixSelectionMode, threeDFixMode, hdop, vdop, pdop].atleastOneIsNil() == true {
return nil
}
}

var fixSelectionMode:FixSelectionMode?
var threeDFixMode:FixMode?
var prn = [Int?]()
var pdop:Float?
var hdop:Float?
var vdop:Float?
}



struct Coordinate {
var coordinate:Float?
var direction:Direction?

init?(_ coordinate:Substring, _ direction:Substring) {
self.coordinate = Float(coordinate)
guard self.coordinate != nil else {
return nil
}
self.direction = Direction(String(direction))
guard self.direction != nil else {
return nil
}
}
}

enum FixSelectionMode:Character {
case manual = "M"
case auto = "A"
}

enum FixMode:Int {
case nofix = 1
case twod = 2
case threed = 3
}

enum Direction:Character {
case north = "N"
case south = "S"
case east = "E"
case west = "W"

init?(_ direction:String) {
switch String(direction) {
case "N":
self = .north
case "S":
self = .south
case "E":
self = .east
case "W":
self = .west
default:
return nil
}
}
}

enum FixQuality:Int {
case Invalid = 0
case GPSFixSPS = 1
case DGPSFix = 2
case PPSFix = 3
case RealTimeKinematic = 4
case FloatRTK = 5
case Estimated = 6
case ManualInputMode = 7
case SimulationMode = 8
}
}


Code doesn't cover full NMEA standard yet, but the most important parts are there.



How to use it:



let gpgga = NMEASentenceParser.shared.parse(correctGPGGASentence) as! NMEASentenceParser.GPGGA


Thanks in advance!



BTW: full codes lies here










share|improve this question









New contributor




X99 is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
Check out our Code of Conduct.
























    up vote
    1
    down vote

    favorite












    I'm quite new to Swift, and although I've been programming as a hobby for many years, I'm still not very confident about my code.



    I'm currently rewriting an old library of mine, an NMEA parser. NMEA is a standard for GPS sentences containing position, time, satellites signal power and so on.



    Basically this code receives a string and extracts values from it. For example:



    $GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,*47


    Where "123519" it current time, "4807.038,N,01131.000,E" are latitude/longitude and so on. There are many, many types of NMEA sentences, reference here.



    import Foundation

    typealias Byte = UInt8

    extension UInt8 {
    var hexValue:String {
    return (self < 16 ? "0":"") + String(self, radix:16, uppercase:true)
    }
    }

    /*
    Allows to test against nil multiple variables aton once.
    */
    extension Collection where Element == Optional<Any> {
    func allNotNil() -> Bool {
    return self.compactMap { $0 }.count > self.count
    }

    func atleastOneNotNil() -> Bool {
    return self.compactMap { $0 }.count > 0
    }

    func allNil() -> Bool {
    return self.compactMap { $0 }.count == 0
    }

    func atleastOneIsNil() -> Bool {
    return self.contains { $0 == nil }
    }
    }

    class NMEASentenceParser {
    static let shared = NMEASentenceParser()

    private init() {}

    func parse(_ nmeaSentence:String) -> Any? {
    if nmeaSentence.isEmpty {
    return nil
    }

    if nmeaSentence.hasPrefix("$GPGGA") {
    return GPGGA(nmeaSentence)
    } else if nmeaSentence.hasPrefix("$GPGSA") {
    return GPGSA(nmeaSentence)
    }

    return nil
    }

    //Generic class
    class NMEASentence {
    private var sentence:String
    var trimmedSentence:String

    var isValid:Bool {
    get {
    return sentence.suffix(2) == checksum().hexValue
    }
    }

    func checksum() -> UInt8 {
    var xor:UInt8 = 0
    for i in 0..<trimmedSentence.utf8.count {
    xor = xor ^ Array(trimmedSentence.utf8)[i]
    }
    return xor
    }

    init?(_ nmeaSentence:String) {
    sentence = nmeaSentence

    //duplicate sentence trimmed from its "$" and checksum
    let start = sentence.index(sentence.startIndex, offsetBy: 1)
    let end = sentence.index(sentence.endIndex, offsetBy: -3)
    trimmedSentence = String(sentence[start..<end])

    if isValid == false {
    return nil
    }

    }

    }

    /*
    Source: https://www.gpsinformation.org/dale/nmea.htm
    GGA - essential fix data which provide 3D location and accuracy data.

    $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 number
    *47 the checksum data, always begins with *
    */
    class GPGGA:NMEASentence {
    override init?(_ nmeaSentence:String) {
    //init will fail if
    super.init(nmeaSentence)

    let splittedSentence = trimmedSentence.split(separator: ",", maxSplits: Int.max, omittingEmptySubsequences: false)

    utcTime = Float(splittedSentence[1])
    latitude = Coordinate(splittedSentence[2], splittedSentence[3])
    longitude = Coordinate(splittedSentence[4], splittedSentence[5])
    fixQuality = FixQuality(rawValue: Int(splittedSentence[6]) ?? -1)
    numberOfSatellites = Int(splittedSentence[7])
    horizontalDilutionOfPosition = Float(splittedSentence[8])
    mslAltitude = Float(splittedSentence[9])
    mslAltitudeUnit = String(splittedSentence[10])
    heightOfGeoid = Float(splittedSentence[11])
    heightOfGeoidUnit = String(splittedSentence[10])

    if [utcTime,latitude,longitude,fixQuality,numberOfSatellites,horizontalDilutionOfPosition,mslAltitude,mslAltitudeUnit,heightOfGeoid,heightOfGeoidUnit].atleastOneIsNil() == true {
    return nil
    }
    }
    var utcTime:Float?
    var latitude:Coordinate?
    var longitude:Coordinate?
    var fixQuality:FixQuality?
    var numberOfSatellites:Int?
    var horizontalDilutionOfPosition:Float?
    var mslAltitude:Float?
    var mslAltitudeUnit:String?
    var heightOfGeoid:Float?
    var heightOfGeoidUnit:String?
    }

    /*
    $GPGSA,A,3,04,05,,09,12,,,24,,,,,2.5,1.3,2.1*39

    Where:
    GSA Satellite status
    A Auto selection of 2D or 3D fix (M = manual)
    3 3D fix - values include: 1 = no fix
    2 = 2D fix
    3 = 3D fix
    04,05... PRNs of satellites used for fix (space for 12)
    2.5 PDOP (dilution of precision)
    1.3 Horizontal dilution of precision (HDOP)
    2.1 Vertical dilution of precision (VDOP)
    *39 the checksum data, always begins with *
    */
    class GPGSA:NMEASentence {
    override init?(_ nmeaSentence:String) {
    super.init(nmeaSentence)

    let splittedSentence = trimmedSentence.split(separator: ",", maxSplits: Int.max, omittingEmptySubsequences: false)

    fixSelectionMode = FixSelectionMode(rawValue: Character(String(splittedSentence[1])))
    threeDFixMode = FixMode(rawValue: Int(splittedSentence[2]) ?? 0)
    for i in 0..<12 {
    prn.append(Int(splittedSentence[3+i]))
    }
    pdop = Float(splittedSentence[15])
    hdop = Float(splittedSentence[16])
    vdop = Float(splittedSentence[17])

    //prn can definitely contail nil values, as less than 12 GPS can be in sight
    if [fixSelectionMode, threeDFixMode, hdop, vdop, pdop].atleastOneIsNil() == true {
    return nil
    }
    }

    var fixSelectionMode:FixSelectionMode?
    var threeDFixMode:FixMode?
    var prn = [Int?]()
    var pdop:Float?
    var hdop:Float?
    var vdop:Float?
    }



    struct Coordinate {
    var coordinate:Float?
    var direction:Direction?

    init?(_ coordinate:Substring, _ direction:Substring) {
    self.coordinate = Float(coordinate)
    guard self.coordinate != nil else {
    return nil
    }
    self.direction = Direction(String(direction))
    guard self.direction != nil else {
    return nil
    }
    }
    }

    enum FixSelectionMode:Character {
    case manual = "M"
    case auto = "A"
    }

    enum FixMode:Int {
    case nofix = 1
    case twod = 2
    case threed = 3
    }

    enum Direction:Character {
    case north = "N"
    case south = "S"
    case east = "E"
    case west = "W"

    init?(_ direction:String) {
    switch String(direction) {
    case "N":
    self = .north
    case "S":
    self = .south
    case "E":
    self = .east
    case "W":
    self = .west
    default:
    return nil
    }
    }
    }

    enum FixQuality:Int {
    case Invalid = 0
    case GPSFixSPS = 1
    case DGPSFix = 2
    case PPSFix = 3
    case RealTimeKinematic = 4
    case FloatRTK = 5
    case Estimated = 6
    case ManualInputMode = 7
    case SimulationMode = 8
    }
    }


    Code doesn't cover full NMEA standard yet, but the most important parts are there.



    How to use it:



    let gpgga = NMEASentenceParser.shared.parse(correctGPGGASentence) as! NMEASentenceParser.GPGGA


    Thanks in advance!



    BTW: full codes lies here










    share|improve this question









    New contributor




    X99 is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
    Check out our Code of Conduct.






















      up vote
      1
      down vote

      favorite









      up vote
      1
      down vote

      favorite











      I'm quite new to Swift, and although I've been programming as a hobby for many years, I'm still not very confident about my code.



      I'm currently rewriting an old library of mine, an NMEA parser. NMEA is a standard for GPS sentences containing position, time, satellites signal power and so on.



      Basically this code receives a string and extracts values from it. For example:



      $GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,*47


      Where "123519" it current time, "4807.038,N,01131.000,E" are latitude/longitude and so on. There are many, many types of NMEA sentences, reference here.



      import Foundation

      typealias Byte = UInt8

      extension UInt8 {
      var hexValue:String {
      return (self < 16 ? "0":"") + String(self, radix:16, uppercase:true)
      }
      }

      /*
      Allows to test against nil multiple variables aton once.
      */
      extension Collection where Element == Optional<Any> {
      func allNotNil() -> Bool {
      return self.compactMap { $0 }.count > self.count
      }

      func atleastOneNotNil() -> Bool {
      return self.compactMap { $0 }.count > 0
      }

      func allNil() -> Bool {
      return self.compactMap { $0 }.count == 0
      }

      func atleastOneIsNil() -> Bool {
      return self.contains { $0 == nil }
      }
      }

      class NMEASentenceParser {
      static let shared = NMEASentenceParser()

      private init() {}

      func parse(_ nmeaSentence:String) -> Any? {
      if nmeaSentence.isEmpty {
      return nil
      }

      if nmeaSentence.hasPrefix("$GPGGA") {
      return GPGGA(nmeaSentence)
      } else if nmeaSentence.hasPrefix("$GPGSA") {
      return GPGSA(nmeaSentence)
      }

      return nil
      }

      //Generic class
      class NMEASentence {
      private var sentence:String
      var trimmedSentence:String

      var isValid:Bool {
      get {
      return sentence.suffix(2) == checksum().hexValue
      }
      }

      func checksum() -> UInt8 {
      var xor:UInt8 = 0
      for i in 0..<trimmedSentence.utf8.count {
      xor = xor ^ Array(trimmedSentence.utf8)[i]
      }
      return xor
      }

      init?(_ nmeaSentence:String) {
      sentence = nmeaSentence

      //duplicate sentence trimmed from its "$" and checksum
      let start = sentence.index(sentence.startIndex, offsetBy: 1)
      let end = sentence.index(sentence.endIndex, offsetBy: -3)
      trimmedSentence = String(sentence[start..<end])

      if isValid == false {
      return nil
      }

      }

      }

      /*
      Source: https://www.gpsinformation.org/dale/nmea.htm
      GGA - essential fix data which provide 3D location and accuracy data.

      $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 number
      *47 the checksum data, always begins with *
      */
      class GPGGA:NMEASentence {
      override init?(_ nmeaSentence:String) {
      //init will fail if
      super.init(nmeaSentence)

      let splittedSentence = trimmedSentence.split(separator: ",", maxSplits: Int.max, omittingEmptySubsequences: false)

      utcTime = Float(splittedSentence[1])
      latitude = Coordinate(splittedSentence[2], splittedSentence[3])
      longitude = Coordinate(splittedSentence[4], splittedSentence[5])
      fixQuality = FixQuality(rawValue: Int(splittedSentence[6]) ?? -1)
      numberOfSatellites = Int(splittedSentence[7])
      horizontalDilutionOfPosition = Float(splittedSentence[8])
      mslAltitude = Float(splittedSentence[9])
      mslAltitudeUnit = String(splittedSentence[10])
      heightOfGeoid = Float(splittedSentence[11])
      heightOfGeoidUnit = String(splittedSentence[10])

      if [utcTime,latitude,longitude,fixQuality,numberOfSatellites,horizontalDilutionOfPosition,mslAltitude,mslAltitudeUnit,heightOfGeoid,heightOfGeoidUnit].atleastOneIsNil() == true {
      return nil
      }
      }
      var utcTime:Float?
      var latitude:Coordinate?
      var longitude:Coordinate?
      var fixQuality:FixQuality?
      var numberOfSatellites:Int?
      var horizontalDilutionOfPosition:Float?
      var mslAltitude:Float?
      var mslAltitudeUnit:String?
      var heightOfGeoid:Float?
      var heightOfGeoidUnit:String?
      }

      /*
      $GPGSA,A,3,04,05,,09,12,,,24,,,,,2.5,1.3,2.1*39

      Where:
      GSA Satellite status
      A Auto selection of 2D or 3D fix (M = manual)
      3 3D fix - values include: 1 = no fix
      2 = 2D fix
      3 = 3D fix
      04,05... PRNs of satellites used for fix (space for 12)
      2.5 PDOP (dilution of precision)
      1.3 Horizontal dilution of precision (HDOP)
      2.1 Vertical dilution of precision (VDOP)
      *39 the checksum data, always begins with *
      */
      class GPGSA:NMEASentence {
      override init?(_ nmeaSentence:String) {
      super.init(nmeaSentence)

      let splittedSentence = trimmedSentence.split(separator: ",", maxSplits: Int.max, omittingEmptySubsequences: false)

      fixSelectionMode = FixSelectionMode(rawValue: Character(String(splittedSentence[1])))
      threeDFixMode = FixMode(rawValue: Int(splittedSentence[2]) ?? 0)
      for i in 0..<12 {
      prn.append(Int(splittedSentence[3+i]))
      }
      pdop = Float(splittedSentence[15])
      hdop = Float(splittedSentence[16])
      vdop = Float(splittedSentence[17])

      //prn can definitely contail nil values, as less than 12 GPS can be in sight
      if [fixSelectionMode, threeDFixMode, hdop, vdop, pdop].atleastOneIsNil() == true {
      return nil
      }
      }

      var fixSelectionMode:FixSelectionMode?
      var threeDFixMode:FixMode?
      var prn = [Int?]()
      var pdop:Float?
      var hdop:Float?
      var vdop:Float?
      }



      struct Coordinate {
      var coordinate:Float?
      var direction:Direction?

      init?(_ coordinate:Substring, _ direction:Substring) {
      self.coordinate = Float(coordinate)
      guard self.coordinate != nil else {
      return nil
      }
      self.direction = Direction(String(direction))
      guard self.direction != nil else {
      return nil
      }
      }
      }

      enum FixSelectionMode:Character {
      case manual = "M"
      case auto = "A"
      }

      enum FixMode:Int {
      case nofix = 1
      case twod = 2
      case threed = 3
      }

      enum Direction:Character {
      case north = "N"
      case south = "S"
      case east = "E"
      case west = "W"

      init?(_ direction:String) {
      switch String(direction) {
      case "N":
      self = .north
      case "S":
      self = .south
      case "E":
      self = .east
      case "W":
      self = .west
      default:
      return nil
      }
      }
      }

      enum FixQuality:Int {
      case Invalid = 0
      case GPSFixSPS = 1
      case DGPSFix = 2
      case PPSFix = 3
      case RealTimeKinematic = 4
      case FloatRTK = 5
      case Estimated = 6
      case ManualInputMode = 7
      case SimulationMode = 8
      }
      }


      Code doesn't cover full NMEA standard yet, but the most important parts are there.



      How to use it:



      let gpgga = NMEASentenceParser.shared.parse(correctGPGGASentence) as! NMEASentenceParser.GPGGA


      Thanks in advance!



      BTW: full codes lies here










      share|improve this question









      New contributor




      X99 is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
      Check out our Code of Conduct.











      I'm quite new to Swift, and although I've been programming as a hobby for many years, I'm still not very confident about my code.



      I'm currently rewriting an old library of mine, an NMEA parser. NMEA is a standard for GPS sentences containing position, time, satellites signal power and so on.



      Basically this code receives a string and extracts values from it. For example:



      $GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,*47


      Where "123519" it current time, "4807.038,N,01131.000,E" are latitude/longitude and so on. There are many, many types of NMEA sentences, reference here.



      import Foundation

      typealias Byte = UInt8

      extension UInt8 {
      var hexValue:String {
      return (self < 16 ? "0":"") + String(self, radix:16, uppercase:true)
      }
      }

      /*
      Allows to test against nil multiple variables aton once.
      */
      extension Collection where Element == Optional<Any> {
      func allNotNil() -> Bool {
      return self.compactMap { $0 }.count > self.count
      }

      func atleastOneNotNil() -> Bool {
      return self.compactMap { $0 }.count > 0
      }

      func allNil() -> Bool {
      return self.compactMap { $0 }.count == 0
      }

      func atleastOneIsNil() -> Bool {
      return self.contains { $0 == nil }
      }
      }

      class NMEASentenceParser {
      static let shared = NMEASentenceParser()

      private init() {}

      func parse(_ nmeaSentence:String) -> Any? {
      if nmeaSentence.isEmpty {
      return nil
      }

      if nmeaSentence.hasPrefix("$GPGGA") {
      return GPGGA(nmeaSentence)
      } else if nmeaSentence.hasPrefix("$GPGSA") {
      return GPGSA(nmeaSentence)
      }

      return nil
      }

      //Generic class
      class NMEASentence {
      private var sentence:String
      var trimmedSentence:String

      var isValid:Bool {
      get {
      return sentence.suffix(2) == checksum().hexValue
      }
      }

      func checksum() -> UInt8 {
      var xor:UInt8 = 0
      for i in 0..<trimmedSentence.utf8.count {
      xor = xor ^ Array(trimmedSentence.utf8)[i]
      }
      return xor
      }

      init?(_ nmeaSentence:String) {
      sentence = nmeaSentence

      //duplicate sentence trimmed from its "$" and checksum
      let start = sentence.index(sentence.startIndex, offsetBy: 1)
      let end = sentence.index(sentence.endIndex, offsetBy: -3)
      trimmedSentence = String(sentence[start..<end])

      if isValid == false {
      return nil
      }

      }

      }

      /*
      Source: https://www.gpsinformation.org/dale/nmea.htm
      GGA - essential fix data which provide 3D location and accuracy data.

      $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 number
      *47 the checksum data, always begins with *
      */
      class GPGGA:NMEASentence {
      override init?(_ nmeaSentence:String) {
      //init will fail if
      super.init(nmeaSentence)

      let splittedSentence = trimmedSentence.split(separator: ",", maxSplits: Int.max, omittingEmptySubsequences: false)

      utcTime = Float(splittedSentence[1])
      latitude = Coordinate(splittedSentence[2], splittedSentence[3])
      longitude = Coordinate(splittedSentence[4], splittedSentence[5])
      fixQuality = FixQuality(rawValue: Int(splittedSentence[6]) ?? -1)
      numberOfSatellites = Int(splittedSentence[7])
      horizontalDilutionOfPosition = Float(splittedSentence[8])
      mslAltitude = Float(splittedSentence[9])
      mslAltitudeUnit = String(splittedSentence[10])
      heightOfGeoid = Float(splittedSentence[11])
      heightOfGeoidUnit = String(splittedSentence[10])

      if [utcTime,latitude,longitude,fixQuality,numberOfSatellites,horizontalDilutionOfPosition,mslAltitude,mslAltitudeUnit,heightOfGeoid,heightOfGeoidUnit].atleastOneIsNil() == true {
      return nil
      }
      }
      var utcTime:Float?
      var latitude:Coordinate?
      var longitude:Coordinate?
      var fixQuality:FixQuality?
      var numberOfSatellites:Int?
      var horizontalDilutionOfPosition:Float?
      var mslAltitude:Float?
      var mslAltitudeUnit:String?
      var heightOfGeoid:Float?
      var heightOfGeoidUnit:String?
      }

      /*
      $GPGSA,A,3,04,05,,09,12,,,24,,,,,2.5,1.3,2.1*39

      Where:
      GSA Satellite status
      A Auto selection of 2D or 3D fix (M = manual)
      3 3D fix - values include: 1 = no fix
      2 = 2D fix
      3 = 3D fix
      04,05... PRNs of satellites used for fix (space for 12)
      2.5 PDOP (dilution of precision)
      1.3 Horizontal dilution of precision (HDOP)
      2.1 Vertical dilution of precision (VDOP)
      *39 the checksum data, always begins with *
      */
      class GPGSA:NMEASentence {
      override init?(_ nmeaSentence:String) {
      super.init(nmeaSentence)

      let splittedSentence = trimmedSentence.split(separator: ",", maxSplits: Int.max, omittingEmptySubsequences: false)

      fixSelectionMode = FixSelectionMode(rawValue: Character(String(splittedSentence[1])))
      threeDFixMode = FixMode(rawValue: Int(splittedSentence[2]) ?? 0)
      for i in 0..<12 {
      prn.append(Int(splittedSentence[3+i]))
      }
      pdop = Float(splittedSentence[15])
      hdop = Float(splittedSentence[16])
      vdop = Float(splittedSentence[17])

      //prn can definitely contail nil values, as less than 12 GPS can be in sight
      if [fixSelectionMode, threeDFixMode, hdop, vdop, pdop].atleastOneIsNil() == true {
      return nil
      }
      }

      var fixSelectionMode:FixSelectionMode?
      var threeDFixMode:FixMode?
      var prn = [Int?]()
      var pdop:Float?
      var hdop:Float?
      var vdop:Float?
      }



      struct Coordinate {
      var coordinate:Float?
      var direction:Direction?

      init?(_ coordinate:Substring, _ direction:Substring) {
      self.coordinate = Float(coordinate)
      guard self.coordinate != nil else {
      return nil
      }
      self.direction = Direction(String(direction))
      guard self.direction != nil else {
      return nil
      }
      }
      }

      enum FixSelectionMode:Character {
      case manual = "M"
      case auto = "A"
      }

      enum FixMode:Int {
      case nofix = 1
      case twod = 2
      case threed = 3
      }

      enum Direction:Character {
      case north = "N"
      case south = "S"
      case east = "E"
      case west = "W"

      init?(_ direction:String) {
      switch String(direction) {
      case "N":
      self = .north
      case "S":
      self = .south
      case "E":
      self = .east
      case "W":
      self = .west
      default:
      return nil
      }
      }
      }

      enum FixQuality:Int {
      case Invalid = 0
      case GPSFixSPS = 1
      case DGPSFix = 2
      case PPSFix = 3
      case RealTimeKinematic = 4
      case FloatRTK = 5
      case Estimated = 6
      case ManualInputMode = 7
      case SimulationMode = 8
      }
      }


      Code doesn't cover full NMEA standard yet, but the most important parts are there.



      How to use it:



      let gpgga = NMEASentenceParser.shared.parse(correctGPGGASentence) as! NMEASentenceParser.GPGGA


      Thanks in advance!



      BTW: full codes lies here







      parsing swift






      share|improve this question









      New contributor




      X99 is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
      Check out our Code of Conduct.











      share|improve this question









      New contributor




      X99 is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
      Check out our Code of Conduct.









      share|improve this question




      share|improve this question








      edited Nov 15 at 17:30





















      New contributor




      X99 is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
      Check out our Code of Conduct.









      asked Nov 15 at 9:00









      X99

      62




      62




      New contributor




      X99 is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
      Check out our Code of Conduct.





      New contributor





      X99 is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
      Check out our Code of Conduct.






      X99 is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
      Check out our Code of Conduct.



























          active

          oldest

          votes











          Your Answer





          StackExchange.ifUsing("editor", function () {
          return StackExchange.using("mathjaxEditing", function () {
          StackExchange.MarkdownEditor.creationCallbacks.add(function (editor, postfix) {
          StackExchange.mathjaxEditing.prepareWmdForMathJax(editor, postfix, [["\$", "\$"]]);
          });
          });
          }, "mathjax-editing");

          StackExchange.ifUsing("editor", function () {
          StackExchange.using("externalEditor", function () {
          StackExchange.using("snippets", function () {
          StackExchange.snippets.init();
          });
          });
          }, "code-snippets");

          StackExchange.ready(function() {
          var channelOptions = {
          tags: "".split(" "),
          id: "196"
          };
          initTagRenderer("".split(" "), "".split(" "), channelOptions);

          StackExchange.using("externalEditor", function() {
          // Have to fire editor after snippets, if snippets enabled
          if (StackExchange.settings.snippets.snippetsEnabled) {
          StackExchange.using("snippets", function() {
          createEditor();
          });
          }
          else {
          createEditor();
          }
          });

          function createEditor() {
          StackExchange.prepareEditor({
          heartbeatType: 'answer',
          convertImagesToLinks: false,
          noModals: true,
          showLowRepImageUploadWarning: true,
          reputationToPostImages: null,
          bindNavPrevention: true,
          postfix: "",
          imageUploader: {
          brandingHtml: "Powered by u003ca class="icon-imgur-white" href="https://imgur.com/"u003eu003c/au003e",
          contentPolicyHtml: "User contributions licensed under u003ca href="https://creativecommons.org/licenses/by-sa/3.0/"u003ecc by-sa 3.0 with attribution requiredu003c/au003e u003ca href="https://stackoverflow.com/legal/content-policy"u003e(content policy)u003c/au003e",
          allowUrls: true
          },
          onDemand: true,
          discardSelector: ".discard-answer"
          ,immediatelyShowMarkdownHelp:true
          });


          }
          });






          X99 is a new contributor. Be nice, and check out our Code of Conduct.










           

          draft saved


          draft discarded


















          StackExchange.ready(
          function () {
          StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fcodereview.stackexchange.com%2fquestions%2f207706%2fswift-nmea-parser%23new-answer', 'question_page');
          }
          );

          Post as a guest















          Required, but never shown






























          active

          oldest

          votes













          active

          oldest

          votes









          active

          oldest

          votes






          active

          oldest

          votes








          X99 is a new contributor. Be nice, and check out our Code of Conduct.










           

          draft saved


          draft discarded


















          X99 is a new contributor. Be nice, and check out our Code of Conduct.













          X99 is a new contributor. Be nice, and check out our Code of Conduct.












          X99 is a new contributor. Be nice, and check out our Code of Conduct.















           


          draft saved


          draft discarded














          StackExchange.ready(
          function () {
          StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fcodereview.stackexchange.com%2fquestions%2f207706%2fswift-nmea-parser%23new-answer', 'question_page');
          }
          );

          Post as a guest















          Required, but never shown





















































          Required, but never shown














          Required, but never shown












          Required, but never shown







          Required, but never shown

































          Required, but never shown














          Required, but never shown












          Required, but never shown







          Required, but never shown







          Popular posts from this blog

          Morgemoulin

          Scott Moir

          Souastre