Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8401f62d91 | ||
|
|
c45cf36920 | ||
|
|
47ead9b006 | ||
|
|
b23e96615e | ||
|
|
0b3bf5e815 | ||
|
|
8d58ea6e49 |
@@ -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 {
|
||||
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 {
|
||||
switch self {
|
||||
case .vanster: return VehicleTilt.sideAspect
|
||||
case .california: return 1
|
||||
case .california: return 1212.0 / 790.0
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -92,6 +92,10 @@ struct WatchDevice: Codable, Equatable, Identifiable, Sendable {
|
||||
/// den Neigungsmesser selbst an und braucht die Zuordnung deshalb auch –
|
||||
/// eingestellt wird sie weiterhin nur am iPhone.
|
||||
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.
|
||||
var fridge: WatchFridge?
|
||||
|
||||
|
||||
@@ -1190,13 +1190,14 @@ let watchPayload = WatchPayload(
|
||||
devices: [
|
||||
WatchDevice(id: watchLevelID, name: "Nivellierung", role: .leveling,
|
||||
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,
|
||||
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,
|
||||
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.
|
||||
|
||||
@@ -38,5 +38,22 @@ sips --resampleHeight 600 heckansicht.png --out /tmp/rear.png
|
||||
Jedes Fahrzeug-Grafikset (siehe `VehicleGraphicStyle`) hat sein eigenes
|
||||
Bildpaar `<Name>Side`/`<Name>Rear`. Hat die Vorlage schon einen
|
||||
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
|
||||
mitgefärbt.
|
||||
auf weißem Grund, muss der Rand vorher weg – siehe `remove-white-background.swift`.
|
||||
|
||||
## 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
|
||||
```
|
||||
|
||||
@@ -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")
|
||||
@@ -496,16 +496,20 @@
|
||||
AA0000000000000000000014 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
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_INCLUDE_ALL_APPICON_ASSETS = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
CURRENT_PROJECT_VERSION = 2;
|
||||
DEVELOPMENT_ASSET_PATHS = "";
|
||||
DEVELOPMENT_TEAM = V5C6Q86XJR;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = "Config/VanControl-Info.plist";
|
||||
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_NSSupportsLiveActivities = YES;
|
||||
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
||||
@@ -528,16 +532,20 @@
|
||||
AA0000000000000000000015 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
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_INCLUDE_ALL_APPICON_ASSETS = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
CURRENT_PROJECT_VERSION = 2;
|
||||
DEVELOPMENT_ASSET_PATHS = "";
|
||||
DEVELOPMENT_TEAM = V5C6Q86XJR;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = "Config/VanControl-Info.plist";
|
||||
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_NSSupportsLiveActivities = YES;
|
||||
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
||||
@@ -563,7 +571,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
CURRENT_PROJECT_VERSION = 3;
|
||||
DEVELOPMENT_TEAM = V5C6Q86XJR;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
@@ -595,7 +603,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
CURRENT_PROJECT_VERSION = 3;
|
||||
DEVELOPMENT_TEAM = V5C6Q86XJR;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
@@ -625,7 +633,7 @@
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
CURRENT_PROJECT_VERSION = 2;
|
||||
DEVELOPMENT_TEAM = V5C6Q86XJR;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = "Config/VanControlComplication-Info.plist";
|
||||
@@ -654,7 +662,7 @@
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
CURRENT_PROJECT_VERSION = 2;
|
||||
DEVELOPMENT_TEAM = V5C6Q86XJR;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = "Config/VanControlComplication-Info.plist";
|
||||
@@ -685,7 +693,7 @@
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
CURRENT_PROJECT_VERSION = 2;
|
||||
DEVELOPMENT_TEAM = V5C6Q86XJR;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = VanControlLiveActivity/Info.plist;
|
||||
@@ -714,7 +722,7 @@
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
CURRENT_PROJECT_VERSION = 2;
|
||||
DEVELOPMENT_TEAM = V5C6Q86XJR;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = VanControlLiveActivity/Info.plist;
|
||||
|
||||
|
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 }
|
||||
}
|
||||
|
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 }
|
||||
}
|
||||
|
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 }
|
||||
}
|
||||
|
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 }
|
||||
}
|
||||
|
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" }
|
||||
}
|
||||
|
Before Width: | Height: | Size: 341 KiB After Width: | Height: | Size: 311 KiB |
@@ -135,6 +135,7 @@ final class PhoneWatchLink: NSObject {
|
||||
snapshot: bluetooth.snapshots[device.id],
|
||||
level: device.role == .leveling ? bluetooth.levelStates[device.id] : nil,
|
||||
orientation: device.role == .leveling ? device.sensorOrientation : nil,
|
||||
vehicleGraphicStyle: device.role == .leveling ? device.vehicleGraphicStyle : nil,
|
||||
fridge: device.role == .fridge ? fridgeState(for: device) : nil)
|
||||
}
|
||||
return WatchPayload(generatedAt: Date(),
|
||||
|
||||
|
Before Width: | Height: | Size: 1.1 MiB After Width: | Height: | Size: 1.1 MiB |
|
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" }
|
||||
}
|
||||
|
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" }
|
||||
}
|
||||
@@ -50,7 +50,8 @@ struct LevelReadingPage: View {
|
||||
ScrollView {
|
||||
VStack(spacing: 8) {
|
||||
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 {
|
||||
WatchBubble(pitch: reading.state.pitch, roll: reading.state.roll)
|
||||
.frame(maxWidth: 110)
|
||||
|
||||
@@ -9,15 +9,18 @@ import SwiftUI
|
||||
struct WatchVehicleView: View {
|
||||
let pitch: 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.
|
||||
/// Die angeschriebenen Gradzahlen bleiben echt.
|
||||
var body: some View {
|
||||
VStack(spacing: 8) {
|
||||
panel(image: "VansterSide", angle: pitch, title: "Längs",
|
||||
lower: "Front", upper: "Heck", aspect: VehicleTilt.sideAspect)
|
||||
panel(image: "VansterRear", angle: roll, title: "Quer",
|
||||
lower: "links", upper: "rechts", aspect: VehicleTilt.rearAspect)
|
||||
panel(image: style.sideImageName, angle: pitch, title: "Längs",
|
||||
lower: "Front", upper: "Heck", aspect: style.sideAspect)
|
||||
panel(image: style.rearImageName, angle: roll, title: "Quer",
|
||||
lower: "links", upper: "rechts", aspect: style.rearAspect)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -63,15 +63,17 @@ enum WatchDemo {
|
||||
devices: [
|
||||
WatchDevice(id: levelID, name: "Nivellierung", role: .leveling,
|
||||
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,
|
||||
link: .live, snapshot: battery, level: nil,
|
||||
orientation: nil, fridge: nil),
|
||||
orientation: nil, vehicleGraphicStyle: nil, fridge: nil),
|
||||
WatchDevice(id: victronSolarID, name: "Solar Dach", role: .victronSolarCharger,
|
||||
link: .live, snapshot: victronSolar, level: nil,
|
||||
orientation: nil, fridge: nil),
|
||||
orientation: nil, vehicleGraphicStyle: nil, fridge: nil),
|
||||
WatchDevice(id: fridgeID, name: "Kühlbox", role: .fridge,
|
||||
link: .live, snapshot: fridge, level: nil, orientation: nil,
|
||||
vehicleGraphicStyle: nil,
|
||||
fridge: WatchFridge(isLive: true, updated: now,
|
||||
isPoweredOn: true, isEco: true, isLocked: false,
|
||||
isDualZone: false, unitSymbol: "°C",
|
||||
|
||||