Kühlbox wieder bedienbar, Einbaulage richtig messen

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>
This commit is contained in:
BiasF
2026-08-30 23:43:57 +02:00
co-authored by Claude Opus 5
parent 583aa75595
commit 270c701ad6
6 changed files with 202 additions and 54 deletions
+34 -27
View File
@@ -56,6 +56,9 @@ enum OrientationDetection {
/// 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
@@ -78,51 +81,55 @@ enum OrientationDetection {
return "Zu wenig Bewegung. Deutlicher kippen, mindestens eine "
+ "Handbreit."
case .ambiguous:
return "Es wurde in beide Richtungen gleichzeitig gekippt. "
+ "Möglichst nur um die eine Achse kippen."
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> {
// Welche Achse reagiert auf das Kippen nach vorne?
guard let longitudinal = dominantAxis(nose) else {
return .failure(max(abs(nose.pitch), abs(nose.roll)) < minimumTilt
? .tooLittleMovement : .ambiguous)
guard max(abs(nose.pitch), abs(nose.roll)) >= minimumTilt,
max(abs(side.pitch), abs(side.roll)) >= minimumTilt else {
return .failure(.tooLittleMovement)
}
guard let lateral = dominantAxis(side) else {
return .failure(max(abs(side.pitch), abs(side.roll)) < minimumTilt
? .tooLittleMovement : .ambiguous)
}
// Beide Bewegungen dürfen nicht auf dieselbe Achse zeigen dann wurde
// zweimal dasselbe gekippt.
guard longitudinal.source != lateral.source else { return .failure(.ambiguous) }
// 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 = longitudinal.source
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.change < 0
orientation.invertLongitudinal = longitudinal < 0
// Linke Seite nach unten heisst: rechts steht höher, die Querneigung
// ist positiv.
orientation.invertLateral = lateral.change < 0
orientation.invertLateral = lateral < 0
return .success(orientation)
}
/// Die Achse, die sich deutlich stärker bewegt hat.
private static func dominantAxis(_ change: Reading)
-> (source: SensorOrientation.Source, change: Double)? {
let byPitch = abs(change.pitch), byRoll = abs(change.roll)
guard max(byPitch, byRoll) >= minimumTilt else { return nil }
// Die schwächere Achse muss deutlich zurückstehen, sonst wurde schräg
// gekippt und die Zuordnung wäre Zufall.
guard max(byPitch, byRoll) >= 2 * min(byPitch, byRoll) else { return nil }
return byPitch > byRoll ? (.pitch, change.pitch) : (.roll, change.roll)
}
}