From 0e05ca22d71603c1bf8dad986670e0033dd2a7c9 Mon Sep 17 00:00:00 2001 From: BiasF Date: Mon, 31 Aug 2026 21:10:54 +0200 Subject: [PATCH] =?UTF-8?q?Komplikation=20f=C3=BCrs=20Zifferblatt,=20und?= =?UTF-8?q?=20die=20App=20heisst=20jetzt=20VanControl=20Pro?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Zwei Dinge in einem Commit, weil sie sich dieselben Dateien teilen: project.pbxproj und WatchRootView.swift tragen beides. **Komplikation.** Neues Target CamperMonitorComplication, auf watchOS sind Komplikationen WidgetKit-Widgets. Es wird in die Watch-App eingebettet (PlugIns) und bietet die vier Zubehör-Familien an. Ein Tipp öffnet die App direkt auf der Nivellierung statt in der Geräteliste: Die Komplikation hängt eine Adresse an, die Watch-App führt dafür jetzt einen Navigationsweg, den sie von aussen setzen kann. Die Adresse steht in Shared/WatchDeepLink.swift, weil beide Seiten sie brauchen – ein Tippfehler auf einer Seite bliebe sonst stumm. Bewusst ohne Messwert: Eine Komplikation läuft in einem eigenen Prozess und käme an die Werte nur über eine App-Gruppe, und die verlangt eine bezahlte Mitgliedschaft. Ein Wert, der beim Blick aufs Zifferblatt Stunden alt sein kann, wäre ohnehin schlimmer als keiner – man glaubt ihm. Die Komplikation tut deshalb genau eine Sache, und die zuverlässig. **Rebranding.** Der sichtbare Name ist überall VanControl Pro: CFBundleDisplayName beider Apple-Targets, Android-Label, die Texte in beiden Apps, beide READMEs und der Name der APK im Gitea-Ablauf. Die Kennungen bleiben, wie sie sind – Bundle-IDs, Android-Paket, Target-, Ordner- und Schemanamen. Eine geänderte Bundle-ID ist für iOS eine neue App: Geräte, Fahrzeugprofile und Victron-Schlüssel wären weg, dazu neue Profile und eine neu zu koppelnde Watch-App. Sichtbar ist davon nichts, also gibt es dafür auch keinen Grund. Geprüft: alle drei Apple-Targets bauen, die Erweiterung liegt im Watch-Bundle unter PlugIns, die Anzeigenamen stimmen im gebauten Bundle, Tests grün. Die neue Bundle-ID der Erweiterung braucht beim ersten Lauf aus Xcode ein Profil, das Xcode selbst anlegt. Co-Authored-By: Claude Opus 5 --- .gitea/workflows/release.yml | 8 +- .../de/fritob/campermonitor/ui/Permissions.kt | 2 +- .../fritob/campermonitor/ui/SettingsScreen.kt | 2 +- Android/app/src/main/res/values/strings.xml | 2 +- CamperMonitor.xcodeproj/project.pbxproj | 157 +++++++++++++++++- .../LevelComplication.swift | 92 ++++++++++ CamperMonitorWatch/CamperWatchApp.swift | 2 +- CamperMonitorWatch/Views/WatchLevelView.swift | 4 +- CamperMonitorWatch/Views/WatchRootView.swift | 38 ++++- Config/CamperMonitorComplication-Info.plist | 11 ++ README.md | 6 +- Shared/WatchDeepLink.swift | 19 +++ firmware/vanalign/README.md | 6 +- 13 files changed, 320 insertions(+), 29 deletions(-) create mode 100644 CamperMonitorComplication/LevelComplication.swift create mode 100644 Config/CamperMonitorComplication-Info.plist create mode 100644 Shared/WatchDeepLink.swift diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml index 371d033..3e2775d 100644 --- a/.gitea/workflows/release.yml +++ b/.gitea/workflows/release.yml @@ -59,7 +59,7 @@ jobs: -PversionName="$VERSION" \ -PversionCode="$CODE" --no-daemon mv app/build/outputs/apk/release/app-release.apk \ - "../CamperMonitor-$VERSION.apk" + "../VanControlPro-$VERSION.apk" - name: An das Release hängen env: @@ -88,7 +88,7 @@ jobs: fi curl -sf -X POST -H "$AUTH" \ - -F "attachment=@CamperMonitor-$VERSION.apk" \ - "$API/$ID/assets?name=CamperMonitor-$VERSION.apk" > /dev/null + -F "attachment=@VanControlPro-$VERSION.apk" \ + "$API/$ID/assets?name=VanControlPro-$VERSION.apk" > /dev/null - echo "CamperMonitor-$VERSION.apk hängt an Release $GITHUB_REF_NAME." + echo "VanControlPro-$VERSION.apk hängt an Release $GITHUB_REF_NAME." diff --git a/Android/app/src/main/kotlin/de/fritob/campermonitor/ui/Permissions.kt b/Android/app/src/main/kotlin/de/fritob/campermonitor/ui/Permissions.kt index 51776fd..769a143 100644 --- a/Android/app/src/main/kotlin/de/fritob/campermonitor/ui/Permissions.kt +++ b/Android/app/src/main/kotlin/de/fritob/campermonitor/ui/Permissions.kt @@ -89,7 +89,7 @@ fun WithBluetoothPermission(content: @Composable () -> Unit) { if (asked) { Text( "Die Freigabe wurde abgelehnt. Sie lässt sich in den " + - "Android-Einstellungen unter „Apps → Camper Monitor → " + + "Android-Einstellungen unter „Apps → VanControl Pro → " + "Berechtigungen“ nachholen.", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.error, diff --git a/Android/app/src/main/kotlin/de/fritob/campermonitor/ui/SettingsScreen.kt b/Android/app/src/main/kotlin/de/fritob/campermonitor/ui/SettingsScreen.kt index 36c2740..4fe0d05 100644 --- a/Android/app/src/main/kotlin/de/fritob/campermonitor/ui/SettingsScreen.kt +++ b/Android/app/src/main/kotlin/de/fritob/campermonitor/ui/SettingsScreen.kt @@ -65,7 +65,7 @@ fun SettingsScreen(store: DeviceStore, onBack: () -> Unit) { ) Text( - "Camper Monitor liest Victron-Geräte über ihr Advertisement mit " + + "VanControl Pro liest Victron-Geräte über ihr Advertisement mit " + "(„Instant Readout“) und spricht Batterie, Kühlbox und " + "Neigungsmesser direkt über Bluetooth an.", style = MaterialTheme.typography.bodySmall, diff --git a/Android/app/src/main/res/values/strings.xml b/Android/app/src/main/res/values/strings.xml index 38352cf..0ade3f2 100644 --- a/Android/app/src/main/res/values/strings.xml +++ b/Android/app/src/main/res/values/strings.xml @@ -1,4 +1,4 @@ - Camper Monitor + VanControl Pro diff --git a/CamperMonitor.xcodeproj/project.pbxproj b/CamperMonitor.xcodeproj/project.pbxproj index 004c516..cc2390e 100644 --- a/CamperMonitor.xcodeproj/project.pbxproj +++ b/CamperMonitor.xcodeproj/project.pbxproj @@ -8,6 +8,7 @@ /* Begin PBXBuildFile section */ AA0000000000000000000027 /* CamperMonitorWatch.app in Embed Watch Content */ = {isa = PBXBuildFile; fileRef = AA0000000000000000000016 /* CamperMonitorWatch.app */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + AA0000000000000000000042 /* CamperMonitorComplication.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = AA0000000000000000000032 /* CamperMonitorComplication.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -18,6 +19,13 @@ remoteGlobalIDString = AA0000000000000000000022; remoteInfo = CamperMonitorWatch; }; + AA0000000000000000000043 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = AA0000000000000000000009 /* Project object */; + proxyType = 1; + remoteGlobalIDString = AA0000000000000000000037; + remoteInfo = CamperMonitorComplication; + }; /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -32,12 +40,25 @@ name = "Embed Watch Content"; runOnlyForDeploymentPostprocessing = 0; }; + AA0000000000000000000041 /* Embed Foundation Extensions */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 13; + files = ( + AA0000000000000000000042 /* CamperMonitorComplication.appex in Embed Foundation Extensions */, + ); + name = "Embed Foundation Extensions"; + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ AA0000000000000000000001 /* CamperMonitor.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = CamperMonitor.app; sourceTree = BUILT_PRODUCTS_DIR; }; AA0000000000000000000016 /* CamperMonitorWatch.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = CamperMonitorWatch.app; sourceTree = BUILT_PRODUCTS_DIR; }; AA0000000000000000000030 /* CamperMonitor-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "CamperMonitor-Info.plist"; sourceTree = ""; }; + AA0000000000000000000045 /* CamperMonitorComplication-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "CamperMonitorComplication-Info.plist"; sourceTree = ""; }; + AA0000000000000000000032 /* CamperMonitorComplication.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = CamperMonitorComplication.appex; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ /* Begin PBXFileSystemSynchronizedRootGroup section */ @@ -51,6 +72,11 @@ path = CamperMonitorWatch; sourceTree = ""; }; + AA0000000000000000000033 /* CamperMonitorComplication */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = CamperMonitorComplication; + sourceTree = ""; + }; AA0000000000000000000018 /* Shared */ = { isa = PBXFileSystemSynchronizedRootGroup; path = Shared; @@ -73,6 +99,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + AA0000000000000000000035 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ @@ -81,6 +114,7 @@ children = ( AA0000000000000000000002 /* CamperMonitor */, AA0000000000000000000017 /* CamperMonitorWatch */, + AA0000000000000000000033 /* CamperMonitorComplication */, AA0000000000000000000018 /* Shared */, AA0000000000000000000031 /* Config */, AA0000000000000000000005 /* Products */, @@ -92,6 +126,7 @@ children = ( AA0000000000000000000001 /* CamperMonitor.app */, AA0000000000000000000016 /* CamperMonitorWatch.app */, + AA0000000000000000000032 /* CamperMonitorComplication.appex */, ); name = Products; sourceTree = ""; @@ -100,6 +135,7 @@ isa = PBXGroup; children = ( AA0000000000000000000030 /* CamperMonitor-Info.plist */, + AA0000000000000000000045 /* CamperMonitorComplication-Info.plist */, ); path = Config; sourceTree = ""; @@ -137,10 +173,12 @@ AA0000000000000000000019 /* Sources */, AA0000000000000000000020 /* Frameworks */, AA0000000000000000000021 /* Resources */, + AA0000000000000000000041 /* Embed Foundation Extensions */, ); buildRules = ( ); dependencies = ( + AA0000000000000000000044 /* PBXTargetDependency */, ); fileSystemSynchronizedGroups = ( AA0000000000000000000017 /* CamperMonitorWatch */, @@ -151,6 +189,27 @@ productReference = AA0000000000000000000016 /* CamperMonitorWatch.app */; productType = "com.apple.product-type.application"; }; + AA0000000000000000000037 /* CamperMonitorComplication */ = { + isa = PBXNativeTarget; + buildConfigurationList = AA0000000000000000000040 /* Build configuration list for PBXNativeTarget "CamperMonitorComplication" */; + buildPhases = ( + AA0000000000000000000034 /* Sources */, + AA0000000000000000000035 /* Frameworks */, + AA0000000000000000000036 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + fileSystemSynchronizedGroups = ( + AA0000000000000000000033 /* CamperMonitorComplication */, + AA0000000000000000000018 /* Shared */, + ); + name = CamperMonitorComplication; + productName = CamperMonitorComplication; + productReference = AA0000000000000000000032 /* CamperMonitorComplication.appex */; + productType = "com.apple.product-type.app-extension"; + }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ @@ -167,6 +226,9 @@ AA0000000000000000000022 = { CreatedOnToolsVersion = 26.6; }; + AA0000000000000000000037 = { + CreatedOnToolsVersion = 26.6; + }; }; }; buildConfigurationList = AA0000000000000000000010 /* Build configuration list for PBXProject "CamperMonitor" */; @@ -186,6 +248,7 @@ targets = ( AA0000000000000000000006 /* CamperMonitor */, AA0000000000000000000022 /* CamperMonitorWatch */, + AA0000000000000000000037 /* CamperMonitorComplication */, ); }; /* End PBXProject section */ @@ -205,6 +268,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + AA0000000000000000000036 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -222,6 +292,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + AA0000000000000000000034 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ @@ -231,6 +308,11 @@ target = AA0000000000000000000022 /* CamperMonitorWatch */; targetProxy = AA0000000000000000000028 /* PBXContainerItemProxy */; }; + AA0000000000000000000044 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = AA0000000000000000000037 /* CamperMonitorComplication */; + targetProxy = AA0000000000000000000043 /* PBXContainerItemProxy */; + }; /* End PBXTargetDependency section */ /* Begin XCBuildConfiguration section */ @@ -301,7 +383,7 @@ ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = "Config/CamperMonitor-Info.plist"; - INFOPLIST_KEY_CFBundleDisplayName = "Camper Monitor"; + INFOPLIST_KEY_CFBundleDisplayName = "VanControl Pro"; INFOPLIST_KEY_NSBluetoothAlwaysUsageDescription = "Zum Auslesen von Victron-Geraeten und dem Daly BMS per Bluetooth."; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES; @@ -332,7 +414,7 @@ ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = "Config/CamperMonitor-Info.plist"; - INFOPLIST_KEY_CFBundleDisplayName = "Camper Monitor"; + INFOPLIST_KEY_CFBundleDisplayName = "VanControl Pro"; INFOPLIST_KEY_NSBluetoothAlwaysUsageDescription = "Zum Auslesen von Victron-Geraeten und dem Daly BMS per Bluetooth."; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES; @@ -361,7 +443,7 @@ DEVELOPMENT_TEAM = 6AP73NYV5W; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_KEY_CFBundleDisplayName = "Camper Monitor"; + INFOPLIST_KEY_CFBundleDisplayName = "VanControl Pro"; INFOPLIST_KEY_NSBluetoothAlwaysUsageDescription = "Zum Auslesen des Neigungsmessers direkt an der Uhr, ohne Umweg ueber das iPhone."; INFOPLIST_KEY_WKCompanionAppBundleIdentifier = de.fritob.CamperMonitor; LD_RUNPATH_SEARCH_PATHS = ( @@ -393,7 +475,7 @@ DEVELOPMENT_TEAM = 6AP73NYV5W; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_KEY_CFBundleDisplayName = "Camper Monitor"; + INFOPLIST_KEY_CFBundleDisplayName = "VanControl Pro"; INFOPLIST_KEY_NSBluetoothAlwaysUsageDescription = "Zum Auslesen des Neigungsmessers direkt an der Uhr, ohne Umweg ueber das iPhone."; INFOPLIST_KEY_WKCompanionAppBundleIdentifier = de.fritob.CamperMonitor; LD_RUNPATH_SEARCH_PATHS = ( @@ -415,6 +497,64 @@ }; name = Release; }; + AA0000000000000000000038 = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 6AP73NYV5W; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = "Config/CamperMonitorComplication-Info.plist"; + INFOPLIST_KEY_CFBundleDisplayName = "Nivellierung"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = de.fritob.CamperMonitor.watchkitapp.complication; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = watchos; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "watchos watchsimulator"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 4; + WATCHOS_DEPLOYMENT_TARGET = 10.0; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + AA0000000000000000000039 = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 6AP73NYV5W; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = "Config/CamperMonitorComplication-Info.plist"; + INFOPLIST_KEY_CFBundleDisplayName = "Nivellierung"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = de.fritob.CamperMonitor.watchkitapp.complication; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = watchos; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "watchos watchsimulator"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 4; + WATCHOS_DEPLOYMENT_TARGET = 10.0; + SWIFT_COMPILATION_MODE = wholemodule; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ @@ -445,6 +585,15 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + AA0000000000000000000040 /* Build configuration list for PBXNativeTarget "CamperMonitorComplication" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + AA0000000000000000000038 /* Debug */, + AA0000000000000000000039 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; /* End XCConfigurationList section */ }; rootObject = AA0000000000000000000009 /* Project object */; diff --git a/CamperMonitorComplication/LevelComplication.swift b/CamperMonitorComplication/LevelComplication.swift new file mode 100644 index 0000000..bf1bebd --- /dev/null +++ b/CamperMonitorComplication/LevelComplication.swift @@ -0,0 +1,92 @@ +import SwiftUI +import WidgetKit + +/// Komplikation fürs Zifferblatt: ein Tipp, und die Nivellierung steht da. +/// +/// Bewusst **ohne Messwert**. Eine Komplikation läuft in einem eigenen Prozess +/// und käme an die Werte nur über eine App-Gruppe – die verlangt eine bezahlte +/// Entwicklermitgliedschaft. Ein Wert, der beim Blick aufs Zifferblatt +/// womöglich Stunden alt ist, wäre ausserdem schlimmer als keiner: Man würde +/// ihm glauben. +/// +/// Die Komplikation tut deshalb genau eine Sache, und die zuverlässig: Sie +/// öffnet die App auf der Nivellierung. Von dort sind die Werte live, weil die +/// Uhr den Sensor selbst anfunkt. +struct LevelComplicationEntry: TimelineEntry { + let date: Date +} + +/// Nichts zu planen: Die Anzeige ändert sich nie, also ein einziger Eintrag +/// ohne Nachschub. Das kostet die Uhr auch nichts. +struct LevelComplicationProvider: TimelineProvider { + func placeholder(in context: Context) -> LevelComplicationEntry { + LevelComplicationEntry(date: .now) + } + + func getSnapshot(in context: Context, + completion: @escaping (LevelComplicationEntry) -> Void) { + completion(LevelComplicationEntry(date: .now)) + } + + func getTimeline(in context: Context, + completion: @escaping (Timeline) -> Void) { + completion(Timeline(entries: [LevelComplicationEntry(date: .now)], policy: .never)) + } +} + +struct LevelComplicationView: View { + @Environment(\.widgetFamily) private var family + + var body: some View { + switch family { + case .accessoryInline: + // Eine Zeile Text, mehr lässt diese Familie nicht zu. + Label("Nivellierung", systemImage: "level") + + case .accessoryCorner: + Image(systemName: "level") + .font(.title2) + .widgetLabel("Nivellieren") + + case .accessoryRectangular: + HStack(spacing: 6) { + Image(systemName: "level") + .font(.title3) + VStack(alignment: .leading, spacing: 0) { + Text("Nivellierung") + .font(.headline) + Text("Antippen zum Ausrichten") + .font(.caption2) + .foregroundStyle(.secondary) + } + } + + default: + // Rund – auf dem Zifferblatt der knappste Platz, also nur das + // Symbol, gross genug zum Treffen. + ZStack { + AccessoryWidgetBackground() + Image(systemName: "level") + .font(.title2) + } + } + } +} + +@main +struct LevelComplication: Widget { + var body: some WidgetConfiguration { + StaticConfiguration(kind: "de.fritob.CamperMonitor.complication.level", + provider: LevelComplicationProvider()) { _ in + LevelComplicationView() + // Der Tipp landet nicht irgendwo in der App, sondern auf der + // Nivellierung – dafür wertet die App diese Adresse aus. + .widgetURL(WatchDeepLink.level) + .containerBackground(.clear, for: .widget) + } + .configurationDisplayName("Nivellierung") + .description("Öffnet die Nivellierung direkt vom Zifferblatt.") + .supportedFamilies([.accessoryCircular, .accessoryCorner, + .accessoryInline, .accessoryRectangular]) + } +} diff --git a/CamperMonitorWatch/CamperWatchApp.swift b/CamperMonitorWatch/CamperWatchApp.swift index 678e366..fa69cec 100644 --- a/CamperMonitorWatch/CamperWatchApp.swift +++ b/CamperMonitorWatch/CamperWatchApp.swift @@ -1,6 +1,6 @@ import SwiftUI -/// Camper Monitor am Handgelenk. +/// VanControl Pro am Handgelenk. /// /// Zwei Quellen, jede dort, wo sie hingehört: /// diff --git a/CamperMonitorWatch/Views/WatchLevelView.swift b/CamperMonitorWatch/Views/WatchLevelView.swift index 09f2c76..0061ed3 100644 --- a/CamperMonitorWatch/Views/WatchLevelView.swift +++ b/CamperMonitorWatch/Views/WatchLevelView.swift @@ -41,9 +41,7 @@ struct WatchLevelView: View { .padding(.top, 2) } - NavigationLink { - WatchAlignView() - } label: { + NavigationLink(value: WatchRoute.align) { Label("Ausrichten", systemImage: "arrow.left.and.right") } .buttonStyle(.borderedProminent) diff --git a/CamperMonitorWatch/Views/WatchRootView.swift b/CamperMonitorWatch/Views/WatchRootView.swift index 1bf0df3..18b9f6b 100644 --- a/CamperMonitorWatch/Views/WatchRootView.swift +++ b/CamperMonitorWatch/Views/WatchRootView.swift @@ -6,10 +6,28 @@ struct WatchRootView: View { @Environment(PhoneLink.self) private var link @Environment(WatchLevelRadio.self) private var radio + /// Der Weg durch die Ansichten liegt hier, damit ihn auch die Komplikation + /// setzen kann: Ein Tipp aufs Zifferblatt springt direkt zur Nivellierung, + /// statt in der Geräteliste zu landen. + @State private var path: [WatchRoute] = [] + var body: some View { - NavigationStack { + NavigationStack(path: $path) { content .navigationTitle(link.payload?.profile.name ?? "Camper") + .navigationDestination(for: WatchRoute.self) { route in + switch route { + case .level: WatchLevelView() + case .align: WatchAlignView() + case .device(let id): WatchDeviceDetailView(deviceID: id) + } + } + } + .onOpenURL { url in + guard WatchDeepLink.isLevel(url) else { return } + // Nicht anhängen, sondern ersetzen: Wer vom Zifferblatt kommt, will + // die Nivellierung sehen und nicht einen Stapel von vorhin. + path = [.level] } } @@ -18,9 +36,7 @@ struct WatchRootView: View { // Die Nivellierung steht immer oben und immer da: Sie kommt vom // Sensor selbst und braucht das iPhone nicht. Section { - NavigationLink { - WatchLevelView() - } label: { + NavigationLink(value: WatchRoute.level) { LevelSummaryRow(reading: WatchLevelReading(radio: radio, phone: link)) } } @@ -28,9 +44,7 @@ struct WatchRootView: View { if let payload = link.payload { Section { ForEach(payload.devices.filter { $0.role != .leveling }) { device in - NavigationLink { - WatchDeviceDetailView(deviceID: device.id) - } label: { + NavigationLink(value: WatchRoute.device(device.id)) { DeviceRow(device: device) } } @@ -55,7 +69,7 @@ struct WatchRootView: View { VStack(spacing: 4) { Label("Warte auf das iPhone", systemImage: "iphone.gen3.radiowaves.left.and.right") .font(.footnote.weight(.medium)) - Text("Dort muss Camper Monitor geöffnet sein.") + Text("Dort muss VanControl Pro geöffnet sein.") .font(.caption2) .foregroundStyle(.secondary) } @@ -63,6 +77,14 @@ struct WatchRootView: View { } } +/// Wohin die Uhr springen kann. Als eigener Typ, weil der Weg von aussen +/// gesetzt wird – von der Komplikation. +enum WatchRoute: Hashable { + case level + case align + case device(UUID) +} + /// Eine Gerätezeile: Name klein, Hauptwert gross – auf dem kleinen Bildschirm /// zählt nur der eine Wert, wegen dem man den Arm hebt. struct DeviceRow: View { diff --git a/Config/CamperMonitorComplication-Info.plist b/Config/CamperMonitorComplication-Info.plist new file mode 100644 index 0000000..0f118fb --- /dev/null +++ b/Config/CamperMonitorComplication-Info.plist @@ -0,0 +1,11 @@ + + + + + NSExtension + + NSExtensionPointIdentifier + com.apple.widgetkit-extension + + + diff --git a/README.md b/README.md index 08703b1..c4b56d3 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Camper Monitor +# VanControl Pro iOS-App, die per Bluetooth LE die Energieanlage im Wohnmobil ausliest: @@ -120,7 +120,7 @@ Protokoll erkannt wurde, steht in der Detailansicht unter **Diagnose** – zusammen mit der letzten Rohantwort. Nur **eine** App gleichzeitig kann mit dem BMS verbunden sein. Wenn die -Hersteller-App offen ist, bekommt Camper Monitor keine Verbindung. +Hersteller-App offen ist, bekommt VanControl Pro keine Verbindung. ### Kühlbox @@ -305,7 +305,7 @@ auch dort nur, was das Gerät zurückmeldet, nicht der Tastendruck. ### Was das für den Betrieb heisst -Für Batterie, Solar und Kühlbox muss Camper Monitor auf dem iPhone laufen. +Für Batterie, Solar und Kühlbox muss VanControl Pro auf dem iPhone laufen. Solange die Uhr meldet, dass jemand hinschaut, hält die App das Funkgerät auch im Hintergrund am Leben; die Victron-Werbedaten stehen dabei still, weil iOS im Hintergrund kein ungefiltertes Suchen erlaubt. diff --git a/Shared/WatchDeepLink.swift b/Shared/WatchDeepLink.swift new file mode 100644 index 0000000..0a4ceea --- /dev/null +++ b/Shared/WatchDeepLink.swift @@ -0,0 +1,19 @@ +import Foundation + +/// Die Adressen, über die etwas in der Watch-App geöffnet wird. +/// +/// Sie stehen hier, weil beide Seiten sie brauchen und sie sonst zweimal +/// dastünden: Die Komplikation hängt sie an ihre Anzeige, die App wertet sie +/// beim Öffnen aus. Ein Tippfehler auf einer Seite wäre stumm – der Tipp aufs +/// Zifferblatt öffnete dann einfach die Geräteliste. +enum WatchDeepLink { + private static let scheme = "campermonitor" + + /// Führt direkt auf die Nivellierung. + static let level = URL(string: "\(scheme)://nivellierung")! + + /// Ob diese Adresse zur Nivellierung führt. + static func isLevel(_ url: URL) -> Bool { + url.scheme == scheme && (url.host() == "nivellierung") + } +} diff --git a/firmware/vanalign/README.md b/firmware/vanalign/README.md index fcbba15..0ac375b 100644 --- a/firmware/vanalign/README.md +++ b/firmware/vanalign/README.md @@ -26,7 +26,7 @@ darüber auffindbar). | Roll | `…3425` | lesen | Querneigung, Float32 little-endian, Grad | | Kalibrieren | `…3427` | schreiben | ein Byte: `0` setzt zurück, alles andere kalibriert | -Der Client fragt Pitch und Roll im Takt ab; Camper Monitor tut das zweimal je +Der Client fragt Pitch und Roll im Takt ab; VanControl Pro tut das zweimal je Sekunde. Positiver Pitch heißt: das Heck steht höher. Positiver Roll: die rechte Seite @@ -35,7 +35,7 @@ steht höher. ### Kalibrierung Fahrzeug eben stellen, dann ein beliebiges Byte ungleich `0` auf `…3427` -schreiben – oder in Camper Monitor auf *Auf aktuelle Lage kalibrieren* tippen. +schreiben – oder in VanControl Pro auf *Auf aktuelle Lage kalibrieren* tippen. Die Offsets werden dauerhaft gespeichert und überstehen einen Neustart. ## Was sich gegenüber 1.0 geändert hat @@ -70,7 +70,7 @@ Die UUIDs sind unverändert, die vorhandene WebApp läuft weiter. ## Clients -* **Camper Monitor** (iOS) – die App in diesem Projekt. Geräteart +* **VanControl Pro** (iOS) – die App in diesem Projekt. Geräteart *Nivellierung*: grafische Libelle, Kalibrierung aus der App, zusammen mit den übrigen Geräten im Fahrzeug. * Die ursprüngliche WebBLE-Oberfläche liegt weiterhin im Projekt