diff --git a/Package.resolved b/Package.resolved index e121c46..71cc42f 100644 --- a/Package.resolved +++ b/Package.resolved @@ -6,8 +6,8 @@ "repositoryURL": "https://github.com/PureSwift/Bluetooth.git", "state": { "branch": null, - "revision": "2d9e7c6b7f942e29ebd809687a214025b2192567", - "version": "2.1.3" + "revision": "e3dd044fb61162a3d22885b2da5736986667ea27", + "version": "2.1.4" } }, { @@ -24,8 +24,8 @@ "repositoryURL": "https://github.com/PureSwift/GATT", "state": { "branch": null, - "revision": "9f94eaeb37df390e3d6b5be4fb627f05af4548e7", - "version": "1.6.2" + "revision": "df0132ffd84a418b70c937b5fbaa788cf27b71a8", + "version": "1.7.0" } } ] diff --git a/Sources/gattserver/DarwinLocation.swift b/Sources/gattserver/DarwinLocation.swift new file mode 100644 index 0000000..e42243e --- /dev/null +++ b/Sources/gattserver/DarwinLocation.swift @@ -0,0 +1,218 @@ +// +// DarwinLocationManager.swift +// gattserver +// +// Created by Carlos Duclos on 7/4/18. +// + +//#if os(macOS) + +import Foundation +import CoreLocation + +public final class DarwinLocationManager: LocationManager { + + // MARK: - Properties + + public var didUpdate: ((Location) -> ())? + + private var _location: Location? { + + didSet { if let location = self.location { didUpdate?(location) } } + } + + public fileprivate(set) var location: Location? { + + get { return accessQueue.sync { [unowned self] in return self._location } } + + set { accessQueue.sync { [unowned self] in self._location = newValue } } + } + + internal let internalManager: CLLocationManager + + internal let delegate: InternalDelegate + + internal var internalState = InternalState() + + internal lazy var accessQueue: DispatchQueue = DispatchQueue(label: "\(type(of: self)) Access Queue", attributes: []) + + public static var isEnabled: Bool { + + return CLLocationManager.locationServicesEnabled() + } + + public static var authorizationStatus: CLAuthorizationStatus { + + return CLLocationManager.authorizationStatus() + } + + // MARK: - Initialization + + public init(didUpdate: ((Location) -> ())? = nil) throws { + + // initialize properties + self.internalManager = CLLocationManager() + self.delegate = InternalDelegate() + self.didUpdate = didUpdate + + // set delegate + self.delegate.locationManager = self + internalManager.delegate = self.delegate + + // start updating location + try start() + } + + deinit { + + stop() + } + + // MARK: - Methods + + // only call once + private func start() throws { + + let semaphore = Semaphore(timeout: 30, operation: .startUpdatingLocation) + accessQueue.sync { [unowned self] in self.internalState.start.semaphore = semaphore } + defer { accessQueue.sync { [unowned self] in self.internalState.start.semaphore = nil } } + + internalManager.startUpdatingLocation() + + try semaphore.wait() + } + + private func stop() { + + internalManager.stopUpdatingLocation() + } +} + +// MARK: - CLLocationManagerDelegate + +extension DarwinLocationManager { + + @objc(DarwinLocationManagerInternalDelegate) + final class InternalDelegate: NSObject { + + weak var locationManager: DarwinLocationManager? + } +} + +extension DarwinLocationManager.InternalDelegate: CLLocationManagerDelegate { + + @objc + public func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { + + /** + An array of CLLocation objects containing the location data. This array always contains at least one object representing the current location. If updates were deferred or if multiple locations arrived before they could be delivered, the array may contain additional entries. The objects in the array are organized in the order in which they occurred. Therefore, the most recent location update is at the end of the array. + */ + + guard let location = locations.last + else { assertionFailure("Array always contains at least one object representing the current location."); return } + + self.locationManager?.accessQueue.async(flags: .barrier) { [weak self] in + self?.locationManager?.location = Location(location) + } + } + + @objc + public func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) { + + // check for pending operations + self.locationManager?.accessQueue.sync { [weak self] in + + // return error when starting updating locations + if let semaphore = self?.locationManager?.internalState.start.semaphore { + + semaphore.stopWaiting(error) // throw error + self?.locationManager?.internalState.start.semaphore = nil // stop waiting + } + } + } +} + +internal extension Location { + + init(_ location: CLLocation) { + + self.init(coordinate: Coordinate(location.coordinate)) + } +} + +internal extension LocationCoordinate { + + init(_ location: CLLocationCoordinate2D) { + + self.init(latitude: location.latitude, longitude: location.longitude) + } +} + +// MARK: - Supporting Types + +public enum DarwinLocationError: Error { + + case timeout +} + +internal extension DarwinLocationManager { + + struct InternalState { + + fileprivate init() { } + + struct Start { + + var semaphore: Semaphore? + } + + var start = Start() + } + + enum Operation { + + case startUpdatingLocation + } + + final class Semaphore { + + let operation: Operation + let semaphore: DispatchSemaphore + let timeout: TimeInterval + var error: Swift.Error? + + init(timeout: TimeInterval, + operation: Operation) { + + self.operation = operation + self.timeout = timeout + self.semaphore = DispatchSemaphore(value: 0) + self.error = nil + } + + func wait() throws { + + let dispatchTime: DispatchTime = .now() + timeout + + let success = semaphore.wait(timeout: dispatchTime) == .success + + if let error = self.error { + + throw error + } + + guard success else { throw DarwinLocationError.timeout } + } + + func stopWaiting(_ error: Swift.Error? = nil) { + + // store signal + self.error = error + + // stop blocking + semaphore.signal() + } + } +} + +//#endif diff --git a/Sources/gattserver/GATTServiceController.swift b/Sources/gattserver/GATTServiceController.swift index a0b1578..49bc3b7 100644 --- a/Sources/gattserver/GATTServiceController.swift +++ b/Sources/gattserver/GATTServiceController.swift @@ -21,5 +21,6 @@ public protocol GATTServiceController: class { internal let serviceControllers: [GATTServiceController.Type] = [ GATTBatteryServiceController.self, - GATTDeviceInformationServiceController.self + GATTDeviceInformationServiceController.self, + GATTIndoorPositioningnServiceController.self ] diff --git a/Sources/gattserver/IndoorPositioningService.swift b/Sources/gattserver/IndoorPositioningService.swift new file mode 100644 index 0000000..0c328cc --- /dev/null +++ b/Sources/gattserver/IndoorPositioningService.swift @@ -0,0 +1,187 @@ +// +// IndoorPositioningService.swift +// gattserver +// +// Created by Carlos Duclos on 7/4/18. +// + +import Foundation +import Bluetooth +import GATT + +public final class GATTIndoorPositioningnServiceController: GATTServiceController { + + public static let service: BluetoothUUID = .indoorPositioning + + // MARK: - Properties + + public let peripheral: PeripheralManager + + public private(set) var configuration: GATTIndoorPositioningConfiguration = GATTIndoorPositioningConfiguration(configurations: [.coordinates]) { + + didSet { peripheral[characteristic: configurationHandle] = configuration.data } + } + + public private(set) var latitude: GATTLatitude = 0 { + + didSet { peripheral[characteristic: latitudeHandle] = latitude.data } + } + + public private(set) var longitude: GATTLongitude = 0 { + + didSet { peripheral[characteristic: longitudeHandle] = longitude.data } + } + + public private(set) var localNorthCoordinate: GATTLocalNorthCoordinate = 0 { + + didSet { peripheral[characteristic: localNorthCoordinateHandle] = localNorthCoordinate.data } + } + + public private(set) var localEastCoordinate: GATTLocalEastCoordinate = 0 { + + didSet { peripheral[characteristic: localEastCoordinateHandle] = localEastCoordinate.data } + } + + public private(set) var floorNumber: GATTFloorNumber = 0 { + + didSet { peripheral[characteristic: floorNumberHandle] = floorNumber.data } + } + + public private(set) var altitude: GATTAltitude = GATTAltitude(altitude: 0) { + + didSet { peripheral[characteristic: altitudeHandle] = altitude.data } + } + + public private(set) var uncertainty = GATTUncertainty(stationary: .stationary, updateTime: .upTo3s, precision: .lessThan10cm) { + + didSet { peripheral[characteristic: uncertaintyHandle] = uncertainty.data } + } + + public private(set) var locationName: GATTLocationName = "" { + + didSet { peripheral[characteristic: locationNameHandle] = locationName.data } + } + + internal let serviceHandle: UInt16 + + internal let configurationHandle: UInt16 + internal let latitudeHandle: UInt16 + internal let longitudeHandle: UInt16 + internal let localNorthCoordinateHandle: UInt16 + internal let localEastCoordinateHandle: UInt16 + internal let floorNumberHandle: UInt16 + internal let altitudeHandle: UInt16 + internal let uncertaintyHandle: UInt16 + internal let locationNameHandle: UInt16 + + internal let locationManager: LocationManager + + // MARK: - Initialization + + public init(peripheral: PeripheralManager) throws { + + self.peripheral = peripheral + + let serviceUUID = type(of: self).service + + #if os(Linux) + let descriptors = [GATTClientCharacteristicConfiguration().descriptor] + #else + let descriptors: [GATT.Descriptor] = [] + #endif + + let characteristics = [ + GATT.Characteristic(uuid: type(of: configuration).uuid, + value: configuration.data, + permissions: [.read], + properties: [.read, .notify], + descriptors: descriptors), + + GATT.Characteristic(uuid: type(of: latitude).uuid, + value: latitude.data, + permissions: [.read], + properties: [.read, .notify], + descriptors: descriptors), + + GATT.Characteristic(uuid: type(of: longitude).uuid, + value: longitude.data, + permissions: [.read], + properties: [.read, .notify], + descriptors: descriptors), + + GATT.Characteristic(uuid: type(of: localNorthCoordinate).uuid, + value: localNorthCoordinate.data, + permissions: [.read], + properties: [.read, .notify], + descriptors: descriptors), + + GATT.Characteristic(uuid: type(of: localEastCoordinate).uuid, + value: localEastCoordinate.data, + permissions: [.read], + properties: [.read, .notify], + descriptors: descriptors), + + GATT.Characteristic(uuid: type(of: floorNumber).uuid, + value: floorNumber.data, + permissions: [.read], + properties: [.read, .notify], + descriptors: descriptors), + + GATT.Characteristic(uuid: type(of: altitude).uuid, + value: altitude.data, + permissions: [.read], + properties: [.read, .notify], + descriptors: descriptors), + + GATT.Characteristic(uuid: type(of: uncertainty).uuid, + value: uncertainty.data, + permissions: [.read], + properties: [.read, .notify], + descriptors: descriptors), + + GATT.Characteristic(uuid: type(of: locationName).uuid, + value: locationName.data, + permissions: [.read], + properties: [.read, .notify], + descriptors: descriptors) + ] + + let service = GATT.Service(uuid: serviceUUID, + primary: true, + characteristics: characteristics) + + self.serviceHandle = try peripheral.add(service: service) + self.configurationHandle = peripheral.characteristics(for: type(of: configuration).uuid)[0] + self.latitudeHandle = peripheral.characteristics(for: type(of: latitude).uuid)[0] + self.longitudeHandle = peripheral.characteristics(for: type(of: longitude).uuid)[0] + self.localNorthCoordinateHandle = peripheral.characteristics(for: type(of: localNorthCoordinate).uuid)[0] + self.localEastCoordinateHandle = peripheral.characteristics(for: type(of: localEastCoordinate).uuid)[0] + self.floorNumberHandle = peripheral.characteristics(for: type(of: floorNumber).uuid)[0] + self.altitudeHandle = peripheral.characteristics(for: type(of: altitude).uuid)[0] + self.uncertaintyHandle = peripheral.characteristics(for: type(of: uncertainty).uuid)[0] + self.locationNameHandle = peripheral.characteristics(for: type(of: locationName).uuid)[0] + + // start updating location + #if os(macOS) + self.locationManager = try DarwinLocationManager() + self.locationManager.didUpdate = { [weak self] in self?.updateValues($0) } + print("printing location") + #elseif os(Linux) + #endif + } + + deinit { + + self.peripheral.remove(service: serviceHandle) + } + + // MARK: - Methods + + private func updateValues(_ location: Location) { + + self.configuration = GATTIndoorPositioningConfiguration(configurations: [.coordinates]) + self.locationName = "Location" + self.latitude = GATTLatitude(rawValue: Int32(location.coordinate.latitude)) + self.longitude = GATTLongitude(rawValue: Int32(location.coordinate.longitude)) + } +} diff --git a/Sources/gattserver/LinuxLocation.swift b/Sources/gattserver/LinuxLocation.swift new file mode 100644 index 0000000..f7b5328 --- /dev/null +++ b/Sources/gattserver/LinuxLocation.swift @@ -0,0 +1,29 @@ +// +// LinuxLocation.swift +// gattserver +// +// Created by Carlos Duclos on 7/4/18. +// + +import Foundation + +#if os(Linux) + +public final class LinuxLocation: LocationManagerProtocol { + + public var locationServicesEnabled: Bool { + + return false + } + + public func startUpdatingLocation() { + + } + + public func stopUpdatingLocation() { + + } + +} + +#endif diff --git a/Sources/gattserver/Location.swift b/Sources/gattserver/Location.swift new file mode 100644 index 0000000..be5facc --- /dev/null +++ b/Sources/gattserver/Location.swift @@ -0,0 +1,63 @@ +// +// LocationManager.swift +// gattserver +// +// Created by Carlos Duclos on 7/4/18. +// + +import Foundation + +/// Location Manager protocol +public protocol LocationManager: class { + + /// Location was updated. + var didUpdate: ((Location) -> ())? { get set } + + /// Last reported location. + var location: Location? { get } +} + +public struct Location { + + public var coordinate: Coordinate + + public init(coordinate: Coordinate) { + + self.coordinate = coordinate + } +} + +public extension Location { + + public typealias Coordinate = LocationCoordinate +} + +/* + * LocationCoordinate + * + * Discussion: + * A structure that contains a geographical coordinate. + * + * Fields: + * latitude: + * The latitude in degrees. + * longitude: + * The longitude in degrees. + */ +public struct LocationCoordinate { + + public typealias Degrees = Double + + /// The latitude in degrees. + public var latitude: Degrees + + /// The longitude in degrees. + public var longitude: Degrees + + public init(latitude: Degrees, + longitude: Degrees) { + + self.latitude = latitude + self.longitude = longitude + } +} diff --git a/Sources/gattserver/main.swift b/Sources/gattserver/main.swift index a7006d2..9138bf0 100644 --- a/Sources/gattserver/main.swift +++ b/Sources/gattserver/main.swift @@ -22,6 +22,13 @@ func run(arguments: [String] = CommandLine.arguments) throws { // first argument is always the current directory let arguments = Array(arguments.dropFirst()) + guard let serviceUUIDString = arguments.first + else { throw CommandError.noCommand } + + guard let service = BluetoothUUID(rawValue: serviceUUIDString), + let controllerType = serviceControllers.first(where: { $0.service == service }) + else { throw CommandError.invalidCommandType(serviceUUIDString) } + #if os(Linux) guard let controller = HostController.default else { throw CommandError.bluetoothUnavailible } @@ -45,13 +52,6 @@ func run(arguments: [String] = CommandLine.arguments) throws { while peripheral.state != .poweredOn { sleep(1) } #endif - guard let serviceUUIDString = arguments.first - else { throw CommandError.noCommand } - - guard let service = BluetoothUUID(rawValue: serviceUUIDString), - let controllerType = serviceControllers.first(where: { $0.service == service }) - else { throw CommandError.invalidCommandType(serviceUUIDString) } - serviceController = try controllerType.init(peripheral: peripheral) try peripheral.start()