I am currently using swift to make an app that can generate a list of bluetooth devices nearby. However, I cannot find any documents that use swift to do that. All of the files are in Objective C and I am wondering if I can just make an objective C file and connect directly to the storyboard? (my project file is in swift).
Also, do I have to include any other library from outside? (ex: serialGATT) or coreBluetooth.framework is good enough?
You'll have to import CoreBluetooth
import CoreBluetooth
Add the CBCentralManagerDelegate to your controller. (For a simple app I attached it to my View Controller)
class ViewController: UIViewController, CBPeripheralDelegate, CBCentralManagerDelegate {
You should create a local variable centralManager (or similar) and then initialize in your viewDidLoad function
centralManager = CBCentralManager(delegate: self, queue: nil)
Finally you can create a new function called centralManagerDidUpdateState which will act as a callback when the Bluetooth state changes (it's always called on startup in your app.
// If we're powered on, start scanning
func centralManagerDidUpdateState(_ central: CBCentralManager) {
print("Central state update")
if central.state != .poweredOn {
print("Central is not powered on")
} else {
print("Central scanning for", ParticlePeripheral.particleLEDServiceUUID);
centralManager.scanForPeripherals(withServices: [ParticlePeripheral.particleLEDServiceUUID],
options: [CBCentralManagerScanOptionAllowDuplicatesKey : true])
}
}
The important call is centralManager.scanForPeripherals This will start the scanning process. In my case I'm filtering devices that only have ParticlePeripheral.particleLEDServiceUUID in their advertising packets.
That should get you scanning and on your way. I wrote a full end-to-end tutorial on how to use Swift with Bluetooth. It will go into much more detail. Here it is.
Related
I am currently developing an app that is gonna be used for connecting with BLE devices. Everything works fine for the most part, but I have noticed a weird behaviour from the CBCentralManager class. The longer it runs a search for BLE peripherals, the less often it can actually find the same peripheral (I am talking about discovering advertisement packages). I have tried some 3rd party apps (BLE scanners etc.), and they can find my peripheral without any problems. It does advertise every X seconds, and the apps can usually find it after 1-3 * X. Here is a very simple implementation of the BLE discovery code that shows the discovery deterioration symptoms:
import CoreBluetooth
class BluetoothTestClass: NSObject, CBCentralManagerDelegate, CBPeripheralDelegate {
private let manager: CBCentralManager
override init() {
self.manager = CBCentralManager(delegate: nil, queue: .main, options: [CBCentralManagerScanOptionAllowDuplicatesKey:true])
super.init()
self.manager.delegate = self
}
func startSearch() {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
self.manager.scanForPeripherals(withServices: nil, options: [CBCentralManagerOptionShowPowerAlertKey:true,
CBCentralManagerScanOptionAllowDuplicatesKey:true])
}
}
func centralManagerDidUpdateState(_ central: CBCentralManager) {
}
var peripherals: [CBPeripheral] = []
func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
peripherals.append(peripheral)
print(Date())
}
}
The only thing that I found that helps at this point, is resetting the search every 30 seconds, then the search results are pretty close to what I can see in the 3rd party scanner apps (peripheral found after 1-3 * X seconds).
Any leads, ideas and even breadcrumbs on this topic will be highly appreciated.
The reason why the discovery rate deteriorates as the search continues is likely due to the internal state of the CBCentralManager object. Over time, it may build up internal data structures or allocate more memory, impacting its performance.
Resetting the search every 30 seconds can help to alleviate this issue, as it discards the internal state of the CBCentralManager and starts with a clean slate. Additionally, you may consider using a different dispatch queue for running the BLE discovery code.
Another thing that you may consider is limiting the duration of the scan, as the longer, the scan continues, the more the discovery performance may deteriorate. You can set the scan timeout option when calling scanForPeripherals(withServices:options:) to limit the duration of the scan.
When doing Bluetooth communications one is often placed in a situation where one does a call which gets a response in a delegate, for example the characteristic discovery shown below:
func discoverCharacteristics(device: CBPeripheral)
{
servicesCount = device.services!.count
for service in device.services!
{
print("Discovering characteristics for service \(service.uuid)")
device.discoverCharacteristics([], for: service)
}
}
Now this discovery is not for a specific device but for health devices following the Bluetooth SIG services/profiles, so I don't know exactly what services they might have and I don't know how many characteristics might be in each service. The method is asynchronous and the answers are signaled in the following delegate method:
// Discovered Characteristics event
func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?)
{
for characteristic in service.characteristics!
{
print("Found characteristic \(characteristic.uuid)")
}
servicesCount = servicesCount - 1;
print("Characteristics sets left: \(servicesCount)")
if servicesCount == 0
{
print ("Found all characteristics")
DispatchQueue.main.async{
self.btleManager!.btleManagerDelegate.statusEvent(device: peripheral, statusEvent: Btle.CHARACTERISTICS_DISCOVERED)
}
self.device = peripheral
self.handleMds()
}
}
Now I need to wait until the discovery is done before I can take the next step because what I do next often depends on what I got. In Java and on Android what I do is wait on a CountDownLatch in the calling method and I signal the latch in the callback to release that wait.
The iOS equivalent of the CountDownLatch seems to be the DispatchSemaphore. However, doing so apparently blocks the system and no delegate ever gets called. SO what I have done (as shown in the code above) is to initialize a variable servicesCount with the number of services and in the delegate callback decrement it each time it is signaled. When it gets to zero I am done and then I do the next step.
This approach works but it seems hacky; it can't be correct. And its starting to get really messy when I need to do several reads of the DIS characteristics, the features, the various time services, etc. So what I would like to know is what is the proper method to wait for a delegate to get signaled before moving forward? Recall I do not know what services or characteristics these devices might have.
First of all, if you already have an implementation with CountDownLatch for Android, you may just do the same implementation for iOS. Yes, Swift doesn't have a built-in CountDownLatch, but nice folks from Uber created a good implementation.
Another option is to rely on a variable, like you do, but make it atomic. There are various implementations available online, including one in the same Uber library. Another example is in RxSwift library. There are many other variations. Atomic variable will provide a thread-safety to variable's read/write operations.
But probably the most swifty way is to have a DispatchGroup. Will look something like this:
let dispatchGroup = DispatchGroup() // instance-level definition
// ...
func discoverCharacteristics
{
for service in device.services!
{
dispatchGroup.enter()
// ...
}
dispatchGroup.notify(queue: .main) {
// All done
print ("Found all characteristics")
DispatchQueue.main.async{
self.btleManager!.btleManagerDelegate.statusEvent(device: peripheral, statusEvent: Btle.CHARACTERISTICS_DISCOVERED)
}
self.device = peripheral
self.handleMds()
}
// ...
func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?)
{
// ...
dispatchGroup.leave()
}
In other words, you enter the group when you are about to submit a request, you leave it when request is processed. When all items left the group, notify will execute with the block you provided.
You only get a single call to didDiscoverCharacteristicsFor for each call to device.discoverCharacteristics. That is, the didDiscoverCharacteristicsFor call is only made once all of the service's characteristics have been discovered.
You have the peripheral passed to the delegate call, so you have the information you need to know the context of the delegate call. There is no need to "wait". You can use a simple state machine per peripheral or service if you are discovering data for multiple peripherals/services simultaneously.
You can even keep a set of peripherals that aren't in a completely discovered state if you need to take some action once discovery is complete; e.g. You can remove a peripheral from the set once discovery is complete for that peripheral. If the set is empty, discovery is complete for all devices.
All of this state belongs in your model. All your delegate implementation should need to do is update the model with the data that has been discovered.
I would like to create a Garmin wearable app (Data Field), which would communicate with my iOS app using Bluetooth LE (BluetoothLowEnergy API on Garmin and CoreBluetooth on iOS). There's a limitation of Garmin's API - it can work only as a central device, so I configured my iPhone to act as a "virtual" peripheral (I tested it both with my own debug app and LightBlue).
I managed to establish a connection between my Garmin Vivoactive 3 Music and my iPhone, but I still have some issues to make it work.
From Garmin wearable I managed to search, find and pair a device (my iPhone virtual peripheral), so that both: self.pairedDevice = BluetoothLowEnergy.pairDevice(scanResult) and BluetoothLowEnergy.getPairedDevices().next() don't return nulls.
The problem I have is that this callback is never called on a Garmin device:
function onConnectedStateChanged(device, connectionState) {
if (connectionState==BluetoothLowEnergy.CONNECTION_STATE_CONNECTED) {
displayString = "Connected";
}
if (connectionState==BluetoothLowEnergy.CONNECTION_STATE_DISCONNECTED) {
displayString = "Disconnected";
}
}
Moreover, when discovering my virtual peripheral, I can see available services in advertisement data, but once the devices are paired, calling device.getServices() returns an empty iterator.
I already checked that BluetoothLowEnergy.getAvailableConnectionCount() is 3, so there should be no problem with regard to connections number limit. Is there any way to force the connection?
On iOS I do something like this:
let service = CBMutableService(type: serviceCbuuid, primary: true)
let writeableCharacteristic = CBMutableCharacteristic(type: characteristicCbuuid,
properties: [.write],
value: nil,
permissions: [.writeable])
service.characteristics = [writeableCharacteristic]
currentService = service
peripheralManager = CBPeripheralManager(delegate: self,
queue: DispatchQueue.global(qos: .userInteractive))
then I add currentService using peripheralManager?.add(currentService) and in didAdd service callback I start advertising by calling peripheral.startAdvertising(options).
Maybe I miss some setting on the iOS end to make this work?
In a CoreBluetooth related iOS app written in Swift (4.2), I have the following problem.
The app handles a certain number of Peripherals and Centrals.
Each Peripheral provides a service and has several Centrals connected to it; it keeps an accounting of that and knows how many Centrals are connected.
In the same way each Central is connected to several Peripherals; and also keeps an accounting of that to know how many Peripherals are connected.
The question I have is about the book-keeping to maintain the accounting I was just mentioning, up to date.
From the Peripheral side, it works and I can see when a Central has disconnected.
For that I use:
peripheralManager(_:central:didSubscribeTo:)
and:
peripheralManager(_:central:didUnsubscribeFrom:)
From the Central I want to use:
centralManager(_:didConnect:)
and:
centralManager(_:didDisconnectPeripheral:error:)
But here, for some reason, it does not work. The first function (centralManager(_:didConnect:)) is called as expected but not the second (centralManager(_:didDisconnectPeripheral:error:)).
I suppose this last issue of the function not being called is what I need to solve. But I may be wrong.
As a result, I see when a Peripheral is coming but not when it is leaving.
What am I missing? Or am I just doing it wrong?
I test the app using two devices.
--- Further information --- (Post update)
Here is some of the related code:
func centralManager(_ central: CBCentralManager,
didDiscover peripheral: CBPeripheral,
advertisementData: [String : Any],
rssi RSSI: NSNumber) {
print(#function)
if cbPerifList == nil {cbPerifList = [CBPeripheral]()}
if let perifIndx = cbPerifList!.index(where:{$0.identifier==peripheral.identifier}) {
central.connect(cbPerifList[perifIndx], options: nil)
} else {
peripheral.delegate = self
cbPerifList.append(peripheral)
central.connect(peripheral, options: nil)
}
}
One more possibly relevant thing I noticed is the following. Instead of switching of the peripheral by a button switch as I should normally do, I force kill the app on the device currently playing the peripheral role, then the centralManager(_:didDisconnectPeripheral:error:) function is called on the other device as I would expect it to be; and the accounting is correctly performed.
In my CoreBluetooth App
func centralManager (_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) {
}
Is called when ever peripheral is lost.
Maybe check your syntax of your didDisconnectPeripheral func.
I did not find a direct answer to this question.
Reading the comments seems to show there is now answer. In other words what I wanted to do sounds impossible. If some expert happens to know otherwise, please let us know.
In the meanwhile I made a work-around solution:
When shutting of one peripheral, I use one of its characteristics to set some non-sense value which is then used as hint by the central to know that the peripheral has "said good-bye"..... And it works.
Following is my code which i have written.I found many similar question but could not get proper answer helpful to me.
I am using swift and ios 8 for working and using core bluetooth Did discover peripheral method should be called by itself but is not getting called. what should I do to resolve?
code
Your problem is that you are invoking scanForPeripheralsWithServices immediately after creating the CBCentralManager - You need to wait until the central manager is in the powered on state.
ie -
func centralManagerDidUpdateState(central: CBCentralManager) {
...
else if central.state == CBCentralManagerState.PoweredOn {
central.scanForPeripheralsWithServices(nil,options:nil)
}
...
}
Also, in future you should post your code into your question and use the {} icon to mark it as code rather than inserting images, as images makes it impossible to copy your code into an answer
UPDATE
Your other problem is that you are creating your CBcentralManager as a local variable inside your btnScanClicked so as soon as this method ends your CBCentralManager will be released. You need to create it as a property on your class and then refer to self.central