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
HI Shimin Cai,
If you're still looking to implement your own data source, look at the reference doc here.
Here's what you need to do.
Implement a class that inherits from AGSLocationDataSource. The key functions you need to implement are doStart() and doStop(). These are the entry points to your class.
<< Runtime communicating to your custom location data source:
>> Providing feedback to Runtime during startup/shutdown:
>> Providing location updates to Runtime:
Hope this helps.
Nick.
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.
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,xyz,x.xyz,x.xyz,M, xyz.x,M, x.x,xyz*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
rawAltitudeUnit = splittedString[GgaSentence.Param.ALTITUDEUNIT.rawValue]
else if count == 15
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"
case 2:
ggaData.fixType = "DGPS Fix"
case 3:
ggaData.fixType = "PPS Fix"
case 4:
ggaData.fixType = "RTK"
case 5:
ggaData.fixType = "Float RTK"
case 6:
ggaData.fixType = "Estimated"
case 7:
ggaData.fixType = "Manual Input Mode"
case 8:
ggaData.fixType = "Simulation Mode"
default:
ggaData.numberOfSatellitesInView = rawNumberOfSatelliesInView
ggaData.hdop = rawHDOP
ggaData.altitude = rawAltitude
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
I've since written a series of blog posts that delve into working with Location Display and Location Data Sources:
They discuss the blue dot, how Runtime works with it, and creating custom location data sources.
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?
<SPAN class="operator token"><</SPAN><SPAN class="keyword token">key</SPAN><SPAN class="operator token">></SPAN>UISupportedExternalAccessoryProtocols<SPAN class="operator token"><</SPAN><SPAN class="operator token">/</SPAN><SPAN class="keyword token">key</SPAN><SPAN class="operator token">></SPAN> <SPAN class="operator token"><</SPAN>array<SPAN class="operator token">></SPAN> <SPAN class="operator token"><</SPAN>string<SPAN class="operator token">></SPAN>com<SPAN class="punctuation token">.</SPAN>geneq<SPAN class="punctuation token">.</SPAN>sxbluegps<SPAN class="operator token"><</SPAN><SPAN class="operator token">/</SPAN>string<SPAN class="operator token">></SPAN> <SPAN class="operator token"><</SPAN>string<SPAN class="operator token">></SPAN>com<SPAN class="punctuation token">.</SPAN>geneq<SPAN class="punctuation token">.</SPAN>sxbluegpssource<SPAN class="operator token"><</SPAN><SPAN class="operator token">/</SPAN>string<SPAN class="operator token">></SPAN> <SPAN class="operator token"><</SPAN>string<SPAN class="operator token">></SPAN>com<SPAN class="punctuation token">.</SPAN>geneq<SPAN class="punctuation token">.</SPAN>sxbluegpsstatus<SPAN class="operator token"><</SPAN><SPAN class="operator token">/</SPAN>string<SPAN class="operator token">></SPAN> <SPAN class="operator token"><</SPAN><SPAN class="operator token">/</SPAN>array<SPAN class="operator token">></SPAN><SPAN class="line-numbers-rows"><SPAN></SPAN><SPAN></SPAN><SPAN></SPAN><SPAN></SPAN><SPAN></SPAN><SPAN></SPAN></SPAN>
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
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
Nicholas Furness Ignore my previous reply, i found the solution to work, thanks
You could record a journey to a GPX file using GPS tracking software of your choice and then test your Runtime app using the AGSGPXLocationDataSource. Create a new instance of that pointing at your GPX file, then set your mapView.locationDisplay.dataSource to this new instance.
Hi Divesh Goyal,
What is the actual process to test the Navigation in either device or simulator when we are in development mode using ArcGIS Runtime SDK, like it is not possible to debug when we only really travel to get new AGSLocation Data so rather than doing that, is there any options to test the static points like Source to Destination with realtime Testing and Debugging when we are developing the app. Would be appreciate if you could suggest me on this. Meanwhile the question is how to test and debug the app in Device/Simulator on realtime without traveling and it was not so feasible to debug when we are traveling . So basically we can set the Source and Destination Route Stops in a JSON file format or something like that once the map loaded it will start navigating the app based on the JSON Route stops so that we can able to test for the re-routing and realtime navigation to found dead code or faulty/bugs to fix it rather than a Real Test.
Thanks Muruganandham Kuppan. Great to hear. Could you mark your original question as answered please? It'll help other people home in on the right answer.
Hi @Nick, Its working for me.. thanks
Hi Nick,
Thanks for your reply. NMEA Parser is not a problem. I wrote a simple one and it works fine.
But I'm having trouble making the custom datasource to work and hoping you can help me out. Here is my simple implementation of the custom datasource for trying it out. Sorry I'm still on 10.2.5.
import UIKit
import ExternalAccessory
class FCNSWGPSLocationDataSource: NSObject, AGSLocationDisplayDataSource
var delegate: AGSLocationDisplayDataSourceDelegate!
var error: Error!
var isStarted = false
var sessionController: SessionController!
var accessory: EAAccessory?
required public init(sessionController: SessionController)
self.sessionController = sessionController
self.accessory = sessionController._accessory
func start()
NotificationCenter.default.addObserver(self, selector: #selector(sessionDataReceived), name: NSNotification.Name(rawValue: "EXGPSSessionDataReceivedNotification"), object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(accessoryDidDisconnect), name: NSNotification.Name.EAAccessoryDidDisconnect, object: nil)
let sessionOpened = self.sessionController.openSession()
self.isStarted = sessionOpened
if sessionOpened
self.delegate.locationDisplayDataSourceStarted(self)
self.error = NSError(domain: "", code: -1, userInfo: [NSLocalizedDescriptionKey: "Failed to open an EA session."])
self.delegate.locationDisplayDataSource(self, didFailWithError: self.error)
func stop()
NotificationCenter.default.removeObserver(self, name: NSNotification.Name(rawValue: "EXGPSSessionDataReceivedNotification"), object: nil)
NotificationCenter.default.removeObserver(self, name: NSNotification.Name.EAAccessoryDidDisconnect, object: nil)
self.sessionController.closeSession()
self.isStarted = false
self.delegate.locationDisplayDataSourceStopped(self)
// MARK: - Session Updates
func sessionDataReceived(_ notification: Notification)
if sessionController._dataAsString != nil
let rawNMEAString = sessionController._dataAsString!
let nmeaSentences = rawNMEAString.components(separatedBy: "")
var rmcData: RmcData!
var ggaData: GgaData!
var gsaData: GsaData!
for nmeaSentence in nmeaSentences
if let nmeaData = NmeaParser.parseSentence(data: nmeaSentence)
if nmeaData.isKind(of: RmcData.self)
rmcData = nmeaData as! RmcData
else if nmeaData.isKind(of: GgaData.self)
ggaData = nmeaData as! GgaData
else if nmeaData.isKind(of: GsaData.self)
gsaData = nmeaData as! GsaData
if rmcData != nil
let coordinate = CLLocationCoordinate2D(latitude: rmcData.latitude, longitude: rmcData.longitude)
//these are just for testing purposes and the real data should come from the GGA and GSA sentences
let altitude = CLLocationDistance(0)
let horizontalAccuracy = CLLocationAccuracy(0)
let verticalAccuracy = CLLocationAccuracy(0)
let clLocation = CLLocation(coordinate: coordinate,
altitude: altitude,
horizontalAccuracy: horizontalAccuracy,
verticalAccuracy: verticalAccuracy,
course: rmcData.course,
speed: rmcData.speed,
timestamp: rmcData.timeStamp)
let agsLocation = AGSLocation(clLocation: clLocation)
self.delegate.locationDisplayDataSource(self, didUpdateWith: agsLocation)
self.delegate.locationDisplayDataSource(self, didUpdateWithHeading: rmcData.course)
// MARK: - EAAccessory Disconnection
func accessoryDidDisconnect(_ notification: Notification)
let disconnectedAccessory = notification.userInfo![EAAccessoryKey]
if (disconnectedAccessory as AnyObject).connectionID == accessory?.connectionID
In the above codes the sessionController is from Bad Elf sample app which communicates with the connected accessory to retrieve nmea string data. I think this bit works fine as I can get valid nmea sentences as shown below and the latitude and longitude creating the agsLocation object are correct:
I create an instance of my custom datasource class and set the mapView.locationDisplay.dataSource with the instance. Then after the map fully loads, if I try to invoke the mapView.locationDisplay.startDataSource(), the map always zooms to the centre of the map and displays the gps symbol there, not the expected current location. I checked the mapView.locationDisplay.location and mapLocation() and they are all nil. Obviously the location update did not get to pass through to the location display. What did I do wrong? By the way, Collector does the correct thing with the same gps device!
Thank you very much for your help.
Hey Shimin Cai,
Collector does indeed take a similar approach.
For inspiration you could look at this C# NMEA Parser. You would have to translate from C# to Swift but there might be some useful info for you there in building your Location Data Source.
Thanks a lot for your reply. Yes I will be implementing the custom location display datasource. I figured out what to do after reading the relevant docs in the 100.2.1 which provide clearer explanations about the custom datasource than that in the 10.2.5. Your instructions here verified what I'm thinking to do and are much appreciated.
I also had a look at the external gps receiver support of the Collector app. Basically we would like to achieve the similar functionalities in our apps. Did Collector implement the support in a similar way or do you have any suggestion?
I'm currently looking at the gps hexadecimal string data/NMEA sentences. I haven't found a complete NMEA parser in Swift in the net and had the feeling I have to write it myself... Any advises please?
Thanks,
Hi Divesh,
Just found out that our users have already tried what you suggested pairing Bad Elf GPS receivers and their devices and had the problem of iOS switching location feeds from external GPS receiver and the in-built sensor randomly. They want to be able to control the source of location feeds. Looks like the custom location datasource is the way for me to go...
Thank you very much for your reply. I'm getting a Bad Elf GPS Unit and will be testing that out.
The Bad Elf GPS has its own sdk/api to pull so much info from the receiver. I think soon or later I will be asked to read other extended infos from the receiver and in this case I will have to implement a custom location datasource as you suggested.
I think I also have another use case of custom location datasource: location feed from drones. One of our projects looks at getting drone location and drone status (pitch, roll, yaw and gimbal) using the drone api. It was proposed that the project/app transmit the location data obtained from drones to an ad-hoc peer to peer wireless connected device using the Apple's MultiPeer Connectivity framework. Thus our apps will need to use the location feeds from drones and that's why I'm thinking the custom location datasource class... Any advises on this as well please?
So any advises on how to implement a custom location datasource or directions leading to any documentation about it would be much appreciated, and everyone please?
You do not necessarily need to implement a custom location datasource.
If you successfully pair your external GPS receiver with the device, then iOS should start using the location feed coming from the receiver instead of the in-built sensor. That location feed will come through to all apps using the standard Apple location manager apis, which is what the default
AGSCLLocationDataSource uses, so you don't need to build a custom location datasource.
You really only need a custom datasource if you want to use special apis/sdk that the manufacturer provides for pulling extended or proprietary information from the receiver, or if you want to add additional intelligence in the datasource for dealing with location updates
Los miembros registrados pueden publicar, seguir actualizaciones y más. ¿Nuevo aquí? Regístrate gratis.
Find useful guides, FAQs, and documents to help you navigate and make the most of Esri Community.