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:
@@ -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."
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">Camper Monitor</string>
|
||||
<string name="app_name">VanControl Pro</string>
|
||||
</resources>
|
||||
|
||||
@@ -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 = "<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 */
|
||||
|
||||
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
||||
@@ -51,6 +72,11 @@
|
||||
path = CamperMonitorWatch;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
AA0000000000000000000033 /* CamperMonitorComplication */ = {
|
||||
isa = PBXFileSystemSynchronizedRootGroup;
|
||||
path = CamperMonitorComplication;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
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 = "<group>";
|
||||
@@ -100,6 +135,7 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
AA0000000000000000000030 /* CamperMonitor-Info.plist */,
|
||||
AA0000000000000000000045 /* CamperMonitorComplication-Info.plist */,
|
||||
);
|
||||
path = Config;
|
||||
sourceTree = "<group>";
|
||||
@@ -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 */;
|
||||
|
||||
@@ -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,6 +1,6 @@
|
||||
import SwiftUI
|
||||
|
||||
/// Camper Monitor am Handgelenk.
|
||||
/// VanControl Pro am Handgelenk.
|
||||
///
|
||||
/// Zwei Quellen, jede dort, wo sie hingehört:
|
||||
///
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
@@ -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.
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user