first commit
This commit is contained in:
@@ -0,0 +1,213 @@
|
||||
import Foundation
|
||||
|
||||
/// Hilft beim Ausrichten des Fahrzeugs während des Rangierens.
|
||||
///
|
||||
/// Bewusst **ohne** Positionsbestimmung: Aus einem MEMS-Beschleunigungssensor
|
||||
/// lässt sich keine brauchbare Strecke ableiten, weil der Fehler beim
|
||||
/// zweifachen Integrieren quadratisch mit der Zeit wächst. Beim Rangieren im
|
||||
/// Schritttempo gehen die tatsächlichen Beschleunigungen ohnehin im Rauschen
|
||||
/// unter.
|
||||
///
|
||||
/// Gebraucht wird das auch gar nicht. Die Frage beim Einparken lautet nie „wo
|
||||
/// stehe ich“, sondern „wird es besser oder schlechter, und wo war es am
|
||||
/// besten“. Beides steckt bereits im zeitlichen Verlauf der Neigung – ganz
|
||||
/// ohne Annahmen über das Gelände.
|
||||
struct AlignmentAssistant {
|
||||
|
||||
struct Sample: Equatable {
|
||||
let time: Date
|
||||
let pitch: Double
|
||||
let roll: Double
|
||||
|
||||
/// Gesamtabweichung von der Waagerechten.
|
||||
var deviation: Double { (pitch * pitch + roll * roll).squareRoot() }
|
||||
}
|
||||
|
||||
/// Wie lange zurückgeschaut wird.
|
||||
static let memory: TimeInterval = 90
|
||||
/// Ab dieser Verbesserung lohnt der Hinweis auf einen früheren Punkt.
|
||||
static let worthGoingBack = 0.2
|
||||
/// Unterhalb dieser Änderung gilt die Lage als unverändert.
|
||||
static let trendThreshold = 0.08
|
||||
|
||||
private(set) var samples: [Sample] = []
|
||||
|
||||
enum Trend: Equatable {
|
||||
case improving
|
||||
case worsening
|
||||
case steady
|
||||
case unknown
|
||||
|
||||
var text: String {
|
||||
switch self {
|
||||
case .improving: return "Wird besser"
|
||||
case .worsening: return "Wird schlechter"
|
||||
case .steady: return "Bleibt gleich"
|
||||
case .unknown: return "Messe…"
|
||||
}
|
||||
}
|
||||
|
||||
var symbol: String {
|
||||
switch self {
|
||||
case .improving: return "arrow.down.right.circle.fill"
|
||||
case .worsening: return "arrow.up.right.circle.fill"
|
||||
case .steady: return "equal.circle.fill"
|
||||
case .unknown: return "hourglass.circle"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mutating func add(pitch: Double, roll: Double, at time: Date = Date()) {
|
||||
samples.append(Sample(time: time, pitch: pitch, roll: roll))
|
||||
let cutoff = time.addingTimeInterval(-Self.memory)
|
||||
samples.removeAll { $0.time < cutoff }
|
||||
}
|
||||
|
||||
mutating func reset() {
|
||||
samples.removeAll()
|
||||
}
|
||||
|
||||
var current: Sample? { samples.last }
|
||||
|
||||
/// Der flachste Punkt, den wir gesehen haben.
|
||||
var best: Sample? {
|
||||
samples.min { $0.deviation < $1.deviation }
|
||||
}
|
||||
|
||||
/// Ob und wie stark sich die Lage gerade ändert.
|
||||
///
|
||||
/// Verglichen wird das jüngste Drittel mit dem davorliegenden. Einzelne
|
||||
/// Messwerte wären zu unruhig; das Fahrzeug wippt beim Rangieren.
|
||||
var trend: Trend {
|
||||
guard samples.count >= 6 else { return .unknown }
|
||||
let recent = samples.suffix(3)
|
||||
let previous = samples.dropLast(3).suffix(3)
|
||||
guard !previous.isEmpty else { return .unknown }
|
||||
|
||||
let now = recent.map(\.deviation).reduce(0, +) / Double(recent.count)
|
||||
let before = previous.map(\.deviation).reduce(0, +) / Double(previous.count)
|
||||
let change = now - before
|
||||
|
||||
if abs(change) < Self.trendThreshold { return .steady }
|
||||
return change < 0 ? .improving : .worsening
|
||||
}
|
||||
|
||||
/// Wieviel besser der beste Punkt gegenüber jetzt war – nil, wenn es sich
|
||||
/// nicht lohnt oder wir gerade selbst am besten Punkt stehen.
|
||||
var improvementAtBest: Double? {
|
||||
guard let current, let best, best.time < current.time else { return nil }
|
||||
let gain = current.deviation - best.deviation
|
||||
return gain >= Self.worthGoingBack ? gain : nil
|
||||
}
|
||||
|
||||
/// Wie lange der beste Punkt zurückliegt.
|
||||
var timeSinceBest: TimeInterval? {
|
||||
guard improvementAtBest != nil, let current, let best else { return nil }
|
||||
return current.time.timeIntervalSince(best.time)
|
||||
}
|
||||
|
||||
/// Was der Fahrer jetzt tun soll.
|
||||
var advice: String {
|
||||
guard let current else { return "Warte auf Messwerte…" }
|
||||
if current.deviation <= LevelState.levelTolerance {
|
||||
return "Steht eben – anhalten"
|
||||
}
|
||||
if let seconds = timeSinceBest {
|
||||
return String(format: "Vor %.0f s stand es besser – ein Stück zurück",
|
||||
seconds.rounded())
|
||||
}
|
||||
switch trend {
|
||||
case .improving: return "Wird besser – weiter so"
|
||||
case .worsening: return "Wird schlechter – andere Richtung"
|
||||
case .steady: return "Ändert sich kaum – andere Richtung versuchen"
|
||||
case .unknown: return "Langsam weiterfahren"
|
||||
}
|
||||
}
|
||||
|
||||
var hasReachedTarget: Bool {
|
||||
(current?.deviation ?? .infinity) <= LevelState.levelTolerance
|
||||
}
|
||||
}
|
||||
|
||||
/// Wie hoch jedes einzelne Rad unterlegt werden muss.
|
||||
///
|
||||
/// Getrennte Angaben wie „rechts 8 cm" und „vorne 4 cm" beschreiben dasselbe
|
||||
/// Fahrzeug und lassen sich nicht getrennt ausführen: Unter „rechts" liegen
|
||||
/// zwei Räder, unter „vorne" auch, und zwei davon sind dieselben. Gefragt ist
|
||||
/// deshalb die Höhe **je Rad**.
|
||||
///
|
||||
/// Das ist reine Geometrie und damit exakt. Das Fahrzeug steht auf vier
|
||||
/// Punkten; deren Höhen ergeben sich aus Spurweite, Radstand und den beiden
|
||||
/// Winkeln. Angehoben wird auf die Höhe des höchsten Rades – das bleibt
|
||||
/// liegen, die anderen bekommen die Differenz.
|
||||
struct LevelingLift: Equatable {
|
||||
|
||||
enum Wheel: CaseIterable, Hashable {
|
||||
case frontLeft, frontRight, rearLeft, rearRight
|
||||
|
||||
var text: String {
|
||||
switch self {
|
||||
case .frontLeft: return "vorne links"
|
||||
case .frontRight: return "vorne rechts"
|
||||
case .rearLeft: return "hinten links"
|
||||
case .rearRight: return "hinten rechts"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Höhen in Metern, je Rad. Das höchste Rad steht auf 0.
|
||||
let frontLeft: Double
|
||||
let frontRight: Double
|
||||
let rearLeft: Double
|
||||
let rearRight: Double
|
||||
|
||||
func height(_ wheel: Wheel) -> Double {
|
||||
switch wheel {
|
||||
case .frontLeft: return frontLeft
|
||||
case .frontRight: return frontRight
|
||||
case .rearLeft: return rearLeft
|
||||
case .rearRight: return rearRight
|
||||
}
|
||||
}
|
||||
|
||||
func centimetres(_ wheel: Wheel) -> Double { height(wheel) * 100 }
|
||||
|
||||
/// Das am tiefsten stehende Rad – dort liegt die grösste Höhe.
|
||||
var deepest: Wheel {
|
||||
Wheel.allCases.max { height($0) < height($1) } ?? .frontLeft
|
||||
}
|
||||
|
||||
var maximumCentimetres: Double { centimetres(deepest) }
|
||||
|
||||
/// Unter einem Zentimeter lohnt kein Keil; so genau steht kein Fahrzeug,
|
||||
/// und so genau misst der Sensor auch nicht.
|
||||
var isNegligible: Bool { maximumCentimetres < 1 }
|
||||
|
||||
/// `pitch` positiv heisst, das Heck steht höher; `roll` positiv, die rechte
|
||||
/// Seite steht höher. Ohne Fahrzeugmasse gibt es nichts zu rechnen.
|
||||
static func compute(pitch: Double,
|
||||
roll: Double,
|
||||
trackWidth: Double,
|
||||
wheelbase: Double) -> LevelingLift? {
|
||||
guard trackWidth > 0, wheelbase > 0 else { return nil }
|
||||
|
||||
// Wie viel höher die rechte Seite und wie viel höher das Heck steht.
|
||||
// Vorzeichen inbegriffen: tan(-2°) ist negativ, dann steht links höher.
|
||||
let rightRise = tan(roll * .pi / 180) * trackWidth
|
||||
let rearRise = tan(pitch * .pi / 180) * wheelbase
|
||||
|
||||
// Höhen der vier Aufstandspunkte, bezogen auf vorne links.
|
||||
let heights: [Wheel: Double] = [
|
||||
.frontLeft: 0,
|
||||
.frontRight: rightRise,
|
||||
.rearLeft: rearRise,
|
||||
.rearRight: rightRise + rearRise,
|
||||
]
|
||||
let top = heights.values.max() ?? 0
|
||||
|
||||
return LevelingLift(frontLeft: top - heights[.frontLeft]!,
|
||||
frontRight: top - heights[.frontRight]!,
|
||||
rearLeft: top - heights[.rearLeft]!,
|
||||
rearRight: top - heights[.rearRight]!)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user