Files
Camper-Monitor/Shared/Models/VictronCodes.swift
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

102 lines
4.2 KiB
Swift
Raw Permalink 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
/// Klartexte für die Zustands- und Fehlercodes aus den Victron-Werbedaten.
enum VictronCodes {
/// VE.Reg 0x0201 Betriebszustand des Laders.
static func deviceState(_ code: UInt32?) -> String? {
guard let code else { return nil }
switch code {
case 0: return "Aus"
case 1: return "Stromsparmodus"
case 2: return "Störung"
case 3: return "Konstantstrom (Bulk)"
case 4: return "Konstantspannung (Absorption)"
case 5: return "Erhaltung (Float)"
case 6: return "Lagerung"
case 7: return "Ausgleichsladung"
case 9: return "Wechselrichten"
case 11: return "Netzteilbetrieb"
case 245: return "Startet"
case 246: return "Wiederholte Absorption"
case 247: return "Auto-Ausgleich"
case 248: return "Battery Safe"
case 252: return "Externe Steuerung"
default: return "Zustand \(code)"
}
}
/// VE.Reg 0xEDDA Ladefehler. 0 bedeutet "kein Fehler".
static func chargerError(_ code: UInt32?) -> String? {
guard let code, code != 0 else { return nil }
switch code {
case 1: return "Batterietemperatur zu hoch"
case 2: return "Batteriespannung zu hoch"
case 3: return "Temperatursensor defekt"
case 4: return "Temperatursensor Kurzschluss"
case 5: return "Temperatursensor unplausibel"
case 6: return "Spannungsmessung defekt"
case 7: return "Spannungsmessung Kurzschluss"
case 8: return "Spannungsmessung unplausibel"
case 11: return "Zu hohe Restwelligkeit"
case 14: return "Batterietemperatur zu niedrig"
case 17: return "Lader überhitzt"
case 18: return "Lader Überstrom"
case 19: return "Stromrichtung verkehrt"
case 20: return "Bulk-Zeit überschritten"
case 21: return "Stromsensor defekt"
case 22: return "Interner Temperatursensor defekt"
case 26: return "Anschlussklemme überhitzt"
case 27: return "Kurzschluss im Lader"
case 28: return "Endstufenfehler"
case 29: return "Überladeschutz"
case 33: return "Eingangsspannung zu hoch (PV)"
case 34: return "Eingangsstrom zu hoch (PV)"
case 38: return "Eingang abgeschaltet (Batteriespannung)"
case 39: return "Eingang abgeschaltet (Stromfluss)"
case 65: return "Kommunikation verloren"
case 66: return "Konfiguration synchronisierter Lader fehlerhaft"
case 67: return "BMS-Verbindung verloren"
case 68: return "Netzwerk fehlkonfiguriert"
case 116: return "Kalibrierdaten verloren"
case 117: return "Inkompatible Firmware"
case 119: return "Einstellungen ungültig"
default: return "Fehler \(code)"
}
}
/// VE.Reg 0x0207 Bitmaske, warum das Gerät gerade nicht arbeitet.
static func offReasons(_ mask: UInt32?) -> [String] {
guard let mask, mask != 0 else { return [] }
let table: [(UInt32, String)] = [
(0x0000_0001, "Keine Eingangsspannung"),
(0x0000_0002, "Per Schalter ausgeschaltet"),
(0x0000_0004, "Per Einstellung ausgeschaltet"),
(0x0000_0008, "Remote-Eingang"),
(0x0000_0010, "Schutzfunktion aktiv"),
(0x0000_0020, "Paygo"),
(0x0000_0040, "BMS"),
(0x0000_0080, "Motor-Abschalterkennung"),
(0x0000_0100, "Eingangsspannung wird geprüft"),
]
return table.filter { mask & $0.0 != 0 }.map(\.1)
}
/// VE.Reg 0xEEB8 Alarmgründe des Batteriewächters.
static func alarmReasons(_ mask: UInt32?) -> [String] {
guard let mask, mask != 0 else { return [] }
let table: [(UInt32, String)] = [
(0x0001, "Unterspannung"),
(0x0002, "Überspannung"),
(0x0004, "Niedriger Ladezustand"),
(0x0008, "Starterbatterie Unterspannung"),
(0x0010, "Starterbatterie Überspannung"),
(0x0020, "Temperatur zu niedrig"),
(0x0040, "Temperatur zu hoch"),
(0x0080, "Mittenspannung"),
(0x0100, "Ladung überfällig"),
]
return table.filter { mask & $0.0 != 0 }.map(\.1)
}
}