1 contributor
//
// 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
}
}
func connect() {
administrativeState = .up
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)")
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 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 let error = error { 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 {
}