Files
Camper-Monitor/VanControl/Views/SensorSetupView.swift
fototeddyandClaude Sonnet 5 303a9735d0 App-Namen im iOS-Projekt auf VanControl vereinheitlichen
CamperMonitor (Haupt-Repo) und VanAligneiOS (aus dem gemergten
solar-integration-Branch) liefen unter zwei verschiedenen internen
Namen, obwohl die App nach aussen längst einheitlich "VanControl Pro"
heisst. Jetzt durchgängig VanControl:

- Ordner: CamperMonitor/, CamperMonitorWatch/, CamperMonitorComplication/,
  VanAligneiOSWidget/ → VanControl/, VanControlWatch/,
  VanControlComplication/, VanControlWidget/
- Xcode-Projekt: CamperMonitor.xcodeproj → VanControl.xcodeproj, alle
  Targets/Schemes/Produktnamen entsprechend umbenannt
- Bundle-Identifier auf Wunsch mitgeändert: de.s0.fototeddy.VanControl*
  (App noch nicht veröffentlicht); dabei auch die
  WKCompanionAppBundleIdentifier-Werte korrigiert, die noch das alte
  de.fritob-Präfix statt des tatsächlichen de.s0.fototeddy-Präfixes
  trugen
- Swift-Dateien/Typen: CamperMonitorApp → VanControlApp,
  VanAligneiOSWidget* → VanControlWidget*
- Config/*-Info.plist umbenannt, README.md/Tools/README.md/run-tests.sh
  auf die neuen Pfade angepasst

Bewusst unverändert: firmware/vanalign und alle Bezüge auf "VanAlign"
als Namen der Neigungsmesser-Hardware (eigenständiges Produkt, kein
App-Name) sowie der komplette Android/-Ordner.

Build (App, Watch, Debug) und Protokoll-Testlauf grün.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 21:25:28 +02:00

202 lines
7.6 KiB
Swift
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import SwiftUI
/// Führt durch die Einrichtung der Einbaulage des Neigungsmessers.
///
/// Der Sensor kann quer, gedreht oder kopfüber sitzen. Statt die Lage aus einer
/// Liste raten zu lassen, wird sie gemessen: zweimal kippen, einmal um jede
/// Achse, und aus der Reaktion ergibt sich die Zuordnung.
struct SensorSetupView: View {
let device: ConfiguredDevice
@Environment(DeviceStore.self) private var store
@Environment(BluetoothManager.self) private var bluetooth
@Environment(\.dismiss) private var dismiss
private enum Step {
case intro
case tiltNose
case settle
case tiltSide
case done(SensorOrientation)
case failed(OrientationDetection.Failure)
}
@State private var step: Step = .intro
/// Ruhelage, auf die beide Kippbewegungen bezogen werden.
@State private var reference = OrientationDetection.Reading(pitch: 0, roll: 0)
@State private var noseChange: OrientationDetection.Reading?
private var state: LevelState { bluetooth.levelStates[device.id] ?? LevelState() }
private var live: OrientationDetection.Reading? {
guard let pitch = state.rawPitch, let roll = state.rawRoll else { return nil }
return OrientationDetection.Reading(pitch: pitch, roll: roll)
}
var body: some View {
NavigationStack {
ScrollView {
VStack(spacing: 24) {
content
}
.padding()
.frame(maxWidth: .infinity)
}
.background(Color(.systemGroupedBackground))
.navigationTitle("Einbaulage")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button("Abbrechen") { dismiss() }
}
}
}
}
@ViewBuilder
private var content: some View {
switch step {
case .intro:
card(icon: "arrow.triangle.2.circlepath",
title: "Einbaulage bestimmen",
text: "Sitzt der Sensor quer oder verdreht im Fahrzeug, meldet er "
+ "die Neigung vertauscht. Um das zu klären, wird er gleich "
+ "zweimal gekippt.\n\nBaue ihn dazu so ein oder halte ihn so, "
+ "wie er später sitzen soll. Er muss nicht angeschraubt sein "
+ "nur die Ausrichtung muss stimmen.")
liveReadout
Button("Los gehts") {
reference = live ?? .init(pitch: 0, roll: 0)
step = .tiltNose
}
.buttonStyle(.borderedProminent)
.disabled(live == nil)
case .tiltNose:
card(icon: "arrow.down.forward",
title: "Schritt 1 von 2: nach vorne kippen",
text: "Kippe den Sensor so, als würde das Fahrzeug **vorne "
+ "abwärts** stehen die Front also nach unten.\n\n"
+ "Deutlich kippen, etwa eine Handbreit, und in dieser Lage "
+ "halten. Dann weiter.")
liveReadout
Button("Weiter") { finishNoseStep() }
.buttonStyle(.borderedProminent)
.disabled(live == nil)
case .settle:
card(icon: "equal.circle",
title: "Zurück in die Ruhelage",
text: "Stelle den Sensor wieder so hin wie am Anfang und lass "
+ "ihn kurz ruhen.\n\nVon hier aus wird die zweite Bewegung "
+ "gemessen.")
liveReadout
Button("Weiter") {
reference = live ?? reference
step = .tiltSide
}
.buttonStyle(.borderedProminent)
.disabled(live == nil)
case .tiltSide:
card(icon: "arrow.down.left",
title: "Schritt 2 von 2: nach links kippen",
text: "Kippe den Sensor jetzt so, als würde das Fahrzeug **nach "
+ "links** hängen die linke Seite also nach unten.\n\n"
+ "Wieder deutlich kippen und in dieser Lage halten.")
liveReadout
HStack {
Button("Zurück") { step = .settle }
.buttonStyle(.bordered)
Button("Fertig") { finishSideStep() }
.buttonStyle(.borderedProminent)
.disabled(live == nil)
}
case .done(let orientation):
card(icon: "checkmark.circle.fill",
title: "Einbaulage erkannt",
text: "Ergebnis: **\(orientation.summary)**.\n\nDie Anzeige rechnet "
+ "die Werte des Sensors ab jetzt auf die Achsen des Fahrzeugs "
+ "um. Vergiss nicht, anschliessend im ebenen Stand zu "
+ "kalibrieren.")
Button("Übernehmen") { save(orientation) }
.buttonStyle(.borderedProminent)
case .failed(let failure):
card(icon: "exclamationmark.triangle.fill",
title: "Das hat nicht geklappt",
text: failure.message)
Button("Nochmal versuchen") { step = .intro }
.buttonStyle(.borderedProminent)
}
}
private func card(icon: String, title: String, text: String) -> some View {
VStack(spacing: 12) {
Image(systemName: icon)
.font(.system(size: 44))
.foregroundStyle(.tint)
Text(title)
.font(.title3.weight(.semibold))
.multilineTextAlignment(.center)
Text(.init(text))
.multilineTextAlignment(.center)
.foregroundStyle(.secondary)
}
.frame(maxWidth: .infinity)
.padding()
.background(Color(.secondarySystemGroupedBackground), in: .rect(cornerRadius: 16))
}
/// Was der Sensor gerade meldet ohne Umrechnung, denn die wird hier ja
/// erst bestimmt.
private var liveReadout: some View {
HStack(spacing: 16) {
value("Achse A", state.rawPitch)
value("Achse B", state.rawRoll)
}
}
private func value(_ title: String, _ reading: Double?) -> some View {
VStack(spacing: 4) {
Text(reading.map { String(format: "%.1f°", $0) } ?? "")
.font(.title3.weight(.semibold).monospacedDigit())
Text(title)
.font(.caption)
.foregroundStyle(.secondary)
}
.frame(maxWidth: .infinity)
.padding(.vertical, 10)
.background(Color(.secondarySystemGroupedBackground), in: .rect(cornerRadius: 12))
}
// MARK: - Ablauf
private func finishNoseStep() {
guard let live else { return }
noseChange = live - reference
// Der Bezug bleibt die Ruhelage. Von der gekippten Lage aus zu messen
// wäre falsch: die zweite Messung enthielte dann das Zurückkippen aus
// der ersten, und beide Achsen schlügen aus.
step = .settle
}
private func finishSideStep() {
guard let live, let nose = noseChange else { return }
let side = live - reference
switch OrientationDetection.orientation(nose: nose, side: side) {
case .success(let orientation): step = .done(orientation)
case .failure(let failure): step = .failed(failure)
}
}
private func save(_ orientation: SensorOrientation) {
var updated = device
updated.sensorOrientation = orientation
store.update(updated)
bluetooth.updateSensorOrientation(for: updated)
dismiss()
}
}