XYOracleNetwork/sdk-xyo-swift

View on GitHub
Sources/sdk-xyo-swift/BleInterface/XyoBluetoothDevice.swift

Summary

Maintainability
D
2 days
Test Coverage
//
//  XyoBluetoothDevice.swift
//  mod-ble-swift
//
//  Created by Carter Harrison on 2/10/19.
//  Copyright © 2019 XYO Network. All rights reserved.
//

import Foundation
import Promises
import CoreBluetooth
import XyBleSdk
import sdk_core_swift

/// A class that gets created with XYO pipe enabled devices arround the world. Each device complies to the
/// XyoNetworkPipe interface, meaning that data can be send and recived beetwen them. Please note that
/// one chould not use an instance of this class as a pipe, but tryCreatePipe() to get an instance of
/// a pipe.
open class XyoBluetoothDevice: XYBluetoothDeviceBase, XYBluetoothDeviceNotifyDelegate, XyoNetworkPipe {
  public func getNetworkHeuristics() -> [XyoObjectStructure] {
    return []
  }
  
    /// The defining family for a XyoBluetoothDevice, this helps the process of creatig a device, and making
    /// sure that it complies to the XYO pipe spec.
    public static let family = XYDeviceFamily.init(uuid: UUID(uuidString: XyoBluetoothDevice.uuid)!,
                                                   prefix: XyoBluetoothDevice.prefix,
                                                   familyName: XyoBluetoothDevice.familyName,
                                                   id: XyoBluetoothDevice.id)
    
    /// The ID of an XyoBluetoothDevice
    public static let id = "XYO"
    
    /// The primary service UUID of a XyoBluetoothDevice
    public static let uuid : String = XyoService.pipe.serviceUuid.uuidString
    
    /// The faimly name of a XyoBluetoothDevice
    public static let familyName : String = "XYO"
    
    /// The prefix of a XyoBluetoothDevice
    public static let prefix : String = "xy:ibeacon"
    
    /// The input stream of the device at the other end of the pipe.
    private var inputStream = XyoInputStream()
    
    /// The promise to wait when waiting for a new packed to be completed in the inputStream.
    private var recivePromise : Promise<[UInt8]?>? = nil
    
    /// Creates a new instance of XyoBluetoothDevice using an id and rssi.
    /// - Parameter id: The peripheral id of the device to create.
    /// - Parameter iBeacon: The IBeacon of the device.
    /// - Parameter rssi: The rssi of the device when scaned, will defualt to XYDeviceProximity.none.rawValue.
    public init(_ id: String, iBeacon: XYIBeaconDefinition? = nil, rssi: Int = XYDeviceProximity.none.rawValue) {
        super.init(id, rssi: rssi, family: XyoBluetoothDevice.family, iBeacon: iBeacon)
    }
    
    /// A convenience init that does not need an id.
    /// - Parameter iBeacon: The IBeacon of the device.
    /// - Parameter rssi: The rssi of the device when scaned, will defualt to XYDeviceProximity.none.rawValue.
    public convenience init(iBeacon: XYIBeaconDefinition, rssi: Int = XYDeviceProximity.none.rawValue) {
        self.init(iBeacon.xyId(from: XyoBluetoothDevice.family), iBeacon: iBeacon, rssi: rssi)
    }

    open func getMtu () -> Int {
        return peripheral?.maximumWriteValueLength(for: CBCharacteristicWriteType.withResponse) ?? 22
    }
    
    /// A function to try and create a pipe. This should be the function used to create a pipe, not using this instance
    /// as a pipe, even though it may work, it will not work consistsnatly.
    /// - Warning: This function is blocking while waiting to subscribe to the device, and this function should be called
    /// withen a connection block
    public func tryCreatePipe () -> XyoNetworkPipe? {
        /// make sure to clear the input stream for a new pipe
        self.inputStream = XyoInputStream()
        
        /// we use a unique name as the delegate key to prevent overriding keys
        let result = self.subscribe(to: XyoService.pipe, delegate: (key: "notify [DBG: \(#function)]: \(Unmanaged.passUnretained(self).toOpaque())", delegate: self))
        if (result.error == nil) {
            print("Created PIPE")
            return self
        }
        
        return nil
    }
    
    /// This function tries to attatch a XYPeripheral as the peripheral for this device model, this will work if the
    /// device has a XYO UUID in the advertisement
    /// - Parameter peripheral: The XYO pipe enabled peripheral to try and attatch
    /// - Returns: If the attatchment of the peripheral was sucessfull.
    override open func attachPeripheral(_ peripheral: XYPeripheral) -> Bool {
        guard
            self.peripheral == nil
            else { return false }
        
        if (checkForName(peripheral) || checkForXyoUuid(peripheral)) {
            // Set the peripheral and delegate to self
            self.peripheral = peripheral.peripheral
            self.peripheral?.delegate = self
            
            return true
        }
        
        return false
    }
    
    /// Checks to see if an advertisement contains the XYO service UUID
    /// - Parameter peripheral: The device to check for the XYO service UUID.
    /// - Returns: If the device is advertising the XYO service UUID.
    private func checkForXyoUuid (_ peripheral: XYPeripheral) -> Bool {
        guard
            let services = peripheral.advertisementData?[CBAdvertisementDataServiceUUIDsKey] as? [CBUUID]
            else { return false }
        
        guard
            services.contains(CBUUID(string: XyoBluetoothDevice.uuid))
            else { return false }
        
        return true
    }
    
    /// Checks to see if an advertisement contains the major and minor of this iBeacon device in the name.
    /// - Parameter peripheral: The device to check for name.
    /// - Returns: If the device contains the proper name.
    private func checkForName (_ peripheral: XYPeripheral) -> Bool {
        guard
            let major = self.iBeacon?.major,
            let minor = self.iBeacon?.minor,
            let deviceName = peripheral.advertisementData?[CBAdvertisementDataLocalNameKey] as? String
            else {
                return false
        }
        
        let majorMinorName = XyoGattNameEncoder.encode(major: major, minor: minor)
        return deviceName == majorMinorName
    }
    
    /// Gets the first data that was sent to this device, since the client is allways the one initing, this will
    /// allways return nil
    /// - Returns: Returns the first data srecived through the pipe if not initing.
    public func getInitiationData() -> XyoAdvertisePacket? {
        // this is because we are allways a client
        return nil
    }
    
    /// Sends data to the peripheral and waits for a response if the waitForResponse flag is set.
    /// - Warning: This function is blocking while it waits for bluetooth calls.
    /// - Parameter data: The data to send to the other device at the end of the pipe
    /// - Parameter waitForResponse: Weather or not to wait for a response after sending
    /// - Returns: Will return the response from the other party, will return nil if there was an error or if
    /// waitForResponse was set to false.
    public func send(data: [UInt8], waitForResponse: Bool, completion: @escaping ([UInt8]?) -> ()) {
        print("SENDING: \(data.toHexString())")
        if (!chunkSend(bytes: data, characteristic: XyoService.pipe, sizeOfChunkSize: XyoObjectSize.FOUR)) {
            completion(nil)
            return
        }
        
        if (waitForResponse) {
            completion(waitForRead())
            return
        }
        
        completion(nil)
    }
    
    /// Sends data to the peripheral at the other end of the pipe, via the XYO pipe protocol.
    /// - Parameter bytes: The bytes to send to the other end of the pipe
    /// - Parameter characteristic: The charistic to chunk write to
    /// - Parameter sizeOfChunkSize: The number of bytes to prepend the size with when sening chunks
    /// - Warning: This function is blocking while it waits for bluetooth calls.
    /// - Returns: This function returns the success of the chunk send
    func chunkSend (bytes : [UInt8], characteristic: XYServiceCharacteristic, sizeOfChunkSize: XyoObjectSize) -> Bool {
        let sizeEncodedBytes = XyoBuffer()
        
        switch sizeOfChunkSize {
        case .ONE: sizeEncodedBytes.put(bits: UInt8(bytes.count + 1))
        case .TWO: sizeEncodedBytes.put(bits: UInt16(bytes.count + 2))
        case.FOUR: sizeEncodedBytes.put(bits: UInt32(bytes.count + 4))
        case.EIGHT: sizeEncodedBytes.put(bits: UInt64(bytes.count + 8))
        }
        
        sizeEncodedBytes.put(bytes: bytes)

        let chunks = XyoOutputStream.chunk(bytes: sizeEncodedBytes.toByteArray(), maxChunkSize: getMtu() - 3)
        
        for chunk in chunks {
            print("SENDING CHUNK \(chunk.toHexString())")
            let status = self.set(characteristic, value: XYBluetoothResult(data: Data(chunk)), withResponse: true)
            
            print("DONE SENDING CHUNK")
            
            // break the loop if there was an error
            if (status.error != nil) {
                return false
            }
        }
        
        return true
    }
    
    /// Waits for the next read request to come in, if one has allready come in before calling this function,
    /// it will return it.
    /// - Warning: This function is blocking while it waits for bluetooth calls.
    /// - Returns: This function returns what the divice on the other end of the pipe just sent.
    private func waitForRead () -> [UInt8]? {
        print("WAITING FOR READ")
        var latestPacket : [UInt8]? = inputStream.getOldestPacket()
        if (latestPacket == nil) {
            recivePromise = Promise<[UInt8]?>.pending().timeout(20)
            do {
                latestPacket = try await(recivePromise.unsafelyUnwrapped)
            } catch {
                print("TIMEOUT")
                // timeout has occored
                return nil
            }
        }
        
        inputStream.removePacket()
        
        print("READ ENTIRE \(latestPacket?.toHexString() ?? "--")")
        return latestPacket
    }
    
    /// This function terminates the bluetooth connection and should be called beetwen creating pipes.
    public func close() {
        disconnect()
    }
    
    public func getNetworkHuerestics() -> [XyoObjectStructure] {
        var toReturn = [XyoObjectStructure]()

        let pwr = self.iBeacon?.powerLevel

        if pwr != nil {
            let pwrTag = XyoObjectStructure.newInstance(schema: XyoSchemas.BLE_POWER_LEVEL, bytes: XyoBuffer().put(bits: pwr!))
            toReturn.append(pwrTag)
        }

        let unsignedRssi = UInt8(bitPattern: Int8(self.rssi))
        let rssiTag = XyoObjectStructure.newInstance(schema: XyoSchemas.RSSI, bytes: XyoBuffer().put(bits: (unsignedRssi)))

        toReturn.append(rssiTag)

        return toReturn
    }
    
    /// This function is called whenever a charisteristic is updated, and is how the XYO pipe recives data.
    /// This function will also add to the input stream, and resume a read promise if there is one existing.
    /// - Parameter serviceCharacteristic: The characteristic that is being updated, this should be the XYO serivce
    /// - Parameter value: The value that characteristic has been changed to (or notifyed of)
    public func update(for serviceCharacteristic: XYServiceCharacteristic, value: XYBluetoothResult) {
        if (!value.hasError && value.asByteArray != nil) {
            print("GOT NOTIFACTION \(value.asByteArray!.toHexString())")
            inputStream.addChunk(packet: value.asByteArray!)
            
            guard let donePacket = inputStream.getOldestPacket() else {
                return
            }
            
            recivePromise?.fulfill(donePacket)
        }
    }
}