Files
Camper-Monitor/CamperMonitor/Store/DemoData.swift
T
BiasFandClaude Opus 5 1b9486aad4 Neigungsmesser VanAlign einbinden
Das ESPHome-Projekt VanAlign Pro misst über einen MPU6050 die Längs- und
Querneigung des Fahrzeugs und stellt sie über Bluetooth bereit. Damit
lässt sich der Camper beim Parken ausrichten.

Der Neigungsmesser bewirbt seinen Dienst, wird beim Einrichten also
sicher erkannt und die Geräteart vorbelegt. Anders als bei den übrigen
verbundenen Geräten gibt es hier kein Rahmenprotokoll: jede Messgrösse
liegt in einer eigenen Charakteristik als Float. LevelSession ist deshalb
eine eigene, deutlich einfachere Sitzungsart neben BMSSession - ohne
Protokollerkennung und ohne Kandidatensuche.

Bevorzugt werden die Werte abonniert; bietet das Gerät das nicht an, wird
im Takt abgefragt. Damit läuft die App auch mit der alten Firmware 1.0,
die nur Lesen kennt.

Die Detailansicht zeigt eine Libelle: die Blase wandert dorthin, wo das
Fahrzeug höher steht. Grün bis 0,5 Grad, orange bis zwei, darüber rot -
die Akzentfarbe der App ist selbst grün, "schief" hätte sonst genauso
ausgesehen wie "eben". Die Ringe sind ein echter Massstab (Toleranz und
zwei Grad), sonst sagt die Blasenlage nichts über die verbleibende
Abweichung. Dazu die Ansage in Worten, welche Seite höher steht.

Die Winkel werden fest little-endian gelesen. Die Web-Oberfläche des
Ursprungsprojekts probiert zusätzlich die umgekehrte Reihenfolge, falls
die erste unplausibel wirkt. Das ist nicht nur unnötig, sondern
schädlich: ein vertauschter Float von 4,25 Grad liest sich als etwa 0,0
und wirkt damit völlig plausibel - der Fehler bliebe unbemerkt. Ein Test
hält das fest.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-30 15:05:34 +02:00

146 lines
6.7 KiB
Swift

import Foundation
/// Füllt die App mit erfundenen Messwerten, damit sich die Ansichten ohne
/// Fahrzeug und ohne Bluetooth prüfen lassen.
///
/// Nur in Debug-Builds und nur, wenn beim Start `CAMPER_DEMO=1` gesetzt ist:
///
/// xcrun simctl launch --terminate-running-process \
/// booted de.fritob.CamperMonitor
/// # mit: SIMCTL_CHILD_CAMPER_DEMO=1 davor
///
/// Im normalen Betrieb wird hiervon nichts ausgeführt.
enum DemoData {
static var isEnabled: Bool {
#if DEBUG
return ProcessInfo.processInfo.environment["CAMPER_DEMO"] == "1"
#else
return false
#endif
}
static let mainProfile = Profile(id: Profile.defaultID, name: "Kastenwagen", symbol: "box.truck")
static let secondProfile = Profile(
id: UUID(uuidString: "00000000-0000-0000-0000-0000000000C2")!,
name: "Wohnwagen", symbol: "car.side")
static var profiles: [Profile] { [mainProfile, secondProfile] }
static let booster = ConfiguredDevice(
id: UUID(uuidString: "00000000-0000-0000-0000-0000000000B0")!,
name: "Ladebooster", role: .chargeBooster, profileID: Profile.defaultID,
peripheralID: UUID(uuidString: "00000000-0000-0000-0000-0000000000B1")!)
static let solar = ConfiguredDevice(
id: UUID(uuidString: "00000000-0000-0000-0000-000000000050")!,
name: "Solar Dach", role: .solarCharger, profileID: Profile.defaultID,
peripheralID: UUID(uuidString: "00000000-0000-0000-0000-000000000051")!)
static let battery = ConfiguredDevice(
id: UUID(uuidString: "00000000-0000-0000-0000-0000000000A0")!,
name: "Bulltron 200 Ah", role: .bms, profileID: Profile.defaultID,
peripheralID: UUID(uuidString: "00000000-0000-0000-0000-0000000000A1")!)
static let caravanSolar = ConfiguredDevice(
id: UUID(uuidString: "00000000-0000-0000-0000-0000000000C3")!,
name: "Solar Wohnwagen", role: .solarCharger, profileID: secondProfile.id,
peripheralID: UUID(uuidString: "00000000-0000-0000-0000-0000000000C4")!)
static let fridge = ConfiguredDevice(
id: UUID(uuidString: "00000000-0000-0000-0000-0000000000F0")!,
name: "Kühlbox", role: .fridge, profileID: Profile.defaultID,
peripheralID: UUID(uuidString: "00000000-0000-0000-0000-0000000000F1")!)
static let level = ConfiguredDevice(
id: UUID(uuidString: "00000000-0000-0000-0000-0000000000E0")!,
name: "Nivellierung", role: .leveling, profileID: Profile.defaultID,
peripheralID: UUID(uuidString: "00000000-0000-0000-0000-0000000000E1")!)
static var devices: [ConfiguredDevice] {
[solar, booster, battery, fridge, level, caravanSolar]
}
/// Leicht schräg stehend, damit die Libelle etwas zu zeigen hat.
static var levelState: LevelState {
var state = LevelState()
state.pitch = 1.8
state.roll = -0.9
state.pitchOffset = 0.4
state.rollOffset = -0.2
return state
}
/// Zustand der Demo-Kühlbox: eingeschaltet, Eco, Soll 4 °C, ist 6 °C.
static var fridgeState: AlpicoolState {
var state = AlpicoolState()
state.apply(AlpicoolProtocol.Frame(command: 0x01, payload: [
0x00, 0x01, 0x01, 0x00, 0x04, 0x14, 0xE2, 0x01, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x06, 0x57, 0x0C, 0x07,
]))
return state
}
static func snapshots() -> [DeviceSnapshot] {
var solarSnapshot = DeviceSnapshot(deviceID: solar.id, timestamp: Date(), rssi: -58)
solarSnapshot.state = "Konstantspannung (Absorption)"
solarSnapshot.metrics = [
Metric("pv_power", "PV-Leistung", 284, unit: "W", precision: 0, primary: true),
Metric("battery_power", "Ladeleistung", 262, unit: "W", precision: 0),
Metric("battery_voltage", "Batteriespannung", 14.12, unit: "V", precision: 2),
Metric("battery_current", "Ladestrom", 18.6, unit: "A", precision: 1),
Metric("yield_today", "Ertrag heute", 1.34, unit: "kWh", precision: 2),
]
var boosterSnapshot = DeviceSnapshot(deviceID: booster.id, timestamp: Date(), rssi: -71)
boosterSnapshot.state = "Aus"
boosterSnapshot.offReasons = ["Keine Eingangsspannung"]
boosterSnapshot.metrics = [
Metric("output_voltage", "Ausgang (Aufbaubatterie)", 14.09, unit: "V", precision: 2, primary: true),
Metric("input_voltage", "Eingang (Starterbatterie)", 12.42, unit: "V", precision: 2),
]
let cells = [3.412, 3.418, 3.409, 3.421]
var batterySnapshot = DeviceSnapshot(deviceID: battery.id, timestamp: Date(), rssi: -64)
batterySnapshot.state = "Lädt"
batterySnapshot.cellVoltages = cells
batterySnapshot.temperatures = [21, 22]
batterySnapshot.metrics = [
Metric("soc", "Ladezustand", 78.4, unit: "%", precision: 1, primary: true),
Metric("voltage", "Spannung", 13.66, unit: "V", precision: 2),
Metric("current", "Strom", 18.6, unit: "A", precision: 1),
Metric("power", "Leistung", 254, unit: "W", precision: 0),
Metric("capacity", "Restkapazität", 156.8, unit: "Ah", precision: 1),
Metric("cell_delta", "Zell-Differenz", 12, unit: "mV", precision: 0),
Metric("cell_max", "Höchste Zelle (Zelle 4)", 3.421, unit: "V", precision: 3),
Metric("cell_min", "Niedrigste Zelle (Zelle 3)", 3.409, unit: "V", precision: 3),
Metric("temp_max", "Temperatur", 22, unit: "°C", precision: 0),
Metric("cycles", "Ladezyklen", 143, unit: "", precision: 0),
]
var caravanSnapshot = DeviceSnapshot(deviceID: caravanSolar.id, timestamp: Date(), rssi: -77)
caravanSnapshot.state = "Erhaltung (Float)"
caravanSnapshot.metrics = [
Metric("pv_power", "PV-Leistung", 62, unit: "W", precision: 0, primary: true),
Metric("battery_voltage", "Batteriespannung", 13.62, unit: "V", precision: 2),
Metric("battery_current", "Ladestrom", 4.4, unit: "A", precision: 1),
]
return [solarSnapshot, boosterSnapshot, batterySnapshot,
fridgeState.snapshot(deviceID: fridge.id, rssi: -66),
levelState.snapshot(deviceID: level.id, rssi: -70),
caravanSnapshot]
}
/// Ein paar Stunden Verlauf, damit die Grafik etwas zeigt.
static func history(for deviceID: UUID, around value: Double) -> [HistorySample] {
let now = Date()
return (0..<80).reversed().map { step in
let t = Double(step)
let wave = sin(t / 9) * value * 0.18 + cos(t / 23) * value * 0.07
return HistorySample(time: now.addingTimeInterval(-t * 60),
value: max(0, value + wave))
}
}
}