forked from fritob/Camper-Monitor
App-Namen im iOS-Projekt auf VanControl vereinheitlichen
CamperMonitor (Haupt-Repo) und VanAligneiOS (aus dem gemergten solar-integration-Branch) liefen unter zwei verschiedenen internen Namen, obwohl die App nach aussen längst einheitlich "VanControl Pro" heisst. Jetzt durchgängig VanControl: - Ordner: CamperMonitor/, CamperMonitorWatch/, CamperMonitorComplication/, VanAligneiOSWidget/ → VanControl/, VanControlWatch/, VanControlComplication/, VanControlWidget/ - Xcode-Projekt: CamperMonitor.xcodeproj → VanControl.xcodeproj, alle Targets/Schemes/Produktnamen entsprechend umbenannt - Bundle-Identifier auf Wunsch mitgeändert: de.s0.fototeddy.VanControl* (App noch nicht veröffentlicht); dabei auch die WKCompanionAppBundleIdentifier-Werte korrigiert, die noch das alte de.fritob-Präfix statt des tatsächlichen de.s0.fototeddy-Präfixes trugen - Swift-Dateien/Typen: CamperMonitorApp → VanControlApp, VanAligneiOSWidget* → VanControlWidget* - Config/*-Info.plist umbenannt, README.md/Tools/README.md/run-tests.sh auf die neuen Pfade angepasst Bewusst unverändert: firmware/vanalign und alle Bezüge auf "VanAlign" als Namen der Neigungsmesser-Hardware (eigenständiges Produkt, kein App-Name) sowie der komplette Android/-Ordner. Build (App, Watch, Debug) und Protokoll-Testlauf grün. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
83ea85f3b8
commit
303a9735d0
@@ -0,0 +1,147 @@
|
||||
import Foundation
|
||||
import Observation
|
||||
import WatchConnectivity
|
||||
|
||||
/// Die Gegenstelle zum iPhone auf der Uhr.
|
||||
///
|
||||
/// Hält den zuletzt empfangenen Stand und schickt Stellbefehle zurück. Gefunkt
|
||||
/// wird nur zum iPhone – zu den Geräten im Fahrzeug nie, siehe `WatchLink`.
|
||||
///
|
||||
/// Zwei Quellen laufen zusammen:
|
||||
///
|
||||
/// * Der **Anwendungskontext** liegt schon da, wenn die App startet. Deshalb
|
||||
/// steht sofort etwas auf dem Bildschirm, auch wenn das iPhone in der Tasche
|
||||
/// steckt und gerade nicht antwortet – mit Altersangabe, versteht sich.
|
||||
/// * **Nachrichten** kommen im halben Sekundentakt, solange wir gemeldet haben,
|
||||
/// dass jemand hinschaut. Das Melden wird regelmässig erneuert: Schläft die
|
||||
/// Uhr ein, bleibt die Erneuerung aus und das iPhone hört von selbst auf.
|
||||
@Observable
|
||||
final class PhoneLink: NSObject {
|
||||
|
||||
private(set) var payload: WatchPayload?
|
||||
/// Wann der Stand hier ankam – nicht, wann er entstand. Beides zusammen
|
||||
/// unterscheidet „iPhone weg“ von „Gerät im Fahrzeug antwortet nicht“.
|
||||
private(set) var receivedAt: Date?
|
||||
private(set) var isReachable = false
|
||||
private(set) var isActivated = false
|
||||
|
||||
private var session: WCSession?
|
||||
/// Erneuert die Meldung „hier schaut jemand hin“ – im Vorführbetrieb
|
||||
/// stattdessen der Taktgeber der erfundenen Werte.
|
||||
private var timer: Timer?
|
||||
private var isLive = false
|
||||
|
||||
/// Ohne iPhone lässt sich auf der Uhr sonst nichts vorführen.
|
||||
private let isDemo = ProcessInfo.processInfo.environment["CAMPER_DEMO"] == "1"
|
||||
|
||||
func activate() {
|
||||
if isDemo {
|
||||
startDemo()
|
||||
return
|
||||
}
|
||||
guard WCSession.isSupported() else { return }
|
||||
let session = WCSession.default
|
||||
session.delegate = self
|
||||
self.session = session
|
||||
session.activate()
|
||||
}
|
||||
|
||||
/// Meldet dem iPhone, ob gerade jemand hinschaut. Beim Ausrichten zählt
|
||||
/// jeder halbe Sekundentakt, beim Nachsehen des Ladezustands nicht.
|
||||
func setLive(_ live: Bool) {
|
||||
guard !isDemo else { return }
|
||||
isLive = live
|
||||
timer?.invalidate()
|
||||
timer = nil
|
||||
guard live else {
|
||||
send(.hello(live: false))
|
||||
return
|
||||
}
|
||||
send(.hello(live: true))
|
||||
// Vor Ablauf der Frist erneuern, damit zwischendurch keine Lücke
|
||||
// entsteht. Bleibt die Erneuerung aus, endet der schnelle Takt von
|
||||
// selbst – niemand muss ihn abbestellen.
|
||||
let renew = Timer(timeInterval: WatchLink.liveLease / 3, repeats: true) { [weak self] _ in
|
||||
self?.send(.hello(live: true))
|
||||
}
|
||||
RunLoop.main.add(renew, forMode: .common)
|
||||
timer = renew
|
||||
}
|
||||
|
||||
/// Schickt einen Befehl und übernimmt die Antwort, falls eine kommt.
|
||||
///
|
||||
/// Die Antwort ist der Stand *vor* der Wirkung – die Kühlbox antwortet dem
|
||||
/// iPhone erst ein paar Zehntel später. Sie zeigt also nicht, dass der
|
||||
/// Befehl gewirkt hat, sondern nur, dass er angekommen ist. Der gestellte
|
||||
/// Wert erscheint, wenn das Gerät ihn zurückmeldet, so wie am iPhone auch.
|
||||
func send(_ command: WatchCommand) {
|
||||
guard !isDemo, let session, session.activationState == .activated,
|
||||
let message = try? command.message() else { return }
|
||||
guard session.isReachable else { return }
|
||||
session.sendMessage(message, replyHandler: { [weak self] reply in
|
||||
guard let payload = WatchPayload.decode(from: reply) else { return }
|
||||
DispatchQueue.main.async { self?.accept(payload) }
|
||||
}, errorHandler: nil)
|
||||
}
|
||||
|
||||
private func accept(_ payload: WatchPayload) {
|
||||
// Ein Nachzügler darf einen neueren Stand nicht überschreiben.
|
||||
if let current = self.payload, current.generatedAt > payload.generatedAt { return }
|
||||
self.payload = payload
|
||||
receivedAt = Date()
|
||||
}
|
||||
|
||||
// MARK: - Vorführbetrieb
|
||||
|
||||
private func startDemo() {
|
||||
payload = WatchDemo.payload(at: 0)
|
||||
receivedAt = Date()
|
||||
isReachable = true
|
||||
isActivated = true
|
||||
var step = 0.0
|
||||
let tick = Timer(timeInterval: 0.5, repeats: true) { [weak self] _ in
|
||||
step += 0.5
|
||||
self?.payload = WatchDemo.payload(at: step)
|
||||
self?.receivedAt = Date()
|
||||
}
|
||||
RunLoop.main.add(tick, forMode: .common)
|
||||
timer = tick
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - WCSessionDelegate
|
||||
|
||||
extension PhoneLink: WCSessionDelegate {
|
||||
|
||||
func session(_ session: WCSession,
|
||||
activationDidCompleteWith state: WCSessionActivationState,
|
||||
error: Error?) {
|
||||
DispatchQueue.main.async {
|
||||
self.isActivated = state == .activated
|
||||
self.isReachable = session.isReachable
|
||||
// Was schon dalag, gilt sofort – sonst bliebe der Bildschirm leer,
|
||||
// bis das iPhone das nächste Mal von sich aus sendet.
|
||||
if let payload = WatchPayload.decode(from: session.receivedApplicationContext) {
|
||||
self.accept(payload)
|
||||
}
|
||||
if self.isLive { self.send(.hello(live: true)) }
|
||||
}
|
||||
}
|
||||
|
||||
func session(_ session: WCSession, didReceiveMessage message: [String: Any]) {
|
||||
guard let payload = WatchPayload.decode(from: message) else { return }
|
||||
DispatchQueue.main.async { self.accept(payload) }
|
||||
}
|
||||
|
||||
func session(_ session: WCSession, didReceiveApplicationContext context: [String: Any]) {
|
||||
guard let payload = WatchPayload.decode(from: context) else { return }
|
||||
DispatchQueue.main.async { self.accept(payload) }
|
||||
}
|
||||
|
||||
func sessionReachabilityDidChange(_ session: WCSession) {
|
||||
DispatchQueue.main.async {
|
||||
self.isReachable = session.isReachable
|
||||
if session.isReachable, self.isLive { self.send(.hello(live: true)) }
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user