// // 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 @Published var RSSI: Int private var expectedResponseLength = 0 private var wdTimer: Timer? var peripheral: CBPeripheral /// The characteristic 0xFFE1 we need to write to, of the connectedPeripheral private var writeCharacteristic: 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.RSSI = RSSI Timer.scheduledTimer(withTimeInterval: 3, repeats: true, block: {_ in if peripheral.state == .connected { peripheral.readRSSI() } }) super.init() peripheral.delegate = self } func connect() { administrativeState = .up if operationalState < .peripheralConnected { operationalState = .peripheralConnectionPending track("Connect caled") manager.connect(peripheral, options: nil) } else { track("Peripheral allready connected: \(operationalState)") } } func disconnect() { if operationalState >= .peripheralConnected { manager.cancelPeripheralConnection(peripheral) buffer.removeAll() } } /** 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...") switch radio { case .BT18 : peripheral.writeValue(data, for: writeCharacteristic!, type: .withoutResponse) case .PW0316 : peripheral.writeValue(data, for: writeCharacteristic!, type: .withResponse) default: track("Radio \(radio) Not Implemented!") } // track("Sent!") if self.expectedResponseLength != 0 { setWDT() } } func connectionEstablished () { track("") operationalState = .peripheralConnected peripheral.discoverServices(BluetoothRadioServicesUUIDS[radio]) } func connectionClosed () { track("") operationalState = .peripheralNotConnected expectedResponseLength = 0 writeCharacteristic = nil } 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() }) } } // MARK: CBPeripheralDelegate extension BluetoothSerial : CBPeripheralDelegate { // MARK: didReadRSSI func peripheral(_ peripheral: CBPeripheral, didReadRSSI RSSI: NSNumber, error: Error?) { if error != nil { track( "Error: \(error!)" ) } self.RSSI = 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: // discover the 0xFFE1 characteristic for all services (though there should only be one(service)) for service in peripheral.services! { switch service.uuid { case CBUUID(string: "FFE0"): // check whether the characteristic we're looking for (0xFFE1) is present - just to be sure peripheral.discoverCharacteristics([CBUUID(string: "FFE1")], for: service) default: track ("Unexpected service discovered: '\(service)'") } } case .PW0316: for service in peripheral.services! { //track("\(service.uuid)") switch service.uuid { case CBUUID(string: "FFE0"): //track("\(service.uuid)") // check whether the characteristic we're looking for (0xFFE4) is present - just to be sure peripheral.discoverCharacteristics([CBUUID(string: "FFE4")], for: service) break case CBUUID(string: "FFE5"): //track("\(service.uuid)") // check whether the characteristic we're looking for (0xFFE9) is present - just to be sure peripheral.discoverCharacteristics([CBUUID(string: "FFE9")], for: service) default: track ("Unexpected service discovered: '\(service)'") } } break; 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: // check whether the characteristic we're looking for (0xFFE1) is present - just to be sure for characteristic in service.characteristics! { //track(characteristic.debugDescription) switch characteristic.uuid { case CBUUID(string: "FFE1"): // subscribe to this value (so we'll get notified when there is serial data for us..) peripheral.setNotifyValue(true, for: characteristic) // keep a reference to this characteristic so we can write to it writeCharacteristic = characteristic // Change State operationalState = .peripheralReady default: track ("Unexpected characteristic discovered: '\(characteristic)'") } } case .PW0316: for characteristic in service.characteristics! { switch characteristic.uuid { case CBUUID(string: "FFE9"): //TX //track("characteristic FFE9: \(characteristic.properties & CBCharacteristicProperties.write)") writeCharacteristic = characteristic operationalState = .peripheralReady case CBUUID(string: "FFE4"): //RX peripheral.setNotifyValue(true, for: characteristic) //track("characteristic FFE4: \(characteristic.properties)") default: track ("Unexpected characteristic discovered: '\(characteristic)'") } } break; default: track("Radio \(radio) Not Implemented!") } } // MARK: didUpdateValueFor func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) { // track("") if error != nil { track( "Error: \(error!)" ) } buffer.append( characteristic.value ?? Data() ) // 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 wdTimer?.invalidate() expectedResponseLength = 0 buffer.removeAll() track("Buffer Overflow") 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 { }