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.
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
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.
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
}
}
}
If you log the AGSLocation to the console from within the dispatch to main, does it look correct?
Hi Nick,
it looks ok to me. I think the 4326 is WGS84.
Thanks,
Shimin
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
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
Great stuff. Glad it's working!
Nick.
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
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