forked from fritob/Camper-Monitor
Android: Demo-Modus für den Emulator
Der Emulator hat kein Bluetooth. Damit sich die Ansichten trotzdem ansehen und durchklicken lassen, füttert ein Demo-Modus fertige Werte ein - dasselbe Vorgehen wie in der iOS-Fassung. Nur in Debug-Bauten und nur, wenn beim Start das Extra gesetzt ist: adb shell am start -n de.fritob.campermonitor/.MainActivity --ez demo true Im Demo-Modus funkt nichts und es wird nichts abgelegt; die echten Geräte und Fahrzeuge bleiben unberührt. Der Kühlbox-Zustand kommt dabei aus einem echten Einzonen-Datensatz, wie ihn die Box im Fahrzeug schickt - inklusive des Fühlerplatzhalters, damit die Anzeige auch im Demo zeigt, was sie im Fahrzeug zeigen würde. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -34,6 +34,7 @@ android {
|
|||||||
|
|
||||||
buildFeatures {
|
buildFeatures {
|
||||||
compose = true
|
compose = true
|
||||||
|
buildConfig = true
|
||||||
}
|
}
|
||||||
|
|
||||||
sourceSets["main"].java.srcDirs("src/main/kotlin")
|
sourceSets["main"].java.srcDirs("src/main/kotlin")
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import androidx.navigation.compose.NavHost
|
|||||||
import androidx.navigation.compose.composable
|
import androidx.navigation.compose.composable
|
||||||
import androidx.navigation.compose.rememberNavController
|
import androidx.navigation.compose.rememberNavController
|
||||||
import de.fritob.campermonitor.bluetooth.BluetoothManager
|
import de.fritob.campermonitor.bluetooth.BluetoothManager
|
||||||
|
import de.fritob.campermonitor.store.DemoData
|
||||||
import de.fritob.campermonitor.store.DeviceStore
|
import de.fritob.campermonitor.store.DeviceStore
|
||||||
import de.fritob.campermonitor.ui.AddDeviceScreen
|
import de.fritob.campermonitor.ui.AddDeviceScreen
|
||||||
import de.fritob.campermonitor.ui.AlignmentAssistantScreen
|
import de.fritob.campermonitor.ui.AlignmentAssistantScreen
|
||||||
@@ -35,8 +36,17 @@ class MainActivity : ComponentActivity() {
|
|||||||
store = DeviceStore(applicationContext)
|
store = DeviceStore(applicationContext)
|
||||||
bluetooth = BluetoothManager(applicationContext, store)
|
bluetooth = BluetoothManager(applicationContext, store)
|
||||||
|
|
||||||
|
// Demo-Modus für den Emulator, wo es kein Bluetooth gibt:
|
||||||
|
// adb shell am start -n de.fritob.campermonitor/.MainActivity --ez demo true
|
||||||
|
val demo = BuildConfig.DEBUG && intent?.getBooleanExtra("demo", false) == true
|
||||||
|
if (demo) DemoData.install(store, bluetooth)
|
||||||
|
|
||||||
setContent {
|
setContent {
|
||||||
CamperTheme {
|
CamperTheme {
|
||||||
|
if (demo) {
|
||||||
|
CamperNavigation(store, bluetooth)
|
||||||
|
return@CamperTheme
|
||||||
|
}
|
||||||
WithBluetoothPermission {
|
WithBluetoothPermission {
|
||||||
// Erst wenn die Rechte da sind, darf der Scan starten -
|
// Erst wenn die Rechte da sind, darf der Scan starten -
|
||||||
// ohne sie wirft Android beim Scannen.
|
// ohne sie wirft Android beim Scannen.
|
||||||
|
|||||||
@@ -180,9 +180,38 @@ class BluetoothManager(
|
|||||||
const val HISTORY_LIMIT = 720
|
const val HISTORY_LIMIT = 720
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Ob die Funkschicht stillgelegt ist, weil Demo-Daten angezeigt werden. */
|
||||||
|
private var isDemo = false
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Nur für den Demo-Modus: fertige Werte einsetzen und nicht funken. So
|
||||||
|
* lassen sich die Ansichten ohne Fahrzeug prüfen.
|
||||||
|
*/
|
||||||
|
fun installDemo(
|
||||||
|
snapshots: List<DeviceSnapshot>,
|
||||||
|
fridge: Pair<UUID, AlpicoolState>,
|
||||||
|
level: Pair<UUID, LevelState>,
|
||||||
|
history: Map<UUID, List<Double>>,
|
||||||
|
) {
|
||||||
|
isDemo = true
|
||||||
|
isBluetoothReady = true
|
||||||
|
bluetoothStatusText = "Demo-Daten"
|
||||||
|
snapshots.forEach {
|
||||||
|
this.snapshots[it.deviceID] = it
|
||||||
|
linkStates[it.deviceID] = DeviceLinkState.Live
|
||||||
|
}
|
||||||
|
fridgeStates[fridge.first] = fridge.second
|
||||||
|
levelStates[level.first] = level.second
|
||||||
|
history.forEach { (id, values) ->
|
||||||
|
this.history[id] = values.map { HistorySample(System.currentTimeMillis(), it) }
|
||||||
|
.toMutableList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Start
|
// MARK: - Start
|
||||||
|
|
||||||
fun start() {
|
fun start() {
|
||||||
|
if (isDemo) return
|
||||||
handler.post {
|
handler.post {
|
||||||
refreshAdapterState()
|
refreshAdapterState()
|
||||||
applyConfiguration(currentDevices())
|
applyConfiguration(currentDevices())
|
||||||
|
|||||||
@@ -0,0 +1,130 @@
|
|||||||
|
package de.fritob.campermonitor.store
|
||||||
|
|
||||||
|
import de.fritob.campermonitor.bluetooth.BluetoothManager
|
||||||
|
import de.fritob.campermonitor.protocol.AlpicoolProtocol
|
||||||
|
import de.fritob.campermonitor.protocol.AlpicoolState
|
||||||
|
import de.fritob.campermonitor.protocol.ConfiguredDevice
|
||||||
|
import de.fritob.campermonitor.protocol.DeviceLinkState
|
||||||
|
import de.fritob.campermonitor.protocol.DeviceRole
|
||||||
|
import de.fritob.campermonitor.protocol.DeviceSnapshot
|
||||||
|
import de.fritob.campermonitor.protocol.LevelState
|
||||||
|
import de.fritob.campermonitor.protocol.Metric
|
||||||
|
import de.fritob.campermonitor.protocol.Profile
|
||||||
|
import java.util.UUID
|
||||||
|
import kotlin.math.sin
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Füllt die App mit erfundenen Messwerten, damit sich die Ansichten ohne
|
||||||
|
* Fahrzeug und ohne Bluetooth prüfen lassen.
|
||||||
|
*
|
||||||
|
* Nur in Debug-Bauten und nur, wenn beim Start das Extra `demo` gesetzt ist:
|
||||||
|
* ```
|
||||||
|
* adb shell am start -n de.fritob.campermonitor/.MainActivity --ez demo true
|
||||||
|
* ```
|
||||||
|
* Im normalen Betrieb wird hiervon nichts ausgeführt.
|
||||||
|
*/
|
||||||
|
object DemoData {
|
||||||
|
|
||||||
|
private val profileID = Profile.DEFAULT_ID
|
||||||
|
private val secondProfileID = UUID.fromString("00000000-0000-0000-0000-0000000000c2")
|
||||||
|
|
||||||
|
private fun id(suffix: String) = UUID.fromString("00000000-0000-0000-0000-0000000000$suffix")
|
||||||
|
|
||||||
|
val profiles = listOf(
|
||||||
|
Profile(profileID, "Kastenwagen", "box_truck", trackWidth = 1.85, wheelbase = 3.50),
|
||||||
|
Profile(secondProfileID, "Wohnwagen", "car_side"),
|
||||||
|
)
|
||||||
|
|
||||||
|
val devices = listOf(
|
||||||
|
ConfiguredDevice(id("50"), "Solar Dach", DeviceRole.SOLAR_CHARGER, profileID, "00:00:00:00:00:50"),
|
||||||
|
ConfiguredDevice(id("b0"), "Ladebooster", DeviceRole.CHARGE_BOOSTER, profileID, "00:00:00:00:00:b0"),
|
||||||
|
ConfiguredDevice(id("c0"), "Bulltron 200 Ah", DeviceRole.BMS, profileID, "00:00:00:00:00:c0"),
|
||||||
|
ConfiguredDevice(id("d0"), "Kühlbox", DeviceRole.FRIDGE, profileID, "00:00:00:00:00:d0"),
|
||||||
|
ConfiguredDevice(id("e0"), "Nivellierung", DeviceRole.LEVELING, profileID, "00:00:00:00:00:e0"),
|
||||||
|
)
|
||||||
|
|
||||||
|
fun install(store: DeviceStore, bluetooth: BluetoothManager) {
|
||||||
|
store.loadDemo(profiles, devices)
|
||||||
|
|
||||||
|
val now = System.currentTimeMillis()
|
||||||
|
fun snapshot(deviceID: UUID, state: String?, vararg metrics: Metric) =
|
||||||
|
DeviceSnapshot(deviceID, now, metrics.toList(), state = state)
|
||||||
|
|
||||||
|
bluetooth.installDemo(
|
||||||
|
snapshots = listOf(
|
||||||
|
snapshot(
|
||||||
|
id("50"), "Konstantspannung (Absorption)",
|
||||||
|
Metric("pv_power", "PV-Leistung", 284.0, "W", 0, isPrimary = true),
|
||||||
|
Metric("battery_power", "Ladeleistung", 262.0, "W", 0),
|
||||||
|
Metric("battery_voltage", "Batteriespannung", 14.12, "V", 2),
|
||||||
|
Metric("battery_current", "Ladestrom", 18.6, "A", 1),
|
||||||
|
Metric("yield_today", "Ertrag heute", 1.84, "kWh", 2),
|
||||||
|
),
|
||||||
|
snapshot(
|
||||||
|
id("b0"), "Aus",
|
||||||
|
Metric("output_voltage", "Ausgang (Aufbaubatterie)", 14.09, "V", 2, isPrimary = true),
|
||||||
|
Metric("input_voltage", "Eingang (Starterbatterie)", 12.42, "V", 2),
|
||||||
|
).copy(offReasons = listOf("Keine Eingangsspannung")),
|
||||||
|
snapshot(
|
||||||
|
id("c0"), "Lädt",
|
||||||
|
Metric("soc", "Ladezustand", 78.4, "%", 1, isPrimary = true),
|
||||||
|
Metric("voltage", "Spannung", 13.66, "V", 2),
|
||||||
|
Metric("current", "Strom", 18.6, "A", 1),
|
||||||
|
Metric("power", "Leistung", 254.0, "W", 0),
|
||||||
|
Metric("cell_delta", "Zell-Differenz", 18.0, "mV", 0),
|
||||||
|
).copy(cellVoltages = listOf(3.412, 3.418, 3.409, 3.427)),
|
||||||
|
snapshot(
|
||||||
|
id("d0"), "Eco",
|
||||||
|
Metric("temp_left", "Temperatur", 6.0, "°C", 0, isPrimary = true),
|
||||||
|
Metric("target_left", "Solltemperatur", 4.0, "°C", 0),
|
||||||
|
Metric("supply_voltage", "Bordspannung", 12.7, "V", 1),
|
||||||
|
Metric("battery_percent", "Batterieanzeige", 87.0, "%", 0),
|
||||||
|
),
|
||||||
|
snapshot(
|
||||||
|
id("e0"), "Heck steht höher, links steht höher",
|
||||||
|
Metric("pitch", "Längsneigung", 1.8, "°", 1, isPrimary = true),
|
||||||
|
Metric("roll", "Querneigung", -0.9, "°", 1),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
fridge = id("d0") to demoFridge(),
|
||||||
|
level = id("e0") to LevelState(
|
||||||
|
pitch = 1.8, roll = -0.9,
|
||||||
|
rawPitch = 1.8, rawRoll = -0.9,
|
||||||
|
pitchOffset = 0.4, rollOffset = -0.2,
|
||||||
|
),
|
||||||
|
history = devices.associate { device ->
|
||||||
|
device.id to List(48) { step ->
|
||||||
|
val base = when (device.role) {
|
||||||
|
DeviceRole.SOLAR_CHARGER -> 250.0
|
||||||
|
DeviceRole.LEVELING -> 1.8
|
||||||
|
DeviceRole.BMS -> 78.0
|
||||||
|
DeviceRole.FRIDGE -> 6.0
|
||||||
|
else -> 14.0
|
||||||
|
}
|
||||||
|
base + sin(step / 6.0) * base * 0.12
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun demoFridge(): AlpicoolState {
|
||||||
|
// Ein echter Einzonen-Datensatz, wie ihn die Box im Fahrzeug schickt.
|
||||||
|
val payload = byteArrayOf(
|
||||||
|
0x00, 0x01, 0x01, 0x02, 0x04, 0x14, 0xEC.toByte(), 0x02, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0xFD.toByte(), 0x00,
|
||||||
|
0x06, 0x57, 0x0C, 0x07,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x80.toByte(), 0x00, 0x01, 0x00,
|
||||||
|
)
|
||||||
|
val state = AlpicoolState()
|
||||||
|
val (frames, _) = AlpicoolProtocol.extractFrames(demoResponse(payload))
|
||||||
|
frames.forEach { state.apply(it) }
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun demoResponse(payload: ByteArray): ByteArray {
|
||||||
|
val head = byteArrayOf(0xFE.toByte(), 0xFE.toByte(), (payload.size + 3).toByte(), 0x01) + payload
|
||||||
|
val sum = AlpicoolProtocol.checksum(head)
|
||||||
|
return head + byteArrayOf((sum shr 8).toByte(), (sum and 0xFF).toByte())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -47,10 +47,21 @@ class DeviceStore(context: Context) {
|
|||||||
prefs.edit().putBoolean("showDiagnostics", value).apply()
|
prefs.edit().putBoolean("showDiagnostics", value).apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Im Demo-Modus wird nichts abgelegt – die echten Daten bleiben unberührt. */
|
||||||
|
private var isDemo = false
|
||||||
|
|
||||||
init {
|
init {
|
||||||
load()
|
load()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Nur für den Demo-Modus: Fahrzeuge und Geräte setzen, ohne zu speichern. */
|
||||||
|
fun loadDemo(demoProfiles: List<Profile>, demoDevices: List<ConfiguredDevice>) {
|
||||||
|
isDemo = true
|
||||||
|
profiles.clear(); profiles.addAll(demoProfiles)
|
||||||
|
devices.clear(); devices.addAll(demoDevices)
|
||||||
|
activeProfileID = demoProfiles.first().id
|
||||||
|
}
|
||||||
|
|
||||||
val activeProfile: Profile?
|
val activeProfile: Profile?
|
||||||
get() = profiles.firstOrNull { it.id == activeProfileID } ?: profiles.firstOrNull()
|
get() = profiles.firstOrNull { it.id == activeProfileID } ?: profiles.firstOrNull()
|
||||||
|
|
||||||
@@ -155,6 +166,7 @@ class DeviceStore(context: Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun save() {
|
private fun save() {
|
||||||
|
if (isDemo) return
|
||||||
val root = JSONObject()
|
val root = JSONObject()
|
||||||
root.put("profiles", JSONArray().also { array ->
|
root.put("profiles", JSONArray().also { array ->
|
||||||
profiles.forEach { array.put(profileTo(it)) }
|
profiles.forEach { array.put(profileTo(it)) }
|
||||||
|
|||||||
Reference in New Issue
Block a user