We are implementing a BLE user device that communicates after connecting using our own iOS application.
We exchange data by reading and writing the characteristics of the BLE device
userInfo.peripheral.writeValue(segments[currentSegment], for: userInfo.characteristic, type: .withoutResponse)
After the first successful connection and data exchange using our iOS application, we experience this behavior.
This happens on some iOS devices, and we did not find any similarities for devices that seem to have a problem (for example, iPhone 6s iOS 11.2.1 and iPhone 5s 10.2.2)
When this behavior occurs (the BLE device continues to appear in the list of paired devices and continues to try to connect) affects the flow and reliability of our iOS application.
All connections go through this manager.
import Foundation
import CoreBluetooth
enum BluetoothManagerResponse {
case success(result:String)
case fail(error:String)
case cancelled
}
enum SendEncryption {
case encrypted(publicKey:String)
case notEncrypted
}
enum DeviceType {
case newDevice
case connectedDevices(device:BluetoothManagerConnectedDevice)
}
typealias BluetoothManagerComlpetion = ((_ response:BluetoothManagerResponse,_ connectedDevice:BluetoothManagerConnectedDevice)->Void)?
typealias BluetoothManagerConnectedDevice = (peripheral:CBPeripheral,characteristic:CBCharacteristic)?
class BluetoothManager:NSObject {
var manager: CBCentralManager?
var peripherals = [CBPeripheral]()
var connectedDevice:BluetoothManagerConnectedDevice = nil
static let shared = BluetoothManager()
fileprivate var stx:String {
return String(Character.init(UnicodeScalar(0002)))
}
fileprivate var etx:String {
return String(Character.init(UnicodeScalar(0003)))
}
var minimumDistance:Int?
var currentSegment = 0
var segments:[Data]? {
didSet {
currentSegment = 0
}
}
var completion:BluetoothManagerComlpetion = nil
private func encrypt(data:String , withPublicKey key:String) -> String? {
...
}
var rssiUpdate:((_ rssi:Int) -> Void)?
func startGettingRSSIUpdate(callback:@escaping (_ rssi:Int) -> Void) {
self.rssiUpdate = callback
}
func send(data:String?,to deviceType:DeviceType,withEncryption type:SendEncryption = .notEncrypted,andMinimumDistance distance:Int? = nil, and completion:BluetoothManagerComlpetion) {
self.minimumDistance = distance
hasCanceled = false
let selector = #selector(deviceNotFound(sender:))
self.notFoundTimer = Timer.scheduledTimer(timeInterval: 60, target: self, selector: selector, userInfo: nil, repeats: false)
self.completion = completion
if let data = data {
switch type {
case .encrypted(let publicKey):
let encrypted = self.encrypt(data: data, withPublicKey: publicKey)
self.segments = encrypted?.hexadecimalString?.components(withLength: 20).flatMap({$0.hexadecimalData}) ?? []
case .notEncrypted:
self.segments = data.hexadecimalString?.components(withLength: 20).flatMap({$0.hexadecimalData}) ?? []
}
}
else {
self.segments = data?.hexadecimalString?.components(withLength: 20).flatMap({$0.hexadecimalData}) ?? []
}
switch deviceType {
case .newDevice:
connectedDevice = nil
manager = CBCentralManager(delegate: self, queue: nil)
case .connectedDevices(let device):
guard let device = device else {
completeWithResponse(response: .fail(error: Messages.Bluetooth.deviceDisconnected))
return
}
switch device.peripheral.state {
case .connected:
device.peripheral.setNotifyValue(true, for: device.characteristic)
default:
completeWithResponse(response: .fail(error: Messages.Bluetooth.deviceDisconnected))
}
}
}
var hasCanceled:Bool = false
func disconnect() {
completion = nil
notFoundTimer?.invalidate()
notFoundTimer = nil
manager?.stopScan()
guard let connectedDevice = self.connectedDevice else { return }
manager?.cancelPeripheralConnection(connectedDevice.peripheral)
connectedDevice.peripheral.setNotifyValue(false, for: connectedDevice.characteristic)
self.connectedDevice = nil
}
func cancel() {
hasCanceled = true
completeWithResponse(response: .cancelled)
completion = nil
}
func completeWithResponse(response:BluetoothManagerResponse) {
notFoundTimer?.invalidate()
notFoundTimer = nil
manager?.stopScan()
self.completion?(response,connectedDevice)
}
var notFoundTimer:Timer?
func deviceNotFound(sender:Timer) {
completeWithResponse(response: .fail(error: Messages.Bluetooth.notFound))
}
var rssiValues = [Int]()
var resetRssiValues = true
var receivedData:String? {
didSet {
guard let receivedData = receivedData,receivedData.contains("\u{03}") else {
return
}
let responseString = String(receivedData.dropLast().dropFirst())
self.completeWithResponse(response: .success(result: responseString))
}
}
let easedProximity = EasedValue()
fileprivate func convertRSSItoProximity(rssi:Int) -> Int? {
let absRssi = Swift.abs(rssi)
easedProximity.setValue(value: CGFloat(absRssi))
easedProximity.update()
if let result = easedProximity.value {
return Int(result * -1.0)
}
return nil
}
}
extension BluetoothManager: CBCentralManagerDelegate {
func centralManagerDidUpdateState(_ central: CBCentralManager) {
switch central.state {
case .poweredOn:
central.scanForPeripherals(withServices: nil, options: [CBCentralManagerScanOptionAllowDuplicatesKey:true])
case .poweredOff:
self.completeWithResponse(response:.fail(error: Messages.Bluetooth.poweredOff))
case .resetting:
self.completeWithResponse(response:.fail(error: Messages.Bluetooth.resetting))
case .unauthorized:
self.completeWithResponse(response:.fail(error: Messages.Bluetooth.unauthorized))
case .unknown:
self.completeWithResponse(response:.fail(error: Messages.Bluetooth.unknown))
case .unsupported:
self.completeWithResponse(response:.fail(error: Messages.Bluetooth.unsupported))
}
}
func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
peripheral.delegate = self
peripheral.discoverServices(nil)
}
func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) {
guard let error = error else {
self.completeWithResponse(response:.fail(error: Messages.Bluetooth.unknown))
return
}
self.completeWithResponse(response:.fail(error: error.localizedDescription))
}
func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
func connect(peripheral: CBPeripheral) {
print("Should connect")
peripherals.append(peripheral)
central.connect(peripheral, options: nil)
central.stopScan()
easedProximity.reset()
}
guard let name = peripheral.name , name.isMod10 else {return}
print(name)
if let distance = self.minimumDistance {
let rssi = (RSSI as? Int) ?? 0
let value = self.convertRSSItoProximity(rssi: rssi)
if let value = value {
self.rssiUpdate?(value)
}
guard let _value = value,_value >= distance else{ return }
}
connect(peripheral: peripheral)
}
}
extension BluetoothManager:CBPeripheralDelegate {
func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
guard let error = error else {
guard let service = peripheral.services?.first else {
self.completeWithResponse(response:.fail(error: Messages.Bluetooth.noService))
return
}
peripheral.discoverCharacteristics(nil, for: service)
return
}
self.completeWithResponse(response:.fail(error: error.localizedDescription))
}
func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) {
print("OS Disconect : \(String(describing: error?.localizedDescription))")
}
func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {
guard let error = error else {
guard let characteristic = service.characteristics?.first else {
self.completeWithResponse(response:.fail(error: Messages.Bluetooth.noCharacteristic))
return
}
connectedDevice = (peripheral:peripheral,characteristic:characteristic)
peripheral.setNotifyValue(true, for: characteristic)
return
}
self.completeWithResponse(response:.fail(error: error.localizedDescription))
}
func peripheral(_ peripheral: CBPeripheral, didUpdateNotificationStateFor characteristic: CBCharacteristic, error: Error?) {
guard characteristic.isNotifying else {
manager?.cancelPeripheralConnection(peripheral)
return
}
guard let error = error else {
guard let segments = segments , segments.count > 0 else {
return
}
let selector = #selector(send(sender:))
Timer.scheduledTimer(timeInterval: 0.05, target: self, selector: selector, userInfo: (peripheral:peripheral,characteristic:characteristic), repeats: true)
return
}
self.completeWithResponse(response:.fail(error: error.localizedDescription))
}
internal func send(sender:Timer) {
if let userInfo = sender.userInfo as? (peripheral:CBPeripheral,characteristic:CBCharacteristic),
let segments = segments,
currentSegment >= 0 && currentSegment < segments.count && segments.count > 0
{
userInfo.peripheral.writeValue(segments[currentSegment], for: userInfo.characteristic, type: .withoutResponse)
}
else {
sender.invalidate()
}
currentSegment = currentSegment + 1
}
func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) {
guard let error = error else {
guard let receivedData = characteristic.value?.toString else {
self.completeWithResponse(response:.fail(error: Messages.Bluetooth.invalidReadData))
return
}
if receivedData.first == "\u{02}" {
self.receivedData = nil
}
self.receivedData = (self.receivedData ?? "") + receivedData
return
}
self.completeWithResponse(response:.fail(error: error.localizedDescription))
}
}