I have been working on trying to support the Omron Evolv Blood Pressure Monitor (BPM) in my app, via CoreBluetooth. Using the Bluetooth SIG documentation about BPM’s (https://www.bluetooth.com/specifications/specs/ and then BLP and BLS) I could connect with the monitor.
I used the following characteristics:
Blood pressure measurement, 2A35
Blood pressure feature, 2A49
Page 10 in the BLS documentation states that the Blood Pressure Measurement is the property Indicate, which to my knowledge behaves similar to the Notify property.
To clarify some code I call in the delegate methods of CBPeripheralDelegate:
func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
print("connected!")
bloodPressurePeripheral.discoverServices([bloodPressureService])
}
func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
guard let services = peripheral.services else { return }
services.forEach { service in
print("discovered service: \(service) \n")
peripheral.discoverCharacteristics(nil, for: service)
}
}
In the didDiscoverCharacteristicsFor function I simply loop through the characteristics and check their property.
if char.properties.contains(.read) {
print("\(char.uuid): properties contains .read")
peripheral.readValue(for: char)
}
if char.properties.contains(.indicate) {
print("\(char.uuid): properties contains .indicate")
peripheral.setNotifyValue(true, for: char)
}
if char.properties.contains(.notify) {
print("\(char.uuid): properties contains .notify")
peripheral.setNotifyValue(false, for: char)
I tried both readValue and setNotifyValue for indicate both I still get the following result:
<CBCharacteristic: 0x2829880c0, UUID = 2A35, properties = 0x20, value = (null), notifying = NO>
2A35: properties contains .indicate
<CBCharacteristic: 0x282988180, UUID = 2A49, properties = 0x2, value = {length = 2, bytes = 0x2700}, notifying = NO>
2A49: properties contains .read
I don't really understand why the value of 2A35 is null. I know there are values because with the Omron application I can get the measurements.
My actual questions is: Has anyone has any experience in connecting with (Omron) BPM's using CoreBluetooth and what am I overlooking?
Thanks for answering!
I have tried connecting Ormon Evolv with Android device.
I will tell you what I have learned from it.
NB:- Ble Devices communicates asynchronously, you have to do one GATT operation (eg, read, write, enable notification, enable indication) at a time. The next operation is to be done only after the previous one is successfully done.
My device had the following services.
DEVICE_INFO_SERVICE with UUID 180a
BATTERY_SERVICE_UUID with UUID 180f
CURRENT_TIME_SERVICE_UUID with UUID 1805
BLOOD_PRESSURE_SERVICE_UUID with UUID 1810
The first GATT operation after a successful connect is gatt.discoverServices() . (the event onServicesDiscoverd is trigged, in android)
If you just need the BP reading without reading status and time stamp, just enable indication for UUID 2A35
val bloodPressureService = gatt?.getService(BLOOD_PRESSURE_SERVICE_UUID)
val bloodPressureChar = bloodPressureService?.getCharacteristic(BLOOD_PRESSURE_CHAR_UUID)
gatt?.setCharacteristicNotification(bloodPressureChar, true)
val bloodDescriptor = bloodPressureChar?.getDescriptor(CCC_DESCRIPTOR_UUID)
bloodDescriptor?.value = BluetoothGattDescriptor.ENABLE_INDICATION_VALUE
gatt?.writeDescriptor(bloodDescriptor)
Related
I'd like to write a simple tool in Swift to read battery status of my mouse (Logitech G Pro Wireless). Since it's my first dive into both IOKit and managing HID devices, I am struggling with getting it done.
Here's my current approach:
I used this library to skip for now messing with Obj-C https://github.com/Arti3DPlayer/USBDeviceSwift
On launch of the app I create HID Device with hardcoded ids and start listening for its presence:
struct MyApp: App {
let rfDeviceMonitor = HIDDeviceMonitor([
HIDMonitorData(vendorId: 0x046d, productId: 0xC539)//0xc088)
], reportSize: 64)
var body: some Scene {
Window()
.onAppear {
let rfDeviceDaemon = Thread(target: self.rfDeviceMonitor, selector:#selector(self.rfDeviceMonitor.start), object: nil)
rfDeviceDaemon.start()
}
}
}
Another class is listening for connection and device's data.
func configure() {
NotificationCenter.default.addObserver(self, selector: #selector(self.usbConnected), name: .HIDDeviceConnected, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(self.hidReadData), name: .HIDDeviceDataReceived, object: nil)
}
#objc func usbConnected(notification: NSNotification) {
guard let nobj = notification.object as? NSDictionary else {
return
}
guard let deviceInfo: HIDDevice = nobj["device"] as? HIDDevice else {
return
}
self.deviceInfo = deviceInfo
write()
}
#objc func hidReadData(notification: Notification) {
let obj = notification.object as! NSDictionary
let data = obj["data"] as! Data
print([UInt8](data))
}
func write() {
let payload: [UInt8] = [
0x10, // short message
0xff, // receiver's index
0x06, // feature index - battery voltage for g pro wireless
0x00,
0x00,
0x00
]
var correctData = Data(payload)
var count = correctData.count
let ret: Int32 = IOHIDDeviceGetReport(
self.deviceInfo.device,
kIOHIDReportTypeFeature,
CFIndex(0x10),
&correctData,
&count
)
print(ret) // 0xe0005000
print([UInt8](correctData)) // [16, 255, 6, 0, 0, 0]
}
The issue is that IOKit always returns a value (0xe0005000) after calling IOHIDDeviceGetReport that is not a success. I have no idea what this means, since kIOReturn header doesn't mention this value at all.
Links that I found useful:
receiver's properties: https://github.com/pwr-Solaar/Solaar/blob/78341f87e969fcdb657d912953f919e7bdd7c491/docs/devices/Lightspeed%20Receiver%20C539.txt
Mouse's properties: https://github.com/pwr-Solaar/Solaar/blob/78341f87e969fcdb657d912953f919e7bdd7c491/docs/devices/G%20Pro%20Wireless%20Gaming%20Mouse%204079.txt
feature's and implementation of reading features via hidpp 2.0 https://github.com/pwr-Solaar/Solaar/blob/eac916b57c78b23a40bcded1a9c89e2cc30e06d4/lib/logitech_receiver/hidpp20.py
Logitech's specification's draft for hidpp 2.0 https://drive.google.com/drive/folders/0BxbRzx7vEV7eWmgwazJ3NUFfQ28?resourcekey=0-dQ-Lx1FORQl0KAdOHQaE1A
The most important informations I got from these sites:
My mouse has only a feature of passing current voltage. It's available on index 6. Also, there's mentioned hex value of 0x1001 next to that feature, but I am not sure what it could mean, maybe it's some Logitech's identifier for this feature.
buffer size has 7 or 20 bytes (this call should be 7B), where:
first means message size (short - 7B - 0x10, long - 20B - 0x11)
second is device index (0xFF is for receiver, yet to find which is meant for device connected via a wire)
Third means feature index (which is 6 in this case)
Fourth is divided on function and software identifier (in this order). The second one people say that is used to recognize responses meant for us from the rest of the output.
rest should be filled by report that I'd like to get.
https://github.com/pwr-Solaar/Solaar/blob/d41c60718876957158f2ef7ce51648cab78c72ad/lib/logitech_receiver/base.py#L437 - here's Python's implementation of sending such a request. This line particularly looks like it sends a request, then it waits for a message back? This is the point where I am struggling the most.
There's a line in documentation that describes bytes meaning, says:
"The device index, feature index, function identifier, and software identifier are always returned unchanged.", so I suppose I should use getReport instead of setReport, but again, my knowledge is very limited here, so I tried setReport as well, but with no luck. Same error has been thrown.
My app act as a beacon but I want to add its local name like my appName. I want to know that can we advertise custom packet in which I can add local name while advertising major, minior, proximityuuid, and identifier in Swift.
My current code for advertiseing ibeacon:
func initLocalBeacon() {
if localBeacon != nil {
stopLocalBeacon()
}
let uuid = UUID(uuidString: localBeaconUUID)!
localBeacon = CLBeaconRegion(uuid: uuid, major: localBeaconMajor, minor: localBeaconMinor, identifier: identifier)
beaconPeripheralData = localBeacon.peripheralData(withMeasuredPower: nil)
peripheralManager = CBPeripheralManager(delegate: self, queue: nil, options: nil)
}
func peripheralManagerDidUpdateState(_ peripheral: CBPeripheralManager) {
if peripheral.state == .poweredOn {
peripheralManager.startAdvertising(beaconPeripheralData as? [String: Any])
}
else if peripheral.state == .poweredOff {
peripheralManager.stopAdvertising()
}
}
I am trying to add custom packet in which I add local name for my beacon. Is it possible?
Yor app has very limited control over BLE advertisements on iOS because it is a shared resource across all apps.
Here’s what an app can do:
Trigger an iBeacon advert while it is in the foreground. You only control a 16 byte UUID a 2 byte major and a 2 byte minor.
Trigger a 16-byte service UUID advert while in the foreground.
Trigger a number of bits in a 128 bit bit mask to be turned on in an Overflow Area advert while your app is in the background.
Here is what the phone (not your app) can do:
Trigger a scan response packet containing the name of the phone. This is automatic by the operating system and the name is the name of the phone in Settings.
Your app cannot change the advertised name in the scan response. It cannot advertise additional data over BLE other than shown above, giving you a limited number of bytes to work with.
I have a paired bluetooth peripheral which I have to send some credentials in a JSON like as follows
{"SSID":"WIFI_SSID", "Password": "WIFI_PASSWORD"}
And after the information has been sent the peripheral should connect to the provided WiFi credentials but I'm not completely sure the process is being performed correctly.
As soon the bluetooth peripheral has connected I start the process,
func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
// As soon as the device has connected
peripheral.delegate = self
// Only discover the service I know it's for writing
peripheral.discoverServices([SERVICE_UUID])
}
The discoverServices will call the peripheral(:didDiscoverServices error)
func peripheral( _ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
for service in peripheral.services! {
peripheral.discoverCharacteristics(nil, for: service)
}
}
Calling the following method where I do all the logic
func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {
for characteristic in service.characteristics! {
let characteristic = characteristic as CBCharacteristic
debugPrint(characteristic.properties)
// This prints the following properties, reading + writing
// __C.CBCharacteristicProperties(rawValue: 10)
if characteristic.uuid.isEqual(RX_UUID) {
do {
let msgDictionary = ["SSID": wiFiCredentials.SSID, "Password": wiFiCredentials.Password]
let jsonData = try JSONSerialization.data(withJSONObject: msgDictionary,options:[])
peripheral.writeValue(jsonData, for: characteristic, type: CBCharacteristicWriteType.withResponse)
} catch let error as NSError {
debugPrint("Error in auth message JSON: \(error.description)")
}
}
}
}
Up to this point I think everything it's correct. After calling writeValue and setting the type to CBCharacteristicWriteType.withResponse I should expect something in the peripheral(:didWriteValueFor characteristic: error) method. What I receive in that method is the next error
Error Domain=CBATTErrorDomain Code=3 \"Writing is not permitted.\" UserInfo={NSLocalizedDescription=Writing is not permitted.}"
What I guess is that when I write the JSON value I shouldn't use the .withResponse flag and use .withoutResponse. If I do so I get the following log in the console.
[CoreBluetooth] WARNING: Characteristic <CBCharacteristic: 0x28388a040, UUID = 3E9D2532-2F00-11E9-9602-A44CC81C989A, properties = 0xA, value = (null), notifying = NO> does not specify the "Write Without Response" property - ignoring response-less write
Which confirms to me that I have to use the .writeWithResponse.
Is there something I am missing in the process? The JSON has to be sent using GATT and AFAIK this is the correct approach to do it. Are the printed CBCharacteristicProperties correct?
Things I've done:
The JSON is not the problem. I've tried writing a random variable "1".data(using: .ascii) and still receive the same error.
I am working on NEHotspotHelper and trying to register but not receiving call back. Firstly,
I enabled Capability : Network Extensions
Then added this following code,
let options: [String: NSObject] = [kNEHotspotHelperOptionDisplayName : "ABC" as NSObject]
let queue: DispatchQueue = DispatchQueue(label: "com.ABC", attributes: DispatchQueue.Attributes.concurrent)
NSLog("Started wifi scanning.")
NEHotspotHelper.register(options: options, queue: queue) { (cmd: NEHotspotHelperCommand) in
NSLog("Received command: \(cmd.commandType.rawValue)")
if cmd.commandType == NEHotspotHelperCommandType.filterScanList {
//Get all available hotspots
let list: [NEHotspotNetwork] = cmd.networkList!
//Figure out the hotspot you wish to connect to
print(list)
} else if cmd.commandType == NEHotspotHelperCommandType.evaluate {
if let network = cmd.network {
//Set high confidence for the network
network.setConfidence(NEHotspotHelperConfidence.high)
let response = cmd.createResponse(NEHotspotHelperResult.success)
response.setNetwork(network)
response.deliver() //Respond back
}
} else if cmd.commandType == NEHotspotHelperCommandType.authenticate {
//Perform custom authentication and respond back with success
// if all is OK
let response = cmd.createResponse(NEHotspotHelperResult.success)
response.deliver() //Respond back
}
}
Kindly let me know if I am missing any step.
You should check the result of the register() function. If it's returning false, something is probably not configured correctly. See the full list of configuration instructions below.
Also in the screenshot you provided, you have the entitlements enabled for Hotspot Configuration, but the API you're calling is for Hotspot Helper. The two features require very different entitlements. You'll need to make sure everything is configured for Hotspot Helper to call that API. Again, see below for full details. See Hotspot Helper vs. Hotspot Configuration for more details about the differences of these similarly named APIs.
To use NEHotspotHelper:
Apply for the Network Extension entitlement.
This needs to be done at Apple's website here.
Modify your Provisioning Profile.
Go to http://developer.apple.com. Hit Edit near your profile. On the bottom where it says Entitlements, choose the one that contains the Network Extension entitlement.
Update your app's entitlements file.
The application must set com.apple.developer.networking.HotspotHelper as one of its entitlements. The value of the entitlement is a boolean set to true.
Add Background Mode
The application's Info.plist must include a UIBackgroundModes array containing network-authentication.
Note that unlike all the other background modes that are converted to human readable strings, this one will stay as network-authentication.
Call the NEHotspotHelper.register() function.
This method should be called once when the application starts up. Invoking it again will have no effect and result in false being returned.
You should make sure the function returns true. Otherwise something one of the above steps is probably not configured properly.
Understand when this callback will be called.
From the documentation, it's not entirely clear when exactly this callback will be called. For example, one might assume that NEHotspotHelper could be used to monitor for network connections. However, the callback will (only?) be called when the user navigates to the Settings app and goes to the Wi-Fi page.
Since your callback will be called only while the user in the Settings app, you should attach to the debugger and use print().
Swift Example
let targetSsid = "SFO WiFi"
let targetPassword = "12345678"
let targetAnnotation: String = "Acme Wireless"
let options: [String: NSObject] = [
kNEHotspotHelperOptionDisplayName: targetAnnotation as NSString
]
let queue = DispatchQueue(label: "com.example.test")
let isAvailable = NEHotspotHelper.register(options: options, queue: queue) { (command) in
switch command.commandType {
case .evaluate,
.filterScanList:
let originalNetworklist = command.networkList ?? []
let networkList = originalNetworklist.compactMap { network -> NEHotspotNetwork? in
print("networkName: \(network.ssid); strength: \(network.signalStrength)")
if network.ssid == targetSsid {
network.setConfidence(.high)
network.setPassword(targetPassword)
return network
}
return nil
}
let response = command.createResponse(.success)
response.setNetworkList(networkList)
response.deliver()
default:
break
}
}
assert(isAvailable)
Sources:
https://developer.apple.com/documentation/networkextension/nehotspothelper/1618965-register
https://medium.com/#prvaghela/nehotspothelper-register-an-app-as-a-hotspot-helper-cf92a6ed7b72
https://stackoverflow.com/a/39189063/35690
I am trying to make two programs that run on separate devices communicate with each other over bluetooth with CoreBluetooth. I can find and connect peripherals from the manager, and I can browse services in connected peripherals, but when I try and try and discover characteristics, I get the error The specified UUID is not allowed for this operation. and as expected the service's characteristics come up nil.
What is this supposed to mean? I have tried to discover characteristics with specifying the UUID of the target and without, both show this error.
this is the function that prints the error.
func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {
print(error.localizedDescription)//prints "The specified UUID is not allowed for this operation."
if service.characteristics != nil {
for characteristic in service.characteristics! {
if characteristic.uuid == CBUUID(string: "A4389A32-90D2-402F-A3DF-47996E123DC1") {
print("characteristic found")
peripheral.readValue(for: characteristic)
}
}
}
}
this is where I look for peripherals.
func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
if peripheral.services != nil {
for service in peripheral.services! {
if service.uuid == CBUUID(string: "dc495108-adce-4915-942d-bfc19cea923f") {
peripheral.discoverCharacteristics(nil, for: service)
}
}
}
}
this is how I add the service characteristic on the other device.
service = CBMutableService(type: CBUUID(string:"dc495108-adce-4915-942d-bfc19cea923f"), primary: true)
characteristic = CBMutableCharacteristic(type: CBUUID(string: "A4389A32-90D2-402F-A3DF-47996E123DC1"), properties: .write, value: nil, permissions: .writeable)
service.characteristics = [characteristic]
I tried a number of different combinations of properties and permissions (including .read/.readable) and I get the same error.
You are attempting to read the value of a characteristic that you have set as write-only, so Core Bluetooth gives you an error; the read operation is not valid for the specified characteristic.
If you want your characteristic to be both readable and writable you need to specify this:
service = CBMutableService(type: CBUUID(string:"dc495108-adce-4915-942d-bfc19cea923f"), primary: true)
let characteristic = CBMutableCharacteristic(type: CBUUID(string: "A4389A32-90D2-402F-A3DF-47996E123DC1"), properties: [.write, .read], value: nil, permissions: [.writeable, .readable])
service.characteristics = [characteristic]