forked from fritob/Camper-Monitor
Die drei Libellen widersprachen sich: Die Live Activity liess die Blase bei positivem Pitch nach unten wandern – mit dem Kommentar "wie bei der echten Wasserwaage in der App" –, waehrend iPhone und Uhr genau das Gegenteil taten. Gemeint war immer eine Draufsicht mit Fahrtrichtung oben, in der die Blase zur hoeheren Seite ausschlaegt: Steht das Heck hoeher, gehoert sie nach hinten, also nach unten. iPhone und Uhr ziehen jetzt nach. Die Android-Fassung bleibt vorerst bei der alten Richtung. Dazu `shortInstructions`: dieselbe Aussage wie `instruction`, aber in kurzen Zeilen statt einem Satz. Auf schmalen Anzeigen passt "Heck steht hoeher, links steht hoeher" in keine lesbare Zeile. Beide stehen nebeneinander, damit die Vorzeichen nicht auseinanderlaufen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
100 lines
3.7 KiB
Swift
100 lines
3.7 KiB
Swift
import Foundation
|
||
|
||
/// Zustand des Neigungsmessers.
|
||
struct LevelState: Equatable, Codable, Sendable {
|
||
/// 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: ", ")
|
||
}
|
||
|
||
/// Dasselbe wie ``instruction``, aber in kurzen Zeilen statt einem Satz –
|
||
/// für schmale Anzeigen wie das CarPlay-Display, wo "Heck steht höher,
|
||
/// rechts steht höher" nicht in eine lesbare Zeile passt.
|
||
///
|
||
/// Die Vorzeichen müssen zu ``instruction`` passen; beide stehen deshalb
|
||
/// nebeneinander.
|
||
var shortInstructions: [String] {
|
||
guard let pitch, let roll else { return [] }
|
||
if isLevel { return ["Steht eben"] }
|
||
var parts: [String] = []
|
||
if abs(pitch) > Self.levelTolerance {
|
||
parts.append(pitch > 0 ? "Heck höher" : "Front höher")
|
||
}
|
||
if abs(roll) > Self.levelTolerance {
|
||
parts.append(roll > 0 ? "Rechts höher" : "Links höher")
|
||
}
|
||
return parts
|
||
}
|
||
|
||
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
|
||
}
|
||
}
|