Komplikation fürs Zifferblatt, und die App heisst jetzt VanControl Pro

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 <noreply@anthropic.com>
This commit is contained in:
BiasF
2026-08-31 21:10:54 +02:00
co-authored by Claude Opus 5
parent f0df433421
commit 0e05ca22d7
13 changed files with 320 additions and 29 deletions
+4 -4
View File
@@ -59,7 +59,7 @@ jobs:
-PversionName="$VERSION" \ -PversionName="$VERSION" \
-PversionCode="$CODE" --no-daemon -PversionCode="$CODE" --no-daemon
mv app/build/outputs/apk/release/app-release.apk \ mv app/build/outputs/apk/release/app-release.apk \
"../CamperMonitor-$VERSION.apk" "../VanControlPro-$VERSION.apk"
- name: An das Release hängen - name: An das Release hängen
env: env:
@@ -88,7 +88,7 @@ jobs:
fi fi
curl -sf -X POST -H "$AUTH" \ curl -sf -X POST -H "$AUTH" \
-F "attachment=@CamperMonitor-$VERSION.apk" \ -F "attachment=@VanControlPro-$VERSION.apk" \
"$API/$ID/assets?name=CamperMonitor-$VERSION.apk" > /dev/null "$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."
@@ -89,7 +89,7 @@ fun WithBluetoothPermission(content: @Composable () -> Unit) {
if (asked) { if (asked) {
Text( Text(
"Die Freigabe wurde abgelehnt. Sie lässt sich in den " + "Die Freigabe wurde abgelehnt. Sie lässt sich in den " +
"Android-Einstellungen unter „Apps → Camper Monitor" + "Android-Einstellungen unter „Apps → VanControl Pro" +
"Berechtigungen“ nachholen.", "Berechtigungen“ nachholen.",
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.error, color = MaterialTheme.colorScheme.error,
@@ -65,7 +65,7 @@ fun SettingsScreen(store: DeviceStore, onBack: () -> Unit) {
) )
Text( 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 " + "(„Instant Readout“) und spricht Batterie, Kühlbox und " +
"Neigungsmesser direkt über Bluetooth an.", "Neigungsmesser direkt über Bluetooth an.",
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
+1 -1
View File
@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<string name="app_name">Camper Monitor</string> <string name="app_name">VanControl Pro</string>
</resources> </resources>
+153 -4
View File
@@ -8,6 +8,7 @@
/* Begin PBXBuildFile section */ /* Begin PBXBuildFile section */
AA0000000000000000000027 /* CamperMonitorWatch.app in Embed Watch Content */ = {isa = PBXBuildFile; fileRef = AA0000000000000000000016 /* CamperMonitorWatch.app */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 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 */ /* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */ /* Begin PBXContainerItemProxy section */
@@ -18,6 +19,13 @@
remoteGlobalIDString = AA0000000000000000000022; remoteGlobalIDString = AA0000000000000000000022;
remoteInfo = CamperMonitorWatch; remoteInfo = CamperMonitorWatch;
}; };
AA0000000000000000000043 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = AA0000000000000000000009 /* Project object */;
proxyType = 1;
remoteGlobalIDString = AA0000000000000000000037;
remoteInfo = CamperMonitorComplication;
};
/* End PBXContainerItemProxy section */ /* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */ /* Begin PBXCopyFilesBuildPhase section */
@@ -32,12 +40,25 @@
name = "Embed Watch Content"; name = "Embed Watch Content";
runOnlyForDeploymentPostprocessing = 0; 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 */ /* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */ /* Begin PBXFileReference section */
AA0000000000000000000001 /* CamperMonitor.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = CamperMonitor.app; sourceTree = BUILT_PRODUCTS_DIR; }; 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; }; 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 = "<group>"; }; AA0000000000000000000030 /* CamperMonitor-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "CamperMonitor-Info.plist"; sourceTree = "<group>"; };
AA0000000000000000000045 /* CamperMonitorComplication-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "CamperMonitorComplication-Info.plist"; sourceTree = "<group>"; };
AA0000000000000000000032 /* CamperMonitorComplication.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = CamperMonitorComplication.appex; sourceTree = BUILT_PRODUCTS_DIR; };
/* End PBXFileReference section */ /* End PBXFileReference section */
/* Begin PBXFileSystemSynchronizedRootGroup section */ /* Begin PBXFileSystemSynchronizedRootGroup section */
@@ -51,6 +72,11 @@
path = CamperMonitorWatch; path = CamperMonitorWatch;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
AA0000000000000000000033 /* CamperMonitorComplication */ = {
isa = PBXFileSystemSynchronizedRootGroup;
path = CamperMonitorComplication;
sourceTree = "<group>";
};
AA0000000000000000000018 /* Shared */ = { AA0000000000000000000018 /* Shared */ = {
isa = PBXFileSystemSynchronizedRootGroup; isa = PBXFileSystemSynchronizedRootGroup;
path = Shared; path = Shared;
@@ -73,6 +99,13 @@
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
AA0000000000000000000035 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */ /* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */ /* Begin PBXGroup section */
@@ -81,6 +114,7 @@
children = ( children = (
AA0000000000000000000002 /* CamperMonitor */, AA0000000000000000000002 /* CamperMonitor */,
AA0000000000000000000017 /* CamperMonitorWatch */, AA0000000000000000000017 /* CamperMonitorWatch */,
AA0000000000000000000033 /* CamperMonitorComplication */,
AA0000000000000000000018 /* Shared */, AA0000000000000000000018 /* Shared */,
AA0000000000000000000031 /* Config */, AA0000000000000000000031 /* Config */,
AA0000000000000000000005 /* Products */, AA0000000000000000000005 /* Products */,
@@ -92,6 +126,7 @@
children = ( children = (
AA0000000000000000000001 /* CamperMonitor.app */, AA0000000000000000000001 /* CamperMonitor.app */,
AA0000000000000000000016 /* CamperMonitorWatch.app */, AA0000000000000000000016 /* CamperMonitorWatch.app */,
AA0000000000000000000032 /* CamperMonitorComplication.appex */,
); );
name = Products; name = Products;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -100,6 +135,7 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
AA0000000000000000000030 /* CamperMonitor-Info.plist */, AA0000000000000000000030 /* CamperMonitor-Info.plist */,
AA0000000000000000000045 /* CamperMonitorComplication-Info.plist */,
); );
path = Config; path = Config;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -137,10 +173,12 @@
AA0000000000000000000019 /* Sources */, AA0000000000000000000019 /* Sources */,
AA0000000000000000000020 /* Frameworks */, AA0000000000000000000020 /* Frameworks */,
AA0000000000000000000021 /* Resources */, AA0000000000000000000021 /* Resources */,
AA0000000000000000000041 /* Embed Foundation Extensions */,
); );
buildRules = ( buildRules = (
); );
dependencies = ( dependencies = (
AA0000000000000000000044 /* PBXTargetDependency */,
); );
fileSystemSynchronizedGroups = ( fileSystemSynchronizedGroups = (
AA0000000000000000000017 /* CamperMonitorWatch */, AA0000000000000000000017 /* CamperMonitorWatch */,
@@ -151,6 +189,27 @@
productReference = AA0000000000000000000016 /* CamperMonitorWatch.app */; productReference = AA0000000000000000000016 /* CamperMonitorWatch.app */;
productType = "com.apple.product-type.application"; 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 */ /* End PBXNativeTarget section */
/* Begin PBXProject section */ /* Begin PBXProject section */
@@ -167,6 +226,9 @@
AA0000000000000000000022 = { AA0000000000000000000022 = {
CreatedOnToolsVersion = 26.6; CreatedOnToolsVersion = 26.6;
}; };
AA0000000000000000000037 = {
CreatedOnToolsVersion = 26.6;
};
}; };
}; };
buildConfigurationList = AA0000000000000000000010 /* Build configuration list for PBXProject "CamperMonitor" */; buildConfigurationList = AA0000000000000000000010 /* Build configuration list for PBXProject "CamperMonitor" */;
@@ -186,6 +248,7 @@
targets = ( targets = (
AA0000000000000000000006 /* CamperMonitor */, AA0000000000000000000006 /* CamperMonitor */,
AA0000000000000000000022 /* CamperMonitorWatch */, AA0000000000000000000022 /* CamperMonitorWatch */,
AA0000000000000000000037 /* CamperMonitorComplication */,
); );
}; };
/* End PBXProject section */ /* End PBXProject section */
@@ -205,6 +268,13 @@
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
AA0000000000000000000036 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */ /* End PBXResourcesBuildPhase section */
/* Begin PBXSourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */
@@ -222,6 +292,13 @@
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
AA0000000000000000000034 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */ /* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */ /* Begin PBXTargetDependency section */
@@ -231,6 +308,11 @@
target = AA0000000000000000000022 /* CamperMonitorWatch */; target = AA0000000000000000000022 /* CamperMonitorWatch */;
targetProxy = AA0000000000000000000028 /* PBXContainerItemProxy */; targetProxy = AA0000000000000000000028 /* PBXContainerItemProxy */;
}; };
AA0000000000000000000044 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = AA0000000000000000000037 /* CamperMonitorComplication */;
targetProxy = AA0000000000000000000043 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */ /* End PBXTargetDependency section */
/* Begin XCBuildConfiguration section */ /* Begin XCBuildConfiguration section */
@@ -301,7 +383,7 @@
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "Config/CamperMonitor-Info.plist"; 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_NSBluetoothAlwaysUsageDescription = "Zum Auslesen von Victron-Geraeten und dem Daly BMS per Bluetooth.";
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES;
@@ -332,7 +414,7 @@
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "Config/CamperMonitor-Info.plist"; 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_NSBluetoothAlwaysUsageDescription = "Zum Auslesen von Victron-Geraeten und dem Daly BMS per Bluetooth.";
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES;
@@ -361,7 +443,7 @@
DEVELOPMENT_TEAM = 6AP73NYV5W; DEVELOPMENT_TEAM = 6AP73NYV5W;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = 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_NSBluetoothAlwaysUsageDescription = "Zum Auslesen des Neigungsmessers direkt an der Uhr, ohne Umweg ueber das iPhone.";
INFOPLIST_KEY_WKCompanionAppBundleIdentifier = de.fritob.CamperMonitor; INFOPLIST_KEY_WKCompanionAppBundleIdentifier = de.fritob.CamperMonitor;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
@@ -393,7 +475,7 @@
DEVELOPMENT_TEAM = 6AP73NYV5W; DEVELOPMENT_TEAM = 6AP73NYV5W;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = 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_NSBluetoothAlwaysUsageDescription = "Zum Auslesen des Neigungsmessers direkt an der Uhr, ohne Umweg ueber das iPhone.";
INFOPLIST_KEY_WKCompanionAppBundleIdentifier = de.fritob.CamperMonitor; INFOPLIST_KEY_WKCompanionAppBundleIdentifier = de.fritob.CamperMonitor;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
@@ -415,6 +497,64 @@
}; };
name = Release; 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 */ /* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */ /* Begin XCConfigurationList section */
@@ -445,6 +585,15 @@
defaultConfigurationIsVisible = 0; defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release; defaultConfigurationName = Release;
}; };
AA0000000000000000000040 /* Build configuration list for PBXNativeTarget "CamperMonitorComplication" */ = {
isa = XCConfigurationList;
buildConfigurations = (
AA0000000000000000000038 /* Debug */,
AA0000000000000000000039 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */ /* End XCConfigurationList section */
}; };
rootObject = AA0000000000000000000009 /* Project object */; rootObject = AA0000000000000000000009 /* Project object */;
@@ -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<LevelComplicationEntry>) -> 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])
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
import SwiftUI import SwiftUI
/// Camper Monitor am Handgelenk. /// VanControl Pro am Handgelenk.
/// ///
/// Zwei Quellen, jede dort, wo sie hingehört: /// Zwei Quellen, jede dort, wo sie hingehört:
/// ///
@@ -41,9 +41,7 @@ struct WatchLevelView: View {
.padding(.top, 2) .padding(.top, 2)
} }
NavigationLink { NavigationLink(value: WatchRoute.align) {
WatchAlignView()
} label: {
Label("Ausrichten", systemImage: "arrow.left.and.right") Label("Ausrichten", systemImage: "arrow.left.and.right")
} }
.buttonStyle(.borderedProminent) .buttonStyle(.borderedProminent)
+30 -8
View File
@@ -6,10 +6,28 @@ struct WatchRootView: View {
@Environment(PhoneLink.self) private var link @Environment(PhoneLink.self) private var link
@Environment(WatchLevelRadio.self) private var radio @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 { var body: some View {
NavigationStack { NavigationStack(path: $path) {
content content
.navigationTitle(link.payload?.profile.name ?? "Camper") .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 // Die Nivellierung steht immer oben und immer da: Sie kommt vom
// Sensor selbst und braucht das iPhone nicht. // Sensor selbst und braucht das iPhone nicht.
Section { Section {
NavigationLink { NavigationLink(value: WatchRoute.level) {
WatchLevelView()
} label: {
LevelSummaryRow(reading: WatchLevelReading(radio: radio, phone: link)) LevelSummaryRow(reading: WatchLevelReading(radio: radio, phone: link))
} }
} }
@@ -28,9 +44,7 @@ struct WatchRootView: View {
if let payload = link.payload { if let payload = link.payload {
Section { Section {
ForEach(payload.devices.filter { $0.role != .leveling }) { device in ForEach(payload.devices.filter { $0.role != .leveling }) { device in
NavigationLink { NavigationLink(value: WatchRoute.device(device.id)) {
WatchDeviceDetailView(deviceID: device.id)
} label: {
DeviceRow(device: device) DeviceRow(device: device)
} }
} }
@@ -55,7 +69,7 @@ struct WatchRootView: View {
VStack(spacing: 4) { VStack(spacing: 4) {
Label("Warte auf das iPhone", systemImage: "iphone.gen3.radiowaves.left.and.right") Label("Warte auf das iPhone", systemImage: "iphone.gen3.radiowaves.left.and.right")
.font(.footnote.weight(.medium)) .font(.footnote.weight(.medium))
Text("Dort muss Camper Monitor geöffnet sein.") Text("Dort muss VanControl Pro geöffnet sein.")
.font(.caption2) .font(.caption2)
.foregroundStyle(.secondary) .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 /// Eine Gerätezeile: Name klein, Hauptwert gross auf dem kleinen Bildschirm
/// zählt nur der eine Wert, wegen dem man den Arm hebt. /// zählt nur der eine Wert, wegen dem man den Arm hebt.
struct DeviceRow: View { struct DeviceRow: View {
@@ -0,0 +1,11 @@
<?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>NSExtension</key>
<dict>
<key>NSExtensionPointIdentifier</key>
<string>com.apple.widgetkit-extension</string>
</dict>
</dict>
</plist>
+3 -3
View File
@@ -1,4 +1,4 @@
# Camper Monitor # VanControl Pro
iOS-App, die per Bluetooth LE die Energieanlage im Wohnmobil ausliest: 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. zusammen mit der letzten Rohantwort.
Nur **eine** App gleichzeitig kann mit dem BMS verbunden sein. Wenn die 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 ### 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 ### 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 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 im Hintergrund am Leben; die Victron-Werbedaten stehen dabei still, weil iOS im
Hintergrund kein ungefiltertes Suchen erlaubt. Hintergrund kein ungefiltertes Suchen erlaubt.
+19
View File
@@ -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")
}
}
+3 -3
View File
@@ -26,7 +26,7 @@ darüber auffindbar).
| Roll | `…3425` | lesen | Querneigung, Float32 little-endian, Grad | | Roll | `…3425` | lesen | Querneigung, Float32 little-endian, Grad |
| Kalibrieren | `…3427` | schreiben | ein Byte: `0` setzt zurück, alles andere kalibriert | | 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. Sekunde.
Positiver Pitch heißt: das Heck steht höher. Positiver Roll: die rechte Seite Positiver Pitch heißt: das Heck steht höher. Positiver Roll: die rechte Seite
@@ -35,7 +35,7 @@ steht höher.
### Kalibrierung ### Kalibrierung
Fahrzeug eben stellen, dann ein beliebiges Byte ungleich `0` auf `…3427` 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. Die Offsets werden dauerhaft gespeichert und überstehen einen Neustart.
## Was sich gegenüber 1.0 geändert hat ## 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 ## 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 *Nivellierung*: grafische Libelle, Kalibrierung aus der App, zusammen mit
den übrigen Geräten im Fahrzeug. den übrigen Geräten im Fahrzeug.
* Die ursprüngliche WebBLE-Oberfläche liegt weiterhin im Projekt * Die ursprüngliche WebBLE-Oberfläche liegt weiterhin im Projekt