#if CARPLAY_DEMO import Foundation import Observation /// Woher die CarPlay-Libelle ihre Werte bekommt. /// /// Im Simulator gibt es kein Bluetooth und damit keinen Neigungsmesser. Für /// die Demo läuft deshalb ein simulierter Verlauf: ein Fahrzeug, das langsam /// auf die Keile rollt, gerade wird und wieder abrutscht. Das reicht, um die /// Anzeige zu beurteilen – und für Screenshots, die zeigen, was die Funktion /// tut. /// /// Für gezielte Aufnahmen lassen sich feste Werte über die Umgebung setzen: /// `CARPLAY_DEMO_PITCH` und `CARPLAY_DEMO_ROLL` (in Grad). Dann steht die /// Blase still und der Screenshot ist reproduzierbar. /// /// Am Fahrzeug speist stattdessen der `BluetoothManager` über ``apply(_:)`` /// ein; die Demo-Schleife läuft dann nicht. @Observable final class CarPlayLevelSource { static let shared = CarPlayLevelSource() private(set) var pitch: Double? private(set) var roll: Double? /// Ob die Werte simuliert sind. Die Anzeige weist darauf hin – ein /// Screenshot ohne diesen Hinweis wäre gegenüber Apple nicht ehrlich. private(set) var isSimulated = false @ObservationIgnored private var timer: Timer? @ObservationIgnored private var phase: Double = 0 @ObservationIgnored private lazy var isSequenced = ProcessInfo.processInfo.environment["CARPLAY_DEMO_SEQUENCE"] != nil private init() {} /// Zehn Aktualisierungen pro Sekunde – schnell genug, dass die Blase der /// Bewegung des Fahrzeugs folgt statt ihr nachzulaufen. private static let updateInterval: TimeInterval = 0.1 func start() { guard timer == nil else { return } let environment = ProcessInfo.processInfo.environment if let fixedPitch = environment["CARPLAY_DEMO_PITCH"].flatMap(Double.init), let fixedRoll = environment["CARPLAY_DEMO_ROLL"].flatMap(Double.init) { pitch = fixedPitch roll = fixedRoll isSimulated = true return } isSimulated = true let timer = Timer.scheduledTimer(withTimeInterval: Self.updateInterval, repeats: true) { [weak self] _ in self?.step() } RunLoop.main.add(timer, forMode: .common) self.timer = timer } func stop() { timer?.invalidate() timer = nil } /// Echte Messwerte vom Neigungsmesser. Sobald die kommen, ist nichts mehr /// simuliert. func apply(_ state: LevelState) { stop() isSimulated = false pitch = state.pitch roll = state.roll } /// Feste Zustände im Wechsel, je sieben Sekunden – für Screenshots, die /// beide Fälle zuverlässig zeigen statt auf den richtigen Moment einer /// Schwingung zu warten. private static let sequence: [(pitch: Double, roll: Double)] = [ (1.8, -0.9), // deutlich schief (0.2, -0.1), // ausgerichtet ] /// Ein Fahrzeug, das sich über gut zwanzig Sekunden einpendelt und wieder /// abweicht. Zwei unterschiedlich schnelle Schwingungen, damit Längs- und /// Querneigung nicht im Gleichtakt laufen – das sähe mechanisch aus. private func step() { phase += Self.updateInterval if isSequenced { // Dritte Phase: die Sperre waehrend der Fahrt. let step = Int(phase / 7) % (Self.sequence.count + 1) if step == Self.sequence.count { CarPlaySpeedGate.shared.setDemoDriving(true) } else { CarPlaySpeedGate.shared.setDemoDriving(false) (pitch, roll) = Self.sequence[step] } return } pitch = 3.2 * sin(phase / 3.4) roll = 2.1 * sin(phase / 2.1 + 0.7) } } #endif