Bluetooth auf eine eigene Queue legen
CoreBluetooth lief auf dem Hauptthread, also demselben, der die Oberfläche zeichnet. Gescannt wird mit Duplikaten über alle Geräte in Reichweite, weil Victron seine Werte im Advertisement mitschickt - jedes einzelne davon kam als Rückruf dort an, samt Entschlüsselung und Neuzeichnung. Das war die Ursache der trägen Bedienung. CoreBluetooth arbeitet jetzt auf einer eigenen seriellen Queue. Daraus folgt eine strikte Trennung: * Verbindungen, Puffer und Zeitgeber werden ausschliesslich auf dieser Queue angefasst. Die Zeitgeber sind deshalb DispatchSourceTimer statt Timer, denn auf einer DispatchQueue gibt es keine RunLoop. * Die veröffentlichten Werte werden ausschliesslich auf dem Hauptthread geschrieben, gebündelt über publish(). * Die Geräteliste wird nicht mehr aus der Funk-Queue heraus im Speicher nachgeschlagen. Stattdessen zieht refreshConfiguration auf dem Hauptthread eine Momentaufnahme - Kennung, Transportart, Zonenwahl und den bereits ausgelesenen Victron-Schlüssel - und reicht sie hinüber. Damit greift kein Hintergrundthread mehr auf den beobachteten Speicher zu. Die Stellbefehle der Kühlbox bauen ihren Einstellungsblock ebenfalls auf der Funk-Queue, weil der zugrundeliegende Zustand dort lebt. Geprüft im Simulator: der Statuswechsel von CoreBluetooth kommt über die Funk-Queue und erreicht die Anzeige korrekt, beide Startpfade laufen ohne Absturz. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -65,6 +65,10 @@ final class BMSSession: NSObject {
|
||||
}
|
||||
|
||||
let deviceID: UUID
|
||||
/// Die Queue, auf der CoreBluetooth arbeitet. Alle Zeitgeber und
|
||||
/// verzögerten Aufrufe laufen darauf, damit der Zustand dieser Klasse nur
|
||||
/// von einem Thread aus angefasst wird.
|
||||
private let queue: DispatchQueue
|
||||
private let peripheral: CBPeripheral
|
||||
private let onUpdate: (DeviceSnapshot) -> Void
|
||||
private let onStateChange: (DeviceLinkState) -> Void
|
||||
@@ -88,7 +92,7 @@ final class BMSSession: NSObject {
|
||||
/// Ob die Kühlbox in dieser Sitzung schon angemeldet wurde.
|
||||
private var didBind = false
|
||||
private var buffer: [UInt8] = []
|
||||
private var pollTimer: Timer?
|
||||
private var pollTimer: DispatchSourceTimer?
|
||||
private var lastResponse: Data?
|
||||
private var lastCommand: Data?
|
||||
private var lastCommandAt: Date?
|
||||
@@ -116,10 +120,12 @@ final class BMSSession: NSObject {
|
||||
|
||||
init(deviceID: UUID,
|
||||
peripheral: CBPeripheral,
|
||||
queue: DispatchQueue,
|
||||
onUpdate: @escaping (DeviceSnapshot) -> Void,
|
||||
onStateChange: @escaping (DeviceLinkState) -> Void,
|
||||
onDiagnostics: @escaping (BMSDiagnostics) -> Void) {
|
||||
self.deviceID = deviceID
|
||||
self.queue = queue
|
||||
self.peripheral = peripheral
|
||||
self.onUpdate = onUpdate
|
||||
self.onStateChange = onStateChange
|
||||
@@ -136,7 +142,7 @@ final class BMSSession: NSObject {
|
||||
}
|
||||
|
||||
func stop() {
|
||||
pollTimer?.invalidate()
|
||||
pollTimer?.cancel()
|
||||
pollTimer = nil
|
||||
if let notify = currentEndpoint?.notify, peripheral.state == .connected {
|
||||
peripheral.setNotifyValue(false, for: notify)
|
||||
@@ -148,7 +154,7 @@ final class BMSSession: NSObject {
|
||||
}
|
||||
|
||||
func handleDisconnect() {
|
||||
pollTimer?.invalidate()
|
||||
pollTimer?.cancel()
|
||||
pollTimer = nil
|
||||
endpoints.removeAll()
|
||||
endpointIndex = 0
|
||||
@@ -231,7 +237,7 @@ final class BMSSession: NSObject {
|
||||
|
||||
// Manche Module bestätigen das Abonnieren nie. Ohne Zeitlimit bliebe
|
||||
// die Suche hier für immer stehen, ohne je etwas zu senden.
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 4) { [weak self] in
|
||||
queue.asyncAfter(deadline: .now() + 4) { [weak self] in
|
||||
guard let self, self.activationToken == token, !self.isNotifyActive else { return }
|
||||
if self.endpoints.count > 1 {
|
||||
self.advanceEndpoint()
|
||||
@@ -267,7 +273,7 @@ final class BMSSession: NSObject {
|
||||
return
|
||||
}
|
||||
let token = activationToken
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { [weak self] in
|
||||
queue.asyncAfter(deadline: .now() + 0.2) { [weak self] in
|
||||
guard let self, self.activationToken == token,
|
||||
self.peripheral.state == .connected else { return }
|
||||
let type: CBCharacteristicWriteType =
|
||||
@@ -276,7 +282,7 @@ final class BMSSession: NSObject {
|
||||
self.didUnlock = true
|
||||
self.publishDiagnostics()
|
||||
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { [weak self] in
|
||||
queue.asyncAfter(deadline: .now() + 0.3) { [weak self] in
|
||||
guard let self, self.activationToken == token else { return }
|
||||
self.beginPolling()
|
||||
}
|
||||
@@ -284,12 +290,14 @@ final class BMSSession: NSObject {
|
||||
}
|
||||
|
||||
private func beginPolling() {
|
||||
pollTimer?.invalidate()
|
||||
pollTimer?.cancel()
|
||||
poll()
|
||||
let interval = dialect == .unknown ? searchInterval : pollInterval
|
||||
pollTimer = Timer.scheduledTimer(withTimeInterval: interval, repeats: true) { [weak self] _ in
|
||||
self?.poll()
|
||||
}
|
||||
let timer = DispatchSource.makeTimerSource(queue: queue)
|
||||
timer.schedule(deadline: .now() + interval, repeating: interval)
|
||||
timer.setEventHandler { [weak self] in self?.poll() }
|
||||
timer.resume()
|
||||
pollTimer = timer
|
||||
}
|
||||
|
||||
private func poll() {
|
||||
@@ -337,7 +345,7 @@ final class BMSSession: NSObject {
|
||||
/// die zu dicht aufeinander folgen.
|
||||
private func sendSequence(_ frames: [Data], spacing: TimeInterval, thenGiveUpAfter grace: TimeInterval) {
|
||||
for (index, frame) in frames.enumerated() {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + Double(index) * spacing) { [weak self] in
|
||||
queue.asyncAfter(deadline: .now() + Double(index) * spacing) { [weak self] in
|
||||
self?.send(frame)
|
||||
}
|
||||
}
|
||||
@@ -346,7 +354,7 @@ final class BMSSession: NSObject {
|
||||
|
||||
/// Kommt nichts Brauchbares zurück, wird der nächste Kandidat versucht.
|
||||
private func checkForSilence(after delay: TimeInterval) {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in
|
||||
queue.asyncAfter(deadline: .now() + delay) { [weak self] in
|
||||
guard let self, self.peripheral.state == .connected else { return }
|
||||
guard !self.hasUsableData else {
|
||||
self.silentRounds = 0
|
||||
@@ -390,7 +398,7 @@ final class BMSSession: NSObject {
|
||||
peripheral.writeValue(piece, for: endpoint.write, type: endpoint.writeType)
|
||||
continue
|
||||
}
|
||||
DispatchQueue.main.asyncAfter(
|
||||
queue.asyncAfter(
|
||||
deadline: .now() + Double(index) * AlpicoolProtocol.chunkDelay
|
||||
) { [weak self] in
|
||||
guard let self, self.peripheral.state == .connected,
|
||||
@@ -413,7 +421,7 @@ final class BMSSession: NSObject {
|
||||
lastCommandAt = Date()
|
||||
send(packet)
|
||||
// Genug Abstand, damit ein aufgeteiltes Paket vollständig draußen ist.
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { [weak self] in
|
||||
queue.asyncAfter(deadline: .now() + 1.0) { [weak self] in
|
||||
guard let self, self.dialect == .alpicool else { return }
|
||||
self.send(AlpicoolProtocol.packet(.query))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user