Die WattCycle-Batterie meldete sich am FFF0-Dienst, nahm aber keine Kommandos an. Grund: dort ist FFF1 die erste beschreibbare Charakteristik, sie sieht schreibbar aus und bleibt trotzdem stumm – Kommandos gehören auf FFF2. Statt die richtige Charakteristik zu raten, stellt BMSSession jetzt alle Paare aus schreibbarer und benachrichtigender Charakteristik zusammen, jeweils mit und ohne Schreibbestätigung, und arbeitet sie ab, bis eines antwortet. Bekannte Paare (FFF2/FFF1, FF02/FF01, Nordic UART) kommen zuerst dran; ein Kandidat, der sich nicht abonnieren lässt, wird sofort übersprungen. Auf jedem Weg werden weiterhin Daly klassisch, Daly Modbus und JBD angefragt. Die Diagnose zeigt dazu den vollständigen GATT-Baum des Geräts, den gerade versuchten Weg samt Position in der Kandidatenliste sowie Zähler für gesendete Anfragen und empfangene Bytes. Damit lässt sich unterscheiden, ob das BMS die Kommandos gar nicht annimmt oder ein unbekanntes Protokoll spricht. Nebenbei: "caravan" existiert nicht als SF-Symbol und ließ SwiftUI stillschweigend auf Text zurückfallen – in den Demodaten ersetzt. README auf Profile und WattCycle/JBD nachgezogen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
111 lines
5.3 KiB
Swift
111 lines
5.3 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 var devices: [ConfiguredDevice] { [solar, booster, battery, caravanSolar] }
|
|
|
|
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, 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))
|
|
}
|
|
}
|
|
}
|