From f3efc65e1afcc66c6c43a31de07167459dc22706 Mon Sep 17 00:00:00 2001 From: Lars Alexander Blumberg Date: Fri, 16 Oct 2015 17:58:17 +0200 Subject: [PATCH] Initial commit --- LICENSE | 22 ++ NuimoSwift.Podspec | 24 ++ README.md | 2 + SDK/NuimoBluetoothController.swift | 252 ++++++++++++++++++ SDK/NuimoController.swift | 44 +++ SDK/NuimoDiscoveryManager.swift | 198 ++++++++++++++ SDK/NuimoGesture.swift | 144 ++++++++++ ...stureEvent+BLEGattDataInitialization.swift | 47 ++++ SDK/NuimoGestureEvent.swift | 19 ++ SDK/NuimoMatrixManager.swift | 77 ++++++ SDK/NuimoWebSocketController.swift | 82 ++++++ 11 files changed, 911 insertions(+) create mode 100644 LICENSE create mode 100644 NuimoSwift.Podspec create mode 100644 README.md create mode 100644 SDK/NuimoBluetoothController.swift create mode 100644 SDK/NuimoController.swift create mode 100644 SDK/NuimoDiscoveryManager.swift create mode 100644 SDK/NuimoGesture.swift create mode 100644 SDK/NuimoGestureEvent+BLEGattDataInitialization.swift create mode 100644 SDK/NuimoGestureEvent.swift create mode 100644 SDK/NuimoMatrixManager.swift create mode 100644 SDK/NuimoWebSocketController.swift diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..83e2155 --- /dev/null +++ b/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2015 Senic GmbH + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + diff --git a/NuimoSwift.Podspec b/NuimoSwift.Podspec new file mode 100644 index 0000000..1a39428 --- /dev/null +++ b/NuimoSwift.Podspec @@ -0,0 +1,24 @@ +Pod::Spec.new do |s| + s.name = "NuimoSwift" + s.version = "0.0.1" + s.summary = "Swift library for connecting and communicating with Senic's Nuimo controllers" + + s.description = <<-DESC + Swift library for connecting and communicating with Senic's Nuimo controllers + + * Discover and connect Nuimo controllers via bluetooth low energy or websockets + * Receive user gestures from Nuimo controllers + * Send LED matrices to Nuimo controllers + DESC + + s.homepage = "http://senic.com" + s.license = { :type => "MIT", :file => "LICENSE" } + + s.author = { "Lars Blumberg" => "lars@senic.com" } + s.social_media_url = "http://twitter.com/heysenic" + s.ios.deployment_target = "8.0" + + s.source = { :git => "https://github.com/getSenic/nuimo-swift.git", :tag => "0.0.1" } + s.source_files = "SDK" + s.dependency "SwiftWebSocket" +end diff --git a/README.md b/README.md new file mode 100644 index 0000000..a26d728 --- /dev/null +++ b/README.md @@ -0,0 +1,2 @@ +# nuimo-swift +Swift library for connecting and communicating with Senic's Nuimo controllers diff --git a/SDK/NuimoBluetoothController.swift b/SDK/NuimoBluetoothController.swift new file mode 100644 index 0000000..71c9287 --- /dev/null +++ b/SDK/NuimoBluetoothController.swift @@ -0,0 +1,252 @@ +// +// NuimoController.swift +// Nuimo +// +// Created by Lars Blumberg on 9/23/15. +// Copyright © 2015 Senic. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. + +import CoreBluetooth + +// Represents a bluetooth low energy (BLE) Nuimo controller +public class NuimoBluetoothController: NSObject, NuimoController, CBPeripheralDelegate { + public let uuid: String + public var delegate: NuimoControllerDelegate? + + public var state: NuimoConnectionState { get{ return self.peripheral.state.nuimoConnectionState } } + public var batteryLevel: Int = -1 { didSet { if self.batteryLevel != oldValue { delegate?.nuimoController(self, didUpdateBatteryLevel: self.batteryLevel) } } } + + private let peripheral: CBPeripheral + private let centralManager: CBCentralManager + private var ledMatrixCharacteristic: CBCharacteristic? + private var currentMatrixName: String? + private var isWaitingForLedMatrixWriteResponse: Bool = false + private var writeMatrixOnWriteResponseReceived: Bool = false + private var writeMatrixResponseTimeoutTimer: NSTimer? + private var clearMatrixTimer: NSTimer? + + public init(centralManager: CBCentralManager, uuid: String, peripheral: CBPeripheral) { + self.centralManager = centralManager + self.uuid = uuid + self.peripheral = peripheral + super.init() + peripheral.delegate = self + } + + public func connect() { + if peripheral.state == .Disconnected { + centralManager.connectPeripheral(peripheral, options: nil) + delegate?.nuimoControllerDidStartConnecting(self) + } + } + + internal func didConnect() { + isWaitingForLedMatrixWriteResponse = false + writeMatrixOnWriteResponseReceived = false + // Discover bluetooth services + peripheral.discoverServices(nuimoServiceUUIDs) + delegate?.nuimoControllerDidConnect(self) + } + + internal func didFailToConnect() { + delegate?.nuimoControllerDidFailToConnect(self) + } + + public func disconnect() { + if peripheral.state != .Connected { + return + } + centralManager.cancelPeripheralConnection(peripheral) + } + + internal func didDisconnect() { + peripheral.delegate = nil + ledMatrixCharacteristic = nil + delegate?.nuimoControllerDidDisconnect(self) + } + + internal func invalidate() { + peripheral.delegate = nil + delegate?.nuimoControllerDidInvalidate(self) + } + + //MARK: - CBPeripheralDelegate + + @objc public func peripheral(peripheral: CBPeripheral, didDiscoverServices error: NSError?) { + peripheral.services? + .flatMap{ ($0, charactericUUIDsForServiceUUID[$0.UUID]) } + .forEach{ peripheral.discoverCharacteristics($0.1, forService: $0.0) } + } + + @objc public func peripheral(peripheral: CBPeripheral, didDiscoverCharacteristicsForService service: CBService, error: NSError?) { + service.characteristics?.forEach{ characteristic in + switch characteristic.UUID { + case kBatteryCharacteristicUUID: + peripheral.readValueForCharacteristic(characteristic) + case kLEDMatrixCharacteristicUUID: + ledMatrixCharacteristic = characteristic + delegate?.nuimoControllerDidDiscoverMatrixService(self) + default: + break + } + if characteristicNotificationUUIDs.contains(characteristic.UUID) { + peripheral.setNotifyValue(true, forCharacteristic: characteristic) + } + } + } + + @objc public func peripheral(peripheral: CBPeripheral, didUpdateValueForCharacteristic characteristic: CBCharacteristic, error: NSError?) { + guard let data = characteristic.value else { + return + } + + switch characteristic.UUID { + case kBatteryCharacteristicUUID: + batteryLevel = Int(UnsafePointer(data.bytes).memory) + default: + if let event = characteristic.nuimoGestureEvent() { + delegate?.nuimoController(self, didReceiveGestureEvent: event) + } + } + } + + @objc public func peripheral(peripheral: CBPeripheral, didUpdateNotificationStateForCharacteristic characteristic: CBCharacteristic, error: NSError?) { + // Nothing to do here + } + + @objc public func peripheral(peripheral: CBPeripheral, didWriteValueForCharacteristic characteristic: CBCharacteristic, error: NSError?) { + if characteristic.UUID == kLEDMatrixCharacteristicUUID { + didRetrieveMatrixWriteResponse() + } + } + + //MARK: - LED matrix writing + + public func writeMatrix(matrixName: String) { + // Do not send same matrix again if already shown + if matrixName == currentMatrixName { + return + } + currentMatrixName = matrixName + + // Send matrix later when the write response from previous write request is not yet received + if isWaitingForLedMatrixWriteResponse { + writeMatrixOnWriteResponseReceived = true + } else { + writeMatrixNow(matrixName) + } + } + + public func writeBarMatrix(percent: Int){ + let suffix = min(max(percent / 10, 1), 9) + writeMatrix("bar_\(suffix)") + } + + private func writeMatrixNow(matrixName: String) { + assert(!isWaitingForLedMatrixWriteResponse, "Cannot write matrix now, response from previous write request not yet received") + + let matrixData = NuimoMatrixManager.sharedManager.matrixData(matrixName) + guard let ledMatrixCharacteristic = ledMatrixCharacteristic else { + return + } + peripheral.writeValue(matrixData, forCharacteristic: ledMatrixCharacteristic, type: .WithResponse) + isWaitingForLedMatrixWriteResponse = true + + // When the matrix write response is not retrieved within 100ms we assume the response to have timed out + writeMatrixResponseTimeoutTimer?.invalidate() + writeMatrixResponseTimeoutTimer = NSTimer.scheduledTimerWithTimeInterval(0.1, target: self, selector: "didRetrieveMatrixWriteResponse", userInfo: nil, repeats: false) + + // Clear the matrix after a timeout + clearMatrixTimer?.invalidate() + if shouldClearMatrixAfterTimeout && matrixName != "empty" { + clearMatrixTimer = NSTimer.scheduledTimerWithTimeInterval(3.0, target: self, selector: "clearMatrix", userInfo: nil, repeats: false) + } + } + + func didRetrieveMatrixWriteResponse() { + isWaitingForLedMatrixWriteResponse = false + writeMatrixResponseTimeoutTimer?.invalidate() + + // Write next matrix if any + if writeMatrixOnWriteResponseReceived { + writeMatrixOnWriteResponseReceived = false + guard let matrixName = currentMatrixName else { + assertionFailure("No matrix to write") + return + } + writeMatrixNow(matrixName) + } + } + + func clearMatrix() { + writeMatrix("empty") + } +} + +private let shouldClearMatrixAfterTimeout = false + +private extension CBPeripheralState { + var nuimoConnectionState: NuimoConnectionState { + switch self { + case .Connecting: return .Connecting + case .Connected: return .Connected + case .Disconnecting: return .Disconnecting + case .Disconnected: return .Disconnected + } + } +} + +private extension CBCharacteristic { + func nuimoGestureEvent() -> NuimoGestureEvent? { + guard let data = value else { return nil } + + switch UUID { + case kSensorFlyCharacteristicUUID: return NuimoGestureEvent(gattFlyData: data) + case kSensorTouchCharacteristicUUID: return NuimoGestureEvent(gattTouchData: data) + case kSensorRotationCharacteristicUUID: return NuimoGestureEvent(gattRotationData: data) + case kSensorButtonCharacteristicUUID: return NuimoGestureEvent(gattButtonData: data) + default: return nil + } + } +} + +private let kBatteryServiceUUID = CBUUID(string: "180F") +private let kBatteryCharacteristicUUID = CBUUID(string: "2A19") +private let kDeviceInformationServiceUUID = CBUUID(string: "180A") +private let kDeviceInformationCharacteristicUUID = CBUUID(string: "2A29") +private let kLEDMatrixServiceUUID = CBUUID(string: "F29B1523-CB19-40F3-BE5C-7241ECB82FD1") +private let kLEDMatrixCharacteristicUUID = CBUUID(string: "F29B1524-CB19-40F3-BE5C-7241ECB82FD1") +private let kSensorServiceUUID = CBUUID(string: "F29B1525-CB19-40F3-BE5C-7241ECB82FD2") +private let kSensorFlyCharacteristicUUID = CBUUID(string: "F29B1526-CB19-40F3-BE5C-7241ECB82FD2") +private let kSensorTouchCharacteristicUUID = CBUUID(string: "F29B1527-CB19-40F3-BE5C-7241ECB82FD2") +private let kSensorRotationCharacteristicUUID = CBUUID(string: "F29B1528-CB19-40F3-BE5C-7241ECB82FD2") +private let kSensorButtonCharacteristicUUID = CBUUID(string: "F29B1529-CB19-40F3-BE5C-7241ECB82FD2") + +internal let nuimoServiceUUIDs: [CBUUID] = [ + kBatteryServiceUUID, + kDeviceInformationServiceUUID, + kLEDMatrixServiceUUID, + kSensorServiceUUID +] + +private let charactericUUIDsForServiceUUID = [ + kBatteryServiceUUID: [kBatteryCharacteristicUUID], + kDeviceInformationServiceUUID: [kDeviceInformationCharacteristicUUID], + kLEDMatrixServiceUUID: [kLEDMatrixCharacteristicUUID], + kSensorServiceUUID: [ + kSensorFlyCharacteristicUUID, + kSensorTouchCharacteristicUUID, + kSensorRotationCharacteristicUUID, + kSensorButtonCharacteristicUUID + ] +] + +private let characteristicNotificationUUIDs = [ + kBatteryCharacteristicUUID, + kSensorFlyCharacteristicUUID, + kSensorTouchCharacteristicUUID, + kSensorRotationCharacteristicUUID, + kSensorButtonCharacteristicUUID +] diff --git a/SDK/NuimoController.swift b/SDK/NuimoController.swift new file mode 100644 index 0000000..63caff4 --- /dev/null +++ b/SDK/NuimoController.swift @@ -0,0 +1,44 @@ +// +// NuimoController.swift +// Nuimo +// +// Created by Lars Blumberg on 10/9/15. +// Copyright © 2015 Senic. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. + +@objc public protocol NuimoController { + var uuid: String {get} + var delegate: NuimoControllerDelegate? {get set} + + var state: NuimoConnectionState {get} + var batteryLevel: Int {get set} + + func connect() + + func disconnect() + + func writeMatrix(name: String) + + func writeBarMatrix(percent: Int) +} + +@objc public enum NuimoConnectionState: Int { + case + Connecting, + Connected, + Disconnecting, + Disconnected +} + +@objc public protocol NuimoControllerDelegate { + func nuimoControllerDidStartConnecting(controller: NuimoController) + func nuimoControllerDidConnect(controller: NuimoController) + func nuimoControllerDidFailToConnect(controller: NuimoController) + func nuimoControllerDidDisconnect(controller: NuimoController) + func nuimoControllerDidInvalidate(controller: NuimoController) + func nuimoControllerDidDiscoverMatrixService(controller: NuimoController) + func nuimoController(controller: NuimoController, didUpdateBatteryLevel bateryLevel: Int) + func nuimoController(controller: NuimoController, didReceiveGestureEvent gestureEvent: NuimoGestureEvent) +} diff --git a/SDK/NuimoDiscoveryManager.swift b/SDK/NuimoDiscoveryManager.swift new file mode 100644 index 0000000..8f9b816 --- /dev/null +++ b/SDK/NuimoDiscoveryManager.swift @@ -0,0 +1,198 @@ +// +// NuimoDiscoveryManager.swift +// Nuimo +// +// Created by Lars Blumberg on 9/23/15. +// Copyright © 2015 Senic. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. + +import UIKit +import CoreBluetooth + +private let NuimoControllerName = "Nuimo" + +// Allows for discovering Nuimo BLE hardware controllers and virtual (websocket) controllers +public class NuimoDiscoveryManager: NSObject, CBCentralManagerDelegate { + + public static let sharedManager = NuimoDiscoveryManager() + + public var delegate: NuimoDiscoveryDelegate? + public var centralManager: CBCentralManager? { didSet { oldValue?.delegate = nil; centralManager?.delegate = self } } + public var detectUnreachableControllers: Bool = false + + private var isDiscovering = false + // List of discovered nuimo peripherals + private var controllerForPeripheral = [CBPeripheral : NuimoBluetoothController]() + private lazy var unreachableDevicesDetector: UnreachableDevicesDetector = UnreachableDevicesDetector(discoveryManager: self) + + public convenience init(centralManager: CBCentralManager) { + self.init() + self.centralManager = centralManager + } + + public func discoverControllers() { + // Discover websocket controllers + #if (arch(i386) || arch(x86_64)) && os(iOS) // Simulator + let controller = NuimoWebSocketController(url: "ws://localhost:9999") + delegate?.nuimoDiscoveryManager(self, didDiscoverNuimoController: controller) + #else + //TODO: Read possible URLs addresses from a property + //let websocketAddress = "ws://192.168.1.112:9999" + #endif + + // Discover bluetooth controllers + guard let centralManager = self.centralManager where centralManager.state == .PoweredOn else { + return + } + isDiscovering = true + + centralManager.scanForPeripheralsWithServices(nuimoServiceUUIDs, options: nil) + + unreachableDevicesDetector.stop() + if detectUnreachableControllers { + // Periodically check for unreachable nuimo devices + unreachableDevicesDetector.start() + } + } + + public func stopDiscovery() { + unreachableDevicesDetector.stop() + centralManager?.stopScan() + isDiscovering = false + } + + private func invalidateController(controller: NuimoBluetoothController) { + controller.invalidate() + delegate?.nuimoDiscoveryManager?(self, didInvalidateController: controller) + // Remove all peripherals associated with controller (there should be only one) + controllerForPeripheral.filter{ $0.1 == controller }.forEach { + controllerForPeripheral.removeValueForKey($0.0) + } + } + + //MARK: - CBCentralManagerDelegate + + public func centralManager(central: CBCentralManager, willRestoreState state: [String : AnyObject]) { + if let peripherals = state[CBCentralManagerRestoredStatePeripheralsKey] as? [CBPeripheral] { + for peripheral in peripherals { + centralManager(central, didDiscoverPeripheral: peripheral, advertisementData: [CBAdvertisementDataLocalNameKey: NuimoControllerName], RSSI: 0) + } + } + } + + public func centralManagerDidUpdateState(central: CBCentralManager) { + // If bluetooth turned on and discovery start had already been triggered before, start discovery now + if central.state == .PoweredOn && isDiscovering { + discoverControllers() + } + + // Invalidate all connections when state moves below .PoweredOff as they are then invalid + if central.state.rawValue < CBCentralManagerState.PoweredOff.rawValue { + controllerForPeripheral.values.forEach(invalidateController) + } + } + + public func centralManager(central: CBCentralManager, didDiscoverPeripheral peripheral: CBPeripheral, advertisementData: [String : AnyObject], RSSI: NSNumber) { + //TODO: There's no need to check for the device's name as we only discover controllers with our set of services + if advertisementData[CBAdvertisementDataLocalNameKey] as? String != NuimoControllerName { + return + } + let controller = NuimoBluetoothController(centralManager: central, uuid: peripheral.identifier.UUIDString, peripheral: peripheral) + controllerForPeripheral[peripheral] = controller + unreachableDevicesDetector.didFindController(controller) + delegate?.nuimoDiscoveryManager(self, didDiscoverNuimoController: controller) + } + + public func centralManager(central: CBCentralManager, didConnectPeripheral peripheral: CBPeripheral) { + guard let controller = self.controllerForPeripheral[peripheral] else { + assertionFailure("Peripheral not registered") + return + } + controller.didConnect() + delegate?.nuimoDiscoveryManager?(self, didConnectNuimoController: controller) + } + + public func centralManager(central: CBCentralManager, didFailToConnectPeripheral peripheral: CBPeripheral, error: NSError?) { + guard let controller = self.controllerForPeripheral[peripheral] else { + assertionFailure("Peripheral not registered") + return + } + controller.didFailToConnect() + delegate?.nuimoDiscoveryManager?(self, didFailToConnectNuimoController: controller, error: error) + } + + public func centralManager(central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: NSError?) { + guard let controller = self.controllerForPeripheral[peripheral] else { + assertionFailure("Peripheral not registered") + return + } + controller.didDisconnect() + delegate?.nuimoDiscoveryManager?(self, didDisconnectNuimoController: controller, error: error) + if error != nil { + // Controller probably went offline + invalidateController(controller) + } + } +} + +@objc public protocol NuimoDiscoveryDelegate { + func nuimoDiscoveryManager(discovery: NuimoDiscoveryManager, didDiscoverNuimoController controller: NuimoController) + optional func nuimoDiscoveryManager(discovery: NuimoDiscoveryManager, didConnectNuimoController controller: NuimoController) + optional func nuimoDiscoveryManager(discovery: NuimoDiscoveryManager, didFailToConnectNuimoController controller: NuimoController, error: NSError?) + optional func nuimoDiscoveryManager(discovery: NuimoDiscoveryManager, didDisconnectNuimoController controller: NuimoController, error: NSError?) + optional func nuimoDiscoveryManager(discovery: NuimoDiscoveryManager, didInvalidateController controller: NuimoController) +} + +private class UnreachableDevicesDetector { + // Minimum interval to wait before a device is considered to be unreachable + private let minUnreachableDevicesDetectionInterval: NSTimeInterval = 5.0 + + private let discoveryManager: NuimoDiscoveryManager + private var lastUnreachableDevicesRemovedTimestamp: NSDate? + private var unreachableDevicesDetectionTimer: NSTimer? + // List of unconnected nuimos discovered during the current discovery session + private var currentlyDiscoveredControllers = Set() + // List of unconnected nuimos discovered during the previous discovery session + private var previouslyDiscoveredControllers = Set() + + init(discoveryManager: NuimoDiscoveryManager) { + self.discoveryManager = discoveryManager + } + + func start() { + lastUnreachableDevicesRemovedTimestamp = NSDate() + previouslyDiscoveredControllers = Set() + currentlyDiscoveredControllers = Set() + unreachableDevicesDetectionTimer?.invalidate() + unreachableDevicesDetectionTimer = NSTimer.scheduledTimerWithTimeInterval(minUnreachableDevicesDetectionInterval + 0.5, target: self, selector: "removeUnreachableDevices", userInfo: nil, repeats: true) + } + + func stop() { + unreachableDevicesDetectionTimer?.invalidate() + } + + @objc func removeUnreachableDevices() { + // Remove unreachable devices if the discovery session was running at least for some time + guard let lastTimestamp = lastUnreachableDevicesRemovedTimestamp where NSDate().timeIntervalSinceDate(lastTimestamp) >= minUnreachableDevicesDetectionInterval else { + return + } + lastUnreachableDevicesRemovedTimestamp = NSDate() + + // All nuimo devices found during the *previous* discovery session and not found during the currently running discovery session will assumed to be now unreachable + previouslyDiscoveredControllers.filter { (previouslyDiscoveredController: NuimoBluetoothController) -> Bool in + return (previouslyDiscoveredController.state == .Disconnected) && + (currentlyDiscoveredControllers.filter{$0.uuid == previouslyDiscoveredController.uuid}).count == 0 + }.forEach(discoveryManager.invalidateController) + + // Rescan peripherals + previouslyDiscoveredControllers = currentlyDiscoveredControllers + currentlyDiscoveredControllers = Set() + discoveryManager.centralManager?.scanForPeripheralsWithServices(nuimoServiceUUIDs, options: nil) + } + + func didFindController(controller: NuimoBluetoothController) { + currentlyDiscoveredControllers.insert(controller) + } +} diff --git a/SDK/NuimoGesture.swift b/SDK/NuimoGesture.swift new file mode 100644 index 0000000..915e38e --- /dev/null +++ b/SDK/NuimoGesture.swift @@ -0,0 +1,144 @@ +// +// NuimoGesture.swift +// Nuimo +// +// Created by Lars Blumberg on 9/23/15. +// Copyright © 2015 Senic. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. + +@objc public enum NuimoGesture : Int { + // TODO: Clarify events w/ Philip + case + Undefined = 0, // TODO: Do we really need this enum value? We don't need to handle an "undefined" gesture + ButtonPress, + ButtonDoublePress, + ButtonRelease, + RotateLeft, + RotateRight, + TouchLeftDown, + TouchLeftRelease, + TouchRightDown, + TouchRightRelease, + TouchTopDown, + TouchTopRelease, + TouchBottomDown, + TouchBottomRelease, + SwipeLeft, + SwipeRight, + SwipeUp, + SwipeDown, + FlyLeft, + FlyRight, + FlyTowards, + FlyAway, + FlyUp, + FlyDown, + Program1, + Program2, + Program3, + Program4, + Program5, + Program6 + + public init(identifier: String) throws { + guard let gesture = gestureForIdentifier[identifier] else { throw NuimoGestureError.InvalidIdentifier } + self = gesture + } + + public var identifier: String { return identifierForGesture[self]! } + + // Returns the corresponding touch down gesture if self is a touch gesture, nil if not + public var touchDownGesture: NuimoGesture? { return touchDownGestureForTouchGesture[self] } + + // Returns the corresponding touch up gesture if self is a touch gesture, nil if not + public var touchUpGesture: NuimoGesture? { return touchUpGestureForTouchGesture[self] } + + // Returns the corresponding swipe gesture if self is a touch gesture, nil if not + public var swipeGesture: NuimoGesture? { return swipeGestureForTouchGesture[self] } +} + +public enum NuimoGestureError: ErrorType { + case InvalidIdentifier +} + +private let identifierForGesture: [NuimoGesture : String] = [ + .Undefined : "Undefined", + .ButtonPress : "ButtonPress", + .ButtonRelease : "ButtonRelease", + .ButtonDoublePress : "ButtonDoublePress", + .RotateLeft : "RotateLeft", + .RotateRight : "RotateRight", + .TouchLeftDown : "TouchLeftDown", + .TouchLeftRelease : "TouchLeftRelease", + .TouchRightDown : "TouchRightDown", + .TouchRightRelease : "TouchRightRelease", + .TouchTopDown : "TouchTopDown", + .TouchTopRelease : "TouchTopRelease", + .TouchBottomDown : "TouchBottomDown", + .TouchBottomRelease : "TouchBottomRelease", + .SwipeLeft : "SwipeLeft", + .SwipeRight : "SwipeRight", + .SwipeUp : "SwipeUp", + .SwipeDown : "SwipeDown", + .FlyLeft : "FlyLeft", + .FlyRight : "FlyRight", + .FlyTowards : "FlyTowards", + .FlyAway : "FlyAway", + .FlyUp : "FlyUp", + .FlyDown : "FlyDown" +] + +private let gestureForIdentifier: [String : NuimoGesture] = { + var dictionary = [String : NuimoGesture]() + for (gesture, identifier) in identifierForGesture { + dictionary[identifier] = gesture + } + return dictionary +}() + +private let touchDownGestureForTouchGesture: [NuimoGesture : NuimoGesture] = [ + .TouchLeftDown : .TouchLeftDown, + .TouchLeftRelease : .TouchLeftDown, + .TouchRightDown : .TouchRightDown, + .TouchRightRelease : .TouchRightDown, + .TouchTopDown : .TouchTopDown, + .TouchTopRelease : .TouchTopDown, + .TouchBottomDown : .TouchBottomDown, + .TouchBottomRelease : .TouchBottomDown, + .SwipeLeft : .TouchLeftDown, + .SwipeRight : .TouchRightDown, + .SwipeUp : .TouchTopDown, + .SwipeDown : .TouchBottomDown, +] + +private let touchUpGestureForTouchGesture: [NuimoGesture : NuimoGesture] = [ + .TouchLeftDown : .TouchLeftRelease, + .TouchLeftRelease : .TouchLeftRelease, + .TouchRightDown : .TouchRightRelease, + .TouchRightRelease : .TouchRightRelease, + .TouchTopDown : .TouchTopRelease, + .TouchTopRelease : .TouchTopRelease, + .TouchBottomDown : .TouchBottomRelease, + .TouchBottomRelease : .TouchBottomRelease, + .SwipeLeft : .TouchLeftRelease, + .SwipeRight : .TouchRightRelease, + .SwipeUp : .TouchTopRelease, + .SwipeDown : .TouchBottomRelease, +] + +private let swipeGestureForTouchGesture: [NuimoGesture : NuimoGesture] = [ + .TouchLeftDown : .SwipeLeft, + .TouchLeftRelease : .SwipeLeft, + .TouchRightDown : .SwipeRight, + .TouchRightRelease : .SwipeRight, + .TouchTopDown : .SwipeUp, + .TouchTopRelease : .SwipeUp, + .TouchBottomDown : .SwipeDown, + .TouchBottomRelease : .SwipeDown, + .SwipeLeft : .SwipeLeft, + .SwipeRight : .SwipeRight, + .SwipeUp : .SwipeUp, + .SwipeDown : .SwipeDown, +] diff --git a/SDK/NuimoGestureEvent+BLEGattDataInitialization.swift b/SDK/NuimoGestureEvent+BLEGattDataInitialization.swift new file mode 100644 index 0000000..e2ee562 --- /dev/null +++ b/SDK/NuimoGestureEvent+BLEGattDataInitialization.swift @@ -0,0 +1,47 @@ +// +// NuimoGestureEvent+BLEGattDataInitialization.swift +// Nuimo +// +// Created by Lars Blumberg on 10/15/15. +// Copyright © 2015 Senic. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. + +internal extension NuimoGestureEvent { + convenience init(gattFlyData data: NSData) { + //TODO: Evaluate fly events + self.init(gesture: .Undefined, value: nil) + } + + convenience init(gattTouchData data: NSData) { + let bytes = UnsafePointer(data.bytes) + let buttonByte = bytes.memory + let eventByte = bytes.advancedBy(1).memory + for i: Int16 in 0...7 where (1 << i) & buttonByte != 0 { + let touchDownGesture: NuimoGesture = [.TouchLeftDown, .TouchTopDown, .TouchRightDown, .TouchBottomDown][Int(i / 2)] + if let eventGesture: NuimoGesture = { + switch eventByte { + case 1: return touchDownGesture.self + case 2: return touchDownGesture.touchUpGesture + case 3: return nil //TODO: Do we need to handle double touch gestures here as well? + case 4: return touchDownGesture.swipeGesture + default: return nil}}() { + self.init(gesture: eventGesture, value: Int(i)) + return + } + } + self.init(gesture: .Undefined, value: nil) + } + + convenience init(gattRotationData data: NSData) { + let value = Int(UnsafePointer(data.bytes).memory) + self.init(gesture: value < 0 ? .RotateLeft : .RotateRight, value: value) + } + + convenience init(gattButtonData data: NSData) { + let value = Int(UnsafePointer(data.bytes).memory) + //TODO: Evaluate double press events + self.init(gesture: value == 1 ? .ButtonPress : .ButtonRelease, value: value) + } +} diff --git a/SDK/NuimoGestureEvent.swift b/SDK/NuimoGestureEvent.swift new file mode 100644 index 0000000..c60e007 --- /dev/null +++ b/SDK/NuimoGestureEvent.swift @@ -0,0 +1,19 @@ +// +// NuimoGestureEvent.swift +// Nuimo +// +// Created by je on 8/11/15. +// Copyright © 2015 Senic. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. + +public class NuimoGestureEvent: NSObject { + public var gesture: NuimoGesture = .Undefined + public var value: Int? + + public init(gesture: NuimoGesture, value: Int?) { + self.gesture = gesture + self.value = value + } +} diff --git a/SDK/NuimoMatrixManager.swift b/SDK/NuimoMatrixManager.swift new file mode 100644 index 0000000..a58d79f --- /dev/null +++ b/SDK/NuimoMatrixManager.swift @@ -0,0 +1,77 @@ +// +// NuimoMatrixManager.swift +// Nuimo +// +// Created by je on 9/1/15. +// Copyright © 2015 Senic. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. + +//TODO: This is an early prototype implementation. Define matrices as strings and convert them. +internal class NuimoMatrixManager { + + static let sharedManager = NuimoMatrixManager() + + //TODO: Move data to resource file, improve with human readable string + func matrixData(name: String) -> NSData { + var matrix: [UInt8] + switch name { + case "iosmusic": + matrix = [0b00000000, 0b11111000, 0b11110000, 0b00100001, 0b01000010, 0b10000100, 0b10001000, 0b10011001, 0b00111011, 0b00100010, 0b00000000] + case "sonos": + matrix = [0b00000000, 0b11111000, 0b11110000, 0b00100001, 0b01000010, 0b10000100, 0b10001000, 0b10011001, 0b00111011, 0b00100010, 0b00000000] + case "philipshue": + fallthrough + case "lifx": + matrix = [0b00000000, 0b01110000, 0b00010000, 0b00100001, 0b01000010, 0b00000100, 0b00000111, 0b00001110, 0b00011100, 0b00010000, 0b00000000] + case "empty": + matrix = [0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000] + case "bar_1": + matrix = [0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00010000, 0b00000000] + case "bar_2": + matrix = [0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00001000, 0b00010000, 0b00000000] + case "bar_3": + matrix = [0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000100, 0b00001000, 0b00010000, 0b00000000] + case "bar_4": + matrix = [0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000010, 0b00000100, 0b00001000, 0b00010000, 0b00000000] + case "bar_5": + matrix = [0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000001, 0b00000010, 0b00000100, 0b00001000, 0b00010000, 0b00000000] + case "bar_6": + matrix = [0b00000000, 0b00000000, 0b00000000, 0b10000000, 0b00000000, 0b00000001, 0b00000010, 0b00000100, 0b00001000, 0b00010000, 0b00000000] + case "bar_7": + matrix = [0b00000000, 0b00000000, 0b01000000, 0b10000000, 0b00000000, 0b00000001, 0b00000010, 0b00000100, 0b00001000, 0b00010000, 0b00000000] + case "bar_8": + matrix = [0b00000000, 0b00100000, 0b01000000, 0b10000000, 0b00000000, 0b00000001, 0b00000010, 0b00000100, 0b00001000, 0b00010000, 0b00000000] + case "bar_9": + matrix = [0b00010000, 0b00100000, 0b01000000, 0b10000000, 0b00000000, 0b00000001, 0b00000010, 0b00000100, 0b00001000, 0b00010000, 0b00000000] + case "off": + matrix = [0b00000000, 0b00000000, 0b11100000, 0b00100000, 0b01000010, 0b10000100, 0b00001000, 0b00001110, 0b00000000, 0b00000000, 0b00000000] + case "on": + matrix = [0b00000000, 0b00000000, 0b11100000, 0b11100000, 0b11000011, 0b10000111, 0b00001111, 0b00001110, 0b00000000, 0b00000000, 0b00000000] + case "random": + matrix = [0b00000000, 0b00000000, 0b00011000, 0b01000011, 0b00000001, 0b00000001, 0b10000101, 0b00110001, 0b00000000, 0b00000000, 0b00000000] + case "blue": + matrix = [0b00000000, 0b01110000, 0b00100000, 0b01000001, 0b10000010, 0b00000011, 0b00001001, 0b00010010, 0b00011100, 0b00000000, 0b00000000] + case "orange": + matrix = [0b00000000, 0b01110000, 0b00010000, 0b00100001, 0b01000010, 0b10000100, 0b00001000, 0b00010001, 0b00011100, 0b00000000, 0b00000000] + case "green": + matrix = [0b00000000, 0b01110000, 0b00010000, 0b00100001, 0b01000000, 0b10000111, 0b00001000, 0b00010001, 0b00011100, 0b00000000, 0b00000000] + case "white": + matrix = [0b00000000, 0b00000100, 0b00001001, 0b00010010, 0b00100100, 0b01001000, 0b10010010, 0b00100100, 0b00110110, 0b00000000, 0b00000000] + case "yellow": + matrix = [0b00000000, 0b10001000, 0b00010000, 0b01000001, 0b00000001, 0b00000001, 0b00000010, 0b00000100, 0b00001000, 0b00000000, 0b00000000] + case "play": + matrix = [0b00000000, 0b00010000, 0b01100000, 0b11000000, 0b10000001, 0b00000111, 0b00000111, 0b00000110, 0b00000100, 0b00000000, 0b00000000] + case "pause": + matrix = [0b00000000, 0b11011000, 0b10110000, 0b01100001, 0b11000011, 0b10000110, 0b00001101, 0b00011011, 0b00110110, 0b00000000, 0b00000000] + case "nexttrack": + matrix = [0b00000000, 0b00000000, 0b00100000, 0b11000001, 0b10000010, 0b00000111, 0b00001011, 0b00010010, 0b00000000, 0b00000000, 0b00000000] + case "previoustrack": + matrix = [0b00000000, 0b00000000, 0b10010000, 0b10100000, 0b11000001, 0b10000011, 0b00000110, 0b00001001, 0b00000000, 0b00000000, 0b00000000] + default: + matrix = [0b00110000, 0b10010000, 0b00010000, 0b00000010, 0b00000010, 0b00000010, 0b00000010, 0b00000100, 0b00000000, 0b00010000, 0b00000000] //Question Mark + } + return NSData(bytes: matrix, length: 11) + } +} diff --git a/SDK/NuimoWebSocketController.swift b/SDK/NuimoWebSocketController.swift new file mode 100644 index 0000000..39a51e8 --- /dev/null +++ b/SDK/NuimoWebSocketController.swift @@ -0,0 +1,82 @@ +// +// NuimoWebSocketController.swift +// Nuimo +// +// Created by Lars Blumberg on 10/9/15. +// Copyright © 2015 Senic. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. + +import SwiftWebSocket + +// Represents a virtual (websocket) controller that connects to a web server. Good for testing when you haven't got an actual Nuimo hardware device at hand. +public class NuimoWebSocketController : NSObject, NuimoController { + public let url: String + public var delegate: NuimoControllerDelegate? + public var uuid: String { get { return url } } + + public var state: NuimoConnectionState { return connectionStateForWebSocketReadyState[self.webSocket?.readyState ?? .Closed] ?? .Disconnected } + public var batteryLevel: Int = -1 { didSet { if self.batteryLevel != oldValue { delegate?.nuimoController(self, didUpdateBatteryLevel: self.batteryLevel) } } } + + private var webSocket: WebSocket? + + public init(url: String) { + self.url = url + super.init() + } + + public func connect() { + if webSocket?.readyState ?? .Closed != .Closed { return } + webSocket = { + let webSocket = WebSocket(url) + webSocket.event.open = { + self.delegate?.nuimoControllerDidConnect(self) + } + webSocket.event.close = { _ in + self.webSocket = nil + self.delegate?.nuimoControllerDidDisconnect(self) + } + webSocket.event.end = { _ in + self.webSocket = nil + self.delegate?.nuimoControllerDidDisconnect(self) + } + webSocket.event.error = { error in + //TODO: Figure out which error occurred and eventually call adeguate delegate methode + self.delegate?.nuimoControllerDidFailToConnect(self) + } + webSocket.event.message = { message in + if let text = message as? String { + self.handleMessage(text) ? webSocket.send("OK") : webSocket.send("Invalid gesture event") + } + } + return webSocket + }() + } + + public func disconnect() { + self.webSocket?.close() + } + + public func writeMatrix(name: String) { + //TODO: Send matrix to websocket + } + + public func writeBarMatrix(percent: Int) { + //TODO: Send matrix to websocket + } + + private func handleMessage(message: String) -> Bool { + guard let gesture = try? NuimoGesture(identifier: message) else { return false } + //TODO: Set value + delegate?.nuimoController(self, didReceiveGestureEvent: NuimoGestureEvent(gesture: gesture, value: 0)) + return true + } +} + +private let connectionStateForWebSocketReadyState: [WebSocketReadyState : NuimoConnectionState] = [ + .Connecting: .Connecting, + .Open: .Connected, + .Closing: .Disconnecting, + .Closed: .Disconnected +]