Author SHA1 Message Date
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
fototeddyandClaude Opus 5 b1ffbe324a Libelle: Blase wandert zur höheren Seite, einheitlich auf allen Anzeigen
Die drei Libellen widersprachen sich: Die Live Activity liess die Blase bei
positivem Pitch nach unten wandern – mit dem Kommentar "wie bei der echten
Wasserwaage in der App" –, waehrend iPhone und Uhr genau das Gegenteil taten.
Gemeint war immer eine Draufsicht mit Fahrtrichtung oben, in der die Blase zur
hoeheren Seite ausschlaegt: Steht das Heck hoeher, gehoert sie nach hinten,
also nach unten. iPhone und Uhr ziehen jetzt nach.

Die Android-Fassung bleibt vorerst bei der alten Richtung.

Dazu `shortInstructions`: dieselbe Aussage wie `instruction`, aber in kurzen
Zeilen statt einem Satz. Auf schmalen Anzeigen passt "Heck steht hoeher, links
steht hoeher" in keine lesbare Zeile. Beide stehen nebeneinander, damit die
Vorzeichen nicht auseinanderlaufen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-10 18:35:14 +02:00
fototeddyandClaude Sonnet 5 8401f62d91 California-Seitenansicht eng zuschneiden statt in quadratischer Leinwand
Die Vorlage lag auf einer quadratischen 1254×1254-Leinwand, der Van
darin füllte aber nur rund 59% der Höhe – bei deklariertem
sideAspect = 1 (quadratisch) rechnete SwiftUI die Anzeigegrösse für
dieses volle Quadrat, wodurch der Van in der gemeinsamen Bildhülle
deutlich kleiner erschien als Vanster, dessen Vorlage eng auf die
Silhouette zugeschnitten ist.

Bild jetzt auf den Inhalt zugeschnitten (plus kleiner Rand, wie bei
Vanster), sideAspect entsprechend von 1 auf das tatsächliche Verhältnis
der zugeschnittenen Vorlage (1212×790) angepasst. In beiden
Asset-Katalogen ersetzt (iPhone und Watch, siehe vorherigen Commit zur
Watch-Fahrzeuggrafik).

Die Heckansicht war laut Rückmeldung bereits optisch stimmig und blieb
unverändert.

Build (App + Watch) und Tests grün.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-09 20:27:20 +02:00
fototeddyandClaude Sonnet 5 c45cf36920 Fahrzeuggrafik-Stil (Vanster/California) auf die Watch ausweiten
Die Uhr zeigte in der Fahrzeug-Ansicht bislang immer Vanster fest
verdrahtet, unabhängig von der Auswahl am iPhone – das galt schon vor
dieser Session, nicht erst seit dem Solar-Merge.

WatchDevice trägt jetzt vehicleGraphicStyle (nur beim Neigungsmesser
belegt, wie orientation), von PhoneWatchLink beim Aufbau der Nutzlast
gefüllt. WatchVehicleView nimmt den Stil als Parameter statt der fest
verdrahteten Vanster-Bildnamen, WatchLevelView reicht ihn aus dem
zuletzt vom iPhone übertragenen Stand durch. California-Bilder dafür
zusätzlich in den Watch-eigenen Asset-Katalog kopiert (die Uhr hat
einen eigenen, vom iPhone getrennten Katalog).

Ausgewählt wird der Stil weiterhin nur am iPhone (Neigungsmesser
einrichten → Fahrzeuggrafik). Persistenz war dafür schon vorhanden:
vehicleGraphicStyle ist ein normales, codiertes Feld auf
ConfiguredDevice ohne eigene encode(to:)-Überschreibung – wird also
schon beim Speichern mitgeschrieben.

Nebenbei: Xcode hat beim Bearbeiten den Build-Zähler auf 2 gesetzt und
LSApplicationCategoryType (Dienstprogramme) ergänzt – unverändert
übernommen.

Build (App + Watch) und Tests grün.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-09 20:22:32 +02:00
fototeddyandClaude Sonnet 5 47ead9b006 Watch-App-Icon an das neue Haupt-App-Icon angleichen
VanControlWatch hat ein eigenes, vom iPhone-Target unabhängiges
AppIcon.appiconset – das zeigte beim Einbinden von App1/App2 noch das
alte Logo. Jetzt dieselbe Vorlage wie das primäre iPhone-Icon
(App1-Testflight), damit iPhone- und Watch-App auf dem Homescreen
zusammengehörig aussehen.

Build (Watch + App) und Tests grün.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-09 20:14:17 +02:00
fototeddyandClaude Sonnet 5 b23e96615e App1/App2-Icons als primäres und Alternate-Icon einbinden
Vier neue AppIcon-Asset-Kataloge angelegt (App1/App2, jeweils Standard-
und TestFlight-Variante), aus den bereitgestellten 1024×1024-Vorlagen
(bereits randlos, ohne Alphakanal – nur noch skaliert).

Verdrahtet für den aktuellen TestFlight-Build:
- ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon-App1-Testflight (primär)
- ASSETCATALOG_COMPILER_ALTERNATE_APPICON_NAMES = AppIcon-App2-Testflight
- ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES, damit auch die
  noch unverdrahteten App1/App2-Sets (für den finalen Store-Build) mit
  ins Bundle kommen, statt vom Compiler als ungenutzt verworfen zu werden

Fürs finale Release genügt es, ASSETCATALOG_COMPILER_APPICON_NAME auf
AppIcon-App1 und die Alternate-Liste auf AppIcon-App2 umzustellen – die
TestFlight-Varianten sollen Betatester optisch von der echten Version
unterscheiden.

Nur das Haupt-App-Target ist betroffen; Watch- und Complication-Icon
bleiben unverändert. Build (App + Watch) und Tests grün.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-08 17:14:22 +02:00
fototeddyandClaude Sonnet 5 0b3bf5e815 Neue California-Fahrzeuggrafik einbauen, eigene Heckansicht ergänzt
Seiten- und Heckansicht kamen als weiß gefüllte Vorlage ohne Alphakanal
(Tools/California Side.png, Tools/California Back.png). Dafür ein neues
Werkzeug ergänzt, das den weißen Grund in echte Transparenz übersetzt
(mit weichem statt hartem Schwellwert, um Kanten nicht auszufransen),
danach wie gewohnt durch make-vehicle-art.swift geschickt.

California hatte bisher keine eigene Heckansicht und griff ersatzweise
auf die von Vanster zurück – jetzt eigenes CaliforniaRear-Imageset,
VehicleGraphicStyle.rearImageName entsprechend angepasst. Seiten- und
Heckbild sind quadratisch (1254×1254), passend zu den bestehenden
Seitenverhältnissen (sideAspect/rearAspect = 1) – keine Anpassung dort
nötig.

Build und Tests grün.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-07 21:31:42 +02:00
fototeddyandClaude Sonnet 5 8d58ea6e49 Export-Compliance: ITSAppUsesNonExemptEncryption setzen
App Store Connect fragt beim Hochladen nach Verschlüsselung, wenn der
Info.plist-Schlüssel fehlt. Die App entschlüsselt Victron-Advertisements
per AES-128-CTR über CommonCrypto – ein Standardalgorithmus über eine
vom Betriebssystem gestellte Standardbibliothek, nur Nebenzweck der App
und damit nach US-Exportrecht typischerweise ausgenommen. Schlüssel auf
false gesetzt, damit die Abfrage bei künftigen Uploads entfällt.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 22:53:39 +02:00
fototeddyandClaude Sonnet 5 141e321a6f Watch-Target: MARKETING_VERSION-Komma auf Punkt korrigieren
Beim Setzen der Versionsnummer in Xcode ist bei den beiden
VanControlWatch-Konfigurationen "0,1" statt "0.1" hereingerutscht,
vermutlich durch die deutsche Locale beim Zahlen-Eingabefeld. App Store
Connect erwartet ein Punkt-Format; mit dem Komma wäre das beim
Archivieren/Hochladen vermutlich angeeckt.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 22:34:44 +02:00
fototeddyandClaude Sonnet 5 35f4e6fbcb Demo-Modus auch in Release-/TestFlight-Builds verfügbar machen
Der Einstellungen-Schalter für den Demo-Modus war in #if DEBUG
eingeschlossen und damit in Archiven (Release-Konfiguration, wie sie
Product → Archive und damit jeder TestFlight-Build verwendet) komplett
herauskompiliert. TestFlight-Tester konnten den Demo-Modus dadurch nicht
nutzen.

DemoData.isEnabled und der Schalter in SettingsView sind jetzt
konfigurationsunabhängig; nur die CAMPER_DEMO-Umgebungsvariable bleibt
auf Debug-Builds beschränkt, da sie ohnehin nur über Xcode/simctl
gesetzt werden kann.

Build (Debug + Release) und Tests grün; per strings(1) im
Release-Binary verifiziert, dass der Demo-Modus-Code tatsächlich
mitkompiliert wird.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 22:33:11 +02:00
41 changed files with 969 additions and 66 deletions
+31
View File
@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>UIBackgroundModes</key>
<array>
<string>bluetooth-central</string>
</array>
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>Damit die Nivellierung auf dem CarPlay-Display nur im Stand angezeigt wird.</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>Damit die Nivellierung auf dem CarPlay-Display nur im Stand angezeigt wird.</string>
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>
<true/>
<key>UISceneConfigurations</key>
<dict>
<key>CPTemplateApplicationSceneSessionRoleApplication</key>
<array>
<dict>
<key>UISceneConfigurationName</key>
<string>CarPlay</string>
<key>UISceneDelegateClassName</key>
<string>CarPlaySceneDelegate</string>
</dict>
</array>
</dict>
</dict>
</dict>
</plist>
+8
View File
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.developer.carplay-maps</key>
<true/>
</dict>
</plist>
+10 -8
View File
@@ -430,16 +430,18 @@ eingerichtet wurden, wandern beim Update automatisch ins erste Profil.
## Ohne Fahrzeug ansehen ## Ohne Fahrzeug ansehen
Ein Demo-Modus füllt die App mit erfundenen Fahrzeugen und Messwerten, damit Ein Demo-Modus füllt die App mit erfundenen Fahrzeugen und Messwerten, damit
sich die Ansichten ohne Bluetooth prüfen lassen. Nur in Debug-Builds, auf zwei sich die Ansichten ohne Bluetooth prüfen lassen auch ohne die Hardware zur
Wegen einzuschalten: Hand zu haben. Auf zwei Wegen einzuschalten:
* **Schalter in den Einstellungen** (*Einstellungen → Entwicklung → * **Schalter in den Einstellungen** (*Einstellungen → Entwicklung →
Demo-Modus*) der Weg für einen lokal installierten Build ohne Demo-Modus*) in jeder Build-Konfiguration verfügbar, auch in TestFlight-
Xcode-Verbindung, etwa auf dem eigenen iPhone. Wirkt erst nach einem und App-Store-Builds. Der Weg für jeden lokal installierten Build ohne
Neustart der App (im App-Umschalter nach oben wischen, dann neu öffnen), Xcode-Verbindung. Wirkt erst nach einem Neustart der App (im
weil `DeviceStore` und `BluetoothManager` den Stand nur beim Start lesen. App-Umschalter nach oben wischen, dann neu öffnen), weil `DeviceStore` und
* **Umgebungsvariable** `CAMPER_DEMO=1` in Xcode unter *Product → Scheme → `BluetoothManager` den Stand nur beim Start lesen.
Edit Scheme → Run → Arguments*, oder im Simulator direkt: * **Umgebungsvariable** `CAMPER_DEMO=1` nur in Debug-Builds, in Xcode unter
*Product → Scheme → Edit Scheme → Run → Arguments*, oder im Simulator
direkt:
```bash ```bash
xcrun simctl launch --terminate-running-process booted <deine-bundle-id> xcrun simctl launch --terminate-running-process booted <deine-bundle-id>
+19
View File
@@ -65,6 +65,25 @@ struct LevelState: Equatable, Codable, Sendable {
return parts.joined(separator: ", ") return parts.joined(separator: ", ")
} }
/// Dasselbe wie ``instruction``, aber in kurzen Zeilen statt einem Satz
/// für schmale Anzeigen wie das CarPlay-Display, wo "Heck steht höher,
/// rechts steht höher" nicht in eine lesbare Zeile passt.
///
/// Die Vorzeichen müssen zu ``instruction`` passen; beide stehen deshalb
/// nebeneinander.
var shortInstructions: [String] {
guard let pitch, let roll else { return [] }
if isLevel { return ["Steht eben"] }
var parts: [String] = []
if abs(pitch) > Self.levelTolerance {
parts.append(pitch > 0 ? "Heck höher" : "Front höher")
}
if abs(roll) > Self.levelTolerance {
parts.append(roll > 0 ? "Rechts höher" : "Links höher")
}
return parts
}
func snapshot(deviceID: UUID, rssi: Int?) -> DeviceSnapshot { func snapshot(deviceID: UUID, rssi: Int?) -> DeviceSnapshot {
var snapshot = DeviceSnapshot(deviceID: deviceID, timestamp: Date(), rssi: rssi) var snapshot = DeviceSnapshot(deviceID: deviceID, timestamp: Date(), rssi: rssi)
snapshot.metrics = [ snapshot.metrics = [
+3 -4
View File
@@ -26,11 +26,10 @@ enum VehicleGraphicStyle: String, CaseIterable, Identifiable, Codable, Sendable
} }
} }
/// Für California liegt noch keine eigene Heckansicht vor, deshalb
/// vorerst die von Vanster.
var rearImageName: String { var rearImageName: String {
switch self { switch self {
case .vanster, .california: return "VansterRear" case .vanster: return "VansterRear"
case .california: return "CaliforniaRear"
} }
} }
@@ -40,7 +39,7 @@ enum VehicleGraphicStyle: String, CaseIterable, Identifiable, Codable, Sendable
var sideAspect: Double { var sideAspect: Double {
switch self { switch self {
case .vanster: return VehicleTilt.sideAspect case .vanster: return VehicleTilt.sideAspect
case .california: return 1 case .california: return 1212.0 / 790.0
} }
} }
+4
View File
@@ -92,6 +92,10 @@ struct WatchDevice: Codable, Equatable, Identifiable, Sendable {
/// den Neigungsmesser selbst an und braucht die Zuordnung deshalb auch /// den Neigungsmesser selbst an und braucht die Zuordnung deshalb auch
/// eingestellt wird sie weiterhin nur am iPhone. /// eingestellt wird sie weiterhin nur am iPhone.
var orientation: SensorOrientation? var orientation: SensorOrientation?
/// Ebenfalls nur beim Neigungsmesser: welche Fahrzeuggrafik die
/// Fahrzeug-Ansicht zeigt. Ausgewählt wird sie nur am iPhone, die Uhr
/// übernimmt nur die Anzeige.
var vehicleGraphicStyle: VehicleGraphicStyle?
/// Nur bei der Kühlbox belegt. /// Nur bei der Kühlbox belegt.
var fridge: WatchFridge? var fridge: WatchFridge?
+4 -3
View File
@@ -1190,13 +1190,14 @@ let watchPayload = WatchPayload(
devices: [ devices: [
WatchDevice(id: watchLevelID, name: "Nivellierung", role: .leveling, WatchDevice(id: watchLevelID, name: "Nivellierung", role: .leveling,
link: .live, snapshot: watchLevelSnapshot, link: .live, snapshot: watchLevelSnapshot,
level: watchLevel, orientation: watchOrientation, fridge: nil), level: watchLevel, orientation: watchOrientation,
vehicleGraphicStyle: .vanster, fridge: nil),
WatchDevice(id: watchBoosterID, name: "Ladebooster", role: .chargeBooster, WatchDevice(id: watchBoosterID, name: "Ladebooster", role: .chargeBooster,
link: .failed("Nicht gefunden"), snapshot: boosterSnapshot, link: .failed("Nicht gefunden"), snapshot: boosterSnapshot,
level: nil, orientation: nil, fridge: nil), level: nil, orientation: nil, vehicleGraphicStyle: nil, fridge: nil),
WatchDevice(id: watchFridgeID, name: "Kühlbox", role: .fridge, WatchDevice(id: watchFridgeID, name: "Kühlbox", role: .fridge,
link: .live, snapshot: nil, level: nil, link: .live, snapshot: nil, level: nil,
orientation: nil, fridge: watchFridge), orientation: nil, vehicleGraphicStyle: nil, fridge: watchFridge),
]) ])
// Der ganze Weg: verpacken, als WatchConnectivity-Wörterbuch lesen, vergleichen. // Der ganze Weg: verpacken, als WatchConnectivity-Wörterbuch lesen, vergleichen.
+19 -2
View File
@@ -38,5 +38,22 @@ sips --resampleHeight 600 heckansicht.png --out /tmp/rear.png
Jedes Fahrzeug-Grafikset (siehe `VehicleGraphicStyle`) hat sein eigenes Jedes Fahrzeug-Grafikset (siehe `VehicleGraphicStyle`) hat sein eigenes
Bildpaar `<Name>Side`/`<Name>Rear`. Hat die Vorlage schon einen Bildpaar `<Name>Side`/`<Name>Rear`. Hat die Vorlage schon einen
transparenten Hintergrund, geht die Quelldatei direkt ins Skript. Liegt sie transparenten Hintergrund, geht die Quelldatei direkt ins Skript. Liegt sie
auf weißem Grund, muss der Rand vorher weg sonst wird die ganze Fläche auf weißem Grund, muss der Rand vorher weg siehe `remove-white-background.swift`.
mitgefärbt.
## remove-white-background.swift
Vorstufe zu `make-vehicle-art.swift`, wenn die Vorlage auf weißem statt
transparentem Grund liegt (kein Alphakanal). Ohne diesen Schritt hielte
`make-vehicle-art` die ganze Fläche für Zeichnung und färbte den Hintergrund
mit ein.
Reines Weiß wird voll durchsichtig, alles andere bleibt stehen; ein weicher
Übergang statt einer harten Schwelle vermeidet gezackte Kanten an
kantengeglätteten Linien.
```bash
swiftc -O -o /tmp/remove-white-background Tools/remove-white-background.swift
swiftc -O -o /tmp/make-vehicle-art Tools/make-vehicle-art.swift
/tmp/remove-white-background seitenansicht.png /tmp/seite-transparent.png
/tmp/make-vehicle-art /tmp/seite-transparent.png VanControl/Assets.xcassets/CaliforniaSide.imageset/CaliforniaSide.png
```
+62
View File
@@ -0,0 +1,62 @@
import AppKit
import CoreGraphics
import Foundation
// Vorlagen kommen manchmal auf weißem statt transparentem Grund. Ohne
// Alphakanal hielte `make-vehicle-art.swift` die ganze Fläche für Zeichnung
// und färbte den Hintergrund mit ein. Dieses Werkzeug macht daraus einen
// echten Alphakanal: reines Weiß wird voll durchsichtig, alles andere bleibt
// stehen. Ein weicher Übergang statt einer harten Schwelle verhindert
// gezackte Kanten an den (leicht kantengeglätteten) Linien.
let arguments = CommandLine.arguments
guard arguments.count >= 3,
let source = NSImage(contentsOfFile: arguments[1]),
let image = source.cgImage(forProposedRect: nil, context: nil, hints: nil) else {
print("Quelle nicht lesbar"); exit(1)
}
let width = image.width, height = image.height
let bytesPerRow = width * 4
var pixels = [UInt8](repeating: 0, count: bytesPerRow * height)
guard let context = CGContext(data: &pixels, width: width, height: height,
bitsPerComponent: 8, bytesPerRow: bytesPerRow,
space: CGColorSpaceCreateDeviceRGB(),
bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue) else {
print("Puffer nicht anlegbar"); exit(1)
}
context.draw(image, in: CGRect(x: 0, y: 0, width: width, height: height))
/// Unterhalb davon gilt ein Pixel als reiner Hintergrund, oberhalb als
/// sicher Zeichnung. Dazwischen wird linear zwischen 0 und voller Deckkraft
/// überblendet, damit Kanten nicht ausgefranst wirken.
let whiteFloor = 230.0
let whiteCeiling = 250.0
for index in stride(from: 0, to: pixels.count, by: 4) {
// Quelle ist opak (kein Alphakanal), also stehen hier die Rohfarben.
let r = Double(pixels[index])
let g = Double(pixels[index + 1])
let b = Double(pixels[index + 2])
let whiteness = min(r, g, b)
let t = min(1, max(0, (whiteness - whiteFloor) / (whiteCeiling - whiteFloor)))
let alpha = 1 - t
guard alpha > 0.004 else {
pixels[index] = 0; pixels[index + 1] = 0; pixels[index + 2] = 0; pixels[index + 3] = 0
continue
}
// Premultipliziert ablegen.
pixels[index] = UInt8(r * alpha)
pixels[index + 1] = UInt8(g * alpha)
pixels[index + 2] = UInt8(b * alpha)
pixels[index + 3] = UInt8(alpha * 255)
}
guard let result = context.makeImage() else { print("Bild leer"); exit(1) }
let rep = NSBitmapImageRep(cgImage: result)
guard let png = rep.representation(using: .png, properties: [:]) else {
print("PNG nicht erzeugbar"); exit(1)
}
try png.write(to: URL(fileURLWithPath: arguments[2]))
print("\(arguments[2]): fertig")
+202 -18
View File
@@ -496,16 +496,20 @@
AA0000000000000000000014 /* Debug */ = { AA0000000000000000000014 /* Debug */ = {
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_ALTERNATE_APPICON_NAMES = "AppIcon-App2-Testflight";
ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-App1-Testflight";
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 2;
DEVELOPMENT_ASSET_PATHS = ""; DEVELOPMENT_ASSET_PATHS = "";
DEVELOPMENT_TEAM = V5C6Q86XJR; DEVELOPMENT_TEAM = V5C6Q86XJR;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "Config/VanControl-Info.plist"; INFOPLIST_FILE = "Config/VanControl-Info.plist";
INFOPLIST_KEY_CFBundleDisplayName = "VanControl Pro"; INFOPLIST_KEY_CFBundleDisplayName = "VanControl Pro";
INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
INFOPLIST_KEY_NSBluetoothAlwaysUsageDescription = "Zum Auslesen von Victron-Geraeten und dem Daly BMS per Bluetooth."; INFOPLIST_KEY_NSBluetoothAlwaysUsageDescription = "Zum Auslesen von Victron-Geraeten und dem Daly BMS per Bluetooth.";
INFOPLIST_KEY_NSSupportsLiveActivities = YES; INFOPLIST_KEY_NSSupportsLiveActivities = YES;
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
@@ -516,7 +520,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.0; MARKETING_VERSION = 0.1;
PRODUCT_BUNDLE_IDENTIFIER = de.s0.fototeddy.VanControl; PRODUCT_BUNDLE_IDENTIFIER = de.s0.fototeddy.VanControl;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_EMIT_LOC_STRINGS = YES;
@@ -528,16 +532,20 @@
AA0000000000000000000015 /* Release */ = { AA0000000000000000000015 /* Release */ = {
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_ALTERNATE_APPICON_NAMES = "AppIcon-App2-Testflight";
ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-App1-Testflight";
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 2;
DEVELOPMENT_ASSET_PATHS = ""; DEVELOPMENT_ASSET_PATHS = "";
DEVELOPMENT_TEAM = V5C6Q86XJR; DEVELOPMENT_TEAM = V5C6Q86XJR;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "Config/VanControl-Info.plist"; INFOPLIST_FILE = "Config/VanControl-Info.plist";
INFOPLIST_KEY_CFBundleDisplayName = "VanControl Pro"; INFOPLIST_KEY_CFBundleDisplayName = "VanControl Pro";
INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
INFOPLIST_KEY_NSBluetoothAlwaysUsageDescription = "Zum Auslesen von Victron-Geraeten und dem Daly BMS per Bluetooth."; INFOPLIST_KEY_NSBluetoothAlwaysUsageDescription = "Zum Auslesen von Victron-Geraeten und dem Daly BMS per Bluetooth.";
INFOPLIST_KEY_NSSupportsLiveActivities = YES; INFOPLIST_KEY_NSSupportsLiveActivities = YES;
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
@@ -548,7 +556,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.0; MARKETING_VERSION = 0.1;
PRODUCT_BUNDLE_IDENTIFIER = de.s0.fototeddy.VanControl; PRODUCT_BUNDLE_IDENTIFIER = de.s0.fototeddy.VanControl;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_EMIT_LOC_STRINGS = YES;
@@ -563,7 +571,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 3;
DEVELOPMENT_TEAM = V5C6Q86XJR; DEVELOPMENT_TEAM = V5C6Q86XJR;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
@@ -574,7 +582,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.0; MARKETING_VERSION = 0.1;
PRODUCT_BUNDLE_IDENTIFIER = de.s0.fototeddy.VanControl.watchkitapp; PRODUCT_BUNDLE_IDENTIFIER = de.s0.fototeddy.VanControl.watchkitapp;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = watchos; SDKROOT = watchos;
@@ -595,7 +603,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 3;
DEVELOPMENT_TEAM = V5C6Q86XJR; DEVELOPMENT_TEAM = V5C6Q86XJR;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
@@ -606,7 +614,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.0; MARKETING_VERSION = 0.1;
PRODUCT_BUNDLE_IDENTIFIER = de.s0.fototeddy.VanControl.watchkitapp; PRODUCT_BUNDLE_IDENTIFIER = de.s0.fototeddy.VanControl.watchkitapp;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = watchos; SDKROOT = watchos;
@@ -625,7 +633,7 @@
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
buildSettings = { buildSettings = {
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 2;
DEVELOPMENT_TEAM = V5C6Q86XJR; DEVELOPMENT_TEAM = V5C6Q86XJR;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "Config/VanControlComplication-Info.plist"; INFOPLIST_FILE = "Config/VanControlComplication-Info.plist";
@@ -635,7 +643,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 1.0; MARKETING_VERSION = 0.1;
PRODUCT_BUNDLE_IDENTIFIER = de.s0.fototeddy.VanControl.watchkitapp.levelwidget; PRODUCT_BUNDLE_IDENTIFIER = de.s0.fototeddy.VanControl.watchkitapp.levelwidget;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = watchos; SDKROOT = watchos;
@@ -654,7 +662,7 @@
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
buildSettings = { buildSettings = {
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 2;
DEVELOPMENT_TEAM = V5C6Q86XJR; DEVELOPMENT_TEAM = V5C6Q86XJR;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "Config/VanControlComplication-Info.plist"; INFOPLIST_FILE = "Config/VanControlComplication-Info.plist";
@@ -664,7 +672,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 1.0; MARKETING_VERSION = 0.1;
PRODUCT_BUNDLE_IDENTIFIER = de.s0.fototeddy.VanControl.watchkitapp.levelwidget; PRODUCT_BUNDLE_IDENTIFIER = de.s0.fototeddy.VanControl.watchkitapp.levelwidget;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = watchos; SDKROOT = watchos;
@@ -685,7 +693,7 @@
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 2;
DEVELOPMENT_TEAM = V5C6Q86XJR; DEVELOPMENT_TEAM = V5C6Q86XJR;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = VanControlLiveActivity/Info.plist; INFOPLIST_FILE = VanControlLiveActivity/Info.plist;
@@ -696,7 +704,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 1.0; MARKETING_VERSION = 0.1;
PRODUCT_BUNDLE_IDENTIFIER = de.s0.fototeddy.VanControl.VanControlLiveActivity; PRODUCT_BUNDLE_IDENTIFIER = de.s0.fototeddy.VanControl.VanControlLiveActivity;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES; SKIP_INSTALL = YES;
@@ -714,7 +722,7 @@
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 2;
DEVELOPMENT_TEAM = V5C6Q86XJR; DEVELOPMENT_TEAM = V5C6Q86XJR;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = VanControlLiveActivity/Info.plist; INFOPLIST_FILE = VanControlLiveActivity/Info.plist;
@@ -725,7 +733,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 1.0; MARKETING_VERSION = 0.1;
PRODUCT_BUNDLE_IDENTIFIER = de.s0.fototeddy.VanControl.VanControlLiveActivity; PRODUCT_BUNDLE_IDENTIFIER = de.s0.fototeddy.VanControl.VanControlLiveActivity;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES; SKIP_INSTALL = YES;
@@ -737,6 +745,177 @@
}; };
name = Release; name = Release;
}; };
AA0000000000000000000090 /* CarPlay */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
IPHONEOS_DEPLOYMENT_TARGET = 18.0;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG CARPLAY_DEMO $(inherited)";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
};
name = CarPlay;
};
AA0000000000000000000091 /* CarPlay */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_ALTERNATE_APPICON_NAMES = "AppIcon-App2-Testflight";
ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-App1-Testflight";
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES;
CODE_SIGN_ENTITLEMENTS = Config/VanControlCarPlay.entitlements;
"CODE_SIGN_IDENTITY[sdk=iphonesimulator*]" = "-";
"CODE_SIGN_IDENTITY[sdk=watchsimulator*]" = "-";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 2;
DEVELOPMENT_ASSET_PATHS = "";
DEVELOPMENT_TEAM = P8J6283TT5;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "Config/VanControlCarPlay-Info.plist";
INFOPLIST_KEY_CFBundleDisplayName = "VanControl CarPlay";
INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
INFOPLIST_KEY_NSBluetoothAlwaysUsageDescription = "Zum Auslesen von Victron-Geraeten und dem Daly BMS per Bluetooth.";
INFOPLIST_KEY_NSSupportsLiveActivities = YES;
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = NO;
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 0.1;
PRODUCT_BUNDLE_IDENTIFIER = de.s0.fototeddy.VanControlCarPlay;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG CARPLAY_DEMO $(inherited)";
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = CarPlay;
};
AA0000000000000000000092 /* CarPlay */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
"CODE_SIGN_IDENTITY[sdk=iphonesimulator*]" = "-";
"CODE_SIGN_IDENTITY[sdk=watchsimulator*]" = "-";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 3;
DEVELOPMENT_TEAM = P8J6283TT5;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_CFBundleDisplayName = "VanControl CarPlay";
INFOPLIST_KEY_NSBluetoothAlwaysUsageDescription = "Zum Auslesen des Neigungsmessers direkt an der Uhr, ohne Umweg ueber das iPhone.";
INFOPLIST_KEY_WKCompanionAppBundleIdentifier = de.s0.fototeddy.VanControlCarPlay;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 0.1;
PRODUCT_BUNDLE_IDENTIFIER = de.s0.fototeddy.VanControlCarPlay.watchkitapp;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SDKROOT = watchos;
SKIP_INSTALL = YES;
SUPPORTED_PLATFORMS = "watchos watchsimulator";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 4;
WATCHOS_DEPLOYMENT_TARGET = 10.0;
};
name = CarPlay;
};
AA0000000000000000000093 /* CarPlay */ = {
isa = XCBuildConfiguration;
buildSettings = {
"CODE_SIGN_IDENTITY[sdk=iphonesimulator*]" = "-";
"CODE_SIGN_IDENTITY[sdk=watchsimulator*]" = "-";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 2;
DEVELOPMENT_TEAM = P8J6283TT5;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "Config/VanControlComplication-Info.plist";
INFOPLIST_KEY_CFBundleDisplayName = Nivellierung;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 0.1;
PRODUCT_BUNDLE_IDENTIFIER = de.s0.fototeddy.VanControlCarPlay.watchkitapp.levelwidget;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SDKROOT = watchos;
SKIP_INSTALL = YES;
SUPPORTED_PLATFORMS = "watchos watchsimulator";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 4;
WATCHOS_DEPLOYMENT_TARGET = 10.0;
};
name = CarPlay;
};
AA0000000000000000000094 /* CarPlay */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
"CODE_SIGN_IDENTITY[sdk=iphonesimulator*]" = "-";
"CODE_SIGN_IDENTITY[sdk=watchsimulator*]" = "-";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 2;
DEVELOPMENT_TEAM = P8J6283TT5;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = VanControlLiveActivity/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = "VanControl Live Activity";
IPHONEOS_DEPLOYMENT_TARGET = 18.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 0.1;
PRODUCT_BUNDLE_IDENTIFIER = de.s0.fototeddy.VanControlCarPlay.VanControlLiveActivity;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SKIP_INSTALL = YES;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = CarPlay;
};
/* End XCBuildConfiguration section */ /* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */ /* Begin XCConfigurationList section */
@@ -744,6 +923,7 @@
isa = XCConfigurationList; isa = XCConfigurationList;
buildConfigurations = ( buildConfigurations = (
AA0000000000000000000012 /* Debug */, AA0000000000000000000012 /* Debug */,
AA0000000000000000000090 /* CarPlay */,
AA0000000000000000000013 /* Release */, AA0000000000000000000013 /* Release */,
); );
defaultConfigurationIsVisible = 0; defaultConfigurationIsVisible = 0;
@@ -753,6 +933,7 @@
isa = XCConfigurationList; isa = XCConfigurationList;
buildConfigurations = ( buildConfigurations = (
AA0000000000000000000014 /* Debug */, AA0000000000000000000014 /* Debug */,
AA0000000000000000000091 /* CarPlay */,
AA0000000000000000000015 /* Release */, AA0000000000000000000015 /* Release */,
); );
defaultConfigurationIsVisible = 0; defaultConfigurationIsVisible = 0;
@@ -762,6 +943,7 @@
isa = XCConfigurationList; isa = XCConfigurationList;
buildConfigurations = ( buildConfigurations = (
AA0000000000000000000023 /* Debug */, AA0000000000000000000023 /* Debug */,
AA0000000000000000000092 /* CarPlay */,
AA0000000000000000000024 /* Release */, AA0000000000000000000024 /* Release */,
); );
defaultConfigurationIsVisible = 0; defaultConfigurationIsVisible = 0;
@@ -771,6 +953,7 @@
isa = XCConfigurationList; isa = XCConfigurationList;
buildConfigurations = ( buildConfigurations = (
AA0000000000000000000038 /* Debug */, AA0000000000000000000038 /* Debug */,
AA0000000000000000000093 /* CarPlay */,
AA0000000000000000000039 /* Release */, AA0000000000000000000039 /* Release */,
); );
defaultConfigurationIsVisible = 0; defaultConfigurationIsVisible = 0;
@@ -780,6 +963,7 @@
isa = XCConfigurationList; isa = XCConfigurationList;
buildConfigurations = ( buildConfigurations = (
AA0000000000000000000066 /* Debug */, AA0000000000000000000066 /* Debug */,
AA0000000000000000000094 /* CarPlay */,
AA0000000000000000000067 /* Release */, AA0000000000000000000067 /* Release */,
); );
defaultConfigurationIsVisible = 0; defaultConfigurationIsVisible = 0;
@@ -0,0 +1,90 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "2660"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "AA0000000000000000000006"
BuildableName = "VanControl.app"
BlueprintName = "VanControl"
ReferencedContainer = "container:VanControl.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "CarPlay"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "CarPlay"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "AA0000000000000000000006"
BuildableName = "VanControl.app"
BlueprintName = "VanControl"
ReferencedContainer = "container:VanControl.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
<EnvironmentVariables>
<EnvironmentVariable
key = "CARPLAY_DEMO_PITCH"
value = "1.8"
isEnabled = "NO">
</EnvironmentVariable>
<EnvironmentVariable
key = "CARPLAY_DEMO_ROLL"
value = "-0.9"
isEnabled = "NO">
</EnvironmentVariable>
</EnvironmentVariables>
</LaunchAction>
<ProfileAction
buildConfiguration = "CarPlay"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "AA0000000000000000000006"
BuildableName = "VanControl.app"
BlueprintName = "VanControl"
ReferencedContainer = "container:VanControl.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "CarPlay">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "CarPlay"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

@@ -0,0 +1,11 @@
{
"images" : [
{
"filename" : "AppIcon-App1-Testflight-1024.png",
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
}
],
"info" : { "author" : "xcode", "version" : 1 }
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

@@ -0,0 +1,11 @@
{
"images" : [
{
"filename" : "AppIcon-App1-1024.png",
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
}
],
"info" : { "author" : "xcode", "version" : 1 }
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

@@ -0,0 +1,11 @@
{
"images" : [
{
"filename" : "AppIcon-App2-Testflight-1024.png",
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
}
],
"info" : { "author" : "xcode", "version" : 1 }
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

@@ -0,0 +1,11 @@
{
"images" : [
{
"filename" : "AppIcon-App2-1024.png",
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
}
],
"info" : { "author" : "xcode", "version" : 1 }
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 326 KiB

@@ -0,0 +1,10 @@
{
"images" : [
{
"filename" : "CaliforniaRear.png",
"idiom" : "universal"
}
],
"info" : { "author" : "xcode", "version" : 1 },
"properties" : { "template-rendering-intent" : "template" }
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 341 KiB

After

Width:  |  Height:  |  Size: 311 KiB

@@ -0,0 +1,91 @@
#if CARPLAY_DEMO
import SwiftUI
/// Die Libelle auf dem Fahrzeugdisplay.
///
/// Quer geteilt: links die Blase, rechts in Worten, was zu tun ist. Beim
/// Rangieren wird abwechselnd auf Display und Umfeld geschaut deshalb steht
/// die Aussage in einem Satz da, statt sich aus zwei Zahlen ergeben zu müssen.
///
/// Die Groessen sind auf die 400 × 240 **Punkte** ausgelegt, die CarPlay
/// liefert nicht auf die 800 × 480 Pixel. Das ist die Falle: In Pixeln
/// gedachte Schriftgroessen sind auf dem Fahrzeugdisplay doppelt zu gross und
/// schneiden den Text ab.
struct CarPlayLevelScreen: View {
@Environment(CarPlayLevelSource.self) private var source
private var state: LevelState {
LevelState(pitch: source.pitch, roll: source.roll)
}
var body: some View {
ZStack {
Color.black.ignoresSafeArea()
if CarPlaySpeedGate.shared.isStationary {
levelling
} else {
driving
}
}
.onAppear { CarPlaySpeedGate.shared.start() }
.onDisappear { CarPlaySpeedGate.shared.stop() }
}
private var levelling: some View {
HStack(spacing: 18) {
LevelBubble(pitch: source.pitch, roll: source.roll)
VStack(alignment: .leading, spacing: 10) {
VStack(alignment: .leading, spacing: 2) {
let lines = state.shortInstructions
ForEach(lines.isEmpty ? ["Keine Verbindung"] : lines, id: \.self) { line in
Text(line)
.font(.system(size: 20, weight: .semibold, design: .rounded))
.foregroundStyle(state.isLevel ? .green : .white)
.lineLimit(1)
.minimumScaleFactor(0.8)
}
}
HStack(spacing: 16) {
tile("Längs", LevelDirectionFormatting.pitchTile(source.pitch))
tile("Quer", LevelDirectionFormatting.rollTile(source.roll))
}
if source.isSimulated {
// Ohne diesen Hinweis wäre ein Screenshot aus dem Simulator
// eine Behauptung über Messwerte, die es nicht gab.
Text("Simulierte Werte")
.font(.system(size: 11, weight: .medium))
.foregroundStyle(.orange)
}
}
.frame(maxWidth: .infinity, alignment: .leading)
}
.padding(16)
}
private var driving: some View {
VStack(spacing: 10) {
Image(systemName: "car.fill")
.font(.system(size: 30))
Text("Nivellierung nur im Stand")
.font(.system(size: 20, weight: .semibold, design: .rounded))
}
.foregroundStyle(.white.opacity(0.7))
}
private func tile(_ label: String, _ value: String) -> some View {
VStack(alignment: .leading, spacing: 1) {
Text(label)
.font(.system(size: 11, weight: .medium))
.foregroundStyle(.secondary)
Text(value)
.font(.system(size: 22, weight: .semibold, design: .rounded))
.monospacedDigit()
.foregroundStyle(.white)
}
}
}
#endif
+105
View File
@@ -0,0 +1,105 @@
#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
@@ -0,0 +1,40 @@
import SwiftUI
/// Zeigt die CarPlay-Ansicht im iPhone-Simulator, in den Maßen, die CarPlay
/// tatsächlich liefert (400 × 240 Punkte, also 800 × 480 Pixel).
///
/// Nur zum Beurteilen des Layouts, solange der CarPlay-Simulator selbst nicht
/// zur Verfügung steht. Ohne `CARPLAY_DEMO_PREVIEW` in der Umgebung passiert
/// nichts, und ohne das `CARPLAY_DEMO`-Flag existiert der Code gar nicht.
extension View {
@ViewBuilder
func carPlayPreviewOverlay() -> some View {
#if CARPLAY_DEMO
if ProcessInfo.processInfo.environment["CARPLAY_DEMO_PREVIEW"] != nil {
CarPlayPreviewHost()
} else {
self
}
#else
self
#endif
}
}
#if CARPLAY_DEMO
struct CarPlayPreviewHost: View {
var body: some View {
// Ohne Beiwerk und exakt zentriert: so laesst sich der Screenshot
// verlustfrei auf den CarPlay-Bereich zuschneiden.
VStack {
CarPlayLevelScreen()
.environment(CarPlayLevelSource.shared)
.frame(width: 400, height: 240)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.black)
.ignoresSafeArea()
.onAppear { CarPlayLevelSource.shared.start() }
}
}
#endif
@@ -0,0 +1,96 @@
#if CARPLAY_DEMO
import CarPlay
import OSLog
import SwiftUI
/// Einstiegspunkt der CarPlay-Szene.
///
/// Mit dem Navigations-Entitlement liefert CarPlay ein eigenes `CPWindow`, in
/// das frei gezeichnet werden darf anders als bei den Template-Kategorien,
/// die nur vorgegebene Bausteine erlauben und höchstens alle zehn Sekunden
/// aktualisieren. Genau deshalb hängt hier die Libelle drin: Beim Rangieren
/// muss sie der Bewegung des Fahrzeugs folgen, nicht ihr hinterherlaufen.
///
/// Das `CPMapTemplate` darüber ist nur die Bedienleiste. Es bleibt bewusst
/// leer: Auf dem Stellplatz gibt es nichts zu tippen, es gibt nur etwas
/// abzulesen.
@objc(CarPlaySceneDelegate)
final class CarPlaySceneDelegate: UIResponder, CPTemplateApplicationSceneDelegate, CPMapTemplateDelegate {
private var interfaceController: CPInterfaceController?
/// Ohne eigenes Logging ist beim Debuggen nicht zu unterscheiden, ob die
/// Szene gar nicht verbindet oder ob CarPlay das Template ablehnt.
private static let log = Logger(subsystem: "de.s0.fototeddy.VanControl", category: "CarPlay")
func templateApplicationScene(
_ templateApplicationScene: CPTemplateApplicationScene,
didConnect interfaceController: CPInterfaceController,
to window: CPWindow
) {
Self.log.notice("didConnect: Szene verbunden, Fenster \(NSCoder.string(for: window.bounds.size), privacy: .public)")
self.interfaceController = interfaceController
let root = CarPlayLevelScreen()
.environment(CarPlayLevelSource.shared)
let host = UIHostingController(rootView: root)
host.view.backgroundColor = .black
window.rootViewController = host
window.makeKeyAndVisible()
CarPlayLevelSource.shared.start()
// Bewusst nackt konfiguriert: iOS 26 laesst den CarPlayTemplateUIHost
// beim Aufbau der Navigationsleiste ueber den Share-Button stolpern
// (`_updateShareButtonVisibility` -> unrecognized selector), sobald die
// Leiste erzwungen wird. Ohne Buttons und mit der Standard-Automatik
// gibt es nichts zu konfigurieren und auf dem Stellplatz gibt es
// ohnehin nichts zu tippen.
let map = CPMapTemplate()
map.mapButtons = []
map.leadingNavigationBarButtons = []
map.trailingNavigationBarButtons = []
map.hidesButtonsWithNavigationBar = true
map.mapDelegate = self
interfaceController.setRootTemplate(map, animated: false) { success, error in
if let error {
Self.log.error("setRootTemplate abgelehnt: \(error.localizedDescription, privacy: .public)")
} else {
Self.log.notice("setRootTemplate ok: \(success, privacy: .public)")
}
}
}
func templateApplicationScene(
_ templateApplicationScene: CPTemplateApplicationScene,
didDisconnectInterfaceController interfaceController: CPInterfaceController,
from window: CPWindow
) {
Self.log.notice("didDisconnect: Szene getrennt")
self.interfaceController = nil
window.rootViewController = nil
CarPlayLevelSource.shared.stop()
}
// MARK: - Zielteilung
// CarPlay bietet seit iOS 26 an, ein Navigationsziel an das Fahrzeug zu
// übergeben. Wir haben keine Ziele aber das Protokoll vollständig zu
// bedienen ist Teil davon, eine korrekte Navigations-App zu sein. Der
// Verdacht dabei: Apples Template-Host löst seinen Sharing-Delegate nur
// dann sauber auf, wenn die App diese Methoden anbietet.
@available(iOS 26.4, *)
func mapTemplate(_ mapTemplate: CPMapTemplate, willShareDestinationFor trip: CPTrip) {
Self.log.notice("willShareDestination die App teilt keine Ziele")
}
@available(iOS 26.4, *)
func mapTemplate(_ mapTemplate: CPMapTemplate, didShareDestinationFor trip: CPTrip) {
Self.log.notice("didShareDestination")
}
@available(iOS 26.4, *)
func mapTemplate(_ mapTemplate: CPMapTemplate, didFailToShareDestinationFor trip: CPTrip, error: Error) {
Self.log.error("didFailToShareDestination: \(error.localizedDescription, privacy: .public)")
}
}
#endif
+77
View File
@@ -0,0 +1,77 @@
#if CARPLAY_DEMO
import CoreLocation
import Observation
/// Sperrt die Libelle, sobald das Fahrzeug schneller als Schrittgeschwindigkeit
/// fährt.
///
/// Ausrichten passiert im Stand oder im Zentimeterbereich davor. Eine Anzeige,
/// die während der Fahrt etwas zu lesen gäbe, wäre Ablenkung ohne Nutzen also
/// gibt es sie dort nicht. Die Grenze ist bewusst technisch erzwungen und nicht
/// bloss eine Zusage im Text.
@Observable
final class CarPlaySpeedGate: NSObject, CLLocationManagerDelegate {
static let shared = CarPlaySpeedGate()
/// Fünf km/h: schneller als Rangieren, langsamer als jede Verkehrssituation,
/// in der jemand aufs Display sehen dürfte.
static let limit: CLLocationSpeed = 5.0 / 3.6
private(set) var isStationary = true
private(set) var speed: CLLocationSpeed?
@ObservationIgnored private let manager = CLLocationManager()
private override init() {
super.init()
manager.delegate = self
manager.desiredAccuracy = kCLLocationAccuracyNearestTenMeters
}
func start() {
// Fuer Screenshots laesst sich die Geschwindigkeit vorgeben (in km/h);
// eine simulierte GPS-Fahrt nur fuer ein Standbild waere unverhaeltnis-
// maessig. Ohne die Variable zaehlt ausschliesslich der echte Sensor.
if let forced = ProcessInfo.processInfo.environment["CARPLAY_DEMO_SPEED"].flatMap(Double.init) {
speed = forced / 3.6
isStationary = forced / 3.6 <= Self.limit
return
}
manager.requestWhenInUseAuthorization()
manager.startUpdatingLocation()
}
/// Nur für die Demo-Sequenz: erzwingt den Fahrzustand, damit sich die
/// Sperre zeigen lässt, ohne eine GPS-Fahrt zu simulieren.
func setDemoDriving(_ driving: Bool) {
speed = driving ? 30 / 3.6 : 0
isStationary = !driving
}
func stop() {
manager.stopUpdatingLocation()
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
guard let location = locations.last else { return }
// CoreLocation meldet im Stand eine negative Geschwindigkeit: der Wert
// ist dann schlicht unbekannt. Das als "fährt" zu werten, würde die
// Funktion genau dort abschalten, wo sie gebraucht wird auf dem
// Stellplatz, oft unter Bäumen mit schlechtem Empfang.
guard location.speed >= 0 else {
speed = nil
isStationary = true
return
}
speed = location.speed
isStationary = location.speed <= Self.limit
}
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
speed = nil
isStationary = true
}
}
#endif
+9 -13
View File
@@ -3,21 +3,19 @@ import Foundation
/// Füllt die App mit erfundenen Messwerten, damit sich die Ansichten ohne /// Füllt die App mit erfundenen Messwerten, damit sich die Ansichten ohne
/// Fahrzeug und ohne Bluetooth prüfen lassen. /// Fahrzeug und ohne Bluetooth prüfen lassen.
/// ///
/// Nur in Debug-Builds, auf zwei Wegen einzuschalten: /// Auf zwei Wegen einzuschalten:
/// ///
/// * Umgebungsvariable `CAMPER_DEMO=1` beim Start praktisch im Simulator: /// * Schalter in den Einstellungen (`SettingsView`) in jeder
/// Build-Konfiguration verfügbar, auch in TestFlight- und
/// App-Store-Builds. Der Weg für jeden lokal installierten Build ohne
/// Xcode-Verbindung. Wirkt erst nach einem Neustart der App, weil
/// `DeviceStore` und `BluetoothManager` den Stand nur beim Start lesen.
/// * Umgebungsvariable `CAMPER_DEMO=1` beim Start nur in Debug-Builds,
/// praktisch im Simulator:
/// ///
/// xcrun simctl launch --terminate-running-process \ /// xcrun simctl launch --terminate-running-process \
/// booted de.fritob.VanControl /// booted de.fritob.VanControl
/// # mit: SIMCTL_CHILD_CAMPER_DEMO=1 davor /// # mit: SIMCTL_CHILD_CAMPER_DEMO=1 davor
///
/// * Schalter in den Einstellungen (`SettingsView`) der einzige Weg auf
/// einem lokal installierten Build ohne Xcode-Verbindung, da sich dort
/// keine Umgebungsvariable setzen lässt. Wirkt erst nach einem Neustart der
/// App, weil `DeviceStore` und `BluetoothManager` den Stand nur beim Start
/// lesen.
///
/// Im normalen Betrieb wird hiervon nichts ausgeführt.
enum DemoData { enum DemoData {
/// Schlüssel für den Einstellungen-Schalter, siehe `SettingsView`. /// Schlüssel für den Einstellungen-Schalter, siehe `SettingsView`.
@@ -26,10 +24,8 @@ enum DemoData {
static var isEnabled: Bool { static var isEnabled: Bool {
#if DEBUG #if DEBUG
if ProcessInfo.processInfo.environment["CAMPER_DEMO"] == "1" { return true } if ProcessInfo.processInfo.environment["CAMPER_DEMO"] == "1" { return true }
return UserDefaults.standard.bool(forKey: enabledKey)
#else
return false
#endif #endif
return UserDefaults.standard.bool(forKey: enabledKey)
} }
static let mainProfile = Profile(id: Profile.defaultID, name: "Kastenwagen", static let mainProfile = Profile(id: Profile.defaultID, name: "Kastenwagen",
+1
View File
@@ -27,6 +27,7 @@ struct VanControlApp: App {
.environment(watch) .environment(watch)
.environment(levelActivity) .environment(levelActivity)
.task { watch.activate() } .task { watch.activate() }
.carPlayPreviewOverlay()
} }
.onChange(of: scenePhase) { _, phase in .onChange(of: scenePhase) { _, phase in
// Im Hintergrund darf ohne Service-Filter ohnehin nicht gescannt // Im Hintergrund darf ohne Service-Filter ohnehin nicht gescannt
+5 -3
View File
@@ -75,9 +75,11 @@ struct LevelBubble: View {
Circle() Circle()
.fill(bubbleColor) .fill(bubbleColor)
.frame(width: bubble, height: bubble) .frame(width: bubble, height: bubble)
// Positiver Pitch heisst: das Heck steht höher, die Blase // Draufsicht mit Fahrtrichtung oben: Positiver Pitch heisst,
// wandert also nach oben in der Ansicht nach hinten. // das Heck steht höher die Blase wandert nach hinten, also
.offset(x: offsetX, y: -offsetY) // nach unten. Wie bei einer echten Wasserwaage, die zur
// höheren Seite ausschlägt.
.offset(x: offsetX, y: offsetY)
.animation(.spring(duration: 0.35), value: offsetX) .animation(.spring(duration: 0.35), value: offsetX)
.animation(.spring(duration: 0.35), value: offsetY) .animation(.spring(duration: 0.35), value: offsetY)
.opacity(pitch == nil && roll == nil ? 0.25 : 1) .opacity(pitch == nil && roll == nil ? 0.25 : 1)
-4
View File
@@ -4,16 +4,13 @@ import SwiftUI
/// betrifft, steht bei diesem Gerät. /// betrifft, steht bei diesem Gerät.
struct SettingsView: View { struct SettingsView: View {
@AppStorage(AppSettings.showDiagnosticsKey) private var showDiagnostics = false @AppStorage(AppSettings.showDiagnosticsKey) private var showDiagnostics = false
#if DEBUG
@AppStorage(DemoData.enabledKey) private var demoModeEnabled = false @AppStorage(DemoData.enabledKey) private var demoModeEnabled = false
#endif
@Environment(PhoneWatchLink.self) private var watch @Environment(PhoneWatchLink.self) private var watch
@Environment(\.dismiss) private var dismiss @Environment(\.dismiss) private var dismiss
var body: some View { var body: some View {
NavigationStack { NavigationStack {
List { List {
#if DEBUG
Section { Section {
Toggle("Demo-Modus", isOn: $demoModeEnabled) Toggle("Demo-Modus", isOn: $demoModeEnabled)
} header: { } header: {
@@ -24,7 +21,6 @@ struct SettingsView: View {
+ "einem Neustart der App einmal beenden (im App-Umschalter " + "einem Neustart der App einmal beenden (im App-Umschalter "
+ "nach oben wischen) und wieder öffnen.") + "nach oben wischen) und wieder öffnen.")
} }
#endif
Section { Section {
Toggle("Diagnose anzeigen", isOn: $showDiagnostics) Toggle("Diagnose anzeigen", isOn: $showDiagnostics)
+1
View File
@@ -135,6 +135,7 @@ final class PhoneWatchLink: NSObject {
snapshot: bluetooth.snapshots[device.id], snapshot: bluetooth.snapshots[device.id],
level: device.role == .leveling ? bluetooth.levelStates[device.id] : nil, level: device.role == .leveling ? bluetooth.levelStates[device.id] : nil,
orientation: device.role == .leveling ? device.sensorOrientation : nil, orientation: device.role == .leveling ? device.sensorOrientation : nil,
vehicleGraphicStyle: device.role == .leveling ? device.vehicleGraphicStyle : nil,
fridge: device.role == .fridge ? fridgeState(for: device) : nil) fridge: device.role == .fridge ? fridgeState(for: device) : nil)
} }
return WatchPayload(generatedAt: Date(), return WatchPayload(generatedAt: Date(),
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 326 KiB

@@ -0,0 +1,10 @@
{
"images" : [
{
"filename" : "CaliforniaRear.png",
"idiom" : "universal"
}
],
"info" : { "author" : "xcode", "version" : 1 },
"properties" : { "template-rendering-intent" : "template" }
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 311 KiB

@@ -0,0 +1,10 @@
{
"images" : [
{
"filename" : "CaliforniaSide.png",
"idiom" : "universal"
}
],
"info" : { "author" : "xcode", "version" : 1 },
"properties" : { "template-rendering-intent" : "template" }
}
+4 -3
View File
@@ -30,10 +30,11 @@ struct WatchBubble: View {
Circle() Circle()
.fill(color) .fill(color)
.frame(width: bubble, height: bubble) .frame(width: bubble, height: bubble)
// Positiver Pitch heisst: das Heck steht höher, die Blase // Draufsicht mit Fahrtrichtung oben: Positiver Pitch heisst,
// wandert nach oben in der Ansicht also nach hinten. // das Heck steht höher die Blase wandert nach hinten, also
// nach unten.
.offset(x: clamped(roll) / range * travel, .offset(x: clamped(roll) / range * travel,
y: -clamped(pitch) / range * travel) y: clamped(pitch) / range * travel)
.animation(.spring(duration: 0.3), value: pitch) .animation(.spring(duration: 0.3), value: pitch)
.animation(.spring(duration: 0.3), value: roll) .animation(.spring(duration: 0.3), value: roll)
.opacity(pitch == nil && roll == nil ? 0.25 : 1) .opacity(pitch == nil && roll == nil ? 0.25 : 1)
+2 -1
View File
@@ -50,7 +50,8 @@ struct LevelReadingPage: View {
ScrollView { ScrollView {
VStack(spacing: 8) { VStack(spacing: 8) {
if showsVehicle { if showsVehicle {
WatchVehicleView(pitch: reading.state.pitch, roll: reading.state.roll) WatchVehicleView(pitch: reading.state.pitch, roll: reading.state.roll,
style: link.payload?.levelDevice?.vehicleGraphicStyle ?? .vanster)
} else { } else {
WatchBubble(pitch: reading.state.pitch, roll: reading.state.roll) WatchBubble(pitch: reading.state.pitch, roll: reading.state.roll)
.frame(maxWidth: 110) .frame(maxWidth: 110)
+7 -4
View File
@@ -9,15 +9,18 @@ import SwiftUI
struct WatchVehicleView: View { struct WatchVehicleView: View {
let pitch: Double? let pitch: Double?
let roll: Double? let roll: Double?
/// Vom iPhone übernommen ausgewählt wird sie nur dort, siehe
/// `WatchDevice.vehicleGraphicStyle`.
var style: VehicleGraphicStyle = .vanster
/// Wie am iPhone dreifach überhöht zwei Grad wären sonst kaum zu sehen. /// Wie am iPhone dreifach überhöht zwei Grad wären sonst kaum zu sehen.
/// Die angeschriebenen Gradzahlen bleiben echt. /// Die angeschriebenen Gradzahlen bleiben echt.
var body: some View { var body: some View {
VStack(spacing: 8) { VStack(spacing: 8) {
panel(image: "VansterSide", angle: pitch, title: "Längs", panel(image: style.sideImageName, angle: pitch, title: "Längs",
lower: "Front", upper: "Heck", aspect: VehicleTilt.sideAspect) lower: "Front", upper: "Heck", aspect: style.sideAspect)
panel(image: "VansterRear", angle: roll, title: "Quer", panel(image: style.rearImageName, angle: roll, title: "Quer",
lower: "links", upper: "rechts", aspect: VehicleTilt.rearAspect) lower: "links", upper: "rechts", aspect: style.rearAspect)
} }
} }
+5 -3
View File
@@ -63,15 +63,17 @@ enum WatchDemo {
devices: [ devices: [
WatchDevice(id: levelID, name: "Nivellierung", role: .leveling, WatchDevice(id: levelID, name: "Nivellierung", role: .leveling,
link: .live, snapshot: level.snapshot(deviceID: levelID, rssi: -70), link: .live, snapshot: level.snapshot(deviceID: levelID, rssi: -70),
level: level, orientation: .identity, fridge: nil), level: level, orientation: .identity,
vehicleGraphicStyle: .vanster, fridge: nil),
WatchDevice(id: batteryID, name: "Bulltron 200 Ah", role: .bms, WatchDevice(id: batteryID, name: "Bulltron 200 Ah", role: .bms,
link: .live, snapshot: battery, level: nil, link: .live, snapshot: battery, level: nil,
orientation: nil, fridge: nil), orientation: nil, vehicleGraphicStyle: nil, fridge: nil),
WatchDevice(id: victronSolarID, name: "Solar Dach", role: .victronSolarCharger, WatchDevice(id: victronSolarID, name: "Solar Dach", role: .victronSolarCharger,
link: .live, snapshot: victronSolar, level: nil, link: .live, snapshot: victronSolar, level: nil,
orientation: nil, fridge: nil), orientation: nil, vehicleGraphicStyle: nil, fridge: nil),
WatchDevice(id: fridgeID, name: "Kühlbox", role: .fridge, WatchDevice(id: fridgeID, name: "Kühlbox", role: .fridge,
link: .live, snapshot: fridge, level: nil, orientation: nil, link: .live, snapshot: fridge, level: nil, orientation: nil,
vehicleGraphicStyle: nil,
fridge: WatchFridge(isLive: true, updated: now, fridge: WatchFridge(isLive: true, updated: now,
isPoweredOn: true, isEco: true, isLocked: false, isPoweredOn: true, isEco: true, isLocked: false,
isDualZone: false, unitSymbol: "°C", isDualZone: false, unitSymbol: "°C",