I am trying to make an app that will be used as a main control for a bluetooth watch(e.g. fitness bracelets, smart watches). I have done my research about this, and although some people managed to do so, they don't give many details about the process. Below are a few of the "solutions" that I found:
Is it possible to switch central and peripheral each other on iOS?
Can iOS do central and peripheral work on same app at same time?
Peripheral and central at the same time on iOS
All of these are in Objective-C and although I am familiar with it, the posts are 3+ years old so things have changed concerning the code. Another problem is that I need to use the app with another Bluetooth device, not an iOS device as the ones above are doing it, and for the moment the connection request can only come from the iPhone, not from the bluetooth device.
The question is if it's possible to achieve the desired result, and if so, what would be the best way to do it? So far, one of the proposed solutions was to connect to the device, acquire the UUID and then switch the iPhone to peripheral mode so that it can advertise it's services. That is not possible(in my opinion), at least in this current stage.
iOS already has a predefined service that can be discovered and accessed by the device (Current Time Service) when the 2 of them connect, without any modifications from my part so there should be a way to accomplish this.
I hope I made myself clear enough about the problem, if you believe I can add more details to clarify the context, let me know. Thanks for your time.
I am posting below some of the key code from the view in which I discover peripherals:
override func viewDidAppear(_ animated: Bool) {
manager = CBCentralManager(delegate: self, queue: nil)
peripheralManager = CBPeripheralManager(delegate: self, queue: nil)
peripherals = []
if (manager?.state == CBManagerState.poweredOn) {
scanBLEDevices()
self.tableView.reloadData()
}
}
func peripheralManagerDidUpdateState(_ peripheral: CBPeripheralManager) {
switch(peripheral.state)
{
case.unsupported:
print("Peripheral is not supported")
case.unauthorized:
print("Peripheral is unauthorized")
case.unknown:
print("Peripheral is Unknown")
case.resetting:
print("Peripheral is Resetting")
case.poweredOff:
print("Peripheral service is powered off")
case.poweredOn:
print("Peripheral service is powered on")
print("Start advertising.")
let serviceUUID:CBUUID = CBUUID(string: self.service_uuid_string)
let locationUUID:CBUUID = CBUUID(string: self.location_and_speed)
// Start with the CBMutableCharacteristic
self.locationCharacteristic = CBMutableCharacteristic(type: locationUUID, properties: .notify , value: nil, permissions: .readable)
// Then the service
let locationService = CBMutableService(type: serviceUUID, primary: true)
// Add the characteristic to the service
locationService.characteristics?.append(locationCharacteristic!)
// And add it to the peripheral manager
self.peripheralManager?.add(locationService)
peripheralManager?.startAdvertising([CBAdvertisementDataServiceUUIDsKey : serviceUUID])
}
}
I'm getting back with the correct way of achieving the required functionality. After initialising the peripheralManager, create a CBMutableService and hold a reference to it(declared at the top of the class).
var globalService:CBMutableService? = nil
Next step is to check for the peripheralManager state, and do all the required work after you receive the poweredOn state:
func peripheralManagerDidUpdateState(_ peripheral: CBPeripheralManager) {
switch(peripheral.state)
case.poweredOn:
print("Peripheral service is powered on")
createServiceWithCharacteristics()
}
func createServiceWithCharacteristics(){
let serviceUUID:CBUUID = CBUUID(string: self.service_uuid_string)
let featureCharacteristicUUID:CBUUID = CBUUID(string: self.feature_characteristic_uuid_string)
// Start with the CBMutableCharacteristic
let permissions: CBAttributePermissions = [.readable, .writeable]
let properties: CBCharacteristicProperties = [.notify, .read, .write]
self.featureCharacteristic = CBMutableCharacteristic(type: featureCharacteristicUUID, properties: properties , value: nil, permissions: permissions)
// Then the service
let localService = CBMutableService(type: serviceUUID, primary: true)
// Add the characteristic to the service
localService.characteristics = [featureCharacteristic!]
globalService = localService
// And add it to the peripheral manager
self.peripheralManager?.add(globalService!)
print("Start advertising.")
peripheralManager?.startAdvertising([CBAdvertisementDataLocalNameKey:"Name"])
}
Related
I am looking to build an iOS application that uses CoreBluetooth to act as a BLE peripheral and a web app that uses WebBluetooth to scan for, find and exchange messages with the iPhone app.
I started off with this Swift code:
import SwiftUI
import CoreBluetooth
class BLE: NSObject, CBPeripheralManagerDelegate {
var peripheralManager: CBPeripheralManager!
override init() {
super.init()
peripheralManager = CBPeripheralManager(delegate: self, queue: nil)
}
func peripheralManagerDidUpdateState(_ peripheral: CBPeripheralManager) {
switch peripheral.state {
case .unknown: print("unknown")
case .resetting: print("resetting")
case .unsupported: print("unsupported")
case .unauthorized: print("unauthorized")
case .poweredOff:
print("poweredOff")
peripheral.stopAdvertising()
case .poweredOn:
print("poweredOn")
let characteristic1 = CBMutableCharacteristic(
type: CBUUID(nsuuid: UUID()),
properties: [.notify, .write, .read],
value: nil,
permissions: [.readable, .writeable]
)
let characteristic2 = CBMutableCharacteristic(
type: CBUUID(nsuuid: UUID()),
properties: [.read],
value: "test".data(using: .utf8),
permissions: [.readable]
)
let serviceUuid = CBUUID(nsuuid: UUID())
let service = CBMutableService(type: serviceUuid, primary: true)
service.characteristics = [characteristic1, characteristic2]
peripheralManager.add(service)
//let uuid = CBUUID(string: "14b50a17-51be-478f-9b24-7efa8d2a083f")
peripheral.startAdvertising([
CBAdvertisementDataLocalNameKey: "blething",
CBAdvertisementDataServiceUUIDsKey: [serviceUuid]
])
default: print("???")
}
}
func peripheralManagerDidStartAdvertising(_ peripheral: CBPeripheralManager, error: Error?) {
print(error)
}
}
#main
struct BTtestApp: App {
let ble = BLE()
var body: some Scene {
WindowGroup {
Text("Hello, world!")
.padding()
}
}
}
I believe this is all of the code needed to make my iOS app BLE peripheral discoverable. I run this app on my phone and the advertisement call seems to go through without any runtime errors or crashes. The error printed is nil.
However, when I use https://googlechrome.github.io/samples/web-bluetooth/scan.html?allAdvertisements=true and scan for advertisements, nothing at all comes up.
When I uncheck All Advertisements and fill in Device Name to say blething, nothing comes up still.
I tried using both uuid and [uuid] (an array) for the service UUIDs value as I am not sure which is correct. The docs say the type is string https://developer.apple.com/documentation/corebluetooth/cbadvertisementdataserviceuuidskey but also that it should be an array? Anyway, neither worked.
What is the problem? Why is my BLE peripheral implemented in software using CoreBluetooth not showing up?
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!
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.
I'm trying to send a process in a background thread using the following code:
let qualityOfServiceClass = QOS_CLASS_BACKGROUND
let backgroundQueue = dispatch_get_global_queue(qualityOfServiceClass, 0)
dispatch_async(backgroundQueue, {
print("running in the background queue")
btDiscovery
})
but the class is only processing while begin in foreground...any idea ?
EDIT1:
btDiscovery is a class which performs a BLE device scan every X seconds:
let btDiscoverySharedInstance = Beacon();
class Beacon: NSObject, CBCentralManagerDelegate {
private var centralManager: CBCentralManager?
private var peripheralBLE: CBPeripheral?
....
func centralManagerDidUpdateState(central: CBCentralManager) {
switch (central.state) {
case CBCentralManagerState.PoweredOff:
print("BLE powered off")
self.clearDevices()
case CBCentralManagerState.Unauthorized:
// Indicate to user that the iOS device does not support BLE.
print("BLE not supported")
break
case CBCentralManagerState.Unknown:
// Wait for another event
print("BLE unknown event")
break
case CBCentralManagerState.PoweredOn:
print("BLE powered on")
self.startScanning()
break
case CBCentralManagerState.Resetting:
print("BLE reset")
self.clearDevices()
case CBCentralManagerState.Unsupported:
print("BLE unsupported event")
break
}
}
func startScanning() {
print("Start scanning...")
if let central = centralManager {
central.scanForPeripheralsWithServices(nil, options: nil)
}
}
func centralManager(central: CBCentralManager, didDiscoverPeripheral peripheral: CBPeripheral, advertisementData: [String : AnyObject], RSSI: NSNumber) {
print("Discovered peripheral \(RSSI) dBM name: \(peripheral.name)")
print("UUID: \(peripheral.identifier.UUIDString)")
...
sleep(delayPolling)
self.startScanning()
}
when the app is launched and remains in foreground, the scan is performed correctly every "delayPolling" seconds.
but as soon as I put my app is background, the scan is paused. it restarts only when it comes back again in foreground.
I would need to leave this scan running in background every time (even if we set a lower priority to this thread).
EDIT2:
by reading the documentation https://developer.apple.com/library/ios/documentation/NetworkingInternetWeb/Conceptual/CoreBluetooth_concepts/CoreBluetoothBackgroundProcessingForIOSApps/PerformingTasksWhileYourAppIsInTheBackground.html
I can see that
When an app that implements the central role includes the UIBackgroundModes key with the bluetooth-central value in its Info.plist file, the Core Bluetooth framework allows your app to run in the background to perform certain Bluetooth-related tasks. While your app is in the background you can still discover and connect to peripherals, and explore and interact with peripheral data. In addition, the system wakes up your app when any of the CBCentralManagerDelegate or CBPeripheralDelegate delegate methods are invoked
I selected the corresponding options in my Info.plist file:
but my app is not running my thread in background.
I realize this is an old question, but scanning in the background requires that you supply a Service UUID.
central.scanForPeripheralsWithServices(nil, options: nil)
needs to be
central.scanForPeripheralsWithServices(serviceUUID, options: nil)
I'm developing a BLE application for IOS (SWIFT) and I've found a strange behaviour.. my test has 2 controllers, ONE with the CentralManager Role and the other with the PeripheralManager Role..
Here's my code (summary):
Parameters.swift:
...
// a custome UUID created in console
let TRANSFER_SERVICE_UUID = CBUUID(string: "FB694B90-F49E-....-....-171BBA78F846")
...
Peripheral.swift
...
var pManager = CBPeripheralManager()
var transferService = CBMutableService()
override func viewDidLoad() {
super.viewDidLoad()
pManager = CBPeripheralManager(delegate: self, queue: nil)
}
func peripheralManagerDidUpdateState(peripheral: CBPeripheralManager!) {
if(peripheral.state == CBPeripheralManagerState.PoweredOn) {
transferService = CBMutableService(type: TRANSFER_SERVICE_UUID, primary: true)
// add some characteristic
pManager.addService(transferService)
pManager.startAdvertising(nil)
}
}
...
Central.swift
...
var cManager = CBCentralManager()
override func viewDidLoad() {
super.viewDidLoad()
cManager = CBCentralManager(delegate: self, queue: nil)
}
func centralManagerDidUpdateState(central: CBCentralManager!) {
if central.state == CBCentralManagerState.PoweredOn {
cManager.scanForPeripheralsWithServices([TRANSFER_SERVICE_UUID], options: nil)
}
}
...
Now, if I take 2 device, one with the Central and the other with the Peripheral Role the 2 app can't find each other (but LightBlue app and similar will so the device is emitting)
On the other hand, if I change the code to:
cManager.scanForPeripheralsWithServices(nil, options: nil)
my application works perfectly and the 2 devices can communicate each other.. but at the same time I can't filter only the devices that are emitting TRANSFER_SERVICE_UUID.. I don't want to connect to all peripheral finded in order to search for TRANSFER_SERVICE_UUID.. isn't this the right way to proceed ?? Did I miss something ??
There is only limited space in the Bluetooth advertisement area, so iOS does not automatically advertise all services - A device may have a primary service and several supplementary services. Advertising all services is wasteful when all that is required is to discover the primary service in order to identify a candidate device.
To be able to discover the service in scanForPeripheralsWithServices you need to include the service in the advertisement data.
This is done by including the service's UUID in the dictionary you pass to CBPeripheralManager.startAdvertising -
pManager.startAdvertising([CBAdvertisementDataServiceUUIDsKey:[TRANSFER_SERVICE_UUID]])