Get the battery voltage of Logitech Lightspeed device with IOKit? - swift

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.

Related

I make Beacon in my Swift app which is advertising data but its local name is not shown. Can we add custom data packet during advertisement?

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.

What is the best way to get BSD drive names on macOS (Swift)?

What's the best way I can get a list of BSD names of all USB devices (and maybe including internal Mac drives) without using a diskutil CLI wrapper?
I don't want to use any wrappers that interact with the CLI interface, as this way of interacting is quite slow and unreliable:
This is an example of why I'm not happy with using CLI wrappers
(Compare 'Time elapsed for DiskUtil CLI Wrapper.' and 'Time elapsed for Disk Arbitration')
What is the best way to implement the solution for my problem?
Use the data from IOReg?
If yes, how can I get a list of BSD names of connected devices using it?
Here is an example what I want to get:
["disk0", "disk0s1", "disk0s2", "disk0s3", "disk1", "disk1s1", "disk1s2", "disk1s3", "disk1s4", "disk2", "disk2s1", "disk2s2", "disk3", "disk3s1", "disk3s1s1", "disk3s2", "disk3s3", "disk3s4", "disk3s5", "disk3s6", "disk4", "disk4s1", "disk4s2", "disk5", "disk5s1", "disk5s2", "disk6", "disk6s1", "disk6s2", "disk10", "disk10s1", "disk10s2", "disk11", "disk11s1"]
At the moment, I have the following:
static func getMountedBSDNames() -> [String] {
guard let session = DASessionCreate(nil) else { return [] }
guard let mountedVolumeURLs = FileManager.default.mountedVolumeURLs(includingResourceValuesForKeys: nil) else { return [] }
var BSDNames: [String] = []
for volumeURL in mountedVolumeURLs {
if let disk = DADiskCreateFromVolumePath(kCFAllocatorDefault, session, volumeURL as CFURL), let BSDName = DADiskGetBSDName(disk) {
BSDNames.append(
String(cString: BSDName)
)
}
}
return BSDNames
}
But in this case, only mounted are returning.
I want there to have even those, that were ejected
I achieved the desired result using the IOReg lookup method:
Elapsed Time
func getDriveBSDNames() -> [String] {
var iterator: io_iterator_t = 0
let matching: CFDictionary = IOServiceMatching(kIOServicePlane)
// Use 'kIOMasterPortDefault' for macOS older than 12.0 Monterey
IOServiceGetMatchingServices(kIOMainPortDefault, matching, &iterator)
var child: io_object_t = IOIteratorNext(iterator)
var BSDNames: [String] = []
while child > 0 {
if let BSDNameAnyObject = IORegistryEntryCreateCFProperty(child, "BSD Name" as CFString, kCFAllocatorDefault, IOOptionBits(kIORegistryIterateRecursively)) {
if let BSDNameString = (BSDNameAnyObject.takeRetainedValue() as? String), BSDNameString.starts(with: "disk") {
BSDNames.append(
BSDNameString
)
}
}
child = IOIteratorNext(iterator)
}
return BSDNames
}
In this case, it was also necessary to filter the output of the results using:
BSDNameString.starts(with: "disk")
(otherwise, some unnecessary devices were added, such as en0, anpi0, llw0, etc.)
Note that while the Disk Arbitration framework doesn't have a function for synchronously enumerating all disks, it does effectively support asynchronous enumeration by registering a callback for disk appearance. This may or may not be useful depending on your use case - when providing the user with an interactive list of devices, this is usually exactly what you want though, as you'll automatically be notified of newly added devices.
I don't do Swift, sorry, but the following C code should be easy enough to understand to come up with something similar in other languages.
#include <DiskArbitration/DiskArbitration.h>
#include <stdio.h>
static void disk_appeared(DADiskRef disk, void* context)
{
printf("%s\n", DADiskGetBSDName(disk) ?: "(null)");
}
int main()
{
DASessionRef session = DASessionCreate(kCFAllocatorDefault);
DASessionSetDispatchQueue(session, dispatch_get_main_queue());
DARegisterDiskAppearedCallback(session, NULL, disk_appeared, NULL /*context*/);
dispatch_main();
}
Note that the callback will also be called for APFS snapshots, which don't have a BSD name, so DADiskGetBSDName returns NULL and you'll have to do a little bit of filtering.

CoreBluetooth and Omron Evolv Blood Pressure Monitor

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)

Cannot Query Feature Attributes from ArcGIS Online Feature Service

I have created a feature service on ArcGIS online which has approximately 2000 features. Each feature has four fields: name, latitude, longitude and a boolean validation field (true/false). Two custom symbols are used - one for validated features and one for non-validated features.
I have successfully connected to the feature service from my native (xcode/swift) iOS application and the features are displayed properly on top of the basemap.
I have implemented a touch delegate and successfully detect when a feature symbol is tapped. The issue I am having is trying to query (read) the "name" field attribute associated with the symbol that was tapped. I have tried using the code below but have not been able to read the attribute:
func geoView(_ geoView: AGSGeoView, didTapAtScreenPoint screenPoint: CGPoint, mapPoint: AGSPoint) {
if let activeSelectionQuery = activeSelectionQuery {
activeSelectionQuery.cancel()
}
guard let featureLayer = featureLayer else {
return
}
//tolerance level
let toleranceInPoints: Double = 12
//use tolerance to compute the envelope for query
let toleranceInMapUnits = toleranceInPoints * viewMap.unitsPerPoint
let envelope = AGSEnvelope(xMin: mapPoint.x - toleranceInMapUnits,
yMin: mapPoint.y - toleranceInMapUnits,
xMax: mapPoint.x + toleranceInMapUnits,
yMax: mapPoint.y + toleranceInMapUnits,
spatialReference: viewMap.map?.spatialReference)
//create query parameters object
let queryParams = AGSQueryParameters()
queryParams.geometry = envelope
//run the selection query
activeSelectionQuery = featureLayer.selectFeatures(withQuery: queryParams, mode: .new) { [weak self] (queryResult: AGSFeatureQueryResult?, error: Error?) in
if let error = error {
print("error: ",error)
}
if let result = queryResult {
print("\(result.featureEnumerator().allObjects.count) feature(s) selected")
print("name: ", result.fields)
}
}
}
I am using the ArGIS iOS 100.6 SDK.
Any help would be appreciated in solving this issue.
The featureLayer selection methods merely update the map view display to visually highlight the features.
From the featureLayer, you should get the featureTable and then call query() on that. Note that there are two methods. A simple query() that gets minimal attributes back, or an override on AGSServiceFeatureTable that allows you to specify that you want all fields back. You might need to specify .loadAll on that override to get the name field back. We do it this way to avoid downloading too much information (by default we download enough to symbolize and label the feature).

MIDIThruConnectionCreate always creates persistent MIDI thru connection?

Xcode 8 beta 2 / Swift 3:
According to Apple's CoreMIDI API documentation, a MIDI thru connection can be established as persistent (stays in place forever, even after your app quits and your system reboots) or non-persistent/transitory (owned by your application and automatically destroys it on app quit).
The trouble I'm running into is that I can't seem to create a non-persistent connection, even though I am following Apple's guidelines.
It comes down to this API:
func MIDIThruConnectionCreate(_ inPersistentOwnerID: CFString?,
_ inConnectionParams: CFData,
_ outConnection: UnsafeMutablePointer<MIDIThruConnectionRef>) -> OSStatus
If you pass null (nil) to inPersistentOwnerID which is a Swift optional, the connection should be created as transitory. However, regardless of whether I pass nil or a String, connections are always created as persistent. (I can verify this by checking CoreMIDI's persistent thru connections.)
A summation of my code:
public class OTMIDIConnectedThru {
var connectionRef = MIDIThruConnectionRef()
init?(sourceEndpoints: [MIDIEndpointRef], destinationEndpoints: [MIDIEndpointRef], persistentOwnerID: String? = nil) {
var params = MIDIThruConnectionParams()
MIDIThruConnectionParamsInitialize(&params) // fill with defaults
// (... snip: code to prepare parameters here ...)
let paramsData = withUnsafePointer(&params) { p in
NSData(bytes: p, length: MIDIThruConnectionParamsSize(&params))
}
result = MIDIThruConnectionCreate(persistentOwnerID, paramsData, &connectionRef)
guard result == noErr else { return nil }
}
}
Any idea what I'm doing wrong? This couldn't possibly be a bug in the API?
I had the same issue, and yes I think it always does create persistent connections. Probably an ID of NULL is the same as an empty string, because MIDIThruConnectionFind with an empty string returns all those persistent connections. So, a bug in the API or the docs!
I would recommend using a real persistentID, and remove all existing/stale connections when you initialize your MIDI stuff:
CFDataRef data;
MIDIThruConnectionFind(CFSTR("com.yourcompany.yourapp"), &data);
unsigned long n = CFDataGetLength(data) / sizeof(MIDIThruConnectionRef);
MIDIThruConnectionRef * con = (MIDIThruConnectionRef*)CFDataGetBytePtr(data);
for(int i=0;i<n;i++) {
MIDIThruConnectionDispose(*con);
con++;
}