forked from fritob/Camper-Monitor
Einbaulage: Verdrehung um die Hochachse messen – und Geräte dabei nicht verlieren
Sitzt der Sensor schräg statt längs im Fahrzeug, verteilt sich eine reine
Querneigung auf beide Achsen: Das Fahrzeug kippt zur Seite, und die
Längsanzeige kippt sichtbar mit – bei 20° Verdrehung mit gut einem
Drittel des Werts. Achsentausch und Vorzeichen halfen dagegen nicht, die
springen in 90°-Schritten.
SensorOrientation bekommt deshalb "twist", einen stufenlosen Winkel. Der
Einbaulage-Assistent misst ihn ohne zusätzlichen Handgriff mit: Beim
Kippen der Front nach unten dürfte sich nur die Längsneigung ändern;
wandert die Querneigung mit, ist das der gesuchte Winkel. Unter zwei Grad
gilt es als Wackeln der Hand. Angezeigt wird er in der Zusammenfassung
("um 20° verdreht"), herausgerechnet bei jeder Anzeige.
Beim Kalibrieren liesse sich das grundsätzlich nicht ermitteln: Es misst
eine einzige Lage und zieht sie als Nullpunkt ab. Eine Drehung um die
Hochachse steckt darin nicht – eben sieht in jeder Verdrehung gleich aus.
Das neue Feld hat beim Aufspielen sämtliche eingerichteten Geräte
gekostet, und der Grund gehört hierher: Swifts erzeugte Codable-Umsetzung
verlangt beim Lesen jedes Feld, Standardwerte im Code zählen nicht.
Gespeicherte Einbaulagen hatten kein "twist", also scheiterte das Lesen
der Einbaulage, damit des Geräts, damit der ganzen Liste – und das
nächste Speichern schrieb die leere Liste über den Bestand.
Zwei Vorkehrungen dagegen:
* SensorOrientation liest jetzt von Hand und nachsichtig, mit
decodeIfPresent und Rückfallwerten, wie Profile und ConfiguredDevice es
längst tun. Jedes künftige Feld gehört dort hinein.
* DeviceStore liest die Liste zweistufig: scheitert sie als Ganzes, wird
jedes Gerät einzeln versucht und behalten, was lesbar ist. Die
Rohdaten wandern zusätzlich in einen eigenen Schlüssel, statt beim
nächsten Speichern überschrieben zu werden.
Beides in run-tests.sh abgesichert: die Erkennung und Rückrechnung der
Verdrehung samt Gegenprobe ohne Korrektur, und das Lesen gespeicherter
Geräte ohne "twist", ganz ohne Einbaulage und mit unbrauchbarer.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -9,6 +9,8 @@ final class DeviceStore {
|
||||
private static let devicesKey = "configuredDevices"
|
||||
private static let profilesKey = "profiles"
|
||||
private static let activeProfileKey = "activeProfileID"
|
||||
/// Hierhin wird gerettet, was sich nicht lesen liess – siehe `loadDevices`.
|
||||
private static let unreadableDevicesKey = "configuredDevices.unreadable"
|
||||
|
||||
/// Alle Geräte über alle Profile hinweg.
|
||||
private(set) var devices: [ConfiguredDevice] = []
|
||||
@@ -16,6 +18,11 @@ final class DeviceStore {
|
||||
|
||||
private(set) var activeProfileID: UUID = Profile.defaultID
|
||||
|
||||
/// Ob beim Start Geräte im Speicher lagen, die sich nicht lesen liessen.
|
||||
/// Ihre Rohdaten sind aufgehoben, statt beim nächsten Speichern
|
||||
/// überschrieben zu werden.
|
||||
private(set) var hasUnreadableDevices = false
|
||||
|
||||
/// Zwischenspeicher, damit nicht bei jedem Advertisement die Keychain
|
||||
/// befragt wird – das passiert bis zu mehrmals pro Sekunde.
|
||||
@ObservationIgnored private var keyCache: [UUID: [UInt8]] = [:]
|
||||
@@ -151,9 +158,8 @@ final class DeviceStore {
|
||||
profiles = [.initial]
|
||||
}
|
||||
|
||||
if let data = defaults.data(forKey: Self.devicesKey),
|
||||
let decoded = try? JSONDecoder().decode([ConfiguredDevice].self, from: data) {
|
||||
devices = decoded
|
||||
if let data = defaults.data(forKey: Self.devicesKey) {
|
||||
devices = loadDevices(from: data)
|
||||
}
|
||||
|
||||
if let raw = defaults.string(forKey: Self.activeProfileKey),
|
||||
@@ -171,6 +177,42 @@ final class DeviceStore {
|
||||
}
|
||||
}
|
||||
|
||||
/// Liest die Geräteliste so verlustarm wie möglich.
|
||||
///
|
||||
/// Der einfache Weg – die ganze Liste auf einmal – scheitert vollständig,
|
||||
/// sobald ein einziges Gerät nicht lesbar ist: etwa weil eine neuere
|
||||
/// Fassung der App ein Feld hinzugefügt hat, das die ältere nicht kennt,
|
||||
/// oder umgekehrt. Die Liste wäre dann leer, und das nächste Speichern
|
||||
/// schriebe diese Leere über den Bestand. Genau so gehen Einrichtungen
|
||||
/// verloren, ohne dass jemand etwas löscht.
|
||||
///
|
||||
/// Deshalb zweistufig: erst die ganze Liste, und wenn das misslingt, jedes
|
||||
/// Gerät für sich. Was dabei übrig bleibt, wird behalten; die Rohdaten
|
||||
/// wandern zusätzlich in einen eigenen Schlüssel, damit sich der Bestand
|
||||
/// später noch untersuchen lässt.
|
||||
private func loadDevices(from data: Data) -> [ConfiguredDevice] {
|
||||
let decoder = JSONDecoder()
|
||||
if let decoded = try? decoder.decode([ConfiguredDevice].self, from: data) {
|
||||
return decoded
|
||||
}
|
||||
|
||||
let salvaged = (try? decoder.decode([Salvage].self, from: data))?
|
||||
.compactMap(\.device) ?? []
|
||||
UserDefaults.standard.set(data, forKey: Self.unreadableDevicesKey)
|
||||
hasUnreadableDevices = true
|
||||
return salvaged
|
||||
}
|
||||
|
||||
/// Hülle, die ein einzelnes unlesbares Gerät verschluckt, statt die ganze
|
||||
/// Liste scheitern zu lassen.
|
||||
private struct Salvage: Decodable {
|
||||
let device: ConfiguredDevice?
|
||||
|
||||
init(from decoder: Decoder) throws {
|
||||
device = try? ConfiguredDevice(from: decoder)
|
||||
}
|
||||
}
|
||||
|
||||
private func save() {
|
||||
let defaults = UserDefaults.standard
|
||||
if let data = try? JSONEncoder().encode(devices) {
|
||||
|
||||
Reference in New Issue
Block a user