Files
Camper-Monitor/VanControl/Views/LevelSetupView.swift
fototeddyandClaude Sonnet 5 b956324234 Beim ursprünglichen Merge übersehene Dateien nachziehen
Drei Dateien aus solar-integration wurden beim Zusammenführen mit
dev_watch übersehen, weil sie fälschlich als "identisch zwischen den
Branches" eingestuft wurden:

* DemoData.isEnabled kannte nur noch den Umgebungsvariablen-Weg
  (CAMPER_DEMO=1). Der Schalter in den Einstellungen – der einzige Weg,
  Demo-Daten auf einem lokal installierten Build ohne Xcode-Verbindung
  einzuschalten – fehlte ganz, SettingsView.swift war nie zusammengeführt
  worden.
* LevelSetupView.swift fehlte der komplette Abschnitt "Fahrzeuggrafik" –
  die Kachel-Auswahl zwischen den VehicleGraphicStyle-Varianten
  (Vanster/California). Das Feld existierte im Modell und wurde beim
  Rendern gelesen, war aber nirgends einstellbar.
* DeviceCard.swift zeigte die Nivellierungs-Kachel wie jedes andere
  Gerät (Hauptwert + Nebenwerte) statt der grossen Front/Heck- und
  Links/Rechts-Anzeige mit Richtungsbuchstaben.

Anlass: Im Testbuild fehlte deshalb sowohl der Demo-Modus als auch die
Fahrzeuggrafik-Auswahl. Als Konsequenz systematisch alle beim Merge als
"bereits identisch" eingestuften Dateien erneut gegengeprüft – keine
weiteren Lücken gefunden.

Build (App + Watch) und Protokoll-Tests grün.

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

166 lines
7.0 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
/// Einrichtung des Neigungsmessers: Einbaulage und Nullpunkt.
///
/// Beides wird einmal eingestellt und danach kaum wieder angefasst. In der
/// Geräteansicht standen die Knöpfe direkt unter der Libelle ein Fehlgriff
/// beim Ablesen verstellte dort den Nullpunkt. Deshalb liegen sie hier.
///
/// Die Reihenfolge ist nicht beliebig: erst muss klar sein, welche Achse des
/// Sensors welche des Fahrzeugs ist, sonst wird der Nullpunkt auf die falsche
/// Achse gelegt.
struct LevelSetupView: View {
let device: ConfiguredDevice
@Environment(BluetoothManager.self) private var bluetooth
@State private var showSensorSetup = false
@State private var showResetConfirmation = false
private var state: LevelState { bluetooth.levelStates[device.id] ?? LevelState() }
/// Immer der aktuelle Stand `device` veraltet, sobald der Assistent
/// eine neue Einbaulage gespeichert hat.
@Environment(DeviceStore.self) private var store
private var currentDevice: ConfiguredDevice {
store.devices.first { $0.id == device.id } ?? device
}
private let graphicColumns = [GridItem(.adaptive(minimum: 76), spacing: 12)]
var body: some View {
List {
Section {
HStack(spacing: 24) {
reading("Längs", state.pitch)
reading("Quer", state.roll)
}
.frame(maxWidth: .infinity)
.padding(.vertical, 4)
}
Section {
Button {
showSensorSetup = true
} label: {
LabeledContent {
Text(currentDevice.sensorOrientation.summary)
.font(.caption)
} label: {
Label("Einbaulage bestimmen",
systemImage: "arrow.triangle.2.circlepath")
}
}
.disabled(!state.hasReading)
} header: {
Text("Schritt 1 Einbaulage")
} footer: {
Text("Sitzt der Sensor quer oder verdreht im Fahrzeug, meldet er längs "
+ "und quer vertauscht. Der Assistent klärt das durch zweimaliges "
+ "Kippen. Danach den Nullpunkt setzen.")
}
Section {
Button {
bluetooth.calibrateLevel(for: device.id)
} label: {
Label("Aktuelle Lage als eben übernehmen", systemImage: "scope")
}
.disabled(!state.hasReading)
if !state.isKnownUncalibrated {
Button(role: .destructive) {
showResetConfirmation = true
} label: {
// Ohne das bliebe das Symbol in der Akzentfarbe, während
// die Beschriftung rot ist.
Label("Nullpunkt verwerfen", systemImage: "arrow.uturn.backward")
.foregroundStyle(.red)
}
}
} header: {
Text("Schritt 2 Nullpunkt")
} footer: {
Text(calibrationHint)
}
Section {
LazyVGrid(columns: graphicColumns, spacing: 12) {
ForEach(VehicleGraphicStyle.allCases) { style in
Button {
var updated = currentDevice
updated.vehicleGraphicStyle = style
store.update(updated)
} label: {
VStack(spacing: 4) {
Image(style.sideImageName)
.resizable()
.scaledToFit()
.padding(10)
.frame(width: 60, height: 60)
.background(currentDevice.vehicleGraphicStyle == style
? Color.accentColor.opacity(0.18)
: Color.secondary.opacity(0.08),
in: .rect(cornerRadius: 12))
.foregroundStyle(currentDevice.vehicleGraphicStyle == style
? Color.accentColor : Color.primary)
Text(style.title)
.font(.caption2)
.foregroundStyle(currentDevice.vehicleGraphicStyle == style
? Color.accentColor : Color.secondary)
}
}
.buttonStyle(.plain)
.accessibilityLabel(style.title)
}
}
.padding(.vertical, 4)
} header: {
Text("Fahrzeuggrafik")
} footer: {
Text("Wird in der Fahrzeug-Darstellung des Neigungsmessers verwendet. "
+ "Weitere Grafiken folgen.")
}
}
.navigationTitle("Neigungsmesser einrichten")
.navigationBarTitleDisplayMode(.inline)
.sheet(isPresented: $showSensorSetup) {
SensorSetupView(device: currentDevice)
}
.confirmationDialog("Nullpunkt verwerfen?",
isPresented: $showResetConfirmation,
titleVisibility: .visible) {
Button("Verwerfen", role: .destructive) {
bluetooth.resetLevelCalibration(for: device.id)
}
} message: {
Text("Die Anzeige zeigt danach wieder die Lage des Sensors.")
}
}
private var calibrationHint: String {
if state.isCalibrated, let pitchOffset = state.pitchOffset,
let rollOffset = state.rollOffset {
return String(format: "Der Nullpunkt liegt bei %.1f° längs und %.1f° quer. "
+ "Zum Neusetzen das Fahrzeug eben stellen und dann tippen.",
pitchOffset, rollOffset)
}
if state.isKnownUncalibrated {
return "Noch kein Nullpunkt gesetzt die Anzeige zeigt die Lage des "
+ "Sensors, nicht die des Fahrzeugs. Fahrzeug eben stellen, dann tippen."
}
// Ältere Firmware gibt die Offsets nicht heraus.
return "Zum Setzen das Fahrzeug eben stellen und dann tippen. Ob schon ein "
+ "Nullpunkt gesetzt wurde, meldet dieses Gerät nicht zurück."
}
private func reading(_ title: String, _ value: Double?) -> some View {
VStack(spacing: 2) {
Text(value.map { String(format: "%.1f°", $0) } ?? "")
.font(.title2.weight(.semibold).monospacedDigit())
Text(title)
.font(.caption)
.foregroundStyle(.secondary)
}
}
}