Select to view content in your preferred language

How to use external gps location data

15033
34
Jump to solution
05-03-2018 10:02 PM
ShiminCai
Frequent Contributor

Hi All,

I have a new requirement for our apps to use external gps location updates (Bad Elf GPS Unit) besides the default Apple location service updates. I understood that I need to create a custom location display data source class that conforms to the protocol <AGSLocationDisplayDataSource> and replace the mapVidw.locationDisplay.dataSource. My question is that: how do I implement the start method in the  protocol of <AGSLocationDisplayDataSource>? I'm currently still on 10.2.5 and later will look at the 100.2.1 as well. Can anyone please shed any light on this or have any working code to share? Many thanks for your help.

Cheers,

Shimin

34 Replies
Ting
by Esri Contributor
Esri Contributor

Hey Duane, would you mind me moving your question to the Swift board? The problem might lies in how an external Bluetooth accessory behaves when the app that reads from it goes into background.

For the built in GPS sensor, it has allowsBackgroundLocationUpdates Boolean to keep it alive in the background, so you can even receive updates while the app is backgrounded. For external receivers, things are more complicated, but at least we want to resume receiving updates once the app comes live.

My guess is that the NMEALocationDataSource object is not handling the disconnect/reconnect to the receiver correctly. I'll need to do some field testing to check that.

0 Kudos
DuanePfeiffer
Regular Contributor

This has been resolved.  I switched from 'when in use' to 'always' have access to location.  I was also stopping and starting the location display's data source when app was in the background.  It turns out, stopping/starting breaks the stream somehow.  When I just left it running and location access = 'always', it is working now.  Thanks!

demetakyol
New Contributor

Shimin Cai

Hi,
I am trying to get RRE and GST data from the device, but I cannot find resources about ExternalAccessory. Could you help ? Can I do without using AGSLocationDisplayDataSource. Can you show the place you use on the map? Also how does NmeaParser parse?

I would really appreciate if you can help Shimin Cai‌, Muruganandham Kuppan

Thanks

0 Kudos
JoeHershman
MVP Regular Contributor

You need the LocationDataSource if you want to use the features provided by the API for location tracking.

In terms of the ExternalAccessory have you included the device(s) in info.plist?

<key>UISupportedExternalAccessoryProtocols</key>
<array>
	<string>com.geneq.sxbluegps</string>
	<string>com.geneq.sxbluegpssource</string>
	<string>com.geneq.sxbluegpsstatus</string>
</array>‍‍‍‍‍‍

I wrote a blog post about setting this up in a Xamarin Forms application.  Not sure if that would be useful for someone doing in iOS:

https://community.esri.com/people/minerjoe/blog/2019/08/23/using-external-gps-from-xamarin-forms-ios 

Thanks,
-Joe
0 Kudos
ShiminCai
Frequent Contributor

demet akyol‌,

Sorry I've been away...

Your external GPS device needs to be supported by Apple (MFi program) and the manufacturer should provide the so called protocol string(s) which needs to be included in your app's info.plist, as Joe pointed out. Then once the device is peered up with an iPhone or iPad using Blue tooth or a cable, your app using Apple ExternalAccesory framework should be able to connect to the GPS device and receive NMEA string data from it. Please refer to the code I posted in this thread previously.

NMEA sentence is a comma delimited string data. I only studied and parsed the GGA, GSA and RMC sentences for location data. The following is the GGA sentence as an example.

Hope it helps.

Shimin

import Foundation

public class GgaSentence: NmeaSentence

{

    var rawSentence: [String]

    

    /// GGA is defined as following:

    /// ```

    /// $GPGGA,012506.50,3342.49540,S,15055.45495,E,1,06,1.53,68.9,M, 19.7,M,    ,       *7A

    /// $GPGGA,hhmmss.ss,llll.lllll,a,yyyyy.yyyyy,a,x,xx,x.xx,x.xx,M, xx.x,M, x.x,xxxx*hh

    /// 0      1         2          3 4           5 6 7  8    9    10 11   12 13  14   15

    /// ```

    ///

    /// 0 TYPE: The type of NMEA data, e.g. $GPRMC, $GPGGA, $GPGSA, $GPGSV

    /// 1 TIME: The time of the NMEA data (UTC)

    /// 2 LATITUDE: The latitude

    /// 3 LATITUDEDIR: The latitude direction N or S

    /// 4 LONGITUDE: The longitude

    /// 5 LONGITUDEDIR: The longitude direction E or W

    /// 6 GPSFIXQUALITY: GPS fix quality: 0=fix not available, 1=GPS fix, 2=DGPS fix, 3=PPS fix, 4=RTK, 5=Float RTK, 6=estimated

    ///                  7=Manual inputmode, 8=Simulation mode

    /// 7 NUMBEROFSATELLITEINVIEW: Number of satellites in view 00 - 12

    /// 8 HDOP: Horizontal dilution of precision

    /// 9 ALTITUDE: Altitude above/below mean-sea-leval (geoid)

    /// 10 ALTITUDEUNIT: Altitude unit, meters

    /// 11 GEOIDALSEPARATION: The difference between the WGS-84 earth ellipsoid and mean sea level (geoid), "-" means mean sea level

    ///    below ellipsoid. The height of geoid above WGS84 earth ellipsoid.

    /// 12 GEOIDALSEPARATIONUNIT: Units of geoidal separation, meters

    /// 13 TIMESINCELASTDGPSUPDATE: Time in seconds since last differential GPS update from differential reference station

    /// 14 DIFFERENTIALREFERENCESTATIONID: the differential reference station id.

    /// 15 CHECKSUM: a checksum

    

    enum Param: Int

    {

        case TYPE = 0

        case TIME = 1

        case LATITUDE = 2

        case LATITUDEDIR = 3

        case LONGITUDE = 4

        case LONGITUDEDIR = 5

        case FIXTYPE = 6

        case NUMBEROFSATELLITESINVIEW = 7

        case HDOP = 8

        case ALTITUDE = 9

        case ALTITUDEUNIT = 10

        case GEOIDALSEPARATION = 11

        case GEOIDALSEPARATIONUNIT = 12

        case TIMESINCELASTDGPSUPDATE = 13

        case DIFFERENTIALREFERENCESTATIONID = 14 //the id is with the checksum

    }

    required public init(rawSentence: [String])

    {

        self.rawSentence = rawSentence

    }

    

    func type() -> String

    {

        return "$GPGGA"

    }

    

    func parse() -> AnyObject?

    {

        let splittedString = self.rawSentence

        

        //the original $GPGGA string might be segmented therefore have to treat it differently. Not sure why...

        //the full sentence should have 15 items but found there are cases of 10 and 11 items.

        var rawType: String!

        var rawTime: String!

        var rawLatitude: String!

        var rawLatitudeDir: String!

        var rawLongitude: String!

        var rawLongitudeDir: String!

        var rawFixType: String!

        var rawNumberOfSatelliesInView: String!

        var rawHDOP: String!

        var rawAltitude: String!

        var rawAltitudeUnit: String!

        var rawGeoidalSeparation: String!

        var rawGeoidalSeparationUnit: String!

        var rawTimeSinceLastDGPSUpdate: String!

        var rawDifferentialReferenceStationID: String!

        

        let count = splittedString.count

        

        //print(count)

        

        if count == 10

        {

            rawType = splittedString[GgaSentence.Param.TYPE.rawValue]

            rawTime = splittedString[GgaSentence.Param.TIME.rawValue]

            rawLatitude = splittedString[GgaSentence.Param.LATITUDE.rawValue]

            rawLatitudeDir = splittedString[GgaSentence.Param.LATITUDEDIR.rawValue]

            rawLongitude = splittedString[GgaSentence.Param.LONGITUDE.rawValue]

            rawLongitudeDir = splittedString[GgaSentence.Param.LONGITUDEDIR.rawValue]

            rawFixType = splittedString[GgaSentence.Param.FIXTYPE.rawValue]

            rawNumberOfSatelliesInView = splittedString[GgaSentence.Param.NUMBEROFSATELLITESINVIEW.rawValue]

            rawHDOP = splittedString[GgaSentence.Param.HDOP.rawValue]

            rawAltitude = splittedString[GgaSentence.Param.ALTITUDE.rawValue]

        }

        else if count == 11

        {

            rawType = splittedString[GgaSentence.Param.TYPE.rawValue]

            rawTime = splittedString[GgaSentence.Param.TIME.rawValue]

            rawLatitude = splittedString[GgaSentence.Param.LATITUDE.rawValue]

            rawLatitudeDir = splittedString[GgaSentence.Param.LATITUDEDIR.rawValue]

            rawLongitude = splittedString[GgaSentence.Param.LONGITUDE.rawValue]

            rawLongitudeDir = splittedString[GgaSentence.Param.LONGITUDEDIR.rawValue]

            rawFixType = splittedString[GgaSentence.Param.FIXTYPE.rawValue]

            rawNumberOfSatelliesInView = splittedString[GgaSentence.Param.NUMBEROFSATELLITESINVIEW.rawValue]

            rawHDOP = splittedString[GgaSentence.Param.HDOP.rawValue]

            rawAltitude = splittedString[GgaSentence.Param.ALTITUDE.rawValue]

            rawAltitudeUnit = splittedString[GgaSentence.Param.ALTITUDEUNIT.rawValue]

        }

        else if count == 15

        {

            rawType = splittedString[GgaSentence.Param.TYPE.rawValue]

            rawTime = splittedString[GgaSentence.Param.TIME.rawValue]

            rawLatitude = splittedString[GgaSentence.Param.LATITUDE.rawValue]

            rawLatitudeDir = splittedString[GgaSentence.Param.LATITUDEDIR.rawValue]

            rawLongitude = splittedString[GgaSentence.Param.LONGITUDE.rawValue]

            rawLongitudeDir = splittedString[GgaSentence.Param.LONGITUDEDIR.rawValue]

            rawFixType = splittedString[GgaSentence.Param.FIXTYPE.rawValue]

            rawNumberOfSatelliesInView = splittedString[GgaSentence.Param.NUMBEROFSATELLITESINVIEW.rawValue]

            rawHDOP = splittedString[GgaSentence.Param.HDOP.rawValue]

            rawAltitude = splittedString[GgaSentence.Param.ALTITUDE.rawValue]

            rawAltitudeUnit = splittedString[GgaSentence.Param.ALTITUDEUNIT.rawValue]

            rawGeoidalSeparation = splittedString[GgaSentence.Param.GEOIDALSEPARATION.rawValue]

            rawGeoidalSeparationUnit = splittedString[GgaSentence.Param.GEOIDALSEPARATIONUNIT.rawValue]

            rawTimeSinceLastDGPSUpdate = splittedString[GgaSentence.Param.TIMESINCELASTDGPSUPDATE.rawValue]

            rawDifferentialReferenceStationID = splittedString[GgaSentence.Param.DIFFERENTIALREFERENCESTATIONID.rawValue]

        }

        else

        {

            return nil //invalid $GPGGA string

        }

        

        if rawLatitude == nil || rawLatitude.isEmpty || rawLongitude == nil || rawLongitude.isEmpty

        {

            return nil //no locaiton info

        }

        

        let ggaData = GgaData()

        ggaData.type = rawType

        

        let dateFormatter = DateFormatter()

        //dateFormatter.timeZone = TimeZone(identifier: "GMT")

        dateFormatter.dateFormat = "hhmmss.SSS"

        if let tempTime = dateFormatter.date(from: rawTime)

        {

            ggaData.time = tempTime

        }

        

        ggaData.latitude = rawLatitude

        ggaData.latitudeDir = rawLatitudeDir

        ggaData.longitude = rawLongitude

        ggaData.longitudeDir = rawLongitudeDir

        

        if rawFixType != nil && !rawFixType.isEmpty, let fixType = Int(rawFixType)

        {

            switch fixType

            {

                case 0:

                    ggaData.fixType  = "Fix Not Available"

                    break

                case 1:

                    ggaData.fixType  = "GPS Fix"

                    break

                case 2:

                    ggaData.fixType  = "DGPS Fix"

                    break

                case 3:

                    ggaData.fixType  = "PPS Fix"

                    break

                case 4:

                    ggaData.fixType  = "RTK"

                    break

                case 5:

                    ggaData.fixType  = "Float RTK"

                    break

                case 6:

                    ggaData.fixType  = "Estimated"

                    break

                case 7:

                    ggaData.fixType  = "Manual Input Mode"

                    break

                case 8:

                    ggaData.fixType  = "Simulation Mode"

                    break

                default:

                    break

            }

        }

        ggaData.numberOfSatellitesInView = rawNumberOfSatelliesInView

        ggaData.hdop = rawHDOP

        ggaData.altitude = rawAltitude

        ggaData.altitudeUnit = rawAltitudeUnit

        ggaData.altitudeUnit = rawAltitudeUnit

        ggaData.geoidalSeparation = rawGeoidalSeparation

        ggaData.geoidalSeparationUnit = rawGeoidalSeparationUnit

        ggaData.timeSinceLastDGPSUpdate = rawTimeSinceLastDGPSUpdate

        

        if rawDifferentialReferenceStationID != nil && !rawDifferentialReferenceStationID.isEmpty

        {

            let starIndex = rawDifferentialReferenceStationID.index(of: "*")

            

            if starIndex != nil

            {

                ggaData.differentialReferenceStationID = String(rawDifferentialReferenceStationID.prefix(upTo: starIndex!))

            }

        }

        

        return ggaData

    }

}

0 Kudos