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
Solved! Go to Solution.
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.
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!
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
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
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
}
}