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>
This commit is contained in:
@@ -49,82 +49,3 @@ enum VanAlignProtocol {
|
||||
return (pitch, roll)
|
||||
}
|
||||
}
|
||||
|
||||
/// Zustand des Neigungsmessers.
|
||||
struct LevelState: Equatable {
|
||||
/// Längsneigung: positiv bedeutet, das Heck steht höher als die Front.
|
||||
/// Bereits auf die Einbaulage des Sensors umgerechnet.
|
||||
var pitch: Double?
|
||||
/// Querneigung: positiv bedeutet, die rechte Seite steht höher.
|
||||
var roll: Double?
|
||||
/// Wie der Sensor selbst meldet – nötig, um die Einbaulage zu bestimmen.
|
||||
var rawPitch: Double?
|
||||
var rawRoll: Double?
|
||||
var pitchOffset: Double?
|
||||
var rollOffset: Double?
|
||||
|
||||
var hasReading: Bool { pitch != nil || roll != nil }
|
||||
|
||||
/// Ob kalibriert wurde – oder nil, wenn das Gerät es nicht verrät.
|
||||
///
|
||||
/// Nicht jede Firmware stellt die Offsets bereit. Fehlen sie, heisst das
|
||||
/// „unbekannt“ und nicht „nicht kalibriert“: eine Warnung, die sich nie
|
||||
/// abstellen lässt, ist schlimmer als keine.
|
||||
var calibrationState: Bool? {
|
||||
guard let pitchOffset, let rollOffset else { return nil }
|
||||
return abs(pitchOffset) > 0.001 || abs(rollOffset) > 0.001
|
||||
}
|
||||
|
||||
/// Nur wahr, wenn es auch belegt ist.
|
||||
var isCalibrated: Bool { calibrationState == true }
|
||||
|
||||
/// Nur wahr, wenn das Gerät ausdrücklich Nulloffsets meldet.
|
||||
var isKnownUncalibrated: Bool { calibrationState == false }
|
||||
|
||||
/// Bis hierhin gilt das Fahrzeug als eben genug.
|
||||
static let levelTolerance = 0.5
|
||||
|
||||
var isLevel: Bool {
|
||||
guard let pitch, let roll else { return false }
|
||||
return abs(pitch) <= Self.levelTolerance && abs(roll) <= Self.levelTolerance
|
||||
}
|
||||
|
||||
/// Die grössere der beiden Abweichungen – das ist die, die man zuerst
|
||||
/// ausgleichen will.
|
||||
var largestDeviation: Double? {
|
||||
switch (pitch, roll) {
|
||||
case let (p?, r?): return max(abs(p), abs(r))
|
||||
case let (p?, nil): return abs(p)
|
||||
case let (nil, r?): return abs(r)
|
||||
default: return nil
|
||||
}
|
||||
}
|
||||
|
||||
/// Was zu tun ist, in Worten. Beim Ausrichten schaut man aufs Handy,
|
||||
/// nicht auf Vorzeichen.
|
||||
var instruction: String? {
|
||||
guard let pitch, let roll else { return nil }
|
||||
if isLevel { return "Steht eben" }
|
||||
var parts: [String] = []
|
||||
if abs(pitch) > Self.levelTolerance {
|
||||
parts.append(pitch > 0 ? "Heck steht höher" : "Front steht höher")
|
||||
}
|
||||
if abs(roll) > Self.levelTolerance {
|
||||
parts.append(roll > 0 ? "rechts steht höher" : "links steht höher")
|
||||
}
|
||||
return parts.joined(separator: ", ")
|
||||
}
|
||||
|
||||
func snapshot(deviceID: UUID, rssi: Int?) -> DeviceSnapshot {
|
||||
var snapshot = DeviceSnapshot(deviceID: deviceID, timestamp: Date(), rssi: rssi)
|
||||
snapshot.metrics = [
|
||||
Metric("pitch", "Längsneigung", pitch, unit: "°", precision: 1, primary: true),
|
||||
Metric("roll", "Querneigung", roll, unit: "°", precision: 1),
|
||||
]
|
||||
snapshot.state = instruction
|
||||
if isKnownUncalibrated {
|
||||
snapshot.offReasons = ["Nicht kalibriert"]
|
||||
}
|
||||
return snapshot
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user