diff --git a/CamperMonitorWatch/Assets.xcassets/VehicleRear.imageset/VehicleRear.png b/CamperMonitorWatch/Assets.xcassets/VehicleRear.imageset/VehicleRear.png index a71ab5e..40b4e42 100644 Binary files a/CamperMonitorWatch/Assets.xcassets/VehicleRear.imageset/VehicleRear.png and b/CamperMonitorWatch/Assets.xcassets/VehicleRear.imageset/VehicleRear.png differ diff --git a/CamperMonitorWatch/Assets.xcassets/VehicleSide.imageset/VehicleSide.png b/CamperMonitorWatch/Assets.xcassets/VehicleSide.imageset/VehicleSide.png index 9f1c333..6a6678f 100644 Binary files a/CamperMonitorWatch/Assets.xcassets/VehicleSide.imageset/VehicleSide.png and b/CamperMonitorWatch/Assets.xcassets/VehicleSide.imageset/VehicleSide.png differ diff --git a/CamperMonitorWatch/WatchLevelRadio.swift b/CamperMonitorWatch/WatchLevelRadio.swift index 3e7b190..2887c71 100644 --- a/CamperMonitorWatch/WatchLevelRadio.swift +++ b/CamperMonitorWatch/WatchLevelRadio.swift @@ -46,6 +46,24 @@ final class WatchLevelRadio: NSObject { private var session: LevelSession? private var isRunning = false + /// Wie lange nach einem Fehlschlag gewartet wird, bevor neu gesucht wird. + /// + /// Ohne diese Bremse dreht es sich im Kreis: Ist der Sensor gerade nicht zu + /// haben – etwa weil er schon mit dem iPhone spricht –, folgt auf jeden + /// Verbindungsversuch sofort der Abbruch, auf den Abbruch sofort die + /// nächste Suche, und das so schnell, wie das Funkmodul es zulässt. Die + /// Uhr ist dann mit Verbindungsaufbau beschäftigt statt mit Anzeigen, und + /// watchOS schiesst die App irgendwann wegen Speicher ab. + private var retryDelay: TimeInterval = 1 + private static let maximumRetryDelay: TimeInterval = 8 + private var isRetryScheduled = false + + /// Anzeigewerte höchstens fünfmal je Sekunde weiterreichen. Der Sensor + /// meldet je nach Firmware deutlich öfter; jede Meldung ist sonst ein + /// Neuzeichnen der Oberfläche, und dafür ist die Uhr zu klein. + private static let publishInterval: TimeInterval = 0.2 + private var lastPublished = Date.distantPast + /// Kennung für die Momentaufnahmen dieser Sitzung. Auf der Uhr gibt es /// keine Geräteliste, in der sie nachgeschlagen würde – sie muss nur /// innerhalb eines Laufs gleich bleiben. @@ -89,6 +107,19 @@ final class WatchLevelRadio: NSObject { } } + /// Nach einem Fehlschlag: erst warten, dann wieder suchen. Die Wartezeit + /// wächst, bis der Sensor wieder antwortet. + private func scanAfterBackOff() { + guard isRunning, !isRetryScheduled else { return } + isRetryScheduled = true + let delay = retryDelay + retryDelay = min(retryDelay * 2, Self.maximumRetryDelay) + queue.asyncAfter(deadline: .now() + delay) { [weak self] in + self?.isRetryScheduled = false + self?.scanIfPossible() + } + } + private func publish(_ work: @escaping () -> Void) { if Thread.isMainThread { work() } else { DispatchQueue.main.async(execute: work) } } @@ -137,6 +168,11 @@ extension WatchLevelRadio: CBCentralManagerDelegate { didDiscover peripheral: CBPeripheral, advertisementData: [String: Any], rssi RSSI: NSNumber) { + // Nach dem Abschalten der Suche können noch Meldungen nachkommen. Ein + // zweiter Verbindungsversuch auf dasselbe Gerät bringt nichts und + // erzeugt nur eine weitere Trennung, auf die wieder eine Suche folgt. + if let existing = self.peripheral, existing.state != .disconnected { return } + // Gesucht wird nach dem Dienst, nicht nach dem Namen: der lässt sich in // der Firmware ändern, die Dienstkennung nicht. central.stopScan() @@ -158,10 +194,18 @@ extension WatchLevelRadio: CBCentralManagerDelegate { self?.publish { self?.link = state } }, onLevelState: { [weak self] state in - self?.publish { - self?.state = state - self?.updatedAt = Date() - self?.statusText = "Direkt am Sensor" + guard let self else { return } + // Es kommen Werte: Der Sensor ist zu haben, die Wartezeit darf + // wieder von vorn beginnen. + self.retryDelay = 1 + let now = Date() + guard now.timeIntervalSince(self.lastPublished) >= Self.publishInterval + else { return } + self.lastPublished = now + self.publish { + self.state = state + self.updatedAt = now + self.statusText = "Direkt am Sensor" } }) session.orientation = orientation @@ -173,7 +217,7 @@ extension WatchLevelRadio: CBCentralManagerDelegate { didFailToConnect peripheral: CBPeripheral, error: Error?) { publish { self.link = .failed(error?.localizedDescription ?? "Verbindung gescheitert") } - scanIfPossible() + scanAfterBackOff() } func centralManager(_ central: CBCentralManager, @@ -188,6 +232,6 @@ extension WatchLevelRadio: CBCentralManagerDelegate { self.link = .searching self.statusText = "Verbindung verloren – suche erneut" } - scanIfPossible() + scanAfterBackOff() } }