fix: instant power/disconnect reactivity and show only icon when Bluetooth is off
This commit is contained in:
parent
76cb478235
commit
a11f6bcc38
4 changed files with 309 additions and 96 deletions
BIN
BLTS-Tracker.dmg
BIN
BLTS-Tracker.dmg
Binary file not shown.
|
|
@ -1,35 +1,54 @@
|
|||
import Foundation
|
||||
import IOBluetooth
|
||||
|
||||
func runProbe() {
|
||||
guard let paired = IOBluetoothDevice.pairedDevices() as? [IOBluetoothDevice] else { return }
|
||||
for dev in paired where dev.isConnected() {
|
||||
let addr = dev.addressString ?? ""
|
||||
let sel = Selector(("batteryPercentSingle"))
|
||||
if dev.responds(to: sel) {
|
||||
let imp = dev.method(for: sel)
|
||||
typealias IntFn = @convention(c) (AnyObject, Selector) -> Int
|
||||
let fn = unsafeBitCast(imp, to: IntFn.self)
|
||||
let val = fn(dev, sel)
|
||||
if val > 0 && val <= 100 {
|
||||
print("\(addr):\(val)")
|
||||
continue
|
||||
}
|
||||
}
|
||||
let isPowerOn = (IOBluetoothHostController.default()?.powerState == kBluetoothHCIPowerStateON)
|
||||
print("POWER:\(isPowerOn ? 1 : 0)")
|
||||
|
||||
let selPeer = Selector(("peer"))
|
||||
if dev.responds(to: selPeer),
|
||||
let peer = dev.perform(selPeer)?.takeUnretainedValue() as? NSObject,
|
||||
peer.responds(to: sel) {
|
||||
let imp = peer.method(for: sel)
|
||||
typealias IntFn = @convention(c) (AnyObject, Selector) -> Int
|
||||
let fn = unsafeBitCast(imp, to: IntFn.self)
|
||||
let val = fn(peer, sel)
|
||||
if val > 0 && val <= 100 {
|
||||
print("\(addr):\(val)")
|
||||
}
|
||||
}
|
||||
}
|
||||
guard isPowerOn else {
|
||||
exit(0)
|
||||
}
|
||||
|
||||
runProbe()
|
||||
typealias IntFn = @convention(c) (AnyObject, Selector) -> Int
|
||||
|
||||
func getInt(_ target: AnyObject, _ selName: String) -> Int? {
|
||||
let sel = Selector((selName))
|
||||
if target.responds(to: sel) {
|
||||
let imp = target.method(for: sel)
|
||||
let fn = unsafeBitCast(imp, to: IntFn.self)
|
||||
let v = fn(target, sel)
|
||||
if v > 0 && v <= 100 { return v }
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if let paired = IOBluetoothDevice.pairedDevices() as? [IOBluetoothDevice] {
|
||||
let selPeer = Selector(("peer"))
|
||||
for dev in paired {
|
||||
let addr = dev.addressString ?? ""
|
||||
let name = dev.nameOrAddress ?? ""
|
||||
let connected = dev.isConnected()
|
||||
|
||||
var single = getInt(dev, "batteryPercentSingle") ?? getInt(dev, "headsetBattery")
|
||||
var left = getInt(dev, "batteryPercentLeft")
|
||||
var right = getInt(dev, "batteryPercentRight")
|
||||
var bCase = getInt(dev, "batteryPercentCase")
|
||||
var combined = getInt(dev, "batteryPercentCombined")
|
||||
|
||||
if dev.responds(to: selPeer),
|
||||
let peer = dev.perform(selPeer)?.takeUnretainedValue() as? NSObject {
|
||||
if single == nil { single = getInt(peer, "batteryPercentSingle") ?? getInt(peer, "headsetBattery") }
|
||||
if left == nil { left = getInt(peer, "batteryPercentLeft") }
|
||||
if right == nil { right = getInt(peer, "batteryPercentRight") }
|
||||
if bCase == nil { bCase = getInt(peer, "batteryPercentCase") }
|
||||
if combined == nil { combined = getInt(peer, "batteryPercentCombined") }
|
||||
}
|
||||
|
||||
let sStr = single.map(String.init) ?? "-1"
|
||||
let lStr = left.map(String.init) ?? "-1"
|
||||
let rStr = right.map(String.init) ?? "-1"
|
||||
let cStr = bCase.map(String.init) ?? "-1"
|
||||
let combStr = combined.map(String.init) ?? "-1"
|
||||
|
||||
print("DEV:\(addr):\(name):\(connected ? 1 : 0):\(sStr):\(lStr):\(rStr):\(cStr):\(combStr)")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,38 +28,51 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate {
|
|||
}
|
||||
|
||||
private static func runBatteryProbe() {
|
||||
let isPowerOn = (IOBluetoothHostController.default()?.powerState == kBluetoothHCIPowerStateON)
|
||||
print("POWER:\(isPowerOn ? 1 : 0)")
|
||||
guard isPowerOn else { exit(0) }
|
||||
|
||||
typealias IntGetter = @convention(c) (AnyObject, Selector) -> Int
|
||||
let selSingle = Selector(("batteryPercentSingle"))
|
||||
let selPeer = Selector(("peer"))
|
||||
func getInt(_ target: AnyObject, _ selName: String) -> Int? {
|
||||
let sel = Selector((selName))
|
||||
if target.responds(to: sel) {
|
||||
let imp = target.method(for: sel)
|
||||
let fn = unsafeBitCast(imp, to: IntGetter.self)
|
||||
let v = fn(target, sel)
|
||||
if v > 0 && v <= 100 { return v }
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if let paired = IOBluetoothDevice.pairedDevices() as? [IOBluetoothDevice] {
|
||||
for dev in paired where dev.isConnected() {
|
||||
let selPeer = Selector(("peer"))
|
||||
for dev in paired {
|
||||
let addr = dev.addressString ?? ""
|
||||
var batt: Int? = nil
|
||||
let name = dev.nameOrAddress ?? ""
|
||||
let connected = dev.isConnected()
|
||||
|
||||
if dev.responds(to: selSingle) {
|
||||
let imp = dev.method(for: selSingle)
|
||||
let fn = unsafeBitCast(imp, to: IntGetter.self)
|
||||
let val = fn(dev, selSingle)
|
||||
if val > 0 && val <= 100 {
|
||||
batt = val
|
||||
}
|
||||
var single = getInt(dev, "batteryPercentSingle") ?? getInt(dev, "headsetBattery")
|
||||
var left = getInt(dev, "batteryPercentLeft")
|
||||
var right = getInt(dev, "batteryPercentRight")
|
||||
var bCase = getInt(dev, "batteryPercentCase")
|
||||
var combined = getInt(dev, "batteryPercentCombined")
|
||||
|
||||
if dev.responds(to: selPeer),
|
||||
let peer = dev.perform(selPeer)?.takeUnretainedValue() as? NSObject {
|
||||
if single == nil { single = getInt(peer, "batteryPercentSingle") ?? getInt(peer, "headsetBattery") }
|
||||
if left == nil { left = getInt(peer, "batteryPercentLeft") }
|
||||
if right == nil { right = getInt(peer, "batteryPercentRight") }
|
||||
if bCase == nil { bCase = getInt(peer, "batteryPercentCase") }
|
||||
if combined == nil { combined = getInt(peer, "batteryPercentCombined") }
|
||||
}
|
||||
|
||||
if batt == nil, dev.responds(to: selPeer),
|
||||
let peer = dev.perform(selPeer)?.takeUnretainedValue() as? NSObject,
|
||||
peer.responds(to: selSingle) {
|
||||
let imp = peer.method(for: selSingle)
|
||||
let fn = unsafeBitCast(imp, to: IntGetter.self)
|
||||
let val = fn(peer, selSingle)
|
||||
if val > 0 && val <= 100 {
|
||||
batt = val
|
||||
}
|
||||
}
|
||||
let sStr = single.map(String.init) ?? "-1"
|
||||
let lStr = left.map(String.init) ?? "-1"
|
||||
let rStr = right.map(String.init) ?? "-1"
|
||||
let cStr = bCase.map(String.init) ?? "-1"
|
||||
let combStr = combined.map(String.init) ?? "-1"
|
||||
|
||||
if let b = batt {
|
||||
print("\(addr):\(b)")
|
||||
}
|
||||
print("DEV:\(addr):\(name):\(connected ? 1 : 0):\(sStr):\(lStr):\(rStr):\(cStr):\(combStr)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -94,8 +107,8 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate {
|
|||
// Start active tracking
|
||||
tracker.startTracking()
|
||||
|
||||
// Direct main thread timer in AppDelegate for guaranteed 1-second UI refresh
|
||||
let t = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
|
||||
// Fast 0.5s timer in AppDelegate
|
||||
let t = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { [weak self] _ in
|
||||
self?.tracker.updateDeviceList()
|
||||
self?.updateStatusItem()
|
||||
}
|
||||
|
|
@ -123,9 +136,9 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate {
|
|||
return
|
||||
}
|
||||
|
||||
// Find best active device (either marked active or any connected device with battery data)
|
||||
let isPowerOn = tracker.isBluetoothPoweredOn
|
||||
let active = tracker.activeHeadphone ?? tracker.devices.first(where: { $0.isConnected && $0.primaryBatteryPercent != nil })
|
||||
let isConn = active?.isConnected == true
|
||||
let isConn = isPowerOn && (active?.isConnected == true)
|
||||
let percent = tracker.currentBatteryPercent ?? active?.primaryBatteryPercent
|
||||
|
||||
if button.image == nil || button.image?.accessibilityDescription != "Bluetooth Battery" {
|
||||
|
|
@ -136,6 +149,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate {
|
|||
if isConn, let p = percent, p > 0 {
|
||||
button.title = " \(p)%"
|
||||
} else {
|
||||
// When Bluetooth is off or headphones disconnected: show ONLY the headphones icon
|
||||
button.title = ""
|
||||
}
|
||||
|
||||
|
|
@ -143,7 +157,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate {
|
|||
button.needsLayout = true
|
||||
button.needsDisplay = true
|
||||
|
||||
let logLine = "[\(Date())] Dev: \(active?.name ?? "none") | Conn: \(isConn) | Battery: \(percent.map(String.init) ?? "nil") | Title: '\(button.title)'\n"
|
||||
let logLine = "[\(Date())] Dev: \(active?.name ?? "none") | Conn: \(isConn) | Power: \(isPowerOn) | Battery: \(percent.map(String.init) ?? "nil") | Title: '\(button.title)'\n"
|
||||
if let data = logLine.data(using: .utf8) {
|
||||
if let handle = try? FileHandle(forWritingTo: URL(fileURLWithPath: "/tmp/blts_live.log")) {
|
||||
handle.seekToEndOfFile()
|
||||
|
|
@ -162,8 +176,14 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate {
|
|||
private func buildMenu(_ menu: NSMenu) {
|
||||
menu.removeAllItems()
|
||||
|
||||
let isPowerOn = tracker.isBluetoothPoweredOn
|
||||
let active = tracker.activeHeadphone ?? tracker.devices.first(where: { $0.isConnected && $0.primaryBatteryPercent != nil })
|
||||
if let dev = active, dev.isConnected {
|
||||
|
||||
if !isPowerOn {
|
||||
let item = NSMenuItem(title: "⚠️ Bluetooth выключен", action: nil, keyEquivalent: "")
|
||||
item.isEnabled = false
|
||||
menu.addItem(item)
|
||||
} else if let dev = active, dev.isConnected {
|
||||
let batt = tracker.currentBatteryPercent ?? dev.primaryBatteryPercent
|
||||
let percentStr = batt != nil ? "\(batt!)%" : "Подключено"
|
||||
let titleItem = NSMenuItem(title: "🎧 \(dev.name): \(percentStr)", action: nil, keyEquivalent: "")
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ public final class BluetoothTracker: ObservableObject {
|
|||
@Published public var currentBatteryPercent: Int?
|
||||
@Published public var currentDeviceName: String = ""
|
||||
@Published public var isConnected: Bool = false
|
||||
@Published public var isBluetoothPoweredOn: Bool = true
|
||||
|
||||
private var timerSource: DispatchSourceTimer?
|
||||
private var knownBatteries: [String: Int] = [:]
|
||||
|
|
@ -23,6 +24,11 @@ public final class BluetoothTracker: ObservableObject {
|
|||
private typealias IntBatteryGetter = @convention(c) (AnyObject, Selector) -> Int
|
||||
private typealias BoolGetter = @convention(c) (AnyObject, Selector) -> Bool
|
||||
|
||||
public static func checkBluetoothPower() -> Bool {
|
||||
guard let controller = IOBluetoothHostController.default() else { return false }
|
||||
return controller.powerState == kBluetoothHCIPowerStateON
|
||||
}
|
||||
|
||||
private func syncFromServer(device: IOBluetoothDevice) {
|
||||
let selUpdate = Selector(("updateFromServer"))
|
||||
if device.responds(to: selUpdate) {
|
||||
|
|
@ -47,7 +53,9 @@ public final class BluetoothTracker: ObservableObject {
|
|||
}
|
||||
|
||||
public init() {
|
||||
self.isBluetoothPoweredOn = BluetoothTracker.checkBluetoothPower()
|
||||
setupBluetoothListeners()
|
||||
setupAudioListeners()
|
||||
startTracking()
|
||||
}
|
||||
|
||||
|
|
@ -60,9 +68,9 @@ public final class BluetoothTracker: ObservableObject {
|
|||
updateDeviceList()
|
||||
pollSubprocessProbe()
|
||||
|
||||
// Kernel-level DispatchSourceTimer: strictly 1 second interval
|
||||
// Strict 500ms DispatchSourceTimer for instant real-time reactivity
|
||||
let timer = DispatchSource.makeTimerSource(flags: .strict, queue: DispatchQueue.main)
|
||||
timer.schedule(deadline: .now() + 1.0, repeating: 1.0, leeway: .milliseconds(50))
|
||||
timer.schedule(deadline: .now() + 0.5, repeating: 0.5, leeway: .milliseconds(50))
|
||||
timer.setEventHandler { [weak self] in
|
||||
self?.updateDeviceList()
|
||||
self?.pollSubprocessProbe()
|
||||
|
|
@ -81,6 +89,14 @@ public final class BluetoothTracker: ObservableObject {
|
|||
public func pollSubprocessProbe() {
|
||||
guard !isProbeRunning else { return }
|
||||
|
||||
// Instant check: if Bluetooth is off, no need to run probe
|
||||
if !BluetoothTracker.checkBluetoothPower() {
|
||||
if isBluetoothPoweredOn || isConnected || currentBatteryPercent != nil {
|
||||
applyPowerOffState()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
var probeBin: String? = nil
|
||||
var probeArgs: [String] = []
|
||||
|
||||
|
|
@ -99,7 +115,7 @@ public final class BluetoothTracker: ObservableObject {
|
|||
guard let binary = probeBin else { return }
|
||||
|
||||
isProbeRunning = true
|
||||
DispatchQueue.global(qos: .utility).async { [weak self] in
|
||||
DispatchQueue.global(qos: .userInitiated).async { [weak self] in
|
||||
defer { self?.isProbeRunning = false }
|
||||
let proc = Process()
|
||||
proc.executableURL = URL(fileURLWithPath: binary)
|
||||
|
|
@ -111,18 +127,7 @@ public final class BluetoothTracker: ObservableObject {
|
|||
proc.waitUntilExit()
|
||||
let data = pipe.fileHandleForReading.readDataToEndOfFile()
|
||||
if let output = String(data: data, encoding: .utf8), !output.isEmpty {
|
||||
var parsed: [String: Int] = [:]
|
||||
for line in output.components(separatedBy: .newlines) {
|
||||
let parts = line.split(separator: ":")
|
||||
if parts.count == 2, let b = Int(parts[1]) {
|
||||
parsed[String(parts[0])] = b
|
||||
}
|
||||
}
|
||||
if !parsed.isEmpty {
|
||||
DispatchQueue.main.async {
|
||||
self?.applyProbeBatteries(parsed)
|
||||
}
|
||||
}
|
||||
self?.parseProbeOutput(output)
|
||||
}
|
||||
} catch {
|
||||
// Ignore probe errors
|
||||
|
|
@ -130,27 +135,173 @@ public final class BluetoothTracker: ObservableObject {
|
|||
}
|
||||
}
|
||||
|
||||
private func applyProbeBatteries(_ probeResults: [String: Int]) {
|
||||
var changed = false
|
||||
for i in 0..<devices.count {
|
||||
if let fresh = probeResults[devices[i].address], fresh > 0 && fresh <= 100 {
|
||||
if devices[i].singleBattery != fresh {
|
||||
devices[i].singleBattery = fresh
|
||||
knownBatteries[devices[i].address] = fresh
|
||||
changed = true
|
||||
private struct ProbeDev {
|
||||
let address: String
|
||||
let name: String
|
||||
let isConnected: Bool
|
||||
let singleBattery: Int?
|
||||
let leftBattery: Int?
|
||||
let rightBattery: Int?
|
||||
let caseBattery: Int?
|
||||
let combinedBattery: Int?
|
||||
}
|
||||
|
||||
private func parseProbeOutput(_ output: String) {
|
||||
var powerOn: Bool = true
|
||||
var devMap: [String: ProbeDev] = [:]
|
||||
|
||||
for line in output.components(separatedBy: .newlines) {
|
||||
let trimmed = line.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if trimmed.hasPrefix("POWER:") {
|
||||
let val = trimmed.replacingOccurrences(of: "POWER:", with: "")
|
||||
powerOn = (val == "1")
|
||||
} else if trimmed.hasPrefix("DEV:") {
|
||||
let parts = trimmed.split(separator: ":", omittingEmptySubsequences: false)
|
||||
if parts.count >= 8 {
|
||||
let addr = String(parts[1])
|
||||
let name = String(parts[2])
|
||||
let conn = parts[3] == "1"
|
||||
let single = Int(parts[4]).flatMap { $0 > 0 && $0 <= 100 ? $0 : nil }
|
||||
let left = Int(parts[5]).flatMap { $0 > 0 && $0 <= 100 ? $0 : nil }
|
||||
let right = Int(parts[6]).flatMap { $0 > 0 && $0 <= 100 ? $0 : nil }
|
||||
let bCase = Int(parts[7]).flatMap { $0 > 0 && $0 <= 100 ? $0 : nil }
|
||||
let comb = (parts.count > 8 ? Int(parts[8]) : nil).flatMap { $0 > 0 && $0 <= 100 ? $0 : nil }
|
||||
|
||||
devMap[addr] = ProbeDev(
|
||||
address: addr,
|
||||
name: name,
|
||||
isConnected: conn,
|
||||
singleBattery: single,
|
||||
leftBattery: left,
|
||||
rightBattery: right,
|
||||
caseBattery: bCase,
|
||||
combinedBattery: comb
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let active = activeHeadphone, let fresh = probeResults[active.address], fresh > 0 && fresh <= 100 {
|
||||
if activeHeadphone?.singleBattery != fresh {
|
||||
activeHeadphone?.singleBattery = fresh
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
guard let self = self else { return }
|
||||
if !powerOn {
|
||||
self.applyPowerOffState()
|
||||
} else {
|
||||
self.applyProbeDevices(devMap)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func applyPowerOffState() {
|
||||
var changed = false
|
||||
if self.isBluetoothPoweredOn {
|
||||
self.isBluetoothPoweredOn = false
|
||||
changed = true
|
||||
}
|
||||
if currentBatteryPercent != fresh {
|
||||
currentBatteryPercent = fresh
|
||||
if self.isConnected {
|
||||
self.isConnected = false
|
||||
changed = true
|
||||
}
|
||||
if self.currentBatteryPercent != nil {
|
||||
self.currentBatteryPercent = nil
|
||||
changed = true
|
||||
}
|
||||
if !self.currentDeviceName.isEmpty {
|
||||
self.currentDeviceName = ""
|
||||
changed = true
|
||||
}
|
||||
if self.activeHeadphone != nil {
|
||||
self.activeHeadphone = nil
|
||||
changed = true
|
||||
}
|
||||
self.knownBatteries.removeAll()
|
||||
for i in 0..<self.devices.count {
|
||||
if self.devices[i].isConnected {
|
||||
self.devices[i].isConnected = false
|
||||
changed = true
|
||||
}
|
||||
if self.devices[i].singleBattery != nil {
|
||||
self.devices[i].singleBattery = nil
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
if changed {
|
||||
self.objectWillChange.send()
|
||||
NotificationCenter.default.post(name: BluetoothTracker.didUpdateNotification, object: self)
|
||||
}
|
||||
}
|
||||
|
||||
private func applyProbeDevices(_ devMap: [String: ProbeDev]) {
|
||||
var changed = false
|
||||
if !self.isBluetoothPoweredOn {
|
||||
self.isBluetoothPoweredOn = true
|
||||
changed = true
|
||||
}
|
||||
|
||||
for i in 0..<devices.count {
|
||||
let addr = devices[i].address
|
||||
if let p = devMap[addr] {
|
||||
if devices[i].isConnected != p.isConnected {
|
||||
devices[i].isConnected = p.isConnected
|
||||
changed = true
|
||||
}
|
||||
if p.isConnected {
|
||||
if let fresh = p.singleBattery, fresh > 0 && fresh <= 100 {
|
||||
if devices[i].singleBattery != fresh {
|
||||
devices[i].singleBattery = fresh
|
||||
knownBatteries[addr] = fresh
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
if let l = p.leftBattery, devices[i].leftBattery != l {
|
||||
devices[i].leftBattery = l
|
||||
changed = true
|
||||
}
|
||||
if let r = p.rightBattery, devices[i].rightBattery != r {
|
||||
devices[i].rightBattery = r
|
||||
changed = true
|
||||
}
|
||||
if let c = p.caseBattery, devices[i].caseBattery != c {
|
||||
devices[i].caseBattery = c
|
||||
changed = true
|
||||
}
|
||||
if let comb = p.combinedBattery, devices[i].combinedBattery != comb {
|
||||
devices[i].combinedBattery = comb
|
||||
changed = true
|
||||
}
|
||||
} else {
|
||||
knownBatteries.removeValue(forKey: addr)
|
||||
if devices[i].singleBattery != nil {
|
||||
devices[i].singleBattery = nil
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let foundActive = devices.first(where: { $0.isConnected && $0.isAudioDevice })
|
||||
?? devices.first(where: { $0.isConnected })
|
||||
|
||||
if activeHeadphone?.address != foundActive?.address {
|
||||
activeHeadphone = foundActive
|
||||
changed = true
|
||||
}
|
||||
|
||||
let newPercent = foundActive?.primaryBatteryPercent
|
||||
if currentBatteryPercent != newPercent {
|
||||
currentBatteryPercent = newPercent
|
||||
changed = true
|
||||
}
|
||||
|
||||
let newName = foundActive?.name ?? ""
|
||||
if currentDeviceName != newName {
|
||||
currentDeviceName = newName
|
||||
changed = true
|
||||
}
|
||||
|
||||
let newConn = foundActive?.isConnected == true
|
||||
if isConnected != newConn {
|
||||
isConnected = newConn
|
||||
changed = true
|
||||
}
|
||||
|
||||
if changed {
|
||||
|
|
@ -161,9 +312,7 @@ public final class BluetoothTracker: ObservableObject {
|
|||
|
||||
public func refreshNow() {
|
||||
updateDeviceList()
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { [weak self] in
|
||||
self?.updateDeviceList()
|
||||
}
|
||||
pollSubprocessProbe()
|
||||
}
|
||||
|
||||
private func setupBluetoothListeners() {
|
||||
|
|
@ -173,7 +322,9 @@ public final class BluetoothTracker: ObservableObject {
|
|||
"IOBluetoothDeviceNotification",
|
||||
"IOBluetoothDeviceServicesResolvedNotification",
|
||||
"IOBluetoothDeviceNameChangedNotification",
|
||||
"IOBluetoothHandsFreeDeviceDidUpdateNotification"
|
||||
"IOBluetoothHandsFreeDeviceDidUpdateNotification",
|
||||
"com.apple.Bluetooth.status",
|
||||
"com.apple.bluetooth.status"
|
||||
]
|
||||
|
||||
for name in notificationNames {
|
||||
|
|
@ -192,18 +343,41 @@ public final class BluetoothTracker: ObservableObject {
|
|||
}
|
||||
}
|
||||
|
||||
private func setupAudioListeners() {
|
||||
var address = AudioObjectPropertyAddress(
|
||||
mSelector: kAudioHardwarePropertyDefaultOutputDevice,
|
||||
mScope: kAudioObjectPropertyScopeGlobal,
|
||||
mElement: kAudioObjectPropertyElementMain
|
||||
)
|
||||
AudioObjectAddPropertyListenerBlock(
|
||||
AudioObjectID(kAudioObjectSystemObject),
|
||||
&address,
|
||||
DispatchQueue.main
|
||||
) { [weak self] _, _ in
|
||||
self?.refreshNow()
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func handleBluetoothChange() {
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
self?.updateDeviceList()
|
||||
self?.refreshNow()
|
||||
}
|
||||
for delay in [0.2, 0.5, 1.0, 1.5, 2.0] {
|
||||
for delay in [0.2, 0.5, 1.0] {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in
|
||||
self?.updateDeviceList()
|
||||
self?.refreshNow()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func updateDeviceList() {
|
||||
let powerOn = BluetoothTracker.checkBluetoothPower()
|
||||
self.isBluetoothPoweredOn = powerOn
|
||||
|
||||
if !powerOn {
|
||||
applyPowerOffState()
|
||||
return
|
||||
}
|
||||
|
||||
var updatedList: [BluetoothDeviceModel] = []
|
||||
let activeAudioName = getActiveCoreAudioDeviceName()
|
||||
|
||||
|
|
@ -225,7 +399,7 @@ public final class BluetoothTracker: ObservableObject {
|
|||
activeAudioName!.localizedCaseInsensitiveContains(name) ||
|
||||
name.localizedCaseInsensitiveContains(activeAudioName!)
|
||||
)
|
||||
let isConnected = device.isConnected() || pairedDev.isConnected() || isAudioOutput
|
||||
let isConnected = (device.isConnected() || pairedDev.isConnected())
|
||||
|
||||
let majorClass = UInt32(device.deviceClassMajor)
|
||||
let minorClass = UInt32(device.deviceClassMinor)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue