Files
Camper-Monitor/Shared/Models/DeviceSnapshot.swift
T
BiasFandClaude Opus 5 6612b07fd5 Apple Watch: Übersicht, Ausrichten mit Vibration, Kühlbox am Handgelenk
Neues Target CamperMonitorWatch, eingebettet in die iPhone-App. Die Uhr
zeigt je Gerät den Hauptwert, die Nivellierung mit Libelle und Keilhöhen,
den Ausrichtungs-Assistenten und stellt die Kühlbox.

Die Uhr funkt nicht selbst, obwohl watchOS das könnte. BMS, Kühlbox und
Neigungsmesser lassen jeweils nur eine Verbindung zu – eine mitlesende Uhr
würde dem iPhone die Verbindung wegnehmen, statt sie zu ergänzen. Und die
Victron-Schlüssel liegen in der Keychain des iPhones; sie ein zweites Mal
auf der Uhr aufzubewahren brächte nichts. Das iPhone bleibt also das
Funkgerät und reicht fertige Messwerte über WatchConnectivity weiter.

Gesendet wird in zwei Takten: ein halber Sekundentakt als Nachricht,
solange die Watch-App im Vordergrund ist und das über eine Frist meldet,
sonst alle zwei Sekunden als Anwendungskontext und nur bei Änderungen.
Läuft die Frist ab, hört das iPhone von selbst wieder auf.

Solange die Uhr hinschaut, hält die App das Funkgerät auch im Hintergrund
am Leben (UIBackgroundModes in Config/CamperMonitor-Info.plist) – beim
Rangieren liegt das iPhone sonst mit dunklem Bildschirm in der Halterung
und die Anzeige am Handgelenk wäre genau dann tot. Die verbundenen Geräte
liefern dabei weiter, die Victron-Werbedaten nicht: ungefiltertes Suchen
lässt iOS im Hintergrund nicht zu.

Der plattformneutrale Modellcode wandert nach Shared/ und wird in beide
Targets übersetzt; LevelState ist dafür aus VanAlignProtocol.swift
herausgelöst. Datensatz und Befehle stehen in Shared/WatchLink und sind
in run-tests.sh abgesichert: Rundlauf, Schutz gegen fremde Formatversion,
Vergleich ohne Zeitstempel und die Auflösung der Zeitangaben.

Geprüft sind der Bau beider Targets samt Einbetten und der Testlauf; auf
echter Hardware noch nicht.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 15:20:16 +02:00

98 lines
2.8 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import Foundation
/// Eine einzelne Messgrösse in einer bereits formatierten Form.
struct Metric: Identifiable, Hashable, Codable, Sendable {
let key: String
let label: String
let value: Double?
let unit: String
/// Nachkommastellen für die Anzeige.
let precision: Int
/// Wird auf der Kachel gross dargestellt.
let isPrimary: Bool
var id: String { key }
init(_ key: String,
_ label: String,
_ value: Double?,
unit: String,
precision: Int = 2,
primary: Bool = false) {
self.key = key
self.label = label
self.value = value
self.unit = unit
self.precision = precision
self.isPrimary = primary
}
var formatted: String {
guard let value else { return "" }
return String(format: "%.\(precision)f", value)
}
var formattedWithUnit: String {
guard value != nil else { return "" }
return unit.isEmpty ? formatted : "\(formatted) \(unit)"
}
}
/// Der komplette, zuletzt empfangene Zustand eines Geräts.
struct DeviceSnapshot: Identifiable, Codable, Equatable, Sendable {
var id: UUID { deviceID }
var deviceID: UUID
var timestamp: Date
var metrics: [Metric] = []
/// z.B. "Bulk", "Float", "Aus"
var state: String?
/// Klartext einer aktiven Störung, sonst nil.
var fault: String?
/// Grund, warum das Gerät gerade nicht lädt (Victron Off-Reason).
var offReasons: [String] = []
var rssi: Int?
/// Einzelzellspannungen in Volt (nur BMS).
var cellVoltages: [Double] = []
/// Temperaturfühler in °C (nur BMS).
var temperatures: [Double] = []
/// Feste Angaben des Geräts, etwa Modell oder Seriennummer.
var info: [InfoItem] = []
struct InfoItem: Identifiable, Hashable, Codable, Sendable {
let label: String
let value: String
var id: String { label }
}
var primaryMetric: Metric? {
metrics.first(where: \.isPrimary) ?? metrics.first
}
var age: TimeInterval { Date().timeIntervalSince(timestamp) }
/// Werte älter als eine Minute gelten als veraltet Victron sendet etwa
/// jede Sekunde, das Daly wird alle paar Sekunden gepollt.
var isStale: Bool { age > 60 }
}
/// Verbindungszustand für die UI.
enum DeviceLinkState: Equatable, Codable, Sendable {
case idle
case searching
case connecting
case live
case needsKey
case failed(String)
var label: String {
switch self {
case .idle: return "Inaktiv"
case .searching: return "Suche…"
case .connecting: return "Verbinde…"
case .live: return "Live"
case .needsKey: return "Schlüssel fehlt"
case .failed(let m): return m
}
}
}