How to use external gps location data

13558
34
Jump to solution
05-03-2018 10:02 PM
ShiminCai
Occasional Contributor III

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
ShiminCai
Occasional Contributor III

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)

        }

        else

        {

            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: "\r\n")

            

            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.

Shimin 

Nicholas-Furness
Esri Regular Contributor

I'm still checking to see if this is a requirement, but you could try dispatching the AGSLocationDisplaySource delegate calls to the main thread. If the sessionDataReceived notification is being fired (and so received) on a background thread I could see that could potentially cause problems.

0 Kudos
ShiminCai
Occasional Contributor III

Hi Nick,

Tried your suggestion but it did not help:

DispatchQueue.main.async {

                    self.delegate.locationDisplayDataSource(self, didUpdateWith: agsLocation)

                    self.delegate.locationDisplayDataSource(self, didUpdateWithHeading: rmcData.course)

                }

Below are the sessionController codes and hopefully it'll help figure out the problem.

Thank you

Shimin

/*

 Copyright (C) 2016 Bad Elf, LLC. All Rights Reserved.

 See LICENSE.txt for this sample’s licensing information

 

 Abstract:

 Controller for managing connected accessory and communicating with the accessory via NSInput & NSOutput streams.

 */

import UIKit

import ExternalAccessory

// FIXME: comparison operators with optionals were removed from the Swift Standard Libary.

// Consider refactoring the code to use the non-optional operators.

fileprivate func < <T : Comparable>(lhs: T?, rhs: T?) -> Bool {

  switch (lhs, rhs) {

  case let (l?, r?):

    return l < r

  case (nil, _?):

    return true

  default:

    return false

  }

}

// FIXME: comparison operators with optionals were removed from the Swift Standard Libary.

// Consider refactoring the code to use the non-optional operators.

fileprivate func >= <T : Comparable>(lhs: T?, rhs: T?) -> Bool {

  switch (lhs, rhs) {

  case let (l?, r?):

    return l >= r

  default:

    return !(lhs < rhs)

  }

}

// FIXME: comparison operators with optionals were removed from the Swift Standard Libary.

// Consider refactoring the code to use the non-optional operators.

fileprivate func > <T : Comparable>(lhs: T?, rhs: T?) -> Bool {

  switch (lhs, rhs) {

  case let (l?, r?):

    return l > r

  default:

    return rhs < lhs

  }

}

class SessionController: NSObject, EAAccessoryDelegate, StreamDelegate {

    static let sharedController = SessionController()

    var _accessory: EAAccessory?

    var _session: EASession?

    var _protocolString: String?

    var _writeData: NSMutableData?

    var _readData: NSMutableData?

    var _dataAsString: NSString?

    

    // MARK: Controller Setup

    

    func setupController(forAccessory accessory: EAAccessory, withProtocolString protocolString: String) {

        _accessory = accessory

        _protocolString = protocolString

    }

    

    // MARK: Opening & Closing Sessions

    

    func openSession() -> Bool {

        _accessory?.delegate = self

        _session = EASession(accessory: _accessory!, forProtocol: _protocolString!)

        

        if _session != nil {

            _session?.inputStream?.delegate = self

            _session?.inputStream?.schedule(in: RunLoop.current, forMode: RunLoopMode.defaultRunLoopMode)

            _session?.inputStream?.open()

            

            _session?.outputStream?.delegate = self

            _session?.outputStream?.schedule(in: RunLoop.current, forMode: RunLoopMode.defaultRunLoopMode)

            _session?.outputStream?.open()

        } else {

            print("Failed to create session")

        }

        

        return _session != nil

    }

    

    func closeSession() {

        

        _session?.inputStream?.close()

        _session?.inputStream?.remove(from: RunLoop.current, forMode: RunLoopMode.defaultRunLoopMode)

        _session?.inputStream?.delegate = nil

        

        _session?.outputStream?.close()

        _session?.outputStream?.remove(from: RunLoop.current, forMode: RunLoopMode.defaultRunLoopMode)

        _session?.outputStream?.delegate = nil

        

        _session = nil

        _writeData = nil

        _readData = nil

    }

    

    // MARK: Write & Read Data

    

    func writeData(_ data: Data) {

        if _writeData == nil {

            _writeData = NSMutableData()

        }

        

        _writeData?.append(data)

        self.writeData()

    }

    

    func readData(_ bytesToRead: Int) -> Data {

        

        var data: Data?

        if _readData?.length >= bytesToRead {

            let range = NSMakeRange(0, bytesToRead)

            data = _readData?.subdata(with: range)

            _readData?.replaceBytes(in: range, withBytes: nil, length: 0)

        }

        

        return data!

    }

    

    func readBytesAvailable() -> Int {

        return (_readData?.length)!

    }

    

    // MARK: - Helpers

    func updateReadData() {

        let bufferSize = 128

        var buffer = [UInt8](repeating: 0, count: bufferSize)

        

        while _session?.inputStream?.hasBytesAvailable == true {

            let bytesRead = _session?.inputStream?.read(&buffer, maxLength: bufferSize)

            if _readData == nil {

                _readData = NSMutableData()

            }

            _readData?.append(buffer, length: bytesRead!)

            _dataAsString = NSString(bytes: buffer, length: bytesRead!, encoding: String.Encoding.utf8.rawValue)

            NotificationCenter.default.post(name: Notification.Name(rawValue: "EXGPSSessionDataReceivedNotification"), object: nil)

        }

    }

    

    fileprivate func writeData() {

        while _session?.outputStream?.hasSpaceAvailable == true && _writeData?.length > 0 {

            var buffer = [UInt8](repeating: 0, count: _writeData!.length)

            _writeData?.getBytes(&buffer, length: (_writeData?.length)!)

            let bytesWritten = _session?.outputStream?.write(&buffer, maxLength: _writeData!.length)

            if bytesWritten == -1 {

                print("Write Error")

                return

            } else if bytesWritten > 0 {

                _writeData?.replaceBytes(in: NSMakeRange(0, bytesWritten!), withBytes: nil, length: 0)

            }

        }

    }

    

    // MARK: - EAAcessoryDelegate

    

    func accessoryDidDisconnect(_ accessory: EAAccessory) {

        // Accessory diconnected from iOS, updating accordingly

    }

    

    // MARK: - NSStreamDelegateEventExtensions

    func stream(_ aStream: Stream, handle eventCode: Stream.Event) {

        switch eventCode {

        case Stream.Event():

            break

        case Stream.Event.openCompleted:

            break

        case Stream.Event.hasBytesAvailable:

            // Read Data

            updateReadData()

            break

        case Stream.Event.hasSpaceAvailable:

            // Write Data

            self.writeData()

            break

        case Stream.Event.errorOccurred:

            break

        case Stream.Event.endEncountered:

            break

            

        default:

            break

        }

    }

}

Nicholas-Furness
Esri Regular Contributor

If you log the AGSLocation to the console from within the dispatch to main, does it look correct?

0 Kudos
ShiminCai
Occasional Contributor III

Hi Nick,

it looks ok to me. I think the 4326 is WGS84.

Thanks,

Shimin

0 Kudos
ShiminCai
Occasional Contributor III

Hi Nick,

Sorry Nick. Checked again and the coordinate does not look correct at all. It is NOT my current location! Something went wrong with my calculation from nmea string to lat and lon. I'm checking now and will report back here.

Thanks you very much for picking this up!!!

Shimin

0 Kudos
ShiminCai
Occasional Contributor III

Hi Nick,

Found the cause of the problem that is convert minutes to decimal degrees where I was using 3 (probably a typo) in the second offsetBy. Now after changed to 2 things start working as expected!!!

/// Lat stringValue Format XXYY.ZZZZ -> XX° + (YY.ZZZZ / 60)°

func convertLatitudeToDegree(with stringValue: String) -> Double { 

        return Double(stringValue.substring(to: stringValue.index(stringValue.startIndex, offsetBy: 2)))! + Double(stringValue.substring(from: stringValue.index(stringValue.startIndex, offsetBy: 2)))! / 60

    }

 

I think my problem has been resolved. Thank you very much for your help.

Cheers,

Shimin

Nicholas-Furness
Esri Regular Contributor

Great stuff. Glad it's working!

Nick.

JoeHershman
MVP Regular Contributor

Shimin Cai

Off topic, but how are you connecting to your device?  We have an external bluetooth device and it just does not seem to be recognized using standard approach.  Wondering if you did something different

Thanks

Thanks,
-Joe
0 Kudos
ShiminCai
Occasional Contributor III

Hi Joe,

I'm using Apple ExternalAccessory framework to detect external bluetooth connected devices. Recently we found an external gps unit connected with a USB cable can also be picked up by our app without modifying any code.

Cheers,

Shimin