iOS - BLE peripherals aren't scanned on background mode - ios

I enabled to “Uses Bluetooth LE accessories” and “Acts as a Bluetooth LE accessory” in “Background Modes” settings, and these are added in Info.plist.
 and also I allowed my app to access “Bluetooth sharing”.

And my app does scan BLE peripherals well on foreground mode,
 but when it enter background mode, it doesn’t scan anything no more. If it enter foreground mode again, it does scan well again.
More specifically, centralManagerDidUpdateState(_ central: CBCentralManager) always does work well on any modes. but didDiscover doesn't work on background mode.
I want to BLE scanning does work well on any modes and I think I have done everything I can.
Relevant Code
BLEManager.swift
import UIKit
import CoreBluetooth
final class BLEManager: NSObject, CBCentralManagerDelegate, CBPeripheralManagerDelegate {
static let shared: BLEManager = .init()
var centralManager: CBCentralManager?
var peripheralManager: CBPeripheralManager?
var scannedPeripherals: [CBPeripheral] = .init()
var confirmedPeripheralStates: [String: CBPeripheralState] = .init()
override init() {
super.init()
self.centralManager = .init(delegate: self, queue: nil)
self.peripheralManager = .init(delegate: self, queue: nil)
}
func centralManagerDidUpdateState(_ central: CBCentralManager) {
switch central.state {
case .poweredOn:
self.startScan()
default:
central.stopScan()
}
}
func peripheralManagerDidUpdateState(_ peripheral: CBPeripheralManager) {
print("[BLE] state: \(peripheral.state.rawValue)")
}
func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
self.scannedPeripherals.appendAsUnique(peripheral)
central.connect(peripheral)
self.confirm(peripheral)
}
func startScan() {
guard let central = self.centralManager else {
return
}
let isBackgroundMode: Bool = (UIApplication.shared.applicationState == .background)
let cbuuids: [CBUUID] = self.scannedPeripherals.map { CBUUID(nsuuid: $0.identifier) }
central.stopScan()
central.scanForPeripherals(withServices: isBackgroundMode ? cbuuids : nil)
}
func confirm(_ peripheral: CBPeripheral) {
let uuid: String = peripheral.identifier.uuidString
// Prevent duplicate logging.
if peripheral.state != self.confirmedPeripheralStates[uuid] {
self.log(peripheral)
}
self.confirmedPeripheralStates[uuid] = peripheral.state
}
func log(_ peripheral: CBPeripheral, completion: (() -> Void)? = nil) {
guard let name = peripheral.name,
let state = self.getPeripheralStateAsString(peripheral.state) else {
return
}
print("[BLE] \(name) was \(state) on \(self.getApplicationStateAsString())")
}
}
extension BLEManager {
func getApplicationStateAsString() -> String {
switch UIApplication.shared.applicationState {
case .active:
return "active"
case .inactive:
return "inactive"
case .background:
return "background"
}
}
func getPeripheralStateAsString(_ state: CBPeripheralState) -> String? {
switch state {
case .disconnected:
return "disconnected"
case .connecting:
return "connecting"
case .connected:
return "connected"
default:
return nil
}
}
}
extension Array where Element: Equatable {
mutating func appendAsUnique(_ element: Element) {
if let index = self.index(where: { $0 == element }) {
self.remove(at: index)
}
self.append(element)
}
}
AppDelegate.swift
func applicationDidEnterBackground(_ application: UIApplication) {
// Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
// If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
BLEManager.shared.startScan()
}
Did I miss anything?

Related

CBPeripheral services are not rediscovered properly after change

I have an iOS Swift 5 project on which CBCentralManager is used to connect to a BLE device. There is a write CBCharacteristic which should be used to write a certain value from the iOS device and after a successful write, the device changes its services adding a new service into its GATT table. CoreBluetooth seems to not recognize the updated services. The BLE device is paired with iOS.
Things tried and what happened:
If you don't implement didModifyServices from CBPeripheralDelegate, CoreBluetooth complains that the services changed but you don't have a method to handle these changes
If you implement didModifyServices, it never gets called and the above message never gets shown
Tried manually rediscovering services after a successful write but the CBPerpiheral returns the old service only, despite the BLE device actually having another one now in the GATT table, as if CoreBluetooth caches the old services and doesn't actually rediscover them
Tried disconnecting and discarding the CBPeripheral instance, in order to rediscover it and reconnect - in this case, the peripheral never gets rediscovered again after disconnecting.
Any ideas what could be possibly done to be able to rediscover a new service?
Relevant CoreBluetooth code:
enum BluetoothStateChange
{
case unknown
case unsupported
case unauthorized
case poweredOff
}
class BluetoothCentralService: NSObject
{
var onStateChange: ((BluetoothStateChange) -> Void)?
var onServicesScannedPeripheralsUpdate: ((_ peripherals: [UUID]) -> Void)?
var onCharacteristicsDiscovered: ((_ peripheral: UUID, _ service: CBUUID, _ characteristics: [CBUUID]) -> Void)?
var onCharacteristicWriteFinished: ((_ peripheral: UUID, _ characteristic: CBUUID, _ isSuccessful: Bool) -> Void)?
var onCharacteristicReadFinished: ((_ peripheral: UUID, _ characteristic: CBUUID, _ value: Data?) -> Void)?
private var connectingPeripherals = Set<CBPeripheral>()
private var connectedPeripherals = Set<CBPeripheral>()
private var servicesDiscoveredPeripherals = Set<CBPeripheral>()
private var ignoredPeripherals = Set<CBPeripheral>()
private let central: CBCentralManager
private let logger: LoggerProtocol
var rememberedDeviceUUID: UUID?
init(central: CBCentralManager, logger: LoggerProtocol, rememberedDeviceUUID: UUID? = nil)
{
self.central = central
self.logger = logger
self.rememberedDeviceUUID = rememberedDeviceUUID
super.init()
central.delegate = self
}
func ignore(_ uuid: UUID)
{
guard
let peripheral = connectedPeripherals.first(where: { $0.identifier == uuid })
?? servicesDiscoveredPeripherals.first(where: { $0.identifier == uuid })
else {
return
}
ignoredPeripherals.insert(peripheral)
servicesDiscoveredPeripherals.remove(peripheral)
central.cancelPeripheralConnection(peripheral)
}
func services(for uuid: UUID) -> [CBUUID]
{
return servicesDiscoveredPeripherals.first { $0.identifier == uuid }?.services?.map { $0.uuid } ?? []
}
func discoverCharacteristics(for peripheralUuid: UUID, service uuid: CBUUID)
{
guard
let peripheral = servicesDiscoveredPeripherals.first(where: { $0.identifier == peripheralUuid }),
let service = peripheral.service(with: uuid)
else {
return
}
peripheral.discoverCharacteristics(nil, for: service)
}
func write(
to peripheralUuid: UUID,
service serviceUuid: CBUUID,
characteristic characteristicUuid: CBUUID,
value: Data,
type: CBCharacteristicWriteType
){
guard
let peripheral = servicesDiscoveredPeripherals.first(where: { $0.identifier == peripheralUuid }),
let service = peripheral.service(with: serviceUuid),
let characteristic = service.characteristics?.first(where: { $0.uuid == characteristicUuid })
else {
return
}
peripheral.writeValue(value, for: characteristic, type: type)
}
func read(from peripheralUuid: UUID, service serviceUuid: CBUUID, characteristic characteristicUuid: CBUUID)
{
guard
let peripheral = servicesDiscoveredPeripherals.first(where: { $0.identifier == peripheralUuid }),
let service = peripheral.service(with: serviceUuid),
let characteristic = service.characteristics?.first(where: { $0.uuid == characteristicUuid })
else {
return
}
peripheral.readValue(for: characteristic)
}
}
extension BluetoothCentralService: CBCentralManagerDelegate
{
func centralManagerDidUpdateState(_ central: CBCentralManager)
{
switch central.state
{
case .resetting:
break
case .unknown:
onStateChange?(.unknown)
case .unsupported:
onStateChange?(.unsupported)
case .unauthorized:
onStateChange?(.unauthorized)
case .poweredOff:
onStateChange?(.poweredOff)
connectedPeripherals = []
servicesDiscoveredPeripherals = []
ignoredPeripherals = []
onServicesScannedPeripheralsUpdate?([])
case .poweredOn:
scanRemembered()
default:
logger.log(.warning, "\(#function): Unhandled state type.")
}
}
private func scanRemembered()
{
guard
let uuid = rememberedDeviceUUID,
let peripheral = central.retrievePeripherals(withIdentifiers: [uuid]).first
else {
scanConnected(); return
}
connect(peripheral)
}
private func scanConnected()
{
let connected = central.retrieveConnectedPeripherals(withServices: [])
connectedPeripherals.formUnion(connected)
connectedPeripherals.forEach { $0.discoverServices(nil) }
scanNew()
}
private func scanNew()
{
central.scanForPeripherals(withServices: nil)
}
private func connect(_ peripheral: CBPeripheral)
{
guard
!ignoredPeripherals.contains(peripheral),
!connectingPeripherals.contains(peripheral),
!connectedPeripherals.contains(peripheral),
!servicesDiscoveredPeripherals.contains(peripheral)
else {
return
}
connectingPeripherals.insert(peripheral)
peripheral.delegate = self
central.connect(peripheral)
}
func centralManager(
_ central: CBCentralManager,
didDiscover peripheral: CBPeripheral,
advertisementData: [String : Any],
rssi RSSI: NSNumber
){
guard
!ignoredPeripherals.contains(peripheral),
!connectingPeripherals.contains(peripheral),
!connectedPeripherals.contains(peripheral),
!servicesDiscoveredPeripherals.contains(peripheral)
else {
return
}
connect(peripheral)
}
func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral)
{
connectingPeripherals.remove(peripheral)
connectedPeripherals.insert(peripheral)
peripheral.discoverServices(nil)
}
func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?)
{
connectedPeripherals.remove(peripheral)
}
}
extension BluetoothCentralService: CBPeripheralDelegate
{
func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?)
{
guard error == nil else { return }
servicesDiscoveredPeripherals.insert(peripheral)
connectedPeripherals.remove(peripheral)
onServicesScannedPeripheralsUpdate?(servicesDiscoveredPeripherals.map { $0.identifier })
}
func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?)
{
onCharacteristicsDiscovered?(
peripheral.identifier,
service.uuid,
service.characteristics?.compactMap { $0.uuid } ?? []
)
}
func peripheral(_ peripheral: CBPeripheral, didWriteValueFor characteristic: CBCharacteristic, error: Error?)
{
onCharacteristicWriteFinished?(peripheral.identifier, characteristic.uuid, error == nil)
}
func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?)
{
onCharacteristicReadFinished?(peripheral.identifier, characteristic.uuid, characteristic.value)
}
func peripheral(_ peripheral: CBPeripheral, didModifyServices invalidatedServices: [CBService])
{
// This never gets called if it's actually implemented here!
}
}

Swift: how to stop exposing the delegate methods

WorkAround:
I am creating a utility class let's say BLEScanManager, which is responsible to scan nearby BLE devices. The only job of the utility class is to scan BLE devices and make a list of it.
The other classes can create an object of this BLEScanManager and get an array of BLE devices, like [Bluetooth] (here Bluetooth is a custom modal class).
Now to scan BLE devices, I have created the extension of BLEScanManager in the same class and override its delegate methods like below:
import UIKit
import CoreBluetooth
protocol BLEScanDelegate {
func reloadDeviceList()
}
internal class BLEScanManager: NSObject {
private var centralManager: CBCentralManager?
var devices : [Bluetooth] = []
var delegate: BLEScanDelegate?
override init() {
super.init()
centralManager = CBCentralManager(delegate: self, queue: .main)
}
// MARK:- Custom methods
func isScanning() -> Bool {
return centralManager?.isScanning ?? false
}
func stopScanning() {
centralManager?.stopScan()
}
func startScanning() {
devices.removeAll()
let options: [String: Any] = [CBCentralManagerScanOptionAllowDuplicatesKey:
NSNumber(value: false)]
centralManager?.scanForPeripherals(withServices: nil, options: options)
}
}
extension BLEScanManager : CBCentralManagerDelegate {
func centralManagerDidUpdateState(_ central: CBCentralManager) {
switch central.state {
case .unknown: print("central.state is .unknown")
case .resetting: print("central.state is .resetting")
case .unsupported: print("central.state is .unsupported")
case .unauthorized: print("central.state is .unauthorized")
case .poweredOff: print("central.state is .poweredOff")
case .poweredOn: print("central.state is .poweredOn")
self.startScanning()
#unknown default: fatalError("unknow state")
}
}
func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
print("Discovered \(peripheral)")
var bluetooth = Bluetooth()
bluetooth.name = peripheral.name
bluetooth.identifier = peripheral.identifier.uuidString
bluetooth.state = peripheral.state.rawValue
bluetooth.advertisementData = advertisementData
let power = advertisementData[CBAdvertisementDataTxPowerLevelKey] as? Double
let value: Double = pow(10, ((power ?? 0 - Double(truncating: RSSI))/20))
bluetooth.signalStrength = String(describing: value.round)
// Do not add duplicate device
let fitlerArray : [Bluetooth] = devices.filter { $0.identifier == bluetooth.identifier}
if fitlerArray.count == 0 {
devices.append(bluetooth)
}
self.delegate?.reloadDeviceList()
}
}
The thing here is these methods are exposed in the other classes too.
For example:
I have created an object of BLEScanManager in other class BLEListViewController to show a list of BLE device in UITableView.
class BLEListViewController: UITableViewController {
var scanManager: BLEScanManager!
}
I can access CBCentralManagerDelegate delegate methods in BLEListViewController class using an object scanManager.
Like below,
self.scanManager.centralManager?(<#T##central: CBCentralManager##CBCentralManager#>, didConnect: <#T##CBPeripheral#>)
This should expose the internal utility delegate methods to the outside world.
Question is how to stop exposing these delegates?
Please note that, if I use internal keyword it only hides that specific method. But it still allows accessing all other CBCentralManagerDelegate methods.
There’s nothing you can do about this. It is a problem with all optional protocol methods. The trouble is that optional protocol methods are an Objective C feature. You cannot hide such a method as private, because now Objective C cannot see it and so the method will never be discovered and called. It’s a flaw in the way Swift interfaces with Objective C. You just have to live with it.
What you need to do, is to hide behind a protocol - see the following example.
protocol Exposed {
func run()
}
struct Bluetooth {
func hidden() {
print("This method is hidden - ish")
}
}
extension Bluetooth: Exposed {
func run() {
print("this method is exposed")
}
}
struct BluetoothScanManager {
static func scan() -> [Exposed] {
return [Bluetooth(), Bluetooth()]
}
}
Now, when you run BluetoothScanManager you will get a list of exposed only with the run() method available, but behind the code it is a Bluetooth
for exposed in BluetoothScanManager.scan() {
exposed.run()
exposed.hidden() // not possible
}
Your only way to do this (which also is the correct way in my opinion) is to create a new private object inside the BLEScanManager class that implements the CBCentralManagerDelegate.
Than you can pass this object to the CBCentralManager init method.
If you then need to comunicate from this object to the BLEScanManager you can simply use another delegate with a private protocol so it doesn't show those methods outside of the class.
It would look something like this:
import UIKit
import CoreBluetooth
protocol BLEScanDelegate {
func reloadDeviceList()
}
internal class BLEScanManager: NSObject {
private var centralManager: CBCentralManager?
var devices : [Bluetooth] = []
var delegate: BLEScanDelegate?
let centralManagerDelegate = MyCentralManagerDelegateImplementation()
override init() {
super.init()
centralManagerDelegate.delegate = self
centralManager = CBCentralManager(delegate: centralManagerDelegate, queue: .main)
}
// MARK:- Custom methods
func isScanning() -> Bool {
return centralManager?.isScanning ?? false
}
func stopScanning() {
centralManager?.stopScan()
}
func startScanning() {
devices.removeAll()
let options: [String: Any] = [CBCentralManagerScanOptionAllowDuplicatesKey:
NSNumber(value: false)]
centralManager?.scanForPeripherals(withServices: nil, options: options)
}
}
private extension BLEScanManager: CentralManagerEventsDelegate {
func didUpdateState(){
// Here goes the switch with the startScanning
}
func didDiscoverPeripheral(...) {
// Here goes the logic on new peripheral and the call on self.delegate?.reloadDeviceList
}
}
internal protocol CentralManagerEventsDelegate: class {
func didUpdateState()
func didDiscoverPeripheral(...)
}
internal class MyCentralManagerDelegateImplementation: NSObject, CBCentralManagerDelegate {
weak var delegate: CentralManagerEventsDelegate?
func centralManagerDidUpdateState(_ central: CBCentralManager) {
self.delegate?.didUpdateState()
}
func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
self.delegate?.didDiscoverPeripheral(actualParams)
}
}

iOS 11 Core Bluetooth restoration not working

I have an iOS app that uses Core Bluetooth to connect to a V.BTTN smart button. Everything worked perfectly fine in iOS 10, but since iOS 11 has come out the restoration process seems to break.
When I launch my app, with a previously paired button, the OS does in fact call the centralManager:willRestoreState, and the accompanying dictionary includes a pointer to a CBPeripheral object that has a status of connected. Just as it did back in iOS 10. However, the problem I am running into is when I call the discoverServices on the peripheral I am never returned any services in the peripheral:didDiscoverServices method. In fact that method, nor any other method, is called at all.
I have been searching all of the internet and have found people having similar issues with button pairing an connections, but those have generally been issues with improper lifecycle management of the peripheral object. I believe I have everything setup correct and at a loss. Does anyone have any idea what could be going on here?
import Foundation
import CoreBluetooth
class SmartButtonManager: NSObject {
//MARK: - Singleton
static let sharedManager = SmartButtonManager()
//MARK: - Properties
var discoveredDevices: [CBPeripheral] {
get {
return perfs
}
}
fileprivate(set) var isBluetoothOn = false
//MARK: - Private Constants & Variables
fileprivate var connectedButtonUUID: String?
fileprivate let queue = DispatchQueue(label: "V.BTTN", qos: .background, attributes: .concurrent, autoreleaseFrequency: .inherit, target: nil)
fileprivate var manager: CBCentralManager?
fileprivate var perfs = [CBPeripheral]()
fileprivate var connectedPerf: CBPeripheral?
fileprivate var isButtonReady = false
//MARK: - Initialization
override init() {
super.init()
}
//MARK: - Configure
func configure(withLaunchOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) {
// create new central manager
manager = CBCentralManager(delegate: self, queue: queue, options: [CBCentralManagerOptionRestoreIdentifierKey: managerIdentifier])
}
}
//MARK: - V.BTTN
extension SmartButtonManager: CBCentralManagerDelegate, CBPeripheralDelegate {
func scanForVbttn() {
perfs.removeAll()
manager?.scanForPeripherals(withServices: services, options: nil)
}
func centralManagerDidUpdateState(_ central: CBCentralManager) {
switch central.state {
case .poweredOff:
print("[centralManagerDidUpdateState] CB BLE hardware is powered off")
perfs.removeAll()
isBluetoothOn = false
case .poweredOn:
print("[centralManagerDidUpdateState] CB BLE hardware is powered on. Start scanning for peripherals")
isBluetoothOn = true
case .unauthorized:
print("[centralManagerDidUpdateState] CB BLE hardware is not authorized")
case .unsupported:
print("[centralManagerDidUpdateState] CB BLE hardware is not supported")
isBluetoothOn = false
default:
print("[centralManagerDidUpdateState] CB BLE hardware state is unknown")
}
}
func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
print("[centralManager:didDiscover peripheral:advertisementData] CB BLE did discover peripheral with advertisement data: \(advertisementData)")
guard let perfName = advertisementData[CBAdvertisementDataLocalNameKey] as? String else {
print("[centralManager:didDiscover peripheral:advertisementData] peripheral name is unknown")
return
}
if perfName.contains("V.ALRT") {
peripheral.delegate = self
perfs.append(peripheral)
if connectedButtonUUID == perfName {
connect(peripheral: peripheral)
}
}
}
func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
print("[central:didConnect peripheral:] CB BLE hardware did connect")
handleDidConnect(toPeripheral: peripheral)
}
func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) {
print("[central:didFailToConnect error:] CB BLE peripheral did failed to connect with error: \(String(describing: error))")
connectedPerf = nil
isButtonReady = false
connectedButtonUUID = nil
}
func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) {
print("[central:didDisconnectPeripheral peripheral:] CB BLE peripheral did disconnect")
connectedPerf = nil
isButtonReady = false
connectedButtonUUID = nil
}
func centralManager(_ central: CBCentralManager, willRestoreState dict: [String : Any]) {
print("[central:willRestoreState dict:] CB BLE hardware will restore state")
print("\(dict)")
guard let ps = dict[CBCentralManagerRestoredStatePeripheralsKey] as? [CBPeripheral] else {
print("[central:willRestoreState dict:] No perfs to restore")
return
}
print("[central:willRestoreState dict:] Will restore perfs")
perfs = ps
print("[central:willRestoreState dict:] Attempt to reconnect to V.BTTN")
print("[central:willRestoreState dict:] perfs \(perfs)")
for p in perfs {
if p.name == connectedButtonUUID {
print("[central:willRestoreState dict:] Connect to perf \(p)")
handleDidConnect(toPeripheral: p)
break
}
}
}
func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
guard let services = peripheral.services else {
print("[peripheral:didDiscoverServices error:] CB BLE peripheral unable to discover services")
return
}
print("[peripheral:didDiscoverServices error:] BLE peripheral did discover services")
for s in services {
print("[peripheral:didDiscoverServices error:] CB BLE Service \(s.description)")
peripheral.discoverCharacteristics(nil, for: s)
}
}
func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {
print("[peripheral:didDiscoverCharacteristicsFor service:] CB BLE did discover characteristics for service \(service.uuid.description)")
if compareCBUUID(uuid1: service.uuid, uuid2: CBUUID(string: BLE_VSN_GATT_SERVICE_UUID)) {
guard let characteristics = service.characteristics else {
return
}
for aChar in characteristics {
// write the verification key
if aChar.uuid.isEqual(CBUUID(string: BLE_VERIFICATION_SERVICE_UUID)) {
self.writeVerificationKey(forPeripheral: peripheral, characteristic: aChar)
self.enable(peripheral: peripheral)
break
}
}
}
}
func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) {
//NOTE: This code has been omitted as there is little need to see this
}
func peripheral(_ peripheral: CBPeripheral, didWriteValueFor characteristic: CBCharacteristic, error: Error?) {
print("[peripheral:didWriteValueFor characteristic:] characteristic: \(characteristic.uuid)")
if let e = error {
print("[peripheral:didWriteValueFor characteristic:] error: \(e)")
}
}
func peripheral(_ peripheral: CBPeripheral, didUpdateNotificationStateFor characteristic: CBCharacteristic, error: Error?) {
if !characteristic.isNotifying {
print("[peripheral:didUpdateNotificationStateFor:][Characteristic is not notifiying so cancel the connection]")
manager?.cancelPeripheralConnection(peripheral)
}
}
//MARK: - Helpers
fileprivate func writeVerificationKey(forPeripheral peripheral: CBPeripheral, characteristic: CBCharacteristic) {
print("[peripheral:didDiscoverCharacteristicsFor service:] Write verification key")
let data = NSData(bytes: [0x80,0xBE,0xF5,0xAC,0xFF] as [UInt8], length: 5)
peripheral.writeValue(data as Data, for: characteristic, type: CBCharacteristicWriteType.withResponse)
}
fileprivate func handleDidConnect(toPeripheral peripheral: CBPeripheral) {
if let n = peripheral.name {
connectedButtonUUID = n
}
connectedPerf = peripheral
peripheral.delegate = self
peripheral.discoverServices(nil)
isButtonReady = true
}
fileprivate func enable(peripheral: CBPeripheral) {
guard let services = peripheral.services else {
return
}
for service: CBService in services {
if compareCBUUID(uuid1: service.uuid, uuid2: CBUUID(string: BLE_VSN_GATT_SERVICE_UUID)) {
guard let characteristics = service.characteristics else {
return
}
for aChar in characteristics {
if aChar.uuid.isEqual(CBUUID(string: BLE_KEYPRESS_DETECTION_UUID)) {
// enable button
print("[peripheral:didDiscoverCharacteristicsFor service:] Enable short press")
let data = NSData(bytes: [0x02] as [UInt8], length: 1)
peripheral.writeValue(data as Data, for: aChar, type: CBCharacteristicWriteType.withResponse);
} else if aChar.uuid.isEqual(CBUUID(string: BLE_SILENT_NORMAL_MODE)) {
// enable button
print("[peripheral:didDiscoverCharacteristicsFor service:] Enable normal mode")
let data = NSData(bytes: [0x00] as [UInt8], length: 1)
peripheral.writeValue(data as Data, for: aChar, type: CBCharacteristicWriteType.withResponse);
} else if aChar.uuid.isEqual(CBUUID(string: BLE_FALL_KEYPRESS_DETECTION_UUID)) {
// enable fall detection
print("[peripheral:didDiscoverCharacteristicsFor service:] Enable fall detection")
peripheral.setNotifyValue(true, for: aChar)
}
}
}
}
checkBatteryLevel()
}
func disconnectVbttn() {
guard let peripheral = connectedPerf else {
return
}
setVbttnToStealthMode()
manager?.cancelPeripheralConnection(peripheral)
isButtonReady = false
}
fileprivate func compareCBUUID(uuid1: CBUUID, uuid2: CBUUID) -> Bool {
if (uuid1.data as NSData).isEqual(to: uuid2.data) {
return true
}
return false
}
func stopScanning() {
manager?.stopScan()
}
}

Best way to manage BLE central object

I am developing an app which is capable of doing something like this.
My app should connects with BLE peripheral and i want to keep monitoring the connection. when disconnects the connection it should take the locatoin and update the map. i need to track the disconnection even if the peripheral's switch off event in my App.
i have two View Controllers. One is for update my Google map. other one is a tableViewController which is listed available device list. i have implemented a BLE Manager using Singolton pattern.
class BleManager: NSObject , CBCentralManagerDelegate, CBPeripheralDelegate {
var centralManager: CBCentralManager?
var peripherals = Array<CBPeripheral>()
var periparal : CBPeripheral!
let dataSaver = DataSaver.sharedInstance
let locationMgr = LocationInfo.sharedInstance
var viewController : CarList!
static let shared = BleManager()
//MARK:- Initialiser
private override init() {
super.init()
centralManager = CBCentralManager(delegate: self, queue: DispatchQueue.main)
}
init(vc : CarList) {
super.init()
viewController = vc
//centralManager = CBCentralManager(delegate: self, queue: DispatchQueue.main)
}
//MARK:- BLE Activities
func startScanForDevices() {
peripherals = []
//self.centralManager?.scanForPeripherals(withServices: [CBUUID(string: "DFB0")], options: nil)
self.centralManager?.scanForPeripherals(withServices: nil, options: nil)
//self.centralManager?.scanForPeripherals(withServices: nil, options: [CBCentralManagerScanOptionAllowDuplicatesKey : true])
}
func stopScanForDevices() {
if centralManager != nil {
centralManager?.stopScan()
}
}
func connectToADevice(pheriperal : CBPeripheral){
periparal = pheriperal
// saving Connected Car in App LifeSycle
connectedCar = pheriperal
centralManager?.connect(pheriperal, options: nil)
(viewController != nil) ? viewController.devTable.reloadData() : print("")
}
func disconnectFromApp(pheriperal : CBPeripheral){
centralManager?.cancelPeripheralConnection(pheriperal)
}
//MARK:- BLE Central Delegates
func centralManagerDidUpdateState(_ central: CBCentralManager) {
if (central.state == .poweredOn){
startScanForDevices()
}
else if (central.state == .poweredOff){
print("Powered Off")
centralManager?.cancelPeripheralConnection(periparal)
}
else if (central.state == .resetting){
print("resetting")
}
else if (central.state == .unauthorized){
print("unauthorized")
}
else if (central.state == .unsupported){
print("unsupported")
}else{
print("Unknown")
}
}
func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
//peripherals.append(peripheral)
//print("DEVICES LIST : \(peripherals)")
updatePheriperals(device: peripheral)
if viewController != nil {
viewController.devTable.reloadData()
if dataSaver.isAnyCarHasBeenSaved() == true && dataSaver.getMyCarIdentification() == peripheral.identifier.uuidString && peripheral.state == .disconnected{
connectToADevice(pheriperal: peripheral)
viewController.devTable.reloadData()
}
}else{
if dataSaver.isAnyCarHasBeenSaved() == true && dataSaver.getMyCarIdentification() == peripheral.identifier.uuidString{
connectToADevice(pheriperal: peripheral)
}
}
}
func updatePheriperals(device : CBPeripheral) {
if peripherals.count != 0 {
for dev in peripherals {
if device.identifier != dev.identifier {
peripherals.append(device)
}
}
}else{
peripherals.append(device)
}
}
func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) {
print("\(ConnectionState(rawValue: peripheral.state.rawValue)?.name()) \(peripheral.name) from App.. Taking Geo cordinates")
locationMgr.didFindLocation = false
locationMgr.getCurrentLocation()
if viewController != nil {
viewController.devTable.reloadData()
}else{
print("Just Updating Locations")
}
showNotification(message: peripheral.name! + " has been Disconnected from Find My Car" )
startScanForDevices()
}
func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
print("\(ConnectionState(rawValue: peripheral.state.rawValue)?.name()) \(peripheral.name) to App.. Removeing previous location")
dataSaver.removeLocation()
dataSaver.saveMyCarIdentification(myCar: peripheral.identifier.uuidString)
(viewController != nil) ? viewController.devTable.reloadData() : print("Just Updating Locations")
//print("Service UUIDs : \(peripheral)")
NotificationCenter.default.post(name: NSNotification.Name(rawValue: AppStrings.updateMapString), object: nil)
showNotification(message: peripheral.name! + " has been connected to Find My Car" )
}
func centralManager(_ central: CBCentralManager, willRestoreState dict: [String : Any]) {
print("Already Connected Pheriperals : \(dict[CBCentralManagerRestoredStatePeripheralsKey])")
}
func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) {
print("Failed To Connect with \(peripheral.name)")
centralManager?.cancelPeripheralConnection(peripheral)
}
In my MapViewController i use sharedInstance and start scanning
even in my TableViewController i do the same.
but sometimes when i try to switch off the peripheral device, my BLE Central does not called did disconnect peripheral delegate.
and i also wanna know how can i handle background scanning. with state prevention and restoration (I dont know anything about it. i just want to waork my app in background As same way in foreground)
Please help

Sending data to Bluno from iOS

I've recently bought a Bluno and am trying to create an iPhone app to talk to it. The Bluno makers include source code but it's in objective-c and I'm trying to port it to swift. Currently I can discover the Bluno, connect to it and see it's services. However I seem to see no characteristics, and get a nil when I print them. I've setup the Bluno so that it flashes once I send it the character "5" but I can't seem to find the correct characteristic in order to do so. Any help would be much appreciated. Here is my current code, the Obj-C code can be found here:
//
// ViewController.swift
// Bluetooth-Interaction
//
// Created by Frederik Lohner on 26/Oct/15.
// Copyright © 2015 JeongGroup. All rights reserved.
//
import UIKit
import CoreBluetooth
import SnapKit
class ViewController: UIViewController, CBCentralManagerDelegate, CBPeripheralDelegate {
let backgroundView = UIView()
let scanButton = UIButton()
var DFRobotServiceId = "0000dfb0-0000-1000-8000-00805f9b34fb"
var kBlunoService = "DFB1"
var kBlunoDataCharacteristic = "dfb1"
var DFRobotCharacteristicsNameSerialPortId = "0000dfb1-0000-1000-8000-00805f9b34fb"
var NameCommandId = "0000dfb2-0000-1000-8000-00805f9b34fb"
// BLE
var centralManager: CBCentralManager!
var sensorTagPeripheral: CBPeripheral!
override func viewDidLoad() {
scanButton.setTitle("Scan", forState: UIControlState.Normal)
scanButton.addTarget(self, action: "startScanning", forControlEvents: UIControlEvents.TouchUpInside)
scanButton.backgroundColor = UIColor.blackColor()
backgroundView.addSubview(scanButton)
self.view.addSubview(backgroundView)
backgroundView.snp_makeConstraints { (make) -> Void in
make.left.right.top.bottom.equalTo(self.view)
}
scanButton.snp_makeConstraints { (make) -> Void in
make.left.bottom.equalTo(backgroundView)
make.width.height.equalTo(60)
}
// Initialize central manager on load
centralManager = CBCentralManager(delegate: self, queue: nil)
// self.centralManager.stopScan()
}
func startScanning() {
// print("Started Scanning!")
// //Could add service UUID here to scan for only relevant services
// self.centralManager.scanForPeripheralsWithServices(nil, options: nil)
let one = "1"
let data = one.dataUsingEncoding(NSUTF8StringEncoding)
// self.sensorTagPeripheral.writeValue(data!, forCharacteristic: , type: .WithoutResponse)
// self.sensorTagPeripheral.writeValue(data!, forCharacteristic: CBCharacteristic., type: .WithoutResponse)
// self.centralManager.
}
// Check status of BLE hardware
func centralManagerDidUpdateState(central: CBCentralManager) {
if central.state == CBCentralManagerState.PoweredOn {
print("Bluetooth is ON")
central.scanForPeripheralsWithServices(nil, options: nil)
} else if central.state == CBCentralManagerState.Resetting {
print("RESETTING")
} else if central.state == CBCentralManagerState.Unauthorized {
print("Not Authorized")
} else {
print("Bluetooth switched off or not initialized")
}
}
func centralManager(central: CBCentralManager, didDiscoverPeripheral peripheral: CBPeripheral, advertisementData: [String : AnyObject], RSSI: NSNumber) {
// print(peripheral)
let deviceName = "Bluno"
if let nameOfDeviceFound = peripheral.name {
if (nameOfDeviceFound == deviceName) {
print("Name was found")
print("")
print("")
print(peripheral)
// for (key, value) in advertisementData {
// print("\(key) -> \(value)")
// }
// Stop scanning
self.centralManager.stopScan()
print("Stopped Scanning")
// Set as the peripheral to use and establish connection
self.sensorTagPeripheral = peripheral
self.sensorTagPeripheral.delegate = self
self.centralManager.connectPeripheral(peripheral, options: nil)
print("")
print("")
print("Connected")
print("")
}
else {
print("NOPE.EXE")
}
}
}
// // Check if the service discovered is a valid IR Temperature Service
func peripheral(peripheral: CBPeripheral, didDiscoverServices error: NSError?) {
if(error != nil) {
print(error?.description)
}
for service in peripheral.services! {
let thisService = service as CBService
print("Discovered Service: \(thisService.description)")
print("Discovered Characteristic: \(thisService.characteristics)")
}
}
func peripheral(peripheral: CBPeripheral, didDiscoverCharacteristicsForService service: CBService, error: NSError?) {
if(error != nil) {
print(error?.description)
}
for characteristic in service.characteristics! {
print("Characteristic found: \(characteristic)")
let one = "1"
let data = one.dataUsingEncoding(NSUTF8StringEncoding)
peripheral.writeValue(data!, forCharacteristic: characteristic, type: .WithoutResponse)
if(String(characteristic.UUID) == kBlunoService) {
print("Found")
}
}
}
func centralManager(central: CBCentralManager, didConnectPeripheral peripheral: CBPeripheral) {
print("Did connect to peripheral.", terminator:"")
peripheral.delegate = self
peripheral.discoverServices(nil)
print(peripheral)
}
func centralManager(central: CBCentralManager, didFailToConnectPeripheral peripheral: CBPeripheral, error: NSError?) {
print("Failed to connect to peripheral.")
}
// func centralManager(central: CBCentralManager, didFailToConnectPeripheral peripheral: CBPeripheral, error: NSError?) {
// print("CONNECTION FAILED")
// }
func centralManager(central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: NSError?) {
print("CONNECTION WAS DISCONNECTED")
}
}
Managed to get this to work. Code can be found here.

Resources