Fix real-time battery tracking

This commit is contained in:
Nikita 2026-10-06 12:14:47 +05:00
parent fdb8c90975
commit 47743a22e8
2 changed files with 18 additions and 15 deletions

View file

@ -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")

View file

@ -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
} }
} }
} }