Zwei Fehler aus dem letzten Stand, beide von mir eingebaut. Die Kühlbox liess sich nicht mehr bedienen. Die Wartesperre gegen den Verbindungssturm hat nicht unterschieden, warum eine Verbindung endete: ein Abbruch einer Verbindung, die stand, wurde genauso lange gesperrt wie ein Fehlschlag. Im Fahrzeug ist so ein Abbruch der Normalfall, und die Box war danach bis zu einer Minute nicht erreichbar. Ein Tippen ging in dieser Zeit lautlos verloren, weil es ohne Sitzung ins Leere fiel. Jetzt zählt die Dauer: was mindestens drei Sekunden stand, gilt als zustandegekommen und wird nach zwei Sekunden wieder aufgenommen. Nur was sofort wieder abbricht, zählt als Fehlschlag und wird gestaffelt gesperrt - gegen das Wechselspiel, wenn eine andere App das Gerät belegt. Dazu gehen Stellbefehle nicht mehr verloren. Fehlt die Verbindung, wird der Befehl aufgehoben, die Sperre für dieses Gerät aufgehoben und sofort ein Versuch angestossen; sobald der Kanal steht, geht er raus. Ein Befehl, der älter als eine halbe Minute ist, verfällt - ein verspätetes Ausschalten würde nur überraschen. Die Ansicht sagt es, statt den Schalter stumm zurückspringen zu lassen. Zweitens der Assistent für die Einbaulage. Er hat die zweite Bewegung von der gekippten Lage aus gemessen statt von der Ruhelage. Damit steckte in jeder zweiten Messung das Zurückkippen aus der ersten, und es schlugen immer beide Achsen aus. Die Ruhelage ist jetzt der Bezug für beide Bewegungen, und ein eigener Schritt dazwischen nimmt sie neu auf. Auch die Auswertung war zu streng: sie hat jede Bewegung für sich beurteilt und abgelehnt, sobald die andere Achse mitlief. Von Hand gekippt läuft sie aber immer mit. Beurteilt werden jetzt beide Bewegungen gemeinsam - es gewinnt die Zuordnung, die zusammen am meisten erklärt. Zweimal um dieselbe Achse gekippt wird weiterhin erkannt. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
136 lines
5.7 KiB
Swift
136 lines
5.7 KiB
Swift
import Foundation
|
||
|
||
/// Wie der Neigungsmesser im Fahrzeug sitzt.
|
||
///
|
||
/// Der Sensor kann quer eingebaut, gedreht oder kopfüber montiert sein. Dann
|
||
/// stimmen seine Achsen nicht mit denen des Fahrzeugs überein: was er als
|
||
/// Längsneigung meldet, ist womöglich die Querneigung, und Vorzeichen können
|
||
/// vertauscht sein.
|
||
///
|
||
/// Statt das raten zu lassen, ermittelt der Einrichtungsassistent die Lage
|
||
/// durch zwei definierte Kippbewegungen.
|
||
struct SensorOrientation: Codable, Equatable, Hashable, Sendable {
|
||
|
||
/// Welche Achse des Sensors die Längsneigung des Fahrzeugs liefert.
|
||
enum Source: String, Codable, Hashable, Sendable {
|
||
case pitch
|
||
case roll
|
||
}
|
||
|
||
/// Woher die Längsneigung kommt. Die Querneigung kommt aus der jeweils
|
||
/// anderen Achse.
|
||
var longitudinalSource: Source = .pitch
|
||
var invertLongitudinal = false
|
||
var invertLateral = false
|
||
|
||
static let identity = SensorOrientation()
|
||
|
||
var isIdentity: Bool { self == .identity }
|
||
|
||
/// Rechnet Sensorwerte in Fahrzeugwerte um.
|
||
func apply(pitch: Double?, roll: Double?) -> (pitch: Double?, roll: Double?) {
|
||
let longitudinal = longitudinalSource == .pitch ? pitch : roll
|
||
let lateral = longitudinalSource == .pitch ? roll : pitch
|
||
return (longitudinal.map { invertLongitudinal ? -$0 : $0 },
|
||
lateral.map { invertLateral ? -$0 : $0 })
|
||
}
|
||
|
||
var summary: String {
|
||
if isIdentity { return "Achsen unverändert" }
|
||
var parts: [String] = []
|
||
if longitudinalSource == .roll { parts.append("Achsen getauscht") }
|
||
if invertLongitudinal { parts.append("längs umgekehrt") }
|
||
if invertLateral { parts.append("quer umgekehrt") }
|
||
return parts.joined(separator: ", ")
|
||
}
|
||
}
|
||
|
||
/// Wertet die Kippbewegungen des Einrichtungsassistenten aus.
|
||
///
|
||
/// Zweimal wird gekippt: einmal die Front nach unten, einmal die linke Seite
|
||
/// nach unten. Welche Sensorachse sich dabei jeweils bewegt und in welche
|
||
/// Richtung, ergibt die Einbaulage.
|
||
enum OrientationDetection {
|
||
|
||
/// So weit muss gekippt werden, damit die Bewegung eindeutig ist.
|
||
/// Darunter wäre nicht zu unterscheiden, ob überhaupt gekippt wurde.
|
||
static let minimumTilt = 5.0
|
||
|
||
/// Soviel deutlicher muss die gewinnende Deutung sein als die andere.
|
||
static let ambiguityMargin = 1.3
|
||
|
||
struct Reading: Equatable {
|
||
let pitch: Double
|
||
let roll: Double
|
||
|
||
static func - (lhs: Reading, rhs: Reading) -> Reading {
|
||
Reading(pitch: lhs.pitch - rhs.pitch, roll: lhs.roll - rhs.roll)
|
||
}
|
||
}
|
||
|
||
enum Failure: Error, Equatable {
|
||
/// Es wurde zu wenig oder gar nicht gekippt.
|
||
case tooLittleMovement
|
||
/// Beide Achsen haben sich ähnlich stark bewegt – dann lässt sich
|
||
/// nicht sagen, welche gemeint war.
|
||
case ambiguous
|
||
|
||
var message: String {
|
||
switch self {
|
||
case .tooLittleMovement:
|
||
return "Zu wenig Bewegung. Deutlicher kippen, mindestens eine "
|
||
+ "Handbreit."
|
||
case .ambiguous:
|
||
return "Beide Schritte haben dieselbe Achse bewegt. Im ersten "
|
||
+ "Schritt nach vorne kippen, im zweiten zur Seite."
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Ermittelt aus beiden Kippbewegungen die Einbaulage.
|
||
///
|
||
/// Beide Bewegungen werden gemeinsam beurteilt. Kippt man von Hand zur
|
||
/// Seite, geht die andere Achse fast immer ein Stück mit – jede Bewegung
|
||
/// für sich betrachtet sähe das nach schrägem Kippen aus. Im Paar ist die
|
||
/// Zuordnung trotzdem eindeutig: es gewinnt die Deutung, die beide
|
||
/// Bewegungen zusammen am besten erklärt.
|
||
///
|
||
/// - Parameters:
|
||
/// - nose: Änderung beim Kippen der Front nach unten.
|
||
/// - side: Änderung beim Kippen der linken Seite nach unten.
|
||
static func orientation(nose: Reading,
|
||
side: Reading) -> Result<SensorOrientation, Failure> {
|
||
guard max(abs(nose.pitch), abs(nose.roll)) >= minimumTilt,
|
||
max(abs(side.pitch), abs(side.roll)) >= minimumTilt else {
|
||
return .failure(.tooLittleMovement)
|
||
}
|
||
|
||
// Zwei mögliche Deutungen, bewertet danach, wieviel Bewegung sie
|
||
// erklären: die erste Achse längs und die zweite quer – oder umgekehrt.
|
||
let pitchIsLongitudinal = abs(nose.pitch) + abs(side.roll)
|
||
let rollIsLongitudinal = abs(nose.roll) + abs(side.pitch)
|
||
let winner = max(pitchIsLongitudinal, rollIsLongitudinal)
|
||
let loser = min(pitchIsLongitudinal, rollIsLongitudinal)
|
||
// Liegen beide Deutungen dicht beieinander, ist die Zuordnung wirklich
|
||
// nicht zu treffen – etwa wenn zweimal um dieselbe Achse gekippt wurde.
|
||
guard winner >= ambiguityMargin * loser else { return .failure(.ambiguous) }
|
||
|
||
var orientation = SensorOrientation()
|
||
orientation.longitudinalSource = pitchIsLongitudinal > rollIsLongitudinal ? .pitch : .roll
|
||
let longitudinal = orientation.longitudinalSource == .pitch ? nose.pitch : nose.roll
|
||
let lateral = orientation.longitudinalSource == .pitch ? side.roll : side.pitch
|
||
// Die zugeordnete Achse muss in ihrem Schritt auch deutlich gewandert
|
||
// sein, sonst stünde das Vorzeichen auf wackligem Grund.
|
||
guard abs(longitudinal) >= minimumTilt, abs(lateral) >= minimumTilt else {
|
||
return .failure(.tooLittleMovement)
|
||
}
|
||
// Front nach unten heisst: das Heck steht höher, die Längsneigung des
|
||
// Fahrzeugs ist also positiv.
|
||
orientation.invertLongitudinal = longitudinal < 0
|
||
// Linke Seite nach unten heisst: rechts steht höher, die Querneigung
|
||
// ist positiv.
|
||
orientation.invertLateral = lateral < 0
|
||
return .success(orientation)
|
||
}
|
||
}
|