USB-Meter / USB Meter / Model / BluetoothSerial.swift
c34c6eb 20 hours ago History
1 contributor
423 lines | 16.843kb
//
//  bluetoothSerial.swift
//  USB Meter
//
//  Created by Bogdan Timofte on 17/03/2020.
//  Copyright © 2020 Bogdan Timofte. All rights reserved.
//
//  https://github.com/hoiberg/HM10-BluetoothSerial-iOS
import CoreBluetooth

final class BluetoothSerial : NSObject, ObservableObject {
    
    enum AdministrativeState {
        case down
        case up
    }
    enum OperationalState: Int, Comparable {
        case peripheralNotConnected
        case peripheralConnectionPending
        case peripheralConnected
        case peripheralReady

        static func < (lhs: OperationalState, rhs: OperationalState) -> Bool {
            return lhs.rawValue < rhs.rawValue
        }
    }
    
    private var administrativeState = AdministrativeState.down
    private var operationalState = OperationalState.peripheralNotConnected {
        didSet {
            delegate?.opertionalStateChanged(to: operationalState)
        }
    }
    
    var macAddress: MACAddress
    private var manager: CBCentralManager
    private var radio: BluetoothRadio
    private(set) var rawRSSI: Int
    private var rssiSamples: [Int] = []
    private let rssiAveragingWindow = 3
    @Published private(set) var averageRSSI: Int
    private(set) var minRSSI: Int
    private(set) var maxRSSI: Int
    
    private var expectedResponseLength = 0
    private var wdTimer: Timer?
        
    var peripheral: CBPeripheral
    
    /// The characteristic used for writes on the connected peripheral.
    private var writeCharacteristic: CBCharacteristic?
    /// The characteristic used for notifications on the connected peripheral.
    private var notifyCharacteristic: CBCharacteristic?
    
    private var buffer = Data()
    
    weak var delegate: SerialPortDelegate?
    
    init( peripheral: CBPeripheral, radio: BluetoothRadio, with macAddress: MACAddress, managedBy manager: CBCentralManager, RSSI: Int ) {

        self.peripheral = peripheral
        self.macAddress = macAddress
        self.radio = radio
        self.manager = manager
        self.rawRSSI = RSSI
        self.rssiSamples = [RSSI]
        self.averageRSSI = RSSI
        self.minRSSI = RSSI
        self.maxRSSI = RSSI
        Timer.scheduledTimer(withTimeInterval: 3, repeats: true, block: {_ in
            if peripheral.state == .connected {
                peripheral.readRSSI()
            }
        })
        super.init()
        peripheral.delegate = self
    }

    func updateRSSI(_ value: Int) {
        rawRSSI = value
        rssiSamples.append(value)
        if rssiSamples.count > rssiAveragingWindow {
            rssiSamples.removeFirst()
        }
        let newAverage = rssiSamples.reduce(0, +) / rssiSamples.count
        minRSSI = Swift.min(minRSSI, newAverage)
        maxRSSI = Swift.max(maxRSSI, newAverage)
        if newAverage != averageRSSI {
            averageRSSI = newAverage
        }
    }

    private func resetCommunicationState(reason: String, clearCharacteristics: Bool) {
        if wdTimer != nil {
            track("Reset communication state (\(reason)) - invalidating watchdog")
        }
        wdTimer?.invalidate()
        wdTimer = nil

        if expectedResponseLength != 0 || !buffer.isEmpty {
            track("Reset communication state (\(reason)) - expected: \(expectedResponseLength), buffered: \(buffer.count)")
        }
        expectedResponseLength = 0
        buffer.removeAll()

        if clearCharacteristics {
            writeCharacteristic = nil
            notifyCharacteristic = nil
        }
    }

    private func forceNotConnected(reason: String, clearCharacteristics: Bool = true) {
        resetCommunicationState(reason: reason, clearCharacteristics: clearCharacteristics)
        guard operationalState != .peripheralNotConnected else {
            return
        }
        operationalState = .peripheralNotConnected
    }
    
    func connect() {
        administrativeState = .up
        guard manager.state == .poweredOn else {
            track("Connect requested for '\(peripheral.identifier)' but central state is \(manager.state)")
            forceNotConnected(reason: "connect() while central is \(manager.state)")
            return
        }
        if operationalState < .peripheralConnected {
            resetCommunicationState(reason: "connect()", clearCharacteristics: true)
            operationalState = .peripheralConnectionPending
            track("Connect called for '\(peripheral.identifier)' while peripheral state is \(peripheral.state.rawValue)")
            manager.connect(peripheral, options: nil)
        } else {
            track("Peripheral allready connected: \(operationalState)")
        }
    }
    
    func disconnect() {
        administrativeState = .down
        resetCommunicationState(reason: "disconnect()", clearCharacteristics: true)
        if peripheral.state != .disconnected || operationalState != .peripheralNotConnected {
            track("Disconnect requested for '\(peripheral.identifier)' while peripheral state is \(peripheral.state.rawValue)")
            guard manager.state == .poweredOn else {
                track("Skipping central cancel for '\(peripheral.identifier)' because central state is \(manager.state)")
                forceNotConnected(reason: "disconnect() while central is \(manager.state)", clearCharacteristics: false)
                return
            }
            manager.cancelPeripheralConnection(peripheral)
        }
    }
        
    /**
     Send data
     
     - parameter data: Data to be sent.
     - parameter expectedResponseLength: Optional If message sent require a respnse the length for that response must be provideed. Incomming data will be buffered before calling delegate.didReceiveData
     */
    func write(_ data: Data, expectedResponseLength: Int = 0) {
        //track("\(self.expectedResponseLength)")
        //track(data.hexEncodedStringValue)
        guard operationalState == .peripheralReady else {
            track("Guard: \(operationalState)")
            return
        }
        guard self.expectedResponseLength == 0 else {
            track("Guard: \(self.expectedResponseLength)")
            return
        }
        
        self.expectedResponseLength = expectedResponseLength
        
//        track("Sending...")
        guard let writeCharacteristic else {
            track("Missing write characteristic for \(radio)")
            self.expectedResponseLength = 0
            return
        }

        let writeType: CBCharacteristicWriteType = writeCharacteristic.properties.contains(.writeWithoutResponse) ? .withoutResponse : .withResponse
        peripheral.writeValue(data, for: writeCharacteristic, type: writeType)
//        track("Sent!")
        if self.expectedResponseLength != 0 {
            setWDT()
        }
    }
        
    func connectionEstablished () {
        resetCommunicationState(reason: "connectionEstablished()", clearCharacteristics: true)
        track("Connection established for '\(peripheral.identifier)'")
        rssiSamples = [rawRSSI]
        averageRSSI = rawRSSI
        minRSSI = rawRSSI
        maxRSSI = rawRSSI
        operationalState = .peripheralConnected
        peripheral.discoverServices(BluetoothRadioServicesUUIDS[radio])
    }
    
    func connectionClosed () {
        track("Connection closed for '\(peripheral.identifier)' while peripheral state is \(peripheral.state.rawValue)")
        resetCommunicationState(reason: "connectionClosed()", clearCharacteristics: true)
        rssiSamples = [rawRSSI]
        averageRSSI = rawRSSI
        minRSSI = rawRSSI
        maxRSSI = rawRSSI
        operationalState = .peripheralNotConnected
    }

    func centralStateChanged(to newState: CBManagerState) {
        switch newState {
        case .poweredOn:
            if administrativeState == .up,
               operationalState == .peripheralNotConnected,
               peripheral.state == .disconnected {
                track("Central returned to poweredOn. Restoring connection to '\(peripheral.identifier)'")
                connect()
            }
        case .poweredOff, .resetting, .unauthorized, .unknown, .unsupported:
            if operationalState != .peripheralNotConnected || expectedResponseLength != 0 || !buffer.isEmpty {
                track("Central changed to \(newState). Forcing '\(peripheral.identifier)' to not connected.")
            }
            forceNotConnected(reason: "centralStateChanged(\(newState))")
        @unknown default:
            track("Central changed to an unknown state. Forcing '\(peripheral.identifier)' to not connected.")
            forceNotConnected(reason: "centralStateChanged(@unknown default)")
        }
    }

    func setWDT() {
        wdTimer?.invalidate()
        wdTimer = Timer.scheduledTimer(withTimeInterval: 10, repeats: false, block: {_ in
            track("Response timeout. Expected: \(self.expectedResponseLength) - buffer: \(self.buffer.count)")
            self.expectedResponseLength = 0
            self.disconnect()
        })
    }

    private func refreshOperationalStateIfReady() {
        guard let notifyCharacteristic, let writeCharacteristic else {
            return
        }
        guard notifyCharacteristic.isNotifying else {
            track("Waiting for notifications on '\(notifyCharacteristic.uuid)' before marking peripheral ready")
            return
        }
        track("Peripheral ready with notify '\(notifyCharacteristic.uuid)' and write '\(writeCharacteristic.uuid)'")
        operationalState = .peripheralReady
    }

    private func updateBT18Characteristics(for service: CBService) {
        for characteristic in service.characteristics ?? [] {
            switch characteristic.uuid {
            case CBUUID(string: "FFE1"):
                if characteristic.properties.contains(.notify) || characteristic.properties.contains(.indicate) {
                    peripheral.setNotifyValue(true, for: characteristic)
                    notifyCharacteristic = characteristic
                }
                if writeCharacteristic == nil &&
                    (characteristic.properties.contains(.write) || characteristic.properties.contains(.writeWithoutResponse)) {
                    writeCharacteristic = characteristic
                }
            case CBUUID(string: "FFE2"):
                if characteristic.properties.contains(.write) || characteristic.properties.contains(.writeWithoutResponse) {
                    // DX-BT18 documents FFE2 as the preferred write-only endpoint when present.
                    writeCharacteristic = characteristic
                }
            default:
                track ("Unexpected characteristic discovered: '\(characteristic)'")
            }
        }
        refreshOperationalStateIfReady()
    }

    private func updatePW0316Characteristics(for service: CBService) {
        for characteristic in service.characteristics ?? [] {
            switch characteristic.uuid {
            case CBUUID(string: "FFE9"): // TX from BLE side into UART
                writeCharacteristic = characteristic
            case CBUUID(string: "FFE4"): // RX notifications from UART side into BLE
                peripheral.setNotifyValue(true, for: characteristic)
                notifyCharacteristic = characteristic
            default:
                track ("Unexpected characteristic discovered: '\(characteristic)'")
            }
        }
        refreshOperationalStateIfReady()
    }

}

//  MARK:   CBPeripheralDelegate
extension BluetoothSerial : CBPeripheralDelegate {

    //  MARK:   didReadRSSI
    func peripheral(_ peripheral: CBPeripheral, didReadRSSI RSSI: NSNumber, error: Error?) {
        if error != nil {
            track( "Error: \(error!)" )
        }
        updateRSSI(RSSI.intValue)
    }
    
    //  MARK:   didDiscoverServices
    func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
        track("\(String(describing: peripheral.services))")
        if error != nil {
            track( "Error: \(error!)" )
        }
        switch radio {
        case .BT18:
            for service in peripheral.services! {
                switch service.uuid {
                case CBUUID(string: "FFE0"):
                    peripheral.discoverCharacteristics(Array(Set((BluetoothRadioNotifyUUIDs[radio] ?? []) + (BluetoothRadioWriteUUIDs[radio] ?? []))), for: service)
                default:
                    track ("Unexpected service discovered: '\(service)'")
                }
            }
        case .PW0316:
            for service in peripheral.services! {
                switch service.uuid {
                case CBUUID(string: "FFE0"):
                    peripheral.discoverCharacteristics(BluetoothRadioNotifyUUIDs[radio], for: service)
                case CBUUID(string: "FFE5"):
                    peripheral.discoverCharacteristics(BluetoothRadioWriteUUIDs[radio], for: service)
                default:
                    track ("Unexpected service discovered: '\(service)'")
                }
            }
        default:
            track("Radio \(radio) Not Implemented!")
        }
    }
    
    //  MARK:   didDiscoverCharacteristicsFor
    func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {
        if error != nil {
            track( "Error: \(error!)" )
        }
        track("\(String(describing: service.characteristics))")
        switch radio {
        case .BT18:
            updateBT18Characteristics(for: service)
        case .PW0316:
            updatePW0316Characteristics(for: service)
        default:
            track("Radio \(radio) Not Implemented!")
        }
    }

    func peripheral(_ peripheral: CBPeripheral, didUpdateNotificationStateFor characteristic: CBCharacteristic, error: Error?) {
        if error != nil {
            track("Error updating notification state for '\(characteristic.uuid)': \(error!)")
        }
        track("Notification state updated for '\(characteristic.uuid)' - isNotifying: \(characteristic.isNotifying)")
        if characteristic.uuid == notifyCharacteristic?.uuid {
            refreshOperationalStateIfReady()
        }
    }
    
    //  MARK:   didUpdateValueFor
    func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) {
//        track("")
        if error != nil {
            track( "Error: \(error!)" )
        }
        let incomingData = characteristic.value ?? Data()
        guard !incomingData.isEmpty else {
            track("Received empty update for '\(characteristic.uuid)'")
            return
        }
        guard expectedResponseLength > 0 else {
            if !buffer.isEmpty {
                track("Dropping unsolicited data (\(incomingData.count) bytes) for '\(characteristic.uuid)' with residual buffer: \(buffer.count)")
                buffer.removeAll()
            } else {
                track("Dropping unsolicited data (\(incomingData.count) bytes) for '\(characteristic.uuid)' while no response is expected")
            }
            return
        }

        let previousBufferCount = buffer.count
        buffer.append(incomingData)
//        track("\n\(buffer.hexEncodedStringValue)")
        switch buffer.count {
        case let x where x < expectedResponseLength:
            setWDT()
            //track("buffering")
            break;
        case let x where x == expectedResponseLength:
            //track("buffer ready")
            wdTimer?.invalidate()
            expectedResponseLength = 0
            delegate?.didReceiveData(buffer)
            buffer.removeAll()
        case let x where x > expectedResponseLength:
            // MARK: De unde stim că asta a fost tot? Probabil o deconectare ar rezolva problema
            let expectedLength = expectedResponseLength
            track("Buffer overflow for '\(characteristic.uuid)'. Chunk: \(incomingData.count), previous buffer: \(previousBufferCount), total: \(x), expected: \(expectedLength). Disconnecting to recover.")
            disconnect()
        default:
            track("This is not possible!")
        }
    }

    func peripheral(_ peripheral: CBPeripheral, didWriteValueFor characteristic: CBCharacteristic, error: Error?) {
        if error != nil { track( "Error: \(error!)" ) }
    }
    
    func peripheralIsReady(toSendWriteWithoutResponse peripheral: CBPeripheral) {
        //track("")
    }
    
}

// MARK: SerialPortDelegate
protocol SerialPortDelegate: AnyObject {
    // MARK: State Changed
    func opertionalStateChanged( to newOperationalState: BluetoothSerial.OperationalState )
    // MARK: Data was received
    func didReceiveData(_ data: Data)
}

// MARK: SerialPortDelegate Optionals
extension SerialPortDelegate {
}