Swift - App BLE reconnect/scan after suspend - ios

I am currently writing an app that connects via BLE to an external device. All operations are fine when the app is in foreground.....including connecting, obtaining data, and reconnecting (in cases of the device going out of range) via a reconnect protocol I wrote. The app also functions properly when it is backgrounded but the BLE connection remains alive.
However, the only instance in which the app does not function is if the app is backgrounded and then the BLE device goes out of range. Once the connection is broken, the app seems to be suspended by iOS after a few seconds and none of the code I wrote will continue to function...even if I bring the device back into range. The only way to restore functionality is to bring the app back into the foreground again. (Note: I have the info.plist file and all other settings configured appropriately for centralManager background functionality)
I've read some documentation and it seems that this comes down to not having state preservation/restore code properly implemented. I went ahead and implemented the "willRestoreState" and "didUpdateState" commands, but the app still doesn't reconnect to a device once it has been suspended when in background mode.
I've shown some relevant code below, including the willRestoreState, didUpdateState, and didDisconnect methods. Any ideas or suggestions? Thanks!
//define service+characteristic UUIDS
let serviceUUID = CBUUID(string: "xxxxxxxxx")
let streamingCharacteristicUUID = CBUUID(string: "xxxxxxxxx")
//Local dictionary of UUIDs for connected devices (the ble code updates this var with each connected device)
var devicesUniqueId:[UUID:String] = [UUID:String]()
//Local dictionary of connected peripherals, with respect to each of their UUIDS (the ble code updates this var with each connected device)
var sensorPeripheral = [UUID:CBPeripheral]()
///restoreState function
func centralManager(_ central: CBCentralManager, willRestoreState dict: [String : Any]) {
if let peripheralsObject = dict[CBCentralManagerRestoredStatePeripheralsKey] {
let peripherals = peripheralsObject as! Array<CBPeripheral>
print ("starting restorestate code")
if peripherals.count > 0 {
for i in 0 ..< peripherals.count {
print ("starting restorecheck")
//Check if the peripheral exists within our list of connected peripherals, and assign delegate if it does
if self.devicesUniqueId.keys.contains(peripherals[i].identifier) {
peripherals[i].delegate = self
}
}
}
}
}
func centralManagerDidUpdateState(_ central: CBCentralManager)
{
if central.state != .poweredOn
{
return
}
self.startScanning()
//////Preservation + Restoration code////////
//Iterate through array of connected UUIDS
let keysArray = Array(self.patchDevicesUniqueId.keys)
for i in 0..<keysArray.count {
//Check if peripheral exists for given UUID
if let peripheral = self.sensorPeripheral[keysArray[i]] {
print("peripheral exists")
//Check if services exist within the peripheral
if let services = peripheral.services {
print("services exist")
//Check if predefined serviceUUID exists within services
if let serviceIndex = services.index(where: {$0.uuid == serviceUUID}) {
print("serviceUUID exists within services")
let transferService = services[serviceIndex]
let characteristicUUID = streamingCharacteristicUUID
//Check if predefined characteristicUUID exists within serviceUUID
if let characteristics = transferService.characteristics {
print("characteristics exist within serviceUUID")
if let characteristicIndex = characteristics.index(where: {$0.uuid == characteristicUUID}) {
print("characteristcUUID exists within serviceUUID")
let characteristic = characteristics[characteristicIndex]
//If characteristicUUID exists, begin getting notifications from it
if !characteristic.isNotifying {
print("subscribe if not notifying already")
peripheral.setNotifyValue(true, for: characteristic)
}
else {
print("invoke discover characteristics")
peripheral.discoverCharacteristics([characteristicUUID], for: transferService)
}
}
}
}
else {
print("invoke discover characteristics")
peripheral.discoverServices([serviceUUID])
}
}
}
}
}
//didDisconnect method to handle a connect command issue
func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?)
{
//commented out unnecessary code
self.removePeripheralData(peripheral: peripheral)
if(sensorCount>0){
sensorCount -= 1
}
}
//removePeripheralData function used in didDisconnect
func removePeripheralData ( peripheral: CBPeripheral) {
//Commented out unnecessary code
//Issue reconnect command
print ("issuing reconnect command")
centralManager.connect(peripheral, options: nil)
//Commented out unnecessary code
handleDidRemoveDevice()
}

Related

BLE Peripheral Not Advertising

I am attempting to create a simple Bluetooth LE peripheral to connect to a pre-existing Central. From what I can tell, my Peripheral Manager is set up correctly and the call to begin advertising is in place with the correct service UUID. However, when I run a check to make sure my peripheral is actually advertising, I get a negative result
I have double checked my UUID and ensured that Bluetooth on my test device is on. I am also using a physical device for testing, not the simulator.
Here is the code used to set up my peripheral advertisement:
func peripheralManagerDidUpdateState(_ peripheral: CBPeripheralManager) {
if peripheral.state == CBManagerState.poweredOn {
service = CBMutableService.init(type: SERVICE_UUID, primary: true)
characteristic = CBMutableCharacteristic.init(type: CHARACTERISTIC_UUID, properties: .read, value: nil, permissions: .readable)
service?.characteristics = [characteristic!]
peripheralManager?.add(service!)
peripheralManager?.delegate = self
let adData = [CBAdvertisementDataLocalNameKey : "MackJohn_Bluetooth_On_iOS", CBAdvertisementDataServiceUUIDsKey : SERVICE_UUID] as [String : Any]
peripheralManager?.startAdvertising(adData)
}
}
Here is where I'm checking to see if my code is actually advertising and getting a false result:
func peripheralManagerDidStartAdvertising(_ peripheral: CBPeripheralManager, error: Error?) {
print(peripheral.isAdvertising)
}
I've also noticed that this function is not calling at all:
func peripheralManager(_ peripheral: CBPeripheralManager, didReceiveRead request: CBATTRequest) {
print("Added Service")
}
You have made one slight error; the value for CBAdvertisementDataServiceUUIDsKey should be an array of CBUUIDs, not a single CBUUID;
let adData = [CBAdvertisementDataLocalNameKey : "MackJohn_Bluetooth_On_iOS", CBAdvertisementDataServiceUUIDsKey : [SERVICE_UUID]] as [String : Any]

iOS: Is it possible to send a string to a computer via bluetooth just by giving it the MAC address

Im new to programming in swift, & I need help with working with bluetooth.
Im working on a project that involves sending a string to a computer via bluetooth, and Im able to enter the receiving device's MAC address beforehand so it has that to know where to send it.
My only problem at this stage is connecting to said device, & sending the data. I tried looking up tutorials, but they were either for Android (Which I already got working, I need one for iOS now), or they were about how to connect via service UUID (what?).
Heres the code I have so far:
import UIKit
import CoreBluetooth
class transmitter: UIViewController, CBCentralManagerDelegate, CBPeripheralDelegate {
var manager:CBCentralManager!
var peripheral:CBPeripheral!
let SCRATCH_UUID = UUID.init(uuidString: "00001101-0000-1000-8000-00805F9B34FB")
let SERVICE_UUID = CBUUID(string: "00001101-0000-1000-8000-00805F9B34FB")
override func viewDidLoad() {
super.viewDidLoad()
// Define manager
manager = CBCentralManager(delegate: self, queue: nil)
print(globals.data)
// Do any additional setup after loading the view.
}
#IBOutlet weak var console: UILabel!
// Check if teh bluetooth is enabled
func centralManagerDidUpdateState(_ central: CBCentralManager) {
if central.state == CBManagerState.poweredOn {
central.scanForPeripherals(withServices:nil, options: nil)
print (central.isScanning)
console.text = String(describing: central.retrievePeripherals(withIdentifiers: [SCRATCH_UUID!]))
} else {
//print("Bluetooth not available.")
let alert = UIAlertController(title: "Bluetooth unavalible", message: "Bluetooth is unavalibe for this device. Is it even turned on?", preferredStyle: UIAlertControllerStyle.alert)
alert.addAction(UIAlertAction(title: "Ok", style: UIAlertActionStyle.default, handler: nil))
self.present(alert, animated: true, completion: nil)
}
}
// Pair with device....
// TODO: Change to be based on MAC Address instead of name...?
private func centralManager(central: CBCentralManager, didDiscoverPeripheral peripheral: CBPeripheral, advertisementData: [String : AnyObject], RSSI: NSNumber) {
let device = (advertisementData as NSDictionary).object(forKey: CBAdvertisementDataLocalNameKey) as? NSString
// console.text = peripheral.name
/*
if device?.contains(globals.macAddress) == true {
self.manager.stopScan()
self.peripheral = peripheral
self.peripheral.delegate = self
manager.connect(peripheral, options: nil)
}
*/
}
//
// The rest is copied from a tutorial
//
// Once you are connected to a device, you can get a list of services on that device.
func centralManager(central: CBCentralManager, didConnectPeripheral peripheral: CBPeripheral) {
peripheral.discoverServices(nil)
}
// Once you get a list of the services offered by the device, you will want to get a list of the characteristics. You can get crazy here, or limit listing of characteristics to just a specific service. If you go crazy watch for threading issues.
private func peripheral(peripheral: CBPeripheral,didDiscoverServices error: NSError?) {
for service in peripheral.services! {
let thisService = service as CBService
if service.uuid == SERVICE_UUID {
peripheral.discoverCharacteristics(nil, for: thisService)
}
}
}
// There are different ways to approach getting data from the BLE device. One approach would be to read changes incrementally. Another approach, the approach I used in my application, would be to have the BLE device notify you whenever a characteristic value has changed.
private func peripheral(peripheral: CBPeripheral, didDiscoverCharacteristicsForService service: CBService, error: NSError?) {
for characteristic in service.characteristics! {
let thisCharacteristic = characteristic as CBCharacteristic
if thisCharacteristic.uuid == SERVICE_UUID {
self.peripheral.setNotifyValue(true, for: thisCharacteristic)
}
}
}
// This is an optional step, but hey, let us be good programmers and clean up after ourselves. Also a good place to start scanning all over again.
private func centralManager(central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: NSError?) {
central.scanForPeripherals(withServices: nil, options: nil)
}
}
What I was trying to do at this stage was:
Check to make sure bluetooth is enabled (Works)
List devices available, as I was not able to connect via MAC address (Fails)
What would be nice though is that if I didn't have to do step 2, & just connect via the provided MAC address instead of scanning for devices, which didn't show any.
Help?
Ok, so it seems that SPP is not supported. Crap :/
Ill look into what both John Doe & Paulw11 suggested
Thanks!

CBPeripheral objects have always nil value for name and are in disconnected state

I am writing an app that allows me to send an image through BLE to the app of someone else.
In order to test it out properly, I would need to be able to connect to an device, but my scanned peripherals are always disconnected and have nil as value. I am uncertain as to whether I am doing this right. I have been reading through guides and I am following the right procedure so could someone point out to me what I might be doing wrong? What are the peripherals I'm detecting?
output:
<CBPeripheral: 0x165b9c90, identifier = 6B74A074-6F5B-0E3A-94EB-6E7BB890569C, name = (null), state = disconnected>
<CBPeripheral: 0x165a0940, identifier = A35AC32E-5668-BD0D-4DBC-D4BF959B9242, name = (null), state = disconnected>
<CBPeripheral: 0x166a8220, identifier = 4D9FA1A1-0090-465F-B53D-363B0F3BBD27, name = (null), state = disconnected>
Edit: Added more code
Here is my code
import Foundation
import CoreBluetooth
import UIKit
class BluetoothController: UITableViewController, CBCentralManagerDelegate, CBPeripheralManagerDelegate, CBPeripheralDelegate {
var centralManager: CBCentralManager!
var peripheralManager: CBPeripheralManager!
var service : CBMutableService!
let uuid:CBUUID = CBUUID(string: "09d921ff-b80c-47b1-bc2b-5bbaadf62010")
let charaUuid:CBUUID = CBUUID(string: "09d921ff-b80c-47b1-bc2b-5bbaadf62021")
override func viewDidLoad() {
print("Initialzing managers")
peripheralManager = CBPeripheralManager(delegate: self, queue: nil)
centralManager = CBCentralManager(delegate: self, queue: nil)
service = CBMutableService(type: uuid, primary: true) //<--Probably not causing it but without a service...
let properties: CBCharacteristicProperties = [.notify, .read, .write]
let permissions: CBAttributePermissions = [.readable, .writeable]
let characteristic = CBMutableCharacteristic(
type: charaUuid,
properties: properties,
value: nil,
permissions: permissions)
service.characteristics = [characteristic];
peripheralManager.add(service)
}
func peripheralManagerDidUpdateState(_ peripheral: CBPeripheralManager) {
print("peripheral state updated")
print("\(peripheral.description)")
}
func peripheralManagerDidStartAdvertising(_ peripheral: CBPeripheralManager, error: Error?)
{
print("started advertising")
}
// Check if the bluetooth is powered on
// Start looking for devices
func centralManagerDidUpdateState(_ central: CBCentralManager) {
if central.state == .poweredOn {
print("Scanning for devices")
centralManager.scanForPeripherals(withServices: nil, options: nil)
startAdvert()
} else {
print("Bluetooth not available.")
}
}
func startAdvert(){
let advertisingData = [CBAdvertisementDataLocalNameKey:"Test Device", CBAdvertisementDataServiceUUIDsKey: uuid] as [String : Any]
peripheralManager.startAdvertising(advertisingData)
}
func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber){
print(peripheral)
//didReadPeripheral(peripheral, rssi: RSSI)
}
}
As Paulw11 notes, you're not connected because you never call connect(). You're discovering random BLE devices around you that have nothing to do with your app. That's because you're scanning for all possible services:
centralManager.scanForPeripherals(withServices: nil, options: nil)
You should almost never do that. It's bad for the battery and it's rarely what you want. You want to scan for your service.
centralManager.scanForPeripherals(withServices: [uuid], options: nil)
And then when you discover other devices advertising your service, then you need to connect to them.
To #WholeCheese's comment, the details of the question didn't really relate to the nil name, but to address the question in the title, you will not see a name from advertising if the devices does not advertise a local name over BLE. This is quite common. There is not a lot of space in the advertising packet for data (roughly 30 bytes), and local names can be quite large.
If you're building a scanning app and want to display those names, you'll need to connect to the device. At that point, Core Bluetooth will read the GAP name (rather than the advertised Local Name). In most cases the GAP names should be there (it's not promised from all devices, but it generally should be there). Core Bluetooth caches a lot of information, so if you've ever connected to the device before, it is possible that peripheral.name will be filled out without connecting even if it's not advertised. But if it isn't, you'll need to connect.

How to bond/pair to a bluetooth LE device programmatically in swift Xcode?

I am currently trying to develop and application that allows users to bond to a Peripheral via a click of a button and the password will be automatically entered.
Is it possible to Bond and Remove Bond programmatically using swift?
Pairing is initiated any time that you attempt to write to or read from a characteristic on the BLE device. However, if the device is not set to require authentication and/or bonding, you will not see the iOS popup which requests the PIN code.
I struggled with this with my HM-10 because I could write data to the characteristic using the Core Bluetooth (via Swift) function writeValue() without ever seeing the pairing popup.
I couldn't figure it out until I read the HM-10 (implements the IC cc2451) datasheet very closely and found that I needed to set the AT+TYPE to value 3. It defaults to 0 which means that the HM-10 does not require pairing/bonding so you never see the iOS popup.
You can read more about the details where I asked the question and ultimately found the solution and wrote it up: How do I pair and/or bond to BLE on iOS using Swift code and an HM-10 so data sent is encrypted?
Follow the step to connect Ble device into iOS Program.
1) Import
import CoreBluetooth
2) Declared the variables into the class or ViewController.
let kServiceUART = CBUUID(string: "0x1800")
var peripheralHeartRateMonitor: CBPeripheral?
var cbManger: CBCentralManager!
3) Initialize the cbManger into ViewDidLoad function of viewController or initalize function of class.
cbManger = CBCentralManager(delegate: self, queue: .main)
4) Override delegate method of the CBCentralManager .
func centralManagerDidUpdateState(_ central: CBCentralManager) {
switch central.state {
case .unsupported:
print("BLe Unsupported")
break
case .unauthorized:
print("BLe unauthorized")
break
case .poweredOff:
let alertMessgesInst = AlertMessages.sharedInstance
CommonUtils.showAlert(alertMessgesInst.actofit_Title, message: alertMessgesInst.trun_On_blueTooth)
break
case .poweredOn:
if isNewFirmWareOFImpulse {
let uuidString = StorageServices.readFromDefaults(key: Constants.userDefaultKeys.impulseUUID)
let uuid = UUID(uuidString:uuidString as! String )
let device = cbManger.retrievePeripherals(withIdentifiers: [uuid!])
peripheralHeartRateMonitor = device.first
peripheralHeartRateMonitor!.delegate = self
cbManger?.connect(peripheralHeartRateMonitor!)
}else {
let option:[String: Any] = [CBCentralManagerScanOptionAllowDuplicatesKey: NSNumber(value: false)]
cbManger.scanForPeripherals(withServices: nil, options: option)
}
break
case .unknown:
print("BLe unknown")
break
default:
break
} // End Swith
} // End 'centralManagerDidUpdateState' function.
func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
if isNewFirmWareOFImpulse {
peripheralHeartRateMonitor = peripheral
print("UUid of band is :- \(peripheralHeartRateMonitor?.identifier.uuidString)")
if impulseName == peripheral.name {
peripheralHeartRateMonitor!.delegate = self
cbManger.stopScan()
// STEP 6: connect to the discovered peripheral of interest
cbManger?.connect(peripheralHeartRateMonitor!)
} // End impulse condition
}else {
let keysArray = advertisementData.keys
if let tempImpulseName = peripheral.name {
print(impulseName + " and " + tempImpulseName )
if impulseName == tempImpulseName {
for key in keysArray {
if key == "kCBAdvDataManufacturerData"{
let manufactureData = advertisementData[key]
if let stringValue = manufactureData.debugDescription as? String {
var heartValue: String = String()
heartValue = stringValue
heartValue.removeLast()
heartValue.removeLast()
let last = heartValue.removeLast()
let secondLast = heartValue.removeLast()
let hR = String([secondLast, last])
if let value = UInt8(hR, radix: 16){
if Int(value) > 60 {
hrArray.append(Int(value))
}
} // End the value block
} // end of if 'stringValue' condition
} // end 'Key' if condition
} // End for each loop
} // End impulse condition
} // End pheripheral if condition
} // end version condition
} // End function 'didDiscover peripheral'.
func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
// STEP 8: look for services of interest on peripheral
peripheralHeartRateMonitor?.discoverServices(nil)
} // END func centralManager(... didConnect peripheral
func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
if error != nil {

didDisconnectPeripheral function is not being called

I'm going to break down what the flow for connection is for the first time a user wants to connect to a BLE Peripheral Device. I think I may be doing something that upsets the natural flow of the CoreBluetooth Module. My app currently works with regards to connecting and sending commands to a BLE device.
To connect, I first land on a page with a tableview which displays the devices available to connect to.
override func viewDidLoad() {
super.viewDidLoad()
//self.centralManager = CBCentralManager(delegate: blueCoreDelegate, queue: nil)
self.centralManager = CBCentralManager(delegate: self, queue: nil)
// setting the central manager in BlueCoreManager as the same
BlueCoreManager.shared.getCentralManager(centralManager!)
}
BlueCoreManager is a singleton that I have created to easily manage multiple connections. At this point the centralManagerDidUpdateState function is called.
I know that it goes to the centralManagerDidUpdateState function in both the current page and the singleton. In both I scan for peripherals.
In the current page: I append the discovered peripherals onto the tableview.
In the singleton: I keep track the connection states
Now once a cell is tapped on the current page, I connect to it:
// Connect to peripheral on tap
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
let peripheral = peripherals[indexPath.row]
if peripheral.name != nil {
self.centralManager!.connectPeripheral(peripheral, options: nil)
print("Connecting to \(peripheral.name!)")
}
}
After the above, the didConnectPeripheral on the currentPage is called, where I update connectionStates and call the syncPeripheral function in the singleton.
func centralManager(central: CBCentralManager, didConnectPeripheral peripheral: CBPeripheral) {
// setting connection state to 4 (currently connected)
let cstate = over.valueForKey("ConnectionState") as! Int
if cstate == 4 {
print("ConnectionState: One Connected. Connecting to Second")
over.setObject(5, forKey: "ConnectionState")
over.synchronize()
print("Connected To Two")
}
.......
.......
else if cstate == 7 {
print("ConnectionState: Disconnected from secondary. Connecting to Second")
over.setObject(5, forKey: "ConnectionState")
over.synchronize()
print("Connected To Two")
}
// synchronizing the current peripheral in BlueCoreManager
BlueCoreManager.shared.syncPeripheral(peripheral)
displayAlert("Connected!", message: "You are connected to \(peripheral.name!)")
}
This is the syncPeripheral function which is then called, where I set the peripherals' delegates and call discoverServices whichever one is connected.
func syncPeripheral(peripheral: CBPeripheral) {
let cstate = modeData.valueForKey("ConnectionState")
switch(cstate as! Int) {
case 0: print("Connection State Powered On")
print("Not keeping any reference")
case 4: print("Connection State To One")
self.easeDevice = peripheral
print(peripheral.name!)
self.easeDevice!.delegate = self
self.connectedPeripheralArray.insert(peripheral, atIndex: 0)
self.familiarPeripherals.insert(peripheral, atIndex: 0)
print(connectedPeripheralArray[0].name)
self.easeDevice!.discoverServices(nil)
case 5: print("Connection State To Two")
self.phy = peripheral
print(peripheral.name!)
self.phy!.delegate = self
self.connectedPeripheralArray.insert(peripheral, atIndex: 1)
self.familiarPeripherals.insert(peripheral, atIndex: 1)
self.phy!.discoverServices(nil)
case 6: print("Connection State Disconnected from One")
case 7: print("Connection State Disconnected from device two")
default: print("Connection State = \(cstate)")
}
}
I then discover the services and characteristics in the singleton NSObject class. In the disconnect function, I'm updating the disconnect states and trying to reconnect to the disconnected peripheral. Also I'm passing the disconnected peripheral through a protocol I defined.
// If disconnected, connect again
func centralManager(central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: NSError?) {
// Delegate which passes disconnected peripherals
if delegate != nil {
delegate!.peripheralDidDisconnect("\(peripheral.name!)")
}
print("Disconnected")
if peripheral == self.easeDevice {
modeData.setObject(6, forKey: "ConnectionState")
modeData.synchronize()
connectedPeripheralArray.removeAtIndex(0)
self.centralManager!.connectPeripheral(peripheral, options: nil)
} else if peripheral == self.phy {
modeData.setObject(7, forKey: "ConnectionState")
modeData.synchronize()
connectedPeripheralArray.removeAtIndex(1)
self.centralManager!.connectPeripheral(peripheral, options: nil)
}
}
The only problem is that the above function never gets called on the singleton object (The singleton is an NSObject class, could this be the reason why?). What am I missing?! This issue is driving me insane! Any insights are highly appreciated.

Resources