mirror of
https://github.com/morgan9e/macos-stats
synced 2026-04-15 00:34:08 +09:00
feat: rewrite BLE reader. Improve battery levels for Apple BLE devices (Mouse/Keyboard/Trackpad) (#557)
This commit is contained in:
@@ -45,7 +45,7 @@ public protocol ReaderInternal_p {
|
||||
func read()
|
||||
}
|
||||
|
||||
open class Reader<T>: ReaderInternal_p {
|
||||
open class Reader<T>: NSObject, ReaderInternal_p {
|
||||
public var log: NextLog {
|
||||
return NextLog.shared.copy(category: "\(String(describing: self))")
|
||||
}
|
||||
@@ -70,6 +70,7 @@ open class Reader<T>: ReaderInternal_p {
|
||||
public init(popup: Bool = false) {
|
||||
self.popup = popup
|
||||
|
||||
super.init()
|
||||
self.setup()
|
||||
|
||||
debug("Successfully initialize reader", log: self.log)
|
||||
|
||||
@@ -13,19 +13,27 @@ import Foundation
|
||||
import Kit
|
||||
import CoreBluetooth
|
||||
|
||||
public enum BLEConnType: Int {
|
||||
case ioDevice
|
||||
case cache
|
||||
case ble
|
||||
}
|
||||
|
||||
public struct BLEDevice {
|
||||
let uuid: UUID
|
||||
let conn: BLEConnType
|
||||
|
||||
let address: String
|
||||
let name: String
|
||||
var uuid: UUID?
|
||||
|
||||
var RSSI: Int?
|
||||
var batteryLevel: [KeyValue_t]
|
||||
var RSSI: Int? = nil
|
||||
var batteryLevel: [KeyValue_t] = []
|
||||
|
||||
var isConnected: Bool
|
||||
var isPaired: Bool
|
||||
var isInitialized: Bool
|
||||
var isConnected: Bool = false
|
||||
var isPaired: Bool = false
|
||||
|
||||
var peripheral: CBPeripheral?
|
||||
var isPeripheralConnected: Bool = false
|
||||
}
|
||||
|
||||
public class Bluetooth: Module {
|
||||
@@ -79,7 +87,7 @@ public class Bluetooth: Module {
|
||||
let pair = self.selectedBattery.split(separator: "@")
|
||||
|
||||
guard let device = value.first(where: { $0.name == pair.first! }) else {
|
||||
error("cannot find selected battery: \(self.selectedBattery)")
|
||||
// error("cannot find selected battery: \(self.selectedBattery)")
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -32,12 +32,11 @@ internal class Popup: NSStackView, Popup_p {
|
||||
let views = self.subviews.filter{ $0 is BLEView }.map{ $0 as! BLEView }
|
||||
|
||||
list.reversed().forEach { (ble: BLEDevice) in
|
||||
if let view = views.first(where: { $0.uuid == ble.uuid }) {
|
||||
if let view = views.first(where: { $0.address == ble.address }) {
|
||||
view.update(ble.batteryLevel)
|
||||
} else {
|
||||
self.addArrangedSubview(BLEView(
|
||||
width: self.frame.width,
|
||||
uuid: ble.uuid,
|
||||
address: ble.address,
|
||||
name: ble.name,
|
||||
batteryLevel: ble.batteryLevel
|
||||
@@ -54,14 +53,14 @@ internal class Popup: NSStackView, Popup_p {
|
||||
}
|
||||
|
||||
internal class BLEView: NSStackView {
|
||||
public var uuid: UUID
|
||||
public var address: String
|
||||
|
||||
open override var intrinsicContentSize: CGSize {
|
||||
return CGSize(width: self.bounds.width, height: self.bounds.height)
|
||||
}
|
||||
|
||||
public init(width: CGFloat, uuid: UUID, address: String, name: String, batteryLevel: [KeyValue_t]) {
|
||||
self.uuid = uuid
|
||||
public init(width: CGFloat, address: String, name: String, batteryLevel: [KeyValue_t]) {
|
||||
self.address = address
|
||||
|
||||
super.init(frame: NSRect(x: 0, y: 0, width: width, height: 30))
|
||||
|
||||
|
||||
@@ -14,102 +14,100 @@ import Kit
|
||||
import CoreBluetooth
|
||||
import IOBluetooth
|
||||
|
||||
internal class DevicesReader: Reader<[BLEDevice]> {
|
||||
private let ble: BluetoothDelegate = BluetoothDelegate()
|
||||
internal class DevicesReader: Reader<[BLEDevice]>, CBCentralManagerDelegate, CBPeripheralDelegate {
|
||||
private var devices: [BLEDevice] = []
|
||||
|
||||
init() {
|
||||
super.init()
|
||||
}
|
||||
|
||||
public override func read() {
|
||||
self.ble.read()
|
||||
self.callback(self.ble.devices)
|
||||
}
|
||||
}
|
||||
|
||||
class BluetoothDelegate: NSObject, CBCentralManagerDelegate, CBPeripheralDelegate {
|
||||
private var manager: CBCentralManager!
|
||||
|
||||
private var uuidAddress: [UUID: String] = [:]
|
||||
private var peripherals: [CBPeripheral] = []
|
||||
public var devices: [BLEDevice] = []
|
||||
private var characteristicsDict: [UUID: CBCharacteristic] = [:]
|
||||
|
||||
private let batteryServiceUUID = CBUUID(string: "0x180F")
|
||||
private let batteryCharacteristicsUUID = CBUUID(string: "0x2A19")
|
||||
|
||||
private let batteryKeys: [String] = [
|
||||
"BatteryPercent",
|
||||
"BatteryPercentCase",
|
||||
"BatteryPercentLeft",
|
||||
"BatteryPercentRight"
|
||||
]
|
||||
|
||||
override init() {
|
||||
init() {
|
||||
super.init()
|
||||
self.manager = CBCentralManager.init(delegate: self, queue: nil)
|
||||
}
|
||||
|
||||
public func read() {
|
||||
guard let dict = UserDefaults(suiteName: "/Library/Preferences/com.apple.Bluetooth") else {
|
||||
public override func read() {
|
||||
self.IODevices()
|
||||
self.cacheDevices()
|
||||
self.callback(self.devices)
|
||||
}
|
||||
|
||||
// MARK: - IODevices
|
||||
|
||||
private func IODevices() {
|
||||
guard var ioDevices = fetchIOService("AppleDeviceManagementHIDEventService") else {
|
||||
return
|
||||
}
|
||||
ioDevices = ioDevices.filter{ $0.object(forKey: "BluetoothDevice") as? Bool == true }
|
||||
|
||||
IOBluetoothDevice.pairedDevices().forEach { (d) in
|
||||
guard let device = d as? IOBluetoothDevice, device.isPaired() || device.isConnected(),
|
||||
let cache = self.findInCache(dict, address: device.addressString) else {
|
||||
ioDevices.forEach { (d: NSDictionary) in
|
||||
guard let name = d.object(forKey: "Product") as? String, let batteryPercent = d.object(forKey: "BatteryPercent") as? Int else {
|
||||
return
|
||||
}
|
||||
|
||||
let rssi = device.rawRSSI() == 127 ? nil : Int(device.rawRSSI())
|
||||
var address: String = ""
|
||||
if let addr = d.object(forKey: "DeviceAddress") as? String, addr != "" {
|
||||
address = addr
|
||||
} else if let addr = d.object(forKey: "SerialNumber") as? String, addr != "" {
|
||||
address = addr
|
||||
} else if let bleAddr = d.object(forKey: "BD_ADDR") as? Data, let addr = String(data: bleAddr, encoding: .utf8), addr != "" {
|
||||
address = addr
|
||||
}
|
||||
|
||||
if let idx = self.devices.firstIndex(where: { $0.uuid == cache.uuid }) {
|
||||
self.devices[idx].RSSI = rssi
|
||||
if !cache.batteryLevel.isEmpty {
|
||||
self.devices[idx].batteryLevel = cache.batteryLevel
|
||||
}
|
||||
self.devices[idx].isConnected = device.isConnected()
|
||||
self.devices[idx].isPaired = device.isPaired()
|
||||
if let idx = self.devices.firstIndex(where: { $0.address == address && $0.conn == .ioDevice }) {
|
||||
self.devices[idx].batteryLevel = [KeyValue_t(key: "battery", value: "\(batteryPercent)")]
|
||||
} else {
|
||||
self.devices.append(BLEDevice(
|
||||
uuid: cache.uuid,
|
||||
address: device.addressString,
|
||||
name: device.nameOrAddress,
|
||||
RSSI: rssi,
|
||||
batteryLevel: cache.batteryLevel,
|
||||
isConnected: device.isConnected(),
|
||||
isPaired: device.isPaired(),
|
||||
isInitialized: false
|
||||
conn: .ioDevice,
|
||||
address: address,
|
||||
name: name,
|
||||
batteryLevel: [KeyValue_t(key: "battery", value: "\(batteryPercent)")],
|
||||
isConnected: true,
|
||||
isPaired: true
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func findInCache(_ cache: UserDefaults, address: String) -> (uuid: UUID, batteryLevel: [KeyValue_t])? {
|
||||
guard let deviceCache = cache.object(forKey: "DeviceCache") as? [String: [String: Any]],
|
||||
// MARK: - Cache
|
||||
|
||||
private func cacheDevices() {
|
||||
guard let cache = UserDefaults(suiteName: "/Library/Preferences/com.apple.Bluetooth"),
|
||||
let deviceCache = cache.object(forKey: "DeviceCache") as? [String: [String: Any]],
|
||||
let pairedDevices = cache.object(forKey: "PairedDevices") as? [String],
|
||||
let coreCache = cache.object(forKey: "CoreBluetoothCache") as? [String: [String: Any]] else {
|
||||
return nil
|
||||
return
|
||||
}
|
||||
|
||||
guard let uuid = coreCache.compactMap({ (key, dict) -> UUID? in
|
||||
coreCache.forEach { (key: String, dict: [String: Any]) in
|
||||
guard let field = dict.first(where: { $0.key == "DeviceAddress" }),
|
||||
let value = field.value as? String,
|
||||
value == address else {
|
||||
return nil
|
||||
let value = field.value as? String else {
|
||||
return
|
||||
}
|
||||
|
||||
if let uuid = UUID(uuidString: key) {
|
||||
self.uuidAddress[uuid] = value
|
||||
}
|
||||
return UUID(uuidString: key)
|
||||
}).first else {
|
||||
return nil
|
||||
}
|
||||
|
||||
var batteryLevel: [KeyValue_t] = []
|
||||
if let d = deviceCache.first(where: { $0.key == address }) {
|
||||
for key in self.batteryKeys {
|
||||
if let pair = d.value.first(where: { $0.key == key }) {
|
||||
deviceCache.filter({ pairedDevices.contains($0.key) }).forEach { (address: String, dict: [String: Any]) in
|
||||
if self.devices.filter({ $0.conn == .ioDevice || $0.conn == .ble }).contains(where: { $0.address == address }) {
|
||||
return
|
||||
}
|
||||
|
||||
var batteryLevel: [KeyValue_t] = []
|
||||
|
||||
for key in ["BatteryPercent", "BatteryPercentCase", "BatteryPercentLeft", "BatteryPercentRight"] {
|
||||
if let pair = dict.first(where: { $0.key == key }) {
|
||||
var percentage: Int = 0
|
||||
switch pair.value {
|
||||
case let value as Int:
|
||||
percentage = value
|
||||
if "\(pair.value)" == "1.00" {
|
||||
if percentage == 1 {
|
||||
percentage *= 100
|
||||
}
|
||||
case let value as Double:
|
||||
@@ -120,9 +118,34 @@ class BluetoothDelegate: NSObject, CBCentralManagerDelegate, CBPeripheralDelegat
|
||||
batteryLevel.append(KeyValue_t(key: key, value: "\(percentage)"))
|
||||
}
|
||||
}
|
||||
|
||||
if !batteryLevel.isEmpty {
|
||||
let name = dict.first{ $0.key == "Name" }?.value as? String
|
||||
|
||||
if let idx = self.devices.firstIndex(where: { $0.address == address && $0.conn == .cache }) {
|
||||
self.devices[idx].batteryLevel = batteryLevel
|
||||
|
||||
if let device: IOBluetoothDevice = IOBluetoothDevice.pairedDevices().first(where: { d in
|
||||
guard let device = d as? IOBluetoothDevice, device.isPaired() || device.isConnected() else {
|
||||
return false
|
||||
}
|
||||
return device.addressString == address
|
||||
}) as? IOBluetoothDevice {
|
||||
self.devices[idx].RSSI = device.rawRSSI() == 127 ? nil : Int(device.rawRSSI())
|
||||
self.devices[idx].isConnected = device.isConnected()
|
||||
self.devices[idx].isPaired = device.isPaired()
|
||||
}
|
||||
} else {
|
||||
self.devices.append(BLEDevice(
|
||||
conn: .cache,
|
||||
address: address,
|
||||
name: name ?? "",
|
||||
batteryLevel: batteryLevel,
|
||||
isPaired: true
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (uuid, batteryLevel)
|
||||
}
|
||||
|
||||
// MARK: - CBCentralManagerDelegate
|
||||
@@ -136,24 +159,41 @@ class BluetoothDelegate: NSObject, CBCentralManagerDelegate, CBPeripheralDelegat
|
||||
}
|
||||
|
||||
func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String: Any], rssi RSSI: NSNumber) {
|
||||
guard let idx = self.devices.firstIndex(where: { $0.uuid == peripheral.identifier }) else {
|
||||
guard let address = self.uuidAddress[peripheral.identifier] else {
|
||||
return
|
||||
}
|
||||
|
||||
if self.devices[idx].RSSI == nil {
|
||||
self.devices[idx].RSSI = Int(truncating: RSSI)
|
||||
guard let device: IOBluetoothDevice = IOBluetoothDevice.pairedDevices().first(where: { d in
|
||||
guard let device = d as? IOBluetoothDevice, device.isPaired() || device.isConnected() else {
|
||||
return false
|
||||
}
|
||||
return device.addressString == address
|
||||
}) as? IOBluetoothDevice else {
|
||||
return
|
||||
}
|
||||
|
||||
if self.devices[idx].peripheral == nil {
|
||||
self.devices[idx].peripheral = peripheral
|
||||
guard let idx = self.devices.firstIndex(where: { $0.address == address && $0.conn == .ble }) else {
|
||||
self.devices.append(BLEDevice(
|
||||
conn: .ble,
|
||||
address: address,
|
||||
name: peripheral.name ?? "Unknown",
|
||||
uuid: peripheral.identifier,
|
||||
RSSI: Int(truncating: RSSI),
|
||||
peripheral: peripheral
|
||||
))
|
||||
return
|
||||
}
|
||||
|
||||
self.devices[idx].RSSI = Int(truncating: RSSI)
|
||||
self.devices[idx].isConnected = device.isConnected()
|
||||
self.devices[idx].isPaired = device.isPaired()
|
||||
|
||||
if peripheral.state == .disconnected {
|
||||
central.connect(peripheral, options: nil)
|
||||
} else if peripheral.state == .connected && !self.devices[idx].isInitialized {
|
||||
} else if peripheral.state == .connected && !self.devices[idx].isPeripheralConnected {
|
||||
peripheral.delegate = self
|
||||
peripheral.discoverServices([batteryServiceUUID])
|
||||
self.devices[idx].isInitialized = true
|
||||
self.devices[idx].isPeripheralConnected = true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user