Files
Camper-Monitor/VanControl/CarPlay/CarPlayLevelSource.swift
fototeddyandClaude Opus 5 ae12d8ede4 CarPlay-Demo: Nivellierung auf dem Fahrzeugdisplay
Eigene Build-Konfiguration "CarPlay" samt Schema, damit der Versuchsbau neben
der TestFlight-App stehen kann: eigener Name, eigene Bundle-ID, persoenliches
Team, manuelles Signing. Debug und Release bleiben unberuehrt.

Wichtig dabei: Die Signing-Einstellungen stehen ausschliesslich in den
Target-Konfigurationen. Steht CODE_SIGN_STYLE auf Projektebene, schreibt der
naechste xcodebuild-Lauf alle uebrigen Konfigurationen auf Manual um und loescht
ihr Team – der TestFlight-Build waere damit hin.

Der CarPlay-Code haengt an `CARPLAY_DEMO` und existiert im regulaeren Build
nicht. Die Szene zeichnet in das gelieferte CPWindow; das CPMapTemplate darueber
bleibt leer, weil es auf dem Stellplatz nichts zu tippen gibt. Die Anzeige
sperrt sich oberhalb von 5 km/h selbst – erzwungen, nicht zugesagt.

Die Groessen sind auf die 400x240 Punkte ausgelegt, die CarPlay liefert, nicht
auf die 800x480 Pixel. In Pixeln gedachte Schriftgroessen sind auf dem
Fahrzeugdisplay doppelt zu gross und schneiden den Text ab.

Bekannt: Unter iOS 26.5 stuerzt Apples CarPlayTemplateUIHost beim Aufbau der
Navigationsleiste ab (`_updateShareButtonVisibility` fragt seinen
destinationSharingDelegate nach `vehicleSupportsDestinationSharing`, den dieser
nicht kennt). Beides ist privates API und von der App aus nicht erreichbar;
unter iOS 18 laeuft es. Szene und Root-Template werden vorher in beiden
Faellen akzeptiert.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-10 18:35:33 +02:00

106 lines
3.7 KiB
Swift
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#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