first commit
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
import Foundation
|
||||
|
||||
/// Neigungsmesser „VanAlign Pro“ – ein ESP32 mit MPU6050, der Längs- und
|
||||
/// Querneigung des Fahrzeugs über Bluetooth bereitstellt.
|
||||
///
|
||||
/// Anders als die übrigen Geräte gibt es hier kein Rahmenprotokoll: Jede
|
||||
/// Messgrösse liegt in einer eigenen Charakteristik als 32-Bit-Float.
|
||||
enum VanAlignProtocol {
|
||||
|
||||
/// Wird vom Gerät beworben, das Gerät ist darüber auffindbar.
|
||||
static let serviceUUID = "2A24B789-7AAB-4535-AF3E-EE76A35CC42D"
|
||||
|
||||
static let pitchUUID = "CAD48E28-7FBE-41CF-BAE9-D77A6C233424"
|
||||
static let rollUUID = "CAD48E28-7FBE-41CF-BAE9-D77A6C233425"
|
||||
/// Zwei Floats: die gespeicherten Kalibrier-Offsets.
|
||||
static let offsetsUUID = "CAD48E28-7FBE-41CF-BAE9-D77A6C233426"
|
||||
/// Ein Byte: 0 setzt zurück, alles andere kalibriert auf die aktuelle Lage.
|
||||
static let calibrateUUID = "CAD48E28-7FBE-41CF-BAE9-D77A6C233427"
|
||||
/// Acht Byte: die Einbaulage, lesbar und schreibbar. Siehe `orientation`.
|
||||
static let orientationUUID = "CAD48E28-7FBE-41CF-BAE9-D77A6C233428"
|
||||
|
||||
static let calibrateCommand = Data([0x01])
|
||||
static let resetCommand = Data([0x00])
|
||||
|
||||
/// Liest einen Winkel aus vier Bytes, little-endian.
|
||||
///
|
||||
/// Die Firmware legt den Float per `memcpy` ab, und der ESP32 ist
|
||||
/// little-endian – die Reihenfolge steht also fest. Die Web-Oberfläche des
|
||||
/// Ursprungsprojekts probiert zusätzlich die umgekehrte Reihenfolge, falls
|
||||
/// die erste unplausibel aussieht. Das ist nicht nur unnötig, sondern
|
||||
/// schädlich: ein vertauschter Float von 4,25° ergibt gelesen etwa 0,0 und
|
||||
/// wirkt damit völlig plausibel. Ein Vorzeichen- oder Wertfehler bliebe so
|
||||
/// unbemerkt.
|
||||
static func angle(from data: Data) -> Double? {
|
||||
guard data.count >= 4 else { return nil }
|
||||
var raw: UInt32 = 0
|
||||
for (index, byte) in data.prefix(4).enumerated() {
|
||||
raw |= UInt32(byte) << UInt32(8 * index)
|
||||
}
|
||||
let value = Float(bitPattern: raw)
|
||||
// NAN meldet die Firmware, solange der Sensor nichts liefert.
|
||||
guard value.isFinite, abs(value) <= 180 else { return nil }
|
||||
return Double(value)
|
||||
}
|
||||
|
||||
/// Die Einbaulage, wie sie im Gerät liegt.
|
||||
///
|
||||
/// Acht Byte:
|
||||
///
|
||||
/// 0 Version, 1 = gültig gesetzt, 0 = nie geschrieben
|
||||
/// 1 Längsachse: 0 = Pitch des Sensors, 1 = Roll des Sensors
|
||||
/// 2 längs umgekehrt (0/1)
|
||||
/// 3 quer umgekehrt (0/1)
|
||||
/// 4..7 Verdrehung um die Hochachse, float32, Grad
|
||||
///
|
||||
/// Sie gehört ins Gerät, weil sie den Einbau beschreibt und nicht das
|
||||
/// Telefon: iPhone, Uhr und Android sollen dieselbe sehen, ohne sie je
|
||||
/// einzeln zu bestimmen. Gerechnet wird trotzdem in den Apps – das Gerät
|
||||
/// verwahrt sie nur, sonst rechnete ein älterer Client die Korrektur ein
|
||||
/// zweites Mal ein.
|
||||
///
|
||||
/// Version 0 heisst „hier stand noch nie etwas“ und ergibt nil; dann gilt,
|
||||
/// was die App örtlich gespeichert hat, und sie schreibt es hinauf.
|
||||
static func orientation(from data: Data) -> SensorOrientation? {
|
||||
let bytes = [UInt8](data)
|
||||
guard bytes.count >= 8, bytes[0] == 1 else { return nil }
|
||||
var orientation = SensorOrientation()
|
||||
orientation.longitudinalSource = bytes[1] == 1 ? .roll : .pitch
|
||||
orientation.invertLongitudinal = bytes[2] != 0
|
||||
orientation.invertLateral = bytes[3] != 0
|
||||
guard let twist = angle(from: data.dropFirst(4).prefix(4)) else { return nil }
|
||||
orientation.twist = twist
|
||||
return orientation
|
||||
}
|
||||
|
||||
/// Dieselben acht Byte in die andere Richtung.
|
||||
static func encoded(_ orientation: SensorOrientation) -> Data {
|
||||
var bytes = [UInt8](repeating: 0, count: 8)
|
||||
bytes[0] = 1
|
||||
bytes[1] = orientation.longitudinalSource == .roll ? 1 : 0
|
||||
bytes[2] = orientation.invertLongitudinal ? 1 : 0
|
||||
bytes[3] = orientation.invertLateral ? 1 : 0
|
||||
let raw = Float(orientation.twist).bitPattern
|
||||
for index in 0..<4 {
|
||||
bytes[4 + index] = UInt8((raw >> UInt32(8 * index)) & 0xFF)
|
||||
}
|
||||
return Data(bytes)
|
||||
}
|
||||
|
||||
/// Die beiden gespeicherten Offsets.
|
||||
static func offsets(from data: Data) -> (pitch: Double, roll: Double)? {
|
||||
guard data.count >= 8,
|
||||
let pitch = angle(from: data.prefix(4)),
|
||||
let roll = angle(from: data.dropFirst(4).prefix(4)) else { return nil }
|
||||
return (pitch, roll)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user