Fix real-time battery tracking
This commit is contained in:
parent
fdb8c90975
commit
47743a22e8
2 changed files with 18 additions and 15 deletions
|
|
@ -25,7 +25,7 @@ struct BLTSTrackerApp: App {
|
||||||
Label(" \(p)%", systemImage: "headphones")
|
Label(" \(p)%", systemImage: "headphones")
|
||||||
.labelStyle(.titleAndIcon)
|
.labelStyle(.titleAndIcon)
|
||||||
} else if isConn {
|
} else if isConn {
|
||||||
Label(" 100%", systemImage: "headphones")
|
Label(" ...", systemImage: "headphones")
|
||||||
.labelStyle(.titleAndIcon)
|
.labelStyle(.titleAndIcon)
|
||||||
} else {
|
} else {
|
||||||
Label("", systemImage: "headphones")
|
Label("", systemImage: "headphones")
|
||||||
|
|
@ -36,9 +36,15 @@ struct BLTSTrackerApp: App {
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var menuContent: some View {
|
private var menuContent: some View {
|
||||||
if let dev = tracker.activeHeadphone, dev.isConnected {
|
if let dev = tracker.activeHeadphone, dev.isConnected {
|
||||||
let percentStr = dev.primaryBatteryPercent != nil ? "\(dev.primaryBatteryPercent!)%" : "100%"
|
let percentStr = dev.primaryBatteryPercent != nil ? "\(dev.primaryBatteryPercent!)%" : "Определение..."
|
||||||
Text("🎧 \(dev.name): \(percentStr)")
|
Text("🎧 \(dev.name): \(percentStr)")
|
||||||
.font(.headline)
|
.font(.headline)
|
||||||
|
|
||||||
|
if let l = dev.leftBattery, let r = dev.rightBattery {
|
||||||
|
Text("Левый: \(l)% | Правый: \(r)%")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
Text("🎧 Наушники не подключены")
|
Text("🎧 Наушники не подключены")
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
|
|
@ -46,7 +52,7 @@ struct BLTSTrackerApp: App {
|
||||||
|
|
||||||
Divider()
|
Divider()
|
||||||
|
|
||||||
Button("Обновить статус") {
|
Button("Обновить заряд") {
|
||||||
tracker.refreshNow()
|
tracker.refreshNow()
|
||||||
}
|
}
|
||||||
.keyboardShortcut("r")
|
.keyboardShortcut("r")
|
||||||
|
|
|
||||||
|
|
@ -11,8 +11,6 @@ public final class BluetoothTracker: ObservableObject {
|
||||||
@Published public var devices: [BluetoothDeviceModel] = []
|
@Published public var devices: [BluetoothDeviceModel] = []
|
||||||
|
|
||||||
private var timer: Timer?
|
private var timer: Timer?
|
||||||
private var lastKnownBattery: [String: Int] = [:]
|
|
||||||
|
|
||||||
private typealias IntBatteryGetter = @convention(c) (AnyObject, Selector) -> Int
|
private typealias IntBatteryGetter = @convention(c) (AnyObject, Selector) -> Int
|
||||||
private typealias BoolGetter = @convention(c) (AnyObject, Selector) -> Bool
|
private typealias BoolGetter = @convention(c) (AnyObject, Selector) -> Bool
|
||||||
|
|
||||||
|
|
@ -28,6 +26,7 @@ public final class BluetoothTracker: ObservableObject {
|
||||||
public func startTracking() {
|
public func startTracking() {
|
||||||
stopTracking()
|
stopTracking()
|
||||||
updateDeviceList()
|
updateDeviceList()
|
||||||
|
// Fast 1.5s real-time poll
|
||||||
timer = Timer.scheduledTimer(withTimeInterval: 1.5, repeats: true) { [weak self] _ in
|
timer = Timer.scheduledTimer(withTimeInterval: 1.5, repeats: true) { [weak self] _ in
|
||||||
self?.updateDeviceList()
|
self?.updateDeviceList()
|
||||||
}
|
}
|
||||||
|
|
@ -61,8 +60,7 @@ public final class BluetoothTracker: ObservableObject {
|
||||||
DispatchQueue.main.async { [weak self] in
|
DispatchQueue.main.async { [weak self] in
|
||||||
self?.updateDeviceList()
|
self?.updateDeviceList()
|
||||||
}
|
}
|
||||||
// Retries during the first 3 seconds to catch the telemetry packet as soon as handshake finishes
|
for delay in [0.3, 0.8, 1.5, 2.5] {
|
||||||
for delay in [0.4, 0.8, 1.2, 1.8, 2.5] {
|
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in
|
DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in
|
||||||
self?.updateDeviceList()
|
self?.updateDeviceList()
|
||||||
}
|
}
|
||||||
|
|
@ -79,7 +77,11 @@ public final class BluetoothTracker: ObservableObject {
|
||||||
let name = device.nameOrAddress ?? "Unknown Device"
|
let name = device.nameOrAddress ?? "Unknown Device"
|
||||||
let address = device.addressString ?? UUID().uuidString
|
let address = device.addressString ?? UUID().uuidString
|
||||||
|
|
||||||
let isAudioOutput = activeAudioName != nil && (name.caseInsensitiveCompare(activeAudioName!) == .orderedSame || activeAudioName!.localizedCaseInsensitiveContains(name) || name.localizedCaseInsensitiveContains(activeAudioName!))
|
let isAudioOutput = activeAudioName != nil && (
|
||||||
|
name.caseInsensitiveCompare(activeAudioName!) == .orderedSame ||
|
||||||
|
activeAudioName!.localizedCaseInsensitiveContains(name) ||
|
||||||
|
name.localizedCaseInsensitiveContains(activeAudioName!)
|
||||||
|
)
|
||||||
let isConnected = device.isConnected() || isAudioOutput
|
let isConnected = device.isConnected() || isAudioOutput
|
||||||
|
|
||||||
let majorClass = UInt32(device.deviceClassMajor)
|
let majorClass = UInt32(device.deviceClassMajor)
|
||||||
|
|
@ -116,7 +118,7 @@ public final class BluetoothTracker: ObservableObject {
|
||||||
// 2. IOKit Registry enrich
|
// 2. IOKit Registry enrich
|
||||||
enrichFromIORegistry(devices: &updatedList)
|
enrichFromIORegistry(devices: &updatedList)
|
||||||
|
|
||||||
// 3. Sort
|
// 3. Sort: Connected audio first, then connected other, then paired audio
|
||||||
updatedList.sort { d1, d2 in
|
updatedList.sort { d1, d2 in
|
||||||
if d1.isConnected != d2.isConnected {
|
if d1.isConnected != d2.isConnected {
|
||||||
return d1.isConnected && !d2.isConnected
|
return d1.isConnected && !d2.isConnected
|
||||||
|
|
@ -206,14 +208,13 @@ public final class BluetoothTracker: ObservableObject {
|
||||||
let imp = device.method(for: sel)
|
let imp = device.method(for: sel)
|
||||||
let fn = unsafeBitCast(imp, to: IntBatteryGetter.self)
|
let fn = unsafeBitCast(imp, to: IntBatteryGetter.self)
|
||||||
let val = fn(device, sel)
|
let val = fn(device, sel)
|
||||||
|
// Valid battery percentages are 1 to 100
|
||||||
if val > 0 && val <= 100 {
|
if val > 0 && val <= 100 {
|
||||||
return val
|
return val
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
let addr = model.address
|
|
||||||
|
|
||||||
if isMulti {
|
if isMulti {
|
||||||
model.leftBattery = getBatteryValue(selectorName: "batteryPercentLeft")
|
model.leftBattery = getBatteryValue(selectorName: "batteryPercentLeft")
|
||||||
model.rightBattery = getBatteryValue(selectorName: "batteryPercentRight")
|
model.rightBattery = getBatteryValue(selectorName: "batteryPercentRight")
|
||||||
|
|
@ -227,11 +228,7 @@ public final class BluetoothTracker: ObservableObject {
|
||||||
?? getBatteryValue(selectorName: "batteryPercentCombined")
|
?? getBatteryValue(selectorName: "batteryPercentCombined")
|
||||||
|
|
||||||
if let single = single {
|
if let single = single {
|
||||||
lastKnownBattery[addr] = single
|
|
||||||
model.singleBattery = single
|
model.singleBattery = single
|
||||||
} else if let cached = lastKnownBattery[addr] {
|
|
||||||
// Keep smooth previous valid level during connection handshake
|
|
||||||
model.singleBattery = cached
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue