From fcca37bbd00f7fe70e436eb29df0afc87fe75dfa Mon Sep 17 00:00:00 2001 From: BiasF Date: Mon, 31 Aug 2026 10:43:32 +0200 Subject: [PATCH] =?UTF-8?q?Android:=20Oberfl=C3=A4che?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Alle Ansichten der iOS-Fassung in Jetpack Compose: Dashboard mit Kacheln, Gerätedetails samt Messwerten, Verlauf, Zellspannungen und Diagnose, Kühlbox-Steuerung, Nivellierung mit Libelle und Fahrzeugansicht, Ausrichtungs-Assistent, die beiden Einrichtungsassistenten, Fahrzeuge, Gerät hinzufügen, Schlüsseleingabe und Einstellungen. Mitgenommen ist auch, was wir uns in der Oberfläche erarbeitet haben. Die Geräteliste beim Einrichten sortiert nach Fundreihenfolge, nicht nach Signalstärke - danach zu sortieren macht sie unbenutzbar, weil sie sekündlich springt. Empfangspegel und Alter der Werte erscheinen nur mit eingeblendeter Diagnose. Der Victron-Schlüssel liegt auf einer eigenen Seite und meldet sich nur, wenn er fehlt oder nicht passt. Einbaulage und Nullpunkt liegen eine Ebene tiefer, damit ein Fehlgriff beim Ablesen nicht den Nullpunkt verstellt. Der Ausrichtungs-Assistent verlangt eine stehende Verbindung und sagt es, wenn sie abreisst. Drei Dinge sind auf Android anders: Die Berechtigungsabfrage ist eigenständige Arbeit. Bis Android 11 lief ein BLE-Scan über die Standortfreigabe, seither gibt es eigene Bluetooth-Rechte; beide Wege werden bedient, und die App erklärt vorher, wozu sie fragt. Die Fahrzeugauswahl sitzt als Titel mit Aufklappen in der Leiste statt als runder Knopf in der Ecke - so ist es hier üblich. Die Symbole sind Material-Entsprechungen der SF-Symbole. Ausgewählt ist jeweils das, was dieselbe Sache meint, nicht das ähnlichst aussehende. Der Verlauf ist selbst gezeichnet; für eine Linie mit Fläche lohnt keine Diagrammbibliothek. Co-Authored-By: Claude Opus 5 --- .../de/fritob/campermonitor/MainActivity.kt | 129 +++- .../campermonitor/ui/AddDeviceScreen.kt | 255 +++++++ .../ui/AlignmentAssistantScreen.kt | 313 +++++++++ .../campermonitor/ui/DashboardScreen.kt | 152 ++++ .../de/fritob/campermonitor/ui/DeviceCard.kt | 199 ++++++ .../campermonitor/ui/DeviceDetailScreen.kt | 664 ++++++++++++++++++ .../fritob/campermonitor/ui/FridgeControls.kt | 156 ++++ .../de/fritob/campermonitor/ui/Icons.kt | 53 ++ .../campermonitor/ui/LevelSetupScreen.kt | 180 +++++ .../de/fritob/campermonitor/ui/LevelViews.kt | 210 ++++++ .../de/fritob/campermonitor/ui/Permissions.kt | 103 +++ .../fritob/campermonitor/ui/ProfilesScreen.kt | 241 +++++++ .../campermonitor/ui/SensorSetupScreen.kt | 247 +++++++ .../fritob/campermonitor/ui/SettingsScreen.kt | 105 +++ .../de/fritob/campermonitor/ui/Theme.kt | 44 ++ .../campermonitor/ui/VictronKeyScreen.kt | 145 ++++ .../main/res/drawable-nodpi/vehicle_rear.png | Bin 0 -> 132935 bytes .../main/res/drawable-nodpi/vehicle_side.png | Bin 0 -> 258939 bytes 18 files changed, 3193 insertions(+), 3 deletions(-) create mode 100644 Android/app/src/main/kotlin/de/fritob/campermonitor/ui/AddDeviceScreen.kt create mode 100644 Android/app/src/main/kotlin/de/fritob/campermonitor/ui/AlignmentAssistantScreen.kt create mode 100644 Android/app/src/main/kotlin/de/fritob/campermonitor/ui/DashboardScreen.kt create mode 100644 Android/app/src/main/kotlin/de/fritob/campermonitor/ui/DeviceCard.kt create mode 100644 Android/app/src/main/kotlin/de/fritob/campermonitor/ui/DeviceDetailScreen.kt create mode 100644 Android/app/src/main/kotlin/de/fritob/campermonitor/ui/FridgeControls.kt create mode 100644 Android/app/src/main/kotlin/de/fritob/campermonitor/ui/Icons.kt create mode 100644 Android/app/src/main/kotlin/de/fritob/campermonitor/ui/LevelSetupScreen.kt create mode 100644 Android/app/src/main/kotlin/de/fritob/campermonitor/ui/LevelViews.kt create mode 100644 Android/app/src/main/kotlin/de/fritob/campermonitor/ui/Permissions.kt create mode 100644 Android/app/src/main/kotlin/de/fritob/campermonitor/ui/ProfilesScreen.kt create mode 100644 Android/app/src/main/kotlin/de/fritob/campermonitor/ui/SensorSetupScreen.kt create mode 100644 Android/app/src/main/kotlin/de/fritob/campermonitor/ui/SettingsScreen.kt create mode 100644 Android/app/src/main/kotlin/de/fritob/campermonitor/ui/Theme.kt create mode 100644 Android/app/src/main/kotlin/de/fritob/campermonitor/ui/VictronKeyScreen.kt create mode 100644 Android/app/src/main/res/drawable-nodpi/vehicle_rear.png create mode 100644 Android/app/src/main/res/drawable-nodpi/vehicle_side.png diff --git a/Android/app/src/main/kotlin/de/fritob/campermonitor/MainActivity.kt b/Android/app/src/main/kotlin/de/fritob/campermonitor/MainActivity.kt index 1d10601..93bf53a 100644 --- a/Android/app/src/main/kotlin/de/fritob/campermonitor/MainActivity.kt +++ b/Android/app/src/main/kotlin/de/fritob/campermonitor/MainActivity.kt @@ -3,12 +3,135 @@ package de.fritob.campermonitor import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text +import androidx.activity.enableEdgeToEdge +import androidx.compose.runtime.Composable +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.rememberNavController +import de.fritob.campermonitor.bluetooth.BluetoothManager +import de.fritob.campermonitor.store.DeviceStore +import de.fritob.campermonitor.ui.AddDeviceScreen +import de.fritob.campermonitor.ui.AlignmentAssistantScreen +import de.fritob.campermonitor.ui.CamperTheme +import de.fritob.campermonitor.ui.DashboardScreen +import de.fritob.campermonitor.ui.DeviceDetailScreen +import de.fritob.campermonitor.ui.LevelSetupScreen +import de.fritob.campermonitor.ui.ProfilesScreen +import de.fritob.campermonitor.ui.SensorSetupScreen +import de.fritob.campermonitor.ui.SettingsScreen +import de.fritob.campermonitor.ui.VictronKeyScreen +import de.fritob.campermonitor.ui.WithBluetoothPermission +import java.util.UUID class MainActivity : ComponentActivity() { + + private lateinit var store: DeviceStore + private lateinit var bluetooth: BluetoothManager + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContent { MaterialTheme { Text("Camper Monitor") } } + enableEdgeToEdge() + + store = DeviceStore(applicationContext) + bluetooth = BluetoothManager(applicationContext, store) + + setContent { + CamperTheme { + WithBluetoothPermission { + // Erst wenn die Rechte da sind, darf der Scan starten - + // ohne sie wirft Android beim Scannen. + androidx.compose.runtime.LaunchedEffect(Unit) { bluetooth.start() } + CamperNavigation(store, bluetooth) + } + } + } + } + + override fun onDestroy() { + super.onDestroy() + bluetooth.stopEverything() } } + +@Composable +private fun CamperNavigation(store: DeviceStore, bluetooth: BluetoothManager) { + val navController = rememberNavController() + + /** + * Der Weg vom Navigationsargument zurück zum aktuellen Gerät. Bewusst ohne + * Zwischenspeicher: nach einer Umbenennung soll der neue Stand gelten. + */ + fun device(id: String?) = store.devices.firstOrNull { it.id.toString() == id } + + NavHost(navController = navController, startDestination = "dashboard") { + composable("dashboard") { + DashboardScreen( + store = store, + bluetooth = bluetooth, + onOpenDevice = { navController.navigate("device/${it.id}") }, + onAddDevice = { navController.navigate("add") }, + onOpenProfiles = { navController.navigate("profiles") }, + onOpenSettings = { navController.navigate("settings") }, + ) + } + + composable("device/{id}") { entry -> + device(entry.arguments?.getString("id"))?.let { + DeviceDetailScreen( + device = it, + store = store, + bluetooth = bluetooth, + onBack = { navController.popBackStack() }, + onOpenKey = { navController.navigate("key/${it.id}") }, + onOpenLevelSetup = { navController.navigate("levelsetup/${it.id}") }, + onOpenAssistant = { navController.navigate("assistant/${it.id}") }, + ) + } + } + + composable("key/{id}") { entry -> + device(entry.arguments?.getString("id"))?.let { + VictronKeyScreen(it, store, bluetooth) { navController.popBackStack() } + } + } + + composable("levelsetup/{id}") { entry -> + device(entry.arguments?.getString("id"))?.let { + LevelSetupScreen( + device = it, + store = store, + bluetooth = bluetooth, + onBack = { navController.popBackStack() }, + onOpenSensorSetup = { navController.navigate("sensorsetup/${it.id}") }, + ) + } + } + + composable("sensorsetup/{id}") { entry -> + device(entry.arguments?.getString("id"))?.let { + SensorSetupScreen(it, store, bluetooth) { navController.popBackStack() } + } + } + + composable("assistant/{id}") { entry -> + device(entry.arguments?.getString("id"))?.let { + AlignmentAssistantScreen(it, store, bluetooth) { navController.popBackStack() } + } + } + + composable("add") { + AddDeviceScreen(store, bluetooth) { navController.popBackStack() } + } + + composable("profiles") { + ProfilesScreen(store, bluetooth) { navController.popBackStack() } + } + + composable("settings") { + SettingsScreen(store) { navController.popBackStack() } + } + } +} + +/** Kleine Hilfe, damit Bildschirme das Gerät über seine Kennung finden. */ +internal fun DeviceStore.device(id: UUID) = devices.firstOrNull { it.id == id } diff --git a/Android/app/src/main/kotlin/de/fritob/campermonitor/ui/AddDeviceScreen.kt b/Android/app/src/main/kotlin/de/fritob/campermonitor/ui/AddDeviceScreen.kt new file mode 100644 index 0000000..f340351 --- /dev/null +++ b/Android/app/src/main/kotlin/de/fritob/campermonitor/ui/AddDeviceScreen.kt @@ -0,0 +1,255 @@ +package de.fritob.campermonitor.ui + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Button +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExposedDropdownMenuBox +import androidx.compose.material3.ExposedDropdownMenuDefaults +import androidx.compose.material3.FilterChip +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.unit.dp +import de.fritob.campermonitor.bluetooth.BluetoothManager +import de.fritob.campermonitor.bluetooth.Discovery +import de.fritob.campermonitor.protocol.ConfiguredDevice +import de.fritob.campermonitor.protocol.DeviceRole +import de.fritob.campermonitor.store.DeviceStore + +/** + * Die Liste der gefundenen Geräte. + * + * Sortiert wird **nicht** nach Signalstärke, sondern nach der Reihenfolge des + * Auftauchens. Nach dem Pegel zu sortieren macht die Liste unbenutzbar: sie + * springt dann sekündlich, und man trifft das gesuchte Gerät nicht. + */ +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun AddDeviceScreen(store: DeviceStore, bluetooth: BluetoothManager, onBack: () -> Unit) { + var showAll by remember { mutableStateOf(false) } + var selected by remember { mutableStateOf(null) } + + DisposableEffect(Unit) { + bluetooth.setDiscovering(true) + onDispose { bluetooth.setDiscovering(false) } + } + + val alreadyAdded = store.activeDevices().map { it.address }.toSet() + val found = bluetooth.discoveries.values + .filter { showAll || it.looksLikeSupported || it.name != null } + .sortedWith( + compareByDescending { it.isVictron } + .thenByDescending { it.looksLikeSupported } + .thenBy { it.firstSeen } + ) + + Scaffold( + topBar = { + TopAppBar( + title = { Text("Gerät für ${store.activeProfile?.name ?: "Camper"}") }, + navigationIcon = { + IconButton(onClick = onBack) { + Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Zurück") + } + }, + ) + }, + ) { padding -> + LazyColumn(modifier = Modifier.fillMaxSize().padding(padding)) { + item { + Row( + modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + FilterChip( + selected = !showAll, + onClick = { showAll = false }, + label = { Text("Passende") }, + ) + FilterChip( + selected = showAll, + onClick = { showAll = true }, + label = { Text("Alle") }, + ) + } + } + + items(found, key = { it.address }) { discovery -> + val added = discovery.address in alreadyAdded + Row( + modifier = Modifier + .fillMaxWidth() + .clickable(enabled = !added) { selected = discovery } + .padding(horizontal = 16.dp, vertical = 12.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Column(modifier = Modifier.weight(1f)) { + Text( + discovery.name ?: "Ohne Namen", + style = MaterialTheme.typography.bodyLarge, + color = if (added) { + MaterialTheme.colorScheme.onSurfaceVariant + } else { + MaterialTheme.colorScheme.onSurface + }, + ) + Text( + buildString { + append(discovery.address) + discovery.victronRecord?.let { append(" · Victron $it") } + append(" · ${discovery.rssi} dBm") + }, + style = MaterialTheme.typography.bodySmall, + fontFamily = FontFamily.Monospace, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + if (added) { + Text("schon dabei", style = MaterialTheme.typography.labelMedium) + } + } + } + + item { + Text( + "Victron-Geräte werden automatisch erkannt. Damit sie hier " + + "auftauchen, muss „Instant Readout“ in VictronConnect aktiv " + + "sein. Das BMS meldet sich meist als „DL-…“. Kühlboxen tragen " + + "oft einen kryptischen Namen – findest du dein Gerät nicht, auf " + + "„Alle“ umschalten.", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(16.dp), + ) + } + } + } + + selected?.let { discovery -> + ConfigureDeviceDialog( + discovery = discovery, + onDismiss = { selected = null }, + onAdd = { name, role -> + store.update( + ConfiguredDevice( + name = name, + role = role, + profileID = store.activeProfile?.id ?: return@ConfigureDeviceDialog, + address = discovery.address, + advertisedName = discovery.name, + ) + ) + bluetooth.refreshConfiguration() + selected = null + onBack() + }, + ) + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun ConfigureDeviceDialog( + discovery: Discovery, + onDismiss: () -> Unit, + onAdd: (String, DeviceRole) -> Unit, +) { + // Ein Vorschlag, der meistens passt – geraten wird nur, was sich aus dem + // Advertisement ablesen lässt. + val suggestedRole = remember(discovery) { guessRole(discovery) } + var role by remember { mutableStateOf(suggestedRole) } + var name by remember { mutableStateOf(discovery.name ?: suggestedRole.title) } + var expanded by remember { mutableStateOf(false) } + + AlertDialog( + onDismissRequest = onDismiss, + title = { Text("Gerät einrichten") }, + text = { + Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { + OutlinedTextField( + value = name, + onValueChange = { name = it }, + label = { Text("Name") }, + singleLine = true, + ) + ExposedDropdownMenuBox( + expanded = expanded, + onExpandedChange = { expanded = it }, + ) { + OutlinedTextField( + value = role.title, + onValueChange = {}, + readOnly = true, + label = { Text("Art") }, + trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded) }, + modifier = Modifier.menuAnchor( + androidx.compose.material3.MenuAnchorType.PrimaryNotEditable, true + ), + ) + ExposedDropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) { + DeviceRole.entries.forEach { candidate -> + DropdownMenuItem( + text = { Text(candidate.title) }, + onClick = { role = candidate; expanded = false }, + ) + } + } + } + Text( + discovery.address, + style = MaterialTheme.typography.bodySmall, + fontFamily = FontFamily.Monospace, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + }, + confirmButton = { + Button(onClick = { onAdd(name.trim().ifEmpty { role.title }, role) }) { + Text("Hinzufügen") + } + }, + dismissButton = { TextButton(onClick = onDismiss) { Text("Abbrechen") } }, + ) +} + +/** Was sich aus Namen und Advertisement ableiten lässt. */ +private fun guessRole(discovery: Discovery): DeviceRole { + val name = discovery.name?.lowercase() ?: "" + return when { + discovery.victronRecord == "SOLAR_CHARGER" -> DeviceRole.SOLAR_CHARGER + discovery.victronRecord == "BATTERY_MONITOR" -> DeviceRole.BATTERY_MONITOR + discovery.victronRecord == "DCDC_CONVERTER" -> DeviceRole.CHARGE_BOOSTER + discovery.victronRecord == "ORION_XS" -> DeviceRole.CHARGE_BOOSTER + name.contains("vanalign") -> DeviceRole.LEVELING + name.contains("alpicool") || name.contains("icecube") -> DeviceRole.FRIDGE + name.startsWith("dl-") || name.contains("daly") || name.contains("wattcycle") -> + DeviceRole.BMS + else -> DeviceRole.BMS + } +} diff --git a/Android/app/src/main/kotlin/de/fritob/campermonitor/ui/AlignmentAssistantScreen.kt b/Android/app/src/main/kotlin/de/fritob/campermonitor/ui/AlignmentAssistantScreen.kt new file mode 100644 index 0000000..0679faf --- /dev/null +++ b/Android/app/src/main/kotlin/de/fritob/campermonitor/ui/AlignmentAssistantScreen.kt @@ -0,0 +1,313 @@ +package de.fritob.campermonitor.ui + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.CheckCircle +import androidx.compose.material.icons.filled.Flag +import androidx.compose.material.icons.filled.HourglassEmpty +import androidx.compose.material.icons.filled.PortableWifiOff +import androidx.compose.material.icons.filled.Refresh +import androidx.compose.material.icons.filled.TrendingDown +import androidx.compose.material.icons.filled.TrendingFlat +import androidx.compose.material.icons.filled.TrendingUp +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SegmentedButton +import androidx.compose.material3.SegmentedButtonDefaults +import androidx.compose.material3.SingleChoiceSegmentedButtonRow +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalView +import androidx.compose.ui.unit.dp +import de.fritob.campermonitor.bluetooth.BluetoothManager +import de.fritob.campermonitor.protocol.AlignmentAssistant +import de.fritob.campermonitor.protocol.ConfiguredDevice +import de.fritob.campermonitor.protocol.DeviceLinkState +import de.fritob.campermonitor.protocol.LevelState +import de.fritob.campermonitor.protocol.LevelingWedge +import de.fritob.campermonitor.store.DeviceStore + +/** + * Der Ausrichtungs-Assistent: begleitet das Rangieren und sagt, ob es besser + * oder schlechter wird und wo es am besten stand. + */ +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun AlignmentAssistantScreen( + device: ConfiguredDevice, + store: DeviceStore, + bluetooth: BluetoothManager, + onBack: () -> Unit, +) { + val state = bluetooth.levelStates[device.id] ?: LevelState() + val linkState = bluetooth.linkStates[device.id] + val profile = store.activeProfile + + val assistant = remember { AlignmentAssistant() } + // Zwingt die Ansicht zum Neuzeichnen, wenn sich der Verlauf ändert – der + // Assistent selbst ist bewusst kein Compose-Zustand, damit die Protokoll- + // schicht nichts von Compose wissen muss. + var revision by remember { mutableIntStateOf(0) } + var displayStyle by remember { mutableStateOf(0) } + var announced by remember { mutableStateOf(false) } + + val view = LocalView.current + + LaunchedEffect(state) { + val pitch = state.pitch + val roll = state.roll + if (pitch != null && roll != null) { + assistant.add(pitch, roll) + revision += 1 + // Einmal spürbar melden, wenn die Waage erreicht ist – man schaut + // beim Rangieren nicht dauernd aufs Display. + if (assistant.hasReachedTarget) { + if (!announced) { + announced = true + @Suppress("DEPRECATION") + view.performHapticFeedback(android.view.HapticFeedbackConstants.CONFIRM) + } + } else { + announced = false + } + } + } + + // Beim Rangieren schaut man immer wieder aufs Display; es darf dabei nicht + // dunkel werden. + DisposableEffect(Unit) { + view.keepScreenOn = true + onDispose { view.keepScreenOn = false } + } + + Scaffold( + topBar = { + TopAppBar( + title = { Text("Ausrichtungs-Assistent") }, + actions = { TextButton(onClick = onBack) { Text("Fertig") } }, + ) + }, + ) { padding -> + @Suppress("UNUSED_EXPRESSION") revision // liest den Zähler, damit neu gezeichnet wird + Column( + modifier = Modifier + .fillMaxSize() + .padding(padding) + .verticalScroll(rememberScrollState()) + .padding(16.dp), + verticalArrangement = Arrangement.spacedBy(20.dp), + ) { + SingleChoiceSegmentedButtonRow(modifier = Modifier.fillMaxWidth()) { + listOf("Libelle", "Fahrzeug").forEachIndexed { index, label -> + SegmentedButton( + selected = displayStyle == index, + onClick = { displayStyle = index }, + shape = SegmentedButtonDefaults.itemShape(index, 2), + ) { Text(label) } + } + } + + if (linkState != DeviceLinkState.Live) DisconnectedBanner() + + if (displayStyle == 0) { + LevelBubble(state.pitch, state.roll) + } else { + VehicleTiltView(state.pitch, state.roll) + } + + AdviceBanner(assistant) + + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceEvenly) { + Reading("Längs", state.pitch, Modifier.weight(1f)) + Reading("Quer", state.roll, Modifier.weight(1f)) + Reading("Gesamt", assistant.current?.deviation, Modifier.weight(1f)) + } + + val gain = assistant.improvementAtBest + val seconds = assistant.secondsSinceBest + val best = assistant.best + if (gain != null && seconds != null && best != null) { + Card(modifier = Modifier.fillMaxWidth()) { + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(6.dp), + ) { + Row(verticalAlignment = Alignment.CenterVertically) { + Icon(Icons.Filled.Flag, contentDescription = null) + Text( + "Bester Punkt", + style = MaterialTheme.typography.titleSmall, + modifier = Modifier.padding(start = 8.dp), + ) + } + Text( + "Vor %.0f Sekunden stand das Fahrzeug %.1f° flacher (%.1f° statt %.1f°)." + .format(seconds, gain, best.deviation, + assistant.current?.deviation ?: 0.0), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } + } + + WedgeCard(state, profile?.trackWidth, profile?.wheelbase) + + OutlinedButton( + onClick = { assistant.reset(); revision += 1; announced = false }, + modifier = Modifier.fillMaxWidth(), + ) { + Icon(Icons.Filled.Refresh, contentDescription = null) + Text("Neu beginnen", modifier = Modifier.padding(start = 8.dp)) + } + } + } +} + +/** + * Reisst die Verbindung beim Rangieren ab, stehen die Zahlen still. Ohne + * Hinweis sähe das aus, als hinge die App – man rangiert dann nach einem Wert, + * der längst nicht mehr gilt. + */ +@Composable +private fun DisconnectedBanner() { + Card( + modifier = Modifier.fillMaxWidth(), + colors = CardDefaults.cardColors(containerColor = Color(0x33E08600)), + ) { + Row( + modifier = Modifier.padding(16.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + Icons.Filled.PortableWifiOff, contentDescription = null, + tint = Color(0xFFE08600), modifier = Modifier.size(32.dp), + ) + Column(modifier = Modifier.padding(start = 12.dp)) { + Text("Nicht verbunden", style = MaterialTheme.typography.titleSmall) + Text( + "Die Anzeige steht still, bis der Neigungsmesser wieder da ist.", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } + } +} + +@Composable +private fun AdviceBanner(assistant: AlignmentAssistant) { + val reached = assistant.hasReachedTarget + val icon: ImageVector = if (reached) { + Icons.Filled.CheckCircle + } else { + when (assistant.trend) { + AlignmentAssistant.Trend.IMPROVING -> Icons.Filled.TrendingDown + AlignmentAssistant.Trend.WORSENING -> Icons.Filled.TrendingUp + AlignmentAssistant.Trend.STEADY -> Icons.Filled.TrendingFlat + AlignmentAssistant.Trend.UNKNOWN -> Icons.Filled.HourglassEmpty + } + } + Card( + modifier = Modifier.fillMaxWidth(), + colors = CardDefaults.cardColors( + containerColor = if (reached) { + Color(0x331F9E52) + } else { + MaterialTheme.colorScheme.surfaceContainer + }, + ), + ) { + Row( + modifier = Modifier.padding(16.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + icon, contentDescription = null, + tint = if (reached) Color(0xFF1F9E52) else MaterialTheme.colorScheme.primary, + modifier = Modifier.size(32.dp), + ) + Text( + assistant.advice, + style = MaterialTheme.typography.titleMedium, + modifier = Modifier.padding(start = 12.dp), + ) + } + } +} + +@Composable +private fun WedgeCard(state: LevelState, trackWidth: Double?, wheelbase: Double?) { + val across = state.roll?.let { LevelingWedge.across(it, trackWidth) } + val along = state.pitch?.let { LevelingWedge.along(it, wheelbase) } + + Card(modifier = Modifier.fillMaxWidth()) { + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(10.dp), + ) { + Text("Auffahrkeile", style = MaterialTheme.typography.titleSmall) + when { + trackWidth == null && wheelbase == null -> Text( + "Für die Keilhöhe fehlen Spurweite und Radstand. Beides lässt sich " + + "beim Fahrzeug hinterlegen.", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + + across == null && along == null -> Text( + "Keine Keile nötig.", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + + else -> { + listOfNotNull(across, along).forEach { wedge -> + Row(modifier = Modifier.fillMaxWidth()) { + Text( + wedge.side.text.replaceFirstChar { it.uppercase() }, + modifier = Modifier.weight(1f), + ) + Text( + "%.0f cm".format(wedge.heightInCentimetres), + style = MaterialTheme.typography.titleMedium, + ) + } + } + Text( + "Höhe unter die tieferstehende Seite, damit das Fahrzeug eben steht.", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } + } + } +} diff --git a/Android/app/src/main/kotlin/de/fritob/campermonitor/ui/DashboardScreen.kt b/Android/app/src/main/kotlin/de/fritob/campermonitor/ui/DashboardScreen.kt new file mode 100644 index 0000000..f0ecda1 --- /dev/null +++ b/Android/app/src/main/kotlin/de/fritob/campermonitor/ui/DashboardScreen.kt @@ -0,0 +1,152 @@ +package de.fritob.campermonitor.ui + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.Settings +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import de.fritob.campermonitor.bluetooth.BluetoothManager +import de.fritob.campermonitor.protocol.ConfiguredDevice +import de.fritob.campermonitor.protocol.DeviceLinkState +import de.fritob.campermonitor.store.DeviceStore + +/** + * Die Startansicht: alle Geräte des gewählten Fahrzeugs als Kacheln. + * + * Anders als unter iOS sitzt die Fahrzeugauswahl nicht als runder Knopf in der + * Ecke, sondern als Titel mit Aufklapp-Pfeil – so ist es auf Android üblich. + */ +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun DashboardScreen( + store: DeviceStore, + bluetooth: BluetoothManager, + onOpenDevice: (ConfiguredDevice) -> Unit, + onAddDevice: () -> Unit, + onOpenProfiles: () -> Unit, + onOpenSettings: () -> Unit, +) { + val devices = store.activeDevices() + + Scaffold( + topBar = { + TopAppBar( + title = { + TextButton(onClick = onOpenProfiles) { + Icon( + profileIcon(store.activeProfile?.symbol ?: "box_truck"), + contentDescription = null, + ) + Text( + store.activeProfile?.name ?: "Camper", + style = MaterialTheme.typography.titleLarge, + modifier = Modifier.padding(start = 8.dp), + ) + } + }, + actions = { + IconButton(onClick = onAddDevice) { + Icon(Icons.Filled.Add, contentDescription = "Gerät hinzufügen") + } + IconButton(onClick = onOpenSettings) { + Icon(Icons.Filled.Settings, contentDescription = "Einstellungen") + } + }, + ) + }, + ) { padding -> + if (devices.isEmpty()) { + EmptyDashboard( + modifier = Modifier.fillMaxSize().padding(padding), + statusText = bluetooth.bluetoothStatusText, + isReady = bluetooth.isBluetoothReady, + onAddDevice = onAddDevice, + ) + return@Scaffold + } + + LazyColumn( + modifier = Modifier.fillMaxSize(), + contentPadding = PaddingValues( + start = 16.dp, end = 16.dp, + top = padding.calculateTopPadding() + 8.dp, + bottom = padding.calculateBottomPadding() + 24.dp, + ), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + if (!bluetooth.isBluetoothReady) { + item { + Text( + bluetooth.bluetoothStatusText, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.error, + ) + } + } + items(devices, key = { it.id }) { device -> + DeviceCard( + device = device, + snapshot = bluetooth.snapshots[device.id], + linkState = bluetooth.linkStates[device.id] ?: DeviceLinkState.Searching, + onClick = { onOpenDevice(device) }, + ) + } + } + } +} + +@Composable +private fun EmptyDashboard( + modifier: Modifier, + statusText: String, + isReady: Boolean, + onAddDevice: () -> Unit, +) { + Column( + modifier = modifier.padding(32.dp), + verticalArrangement = Arrangement.spacedBy(12.dp, Alignment.CenterVertically), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text( + "Noch keine Geräte", + style = MaterialTheme.typography.headlineSmall, + ) + Text( + "Füge deinen Ladebooster, den Solarladeregler, die Batterie, die " + + "Kühlbox oder den Neigungsmesser hinzu.", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + textAlign = TextAlign.Center, + ) + if (!isReady) { + Text( + statusText, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.error, + textAlign = TextAlign.Center, + ) + } + TextButton(onClick = onAddDevice, modifier = Modifier.fillMaxWidth()) { + Text("Gerät hinzufügen") + } + } +} diff --git a/Android/app/src/main/kotlin/de/fritob/campermonitor/ui/DeviceCard.kt b/Android/app/src/main/kotlin/de/fritob/campermonitor/ui/DeviceCard.kt new file mode 100644 index 0000000..8652b1c --- /dev/null +++ b/Android/app/src/main/kotlin/de/fritob/campermonitor/ui/DeviceCard.kt @@ -0,0 +1,199 @@ +package de.fritob.campermonitor.ui + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Warning +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import de.fritob.campermonitor.protocol.ConfiguredDevice +import de.fritob.campermonitor.protocol.DeviceLinkState +import de.fritob.campermonitor.protocol.DeviceSnapshot +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable + +/** + * Kachel auf dem Dashboard: Hauptwert gross, darunter die wichtigsten + * Nebenwerte und der Verbindungszustand. + */ +@Composable +fun DeviceCard( + device: ConfiguredDevice, + snapshot: DeviceSnapshot?, + linkState: DeviceLinkState, + onClick: () -> Unit, +) { + val isStale = snapshot?.isStale() ?: true + + Card( + modifier = Modifier.fillMaxWidth().clickable(onClick = onClick), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceContainer, + ), + ) { + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + Row(verticalAlignment = Alignment.CenterVertically) { + Icon( + imageVector = roleIcon(device.role), + contentDescription = null, + tint = MaterialTheme.colorScheme.primary, + ) + Column(modifier = Modifier.padding(start = 8.dp).weight(1f)) { + Text(device.name, style = MaterialTheme.typography.titleMedium) + Text( + device.role.title, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + StatusDot(linkState, isStale) + } + + val primary = snapshot?.primaryMetric + if (snapshot != null && primary != null) { + Row(verticalAlignment = Alignment.Bottom) { + Text( + primary.formatted, + fontSize = 40.sp, + fontWeight = FontWeight.SemiBold, + color = if (isStale) { + MaterialTheme.colorScheme.onSurfaceVariant + } else { + MaterialTheme.colorScheme.onSurface + }, + ) + Text( + primary.unit, + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(start = 4.dp, bottom = 6.dp), + ) + } + SecondaryValues(snapshot) + } else { + Text( + placeholderText(linkState), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(vertical = 12.dp), + ) + } + + CardFooter(snapshot, linkState) + } + } +} + +@Composable +private fun SecondaryValues(snapshot: DeviceSnapshot) { + val others = snapshot.metrics + .filter { it.key != snapshot.primaryMetric?.key && it.value != null } + .take(3) + if (others.isEmpty()) return + Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) { + others.forEach { metric -> + Column { + Text( + metric.formattedWithUnit, + style = MaterialTheme.typography.bodyMedium, + fontWeight = FontWeight.Medium, + ) + Text( + metric.label, + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + } + } +} + +@Composable +private fun CardFooter(snapshot: DeviceSnapshot?, linkState: DeviceLinkState) { + val fault = snapshot?.fault + when { + fault != null -> Row(verticalAlignment = Alignment.CenterVertically) { + Icon( + Icons.Filled.Warning, contentDescription = null, + tint = MaterialTheme.colorScheme.error, + modifier = Modifier.size(16.dp), + ) + Text( + fault, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.error, + maxLines = 2, + modifier = Modifier.padding(start = 6.dp), + ) + } + + snapshot?.state != null -> { + // Bei "Aus" ist erst der Grund die eigentliche Information. + val reason = snapshot.offReasons.firstOrNull() + Text( + if (reason != null) "${snapshot.state} · $reason" else snapshot.state!!, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = 2, + ) + } + + linkState is DeviceLinkState.Failed -> Text( + linkState.message, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.tertiary, + maxLines = 2, + ) + } +} + +private fun placeholderText(linkState: DeviceLinkState): String = when (linkState) { + is DeviceLinkState.NeedsKey -> "Verschlüsselungsschlüssel fehlt – im Detail eintragen." + is DeviceLinkState.Failed -> linkState.message + else -> "Warte auf Daten…" +} + +/** Kleiner Punkt, der Verbindungszustand und Aktualität zusammenfasst. */ +@Composable +fun StatusDot(linkState: DeviceLinkState, isStale: Boolean) { + val color = when (linkState) { + is DeviceLinkState.Live -> if (isStale) Color(0xFFE08600) else Color(0xFF1F9E52) + is DeviceLinkState.NeedsKey -> Color(0xFFE08600) + is DeviceLinkState.Failed -> MaterialTheme.colorScheme.error + else -> MaterialTheme.colorScheme.onSurfaceVariant + } + val label = if (linkState is DeviceLinkState.Live && isStale) "Veraltet" else linkState.label + + Row(verticalAlignment = Alignment.CenterVertically) { + Box(modifier = Modifier.size(8.dp).clip(CircleShape).background(color)) + Text( + label, + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(start = 5.dp), + ) + } +} diff --git a/Android/app/src/main/kotlin/de/fritob/campermonitor/ui/DeviceDetailScreen.kt b/Android/app/src/main/kotlin/de/fritob/campermonitor/ui/DeviceDetailScreen.kt new file mode 100644 index 0000000..966f526 --- /dev/null +++ b/Android/app/src/main/kotlin/de/fritob/campermonitor/ui/DeviceDetailScreen.kt @@ -0,0 +1,664 @@ +package de.fritob.campermonitor.ui + +import android.content.ClipData +import android.content.ClipboardManager +import android.content.Context +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight +import androidx.compose.material.icons.filled.ContentCopy +import androidx.compose.material.icons.filled.Delete +import androidx.compose.material.icons.filled.Key +import androidx.compose.material.icons.filled.Tune +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExposedDropdownMenuBox +import androidx.compose.material3.ExposedDropdownMenuDefaults +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SegmentedButton +import androidx.compose.material3.SegmentedButtonDefaults +import androidx.compose.material3.SingleChoiceSegmentedButtonRow +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Path +import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.unit.dp +import de.fritob.campermonitor.bluetooth.BluetoothManager +import de.fritob.campermonitor.bluetooth.BmsDiagnostics +import de.fritob.campermonitor.protocol.ConfiguredDevice +import de.fritob.campermonitor.protocol.DeviceLinkState +import de.fritob.campermonitor.protocol.DeviceTransport +import de.fritob.campermonitor.protocol.FridgeZoneMode +import de.fritob.campermonitor.protocol.LevelState +import de.fritob.campermonitor.store.DeviceStore + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun DeviceDetailScreen( + device: ConfiguredDevice, + store: DeviceStore, + bluetooth: BluetoothManager, + onBack: () -> Unit, + onOpenKey: () -> Unit, + onOpenLevelSetup: () -> Unit, + onOpenAssistant: () -> Unit, +) { + val current = store.devices.firstOrNull { it.id == device.id } ?: device + val snapshot = bluetooth.snapshots[device.id] + val linkState = bluetooth.linkStates[device.id] ?: DeviceLinkState.Searching + val isStale = snapshot?.isStale() ?: true + var showDelete by remember { mutableStateOf(false) } + + /** + * Im Alltag stören die technischen Angaben nur. Meldet ein Gerät aber einen + * Fehler oder fehlt der Schlüssel, sind sie genau das, was weiterhilft – + * dann werden sie unabhängig von der Einstellung gezeigt. + */ + val showsTechnicalDetails = store.showDiagnostics || + linkState is DeviceLinkState.Failed || linkState is DeviceLinkState.NeedsKey + + Scaffold( + topBar = { + TopAppBar( + title = { Text(current.name) }, + navigationIcon = { + IconButton(onClick = onBack) { + Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Zurück") + } + }, + ) + }, + ) { padding -> + Column( + modifier = Modifier + .fillMaxSize() + .padding(padding) + .verticalScroll(rememberScrollState()) + .padding(bottom = 32.dp), + ) { + // MARK: Zustand + LabeledRow("Verbindung") { StatusDot(linkState, isStale) } + snapshot?.state?.let { LabeledRow("Zustand") { Text(it) } } + snapshot?.fault?.let { + Text( + it, + color = MaterialTheme.colorScheme.error, + modifier = Modifier.padding(horizontal = 16.dp, vertical = 4.dp), + ) + } + snapshot?.offReasons?.forEach { + Text( + it, + color = MaterialTheme.colorScheme.tertiary, + style = MaterialTheme.typography.bodyMedium, + modifier = Modifier.padding(horizontal = 16.dp, vertical = 2.dp), + ) + } + // Der Empfangspegel hilft beim Suchen eines Geräts, im Alltag sagt + // er nichts – deshalb nur bei eingeblendeter Diagnose. + if (showsTechnicalDetails) { + snapshot?.rssi?.let { LabeledRow("Signal") { Text("$it dBm") } } + } + + // MARK: Schlüssel, falls er fehlt + if (needsKeyAttention(current, store, bluetooth, snapshot != null, linkState)) { + KeyPrompt(onOpenKey) + } + + // MARK: Steuerung + val fridge = bluetooth.fridgeStates[device.id] + if (current.role == de.fritob.campermonitor.protocol.DeviceRole.FRIDGE && + fridge != null && fridge.hasStatus + ) { + FridgeControls(current, fridge, bluetooth) + } + + if (current.role == de.fritob.campermonitor.protocol.DeviceRole.LEVELING) { + LevelSection( + device = current, + state = bluetooth.levelStates[device.id] ?: LevelState(), + isLive = linkState == DeviceLinkState.Live, + onOpenAssistant = onOpenAssistant, + onOpenSetup = onOpenLevelSetup, + ) + } + + // MARK: Messwerte + if (snapshot != null && snapshot.metrics.isNotEmpty()) { + SectionHeader("Messwerte") + snapshot.metrics.forEach { metric -> + LabeledRow(metric.label) { + Text( + metric.formattedWithUnit, + color = if (metric.value == null) { + MaterialTheme.colorScheme.onSurfaceVariant + } else { + MaterialTheme.colorScheme.onSurface + }, + ) + } + } + } + + // MARK: Verlauf + val samples = bluetooth.history[device.id] + val primary = snapshot?.primaryMetric + if (samples != null && samples.size > 1 && primary != null) { + SectionHeader("Verlauf – ${primary.label}") + HistoryChart(samples.map { it.value }, primary.unit) + } + + // MARK: Zellen + if (snapshot != null && snapshot.cellVoltages.isNotEmpty()) { + SectionHeader("Zellspannungen") + snapshot.cellVoltages.forEachIndexed { index, voltage -> + LabeledRow("Zelle ${index + 1}") { Text("%.3f V".format(voltage)) } + } + } + + if (snapshot != null && snapshot.info.isNotEmpty()) { + SectionHeader("Gerät") + snapshot.info.forEach { LabeledRow(it.label) { Text(it.value) } } + } + + if (snapshot != null && snapshot.temperatures.size > 1) { + SectionHeader("Temperaturen") + snapshot.temperatures.forEachIndexed { index, value -> + LabeledRow("Fühler ${index + 1}") { Text("%.0f °C".format(value)) } + } + } + + // MARK: Diagnose + if (showsTechnicalDetails) { + if (current.role.transport == DeviceTransport.ADVERTISEMENT) { + bluetooth.diagnostics[device.id]?.let { VictronDiagnosticsSection(it) } + } else { + bluetooth.bmsDiagnostics[device.id]?.let { + BmsDiagnosticsSection(current, it) + } + } + } + + // MARK: Einstellungen + SectionHeader("Einstellungen") + NameField(current, store) + LabeledRow("Typ") { Text(current.role.title) } + if (current.role == de.fritob.campermonitor.protocol.DeviceRole.FRIDGE) { + ZoneModePicker(current, store, bluetooth) + } + if (current.role.transport == DeviceTransport.ADVERTISEMENT) { + Row( + modifier = Modifier + .fillMaxWidth() + .clickable(onClick = onOpenKey) + .padding(horizontal = 16.dp, vertical = 12.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text("Verschlüsselung", modifier = Modifier.weight(1f)) + Text( + keyStatusText(current, store, bluetooth), + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + Icon(Icons.AutoMirrored.Filled.KeyboardArrowRight, contentDescription = null) + } + } + LabeledRow("Bluetooth-Adresse") { + Text( + current.address, + fontFamily = FontFamily.Monospace, + style = MaterialTheme.typography.bodySmall, + ) + } + TextButton( + onClick = { showDelete = true }, + modifier = Modifier.padding(horizontal = 8.dp), + ) { + Icon(Icons.Filled.Delete, contentDescription = null, + tint = MaterialTheme.colorScheme.error) + Text( + "Gerät entfernen", + color = MaterialTheme.colorScheme.error, + modifier = Modifier.padding(start = 8.dp), + ) + } + } + } + + if (showDelete) { + AlertDialog( + onDismissRequest = { showDelete = false }, + title = { Text("Gerät entfernen?") }, + text = { Text("Die Einstellungen und der hinterlegte Schlüssel werden gelöscht.") }, + confirmButton = { + TextButton(onClick = { + store.remove(current) + bluetooth.refreshConfiguration() + showDelete = false + onBack() + }) { Text("Entfernen") } + }, + dismissButton = { + TextButton(onClick = { showDelete = false }) { Text("Abbrechen") } + }, + ) + } +} + +// MARK: - Bausteine + +@Composable +fun LabeledRow(label: String, content: @Composable () -> Unit) { + Row( + modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 10.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text(label, modifier = Modifier.weight(1f)) + content() + } +} + +/** + * Ohne passenden Schlüssel bleibt das Gerät stumm – das ist dann keine + * Nebensache, sondern das Einzige, was zu tun ist. + */ +private fun needsKeyAttention( + device: ConfiguredDevice, + store: DeviceStore, + bluetooth: BluetoothManager, + hasSnapshot: Boolean, + linkState: DeviceLinkState, +): Boolean { + if (device.role.transport != DeviceTransport.ADVERTISEMENT) return false + val entered = store.victronKey(device.id) + val expected = bluetooth.diagnostics[device.id]?.expectedKeyFirstByte + if (entered != null && expected != null && (entered[0].toInt() and 0xFF) != expected) return true + // Kommen Werte an, passt der Schlüssel offensichtlich. + if (hasSnapshot && linkState == DeviceLinkState.Live) return false + return entered == null +} + +private fun keyStatusText( + device: ConfiguredDevice, + store: DeviceStore, + bluetooth: BluetoothManager, +): String { + val entered = store.victronKey(device.id) ?: return "fehlt" + val expected = bluetooth.diagnostics[device.id]?.expectedKeyFirstByte ?: return "hinterlegt" + return if ((entered[0].toInt() and 0xFF) == expected) "hinterlegt" else "passt nicht" +} + +@Composable +private fun KeyPrompt(onOpenKey: () -> Unit) { + Card( + modifier = Modifier.fillMaxWidth().padding(16.dp).clickable(onClick = onOpenKey), + colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceContainer), + ) { + Row( + modifier = Modifier.padding(16.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon(Icons.Filled.Key, contentDescription = null, + tint = MaterialTheme.colorScheme.tertiary) + Column(modifier = Modifier.weight(1f).padding(start = 12.dp)) { + Text("Verschlüsselungsschlüssel eintragen") + Text( + "Victron-Geräte senden ihre Werte verschlüsselt. Ohne den Schlüssel " + + "aus VictronConnect bleibt die Anzeige leer.", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + Icon(Icons.AutoMirrored.Filled.KeyboardArrowRight, contentDescription = null) + } + } +} + +@Composable +private fun NameField(device: ConfiguredDevice, store: DeviceStore) { + var name by remember(device.id) { mutableStateOf(device.name) } + OutlinedTextField( + value = name, + onValueChange = { + name = it + // Ein leeres Feld beim Tippen darf den Namen nicht löschen. + val trimmed = it.trim() + if (trimmed.isNotEmpty()) store.update(device.copy(name = trimmed)) + }, + label = { Text("Name") }, + singleLine = true, + modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 4.dp), + ) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun ZoneModePicker( + device: ConfiguredDevice, + store: DeviceStore, + bluetooth: BluetoothManager, +) { + var expanded by remember { mutableStateOf(false) } + ExposedDropdownMenuBox( + expanded = expanded, + onExpandedChange = { expanded = it }, + modifier = Modifier.padding(horizontal = 16.dp, vertical = 4.dp), + ) { + OutlinedTextField( + value = device.fridgeZoneMode.title, + onValueChange = {}, + readOnly = true, + label = { Text("Kühlzonen") }, + trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded) }, + modifier = Modifier + .fillMaxWidth() + .menuAnchor(androidx.compose.material3.MenuAnchorType.PrimaryNotEditable, true), + ) + ExposedDropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) { + FridgeZoneMode.entries.forEach { mode -> + DropdownMenuItem( + text = { Text(mode.title) }, + onClick = { + val updated = device.copy(fridgeZoneMode = mode) + store.update(updated) + bluetooth.updateFridgeZoneMode(updated) + expanded = false + }, + ) + } + } + } +} + +/** + * Der Verlauf des Hauptwerts. + * + * Selbst gezeichnet statt mit einer Diagrammbibliothek – für eine Linie mit + * Fläche lohnt keine weitere Abhängigkeit. + */ +@Composable +private fun HistoryChart(values: List, unit: String) { + if (values.size < 2) return + val lowest = values.min() + val highest = values.max() + // Bei Spannungen zählt der Unterschied von Zehntelvolt, nicht die absolute + // Spannung – deshalb eng um die Messwerte zoomen. + val low = lowest - 0.05 + val high = if (highest - lowest < 0.01) highest + 0.05 else highest + 0.05 + val span = (high - low).coerceAtLeast(0.0001) + val lineColor = MaterialTheme.colorScheme.primary + + Column { + Canvas( + modifier = Modifier + .fillMaxWidth() + .height(160.dp) + .padding(horizontal = 16.dp, vertical = 8.dp), + ) { + val stepX = size.width / (values.size - 1) + fun y(value: Double) = (size.height * (1 - (value - low) / span)).toFloat() + + val line = Path().apply { + moveTo(0f, y(values.first())) + values.forEachIndexed { index, value -> lineTo(index * stepX, y(value)) } + } + val area = Path().apply { + addPath(line) + lineTo(size.width, size.height) + lineTo(0f, size.height) + close() + } + drawPath(area, lineColor.copy(alpha = 0.15f)) + drawPath(line, lineColor, style = Stroke(width = 3f)) + } + Row(modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp)) { + Text( + "%.2f %s".format(lowest, unit), + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.weight(1f), + ) + Text( + "%.2f %s".format(highest, unit), + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } +} + +@Composable +private fun VictronDiagnosticsSection(info: de.fritob.campermonitor.bluetooth.VictronDiagnostics) { + SectionHeader("Diagnose") + LabeledRow("Datensatz") { Text(info.recordName) } + LabeledRow("Produkt-ID") { Text(info.productIDText, fontFamily = FontFamily.Monospace) } + Text("Rohdaten", modifier = Modifier.padding(start = 16.dp, top = 8.dp)) + Text( + info.rawHex, + style = MaterialTheme.typography.bodySmall, + fontFamily = FontFamily.Monospace, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(horizontal = 16.dp), + ) + Text( + "Diese Werte sendet das Gerät unverschlüsselt mit.", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(16.dp), + ) +} + +@Composable +private fun BmsDiagnosticsSection(device: ConfiguredDevice, info: BmsDiagnostics) { + val context = LocalContext.current + var copied by remember { mutableStateOf(false) } + + SectionHeader("Diagnose") + LabeledRow("Erkanntes Protokoll") { Text(info.dialect) } + info.endpointPosition?.let { + LabeledRow("Verbindungsweg") { Text("${it.first} von ${it.second}") } + } + info.endpointLabel?.let { + Text("Aktueller Weg", modifier = Modifier.padding(start = 16.dp, top = 8.dp)) + Text( + it, + style = MaterialTheme.typography.bodySmall, + fontFamily = FontFamily.Monospace, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(horizontal = 16.dp), + ) + } + LabeledRow("Verbunden") { Text(if (info.isConnected) "ja" else "nein") } + LabeledRow("Empfang abonniert") { Text(if (info.isNotifyActive) "ja" else "nein") } + info.isBound?.let { LabeledRow("Angemeldet") { Text(if (it) "ja" else "nein") } } + if (info.confirmedWrites > 0) { + LabeledRow("Schreibvorgänge bestätigt") { Text("${info.confirmedWrites}") } + } + info.lastWriteError?.let { + LabeledRow("Letzter Schreibfehler") { + Text(it, color = MaterialTheme.colorScheme.error, + style = MaterialTheme.typography.bodySmall) + } + } + LabeledRow("Gesendet / empfangen") { + Text("${info.sentFrames} Anfragen / ${info.receivedBytes} Byte") + } + HexBlock("Letzter Stellbefehl", info.lastCommandHex) + HexBlock("Letzte Antwort", info.lastResponseHex) + info.fridgePayloadHex?.let { + HexBlock("Statusdaten der Box (${it.split(" ").size} Byte)", it) + } + + OutlinedButton( + onClick = { + copyToClipboard(context, buildReport(device, info)) + copied = true + }, + modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp), + ) { + Icon(Icons.Filled.ContentCopy, contentDescription = null) + Text( + if (copied) "Diagnose kopiert" else "Diagnose kopieren", + modifier = Modifier.padding(start = 8.dp), + ) + } + + if (info.gattSummary.isNotEmpty()) { + SectionHeader("Bluetooth-Merkmale des Geräts") + Text( + info.gattSummary.joinToString("\n"), + style = MaterialTheme.typography.bodySmall, + fontFamily = FontFamily.Monospace, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(horizontal = 16.dp), + ) + } + HorizontalDivider(modifier = Modifier.padding(top = 16.dp)) +} + +@Composable +private fun HexBlock(title: String, hex: String?) { + if (hex == null) return + Text(title, modifier = Modifier.padding(start = 16.dp, top = 8.dp)) + Text( + hex, + style = MaterialTheme.typography.bodySmall, + fontFamily = FontFamily.Monospace, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(horizontal = 16.dp), + ) +} + +/** + * Alles auf einmal, zum Weitergeben. Einzeln abzutippen ist zuviel verlangt, + * und gerade der Merkmalsbaum ist zu lang dafür. + */ +private fun buildReport(device: ConfiguredDevice, info: BmsDiagnostics): String = buildList { + add("Gerät: ${device.name} (${device.role.title})") + add("Protokoll: ${info.dialect}") + add("Verbunden: ${if (info.isConnected) "ja" else "nein"}") + add("Empfang abonniert: ${if (info.isNotifyActive) "ja" else "nein"}") + info.endpointPosition?.let { add("Weg: ${it.first} von ${it.second}") } + info.endpointLabel?.let { add("Merkmal: $it") } + info.isBound?.let { add("Angemeldet: ${if (it) "ja" else "nein"}") } + add("Gesendet: ${info.sentFrames} · empfangen: ${info.receivedBytes} Byte" + + " · bestätigt: ${info.confirmedWrites}") + info.lastWriteError?.let { add("Schreibfehler: $it") } + info.lastCommandHex?.let { add("Letzter Stellbefehl: $it") } + info.lastResponseHex?.let { add("Letzte Antwort: $it") } + info.fridgePayloadHex?.let { add("Statusdaten: $it") } + if (info.gattSummary.isNotEmpty()) { + add("Merkmale:") + addAll(info.gattSummary) + } +}.joinToString("\n") + +private fun copyToClipboard(context: Context, text: String) { + val manager = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + manager.setPrimaryClip(ClipData.newPlainText("Diagnose", text)) +} + +/** + * Die Nivellierung in der Geräteansicht: Anzeige, Assistent und der Weg zur + * Einrichtung. Einbaulage und Nullpunkt liegen bewusst eine Ebene tiefer – + * direkt unter der Libelle verstellte ein Fehlgriff beim Ablesen den Nullpunkt. + */ +@Composable +private fun LevelSection( + device: ConfiguredDevice, + state: LevelState, + isLive: Boolean, + onOpenAssistant: () -> Unit, + onOpenSetup: () -> Unit, +) { + var displayStyle by remember { mutableStateOf(0) } + + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + SingleChoiceSegmentedButtonRow(modifier = Modifier.fillMaxWidth()) { + listOf("Libelle", "Fahrzeug").forEachIndexed { index, label -> + SegmentedButton( + selected = displayStyle == index, + onClick = { displayStyle = index }, + shape = SegmentedButtonDefaults.itemShape(index, 2), + ) { Text(label) } + } + } + if (displayStyle == 0) { + LevelBubble(state.pitch, state.roll) + } else { + VehicleTiltView(state.pitch, state.roll) + } + state.instruction?.let { + Text( + it, + style = MaterialTheme.typography.titleMedium, + modifier = Modifier.fillMaxWidth(), + ) + } + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceEvenly) { + Reading("Längs", state.pitch, Modifier.weight(1f)) + Reading("Quer", state.roll, Modifier.weight(1f)) + } + + OutlinedButton( + onClick = onOpenAssistant, + // Der Assistent lebt von laufenden Messwerten. Ein einmal + // empfangener Wert genügt nicht: nach einem Verbindungsabbruch + // bliebe er stehen und die Ansicht sähe eingefroren aus. + enabled = isLive && state.hasReading, + modifier = Modifier.fillMaxWidth(), + ) { + Text("Ausrichtungs-Assistent") + } + if (!isLive) { + Text( + "Der Assistent braucht laufende Messwerte. Der Neigungsmesser ist " + + "gerade nicht verbunden.", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + + OutlinedButton(onClick = onOpenSetup, modifier = Modifier.fillMaxWidth()) { + Icon(Icons.Filled.Tune, contentDescription = null) + Text("Neigungsmesser einrichten", modifier = Modifier.weight(1f).padding(start = 8.dp)) + Text( + if (state.isKnownUncalibrated) "Nullpunkt fehlt" else device.sensorOrientation.summary, + style = MaterialTheme.typography.labelSmall, + ) + } + } +} diff --git a/Android/app/src/main/kotlin/de/fritob/campermonitor/ui/FridgeControls.kt b/Android/app/src/main/kotlin/de/fritob/campermonitor/ui/FridgeControls.kt new file mode 100644 index 0000000..860c598 --- /dev/null +++ b/Android/app/src/main/kotlin/de/fritob/campermonitor/ui/FridgeControls.kt @@ -0,0 +1,156 @@ +package de.fritob.campermonitor.ui + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.Remove +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.SegmentedButton +import androidx.compose.material3.SegmentedButtonDefaults +import androidx.compose.material3.SingleChoiceSegmentedButtonRow +import androidx.compose.material3.Switch +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import de.fritob.campermonitor.bluetooth.BluetoothManager +import de.fritob.campermonitor.protocol.AlpicoolState +import de.fritob.campermonitor.protocol.ConfiguredDevice +import de.fritob.campermonitor.protocol.DeviceLinkState +import de.fritob.campermonitor.protocol.FridgeZone + +/** + * Bedienelemente einer Alpicool-Kühlbox. + * + * Alle Schalter folgen dem Gerät, nicht der Vermutung: nach jedem Stellbefehl + * fragt die Sitzung den Zustand neu ab, und die Ansicht zeigt, was zurückkam. + */ +@Composable +fun FridgeControls( + device: ConfiguredDevice, + state: AlpicoolState, + bluetooth: BluetoothManager, +) { + val isLinked = bluetooth.linkStates[device.id] == DeviceLinkState.Live + + Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { + SectionHeader("Steuerung") + + Row( + modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text("Eingeschaltet", modifier = Modifier.weight(1f)) + Switch( + checked = state.isPoweredOn, + onCheckedChange = { bluetooth.setFridgePower(it, device.id) }, + ) + } + + SingleChoiceSegmentedButtonRow( + modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp), + ) { + listOf("Max" to false, "Eco" to true).forEachIndexed { index, (label, eco) -> + SegmentedButton( + selected = state.isEco == eco, + onClick = { bluetooth.setFridgeEco(eco, device.id) }, + shape = SegmentedButtonDefaults.itemShape(index, 2), + enabled = state.isPoweredOn, + ) { Text(label) } + } + } + + TargetStepper( + title = if (state.isDualZone) "Soll links" else "Solltemperatur", + value = state.leftTarget, + state = state, + enabled = state.isPoweredOn, + onChange = { bluetooth.setFridgeTarget(it, FridgeZone.LEFT, device.id) }, + ) + + if (state.isDualZone) { + TargetStepper( + title = "Soll rechts", + value = state.rightTarget, + state = state, + enabled = state.isPoweredOn, + onChange = { bluetooth.setFridgeTarget(it, FridgeZone.RIGHT, device.id) }, + ) + } + + Row( + modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text("Bedienfeld gesperrt", modifier = Modifier.weight(1f)) + Switch( + checked = state.isLocked, + onCheckedChange = { bluetooth.setFridgeLock(it, device.id) }, + ) + } + + Text( + // Ist die Box gerade nicht erreichbar, wird ein Befehl aufgehoben + // statt verworfen – das gehört gesagt, sonst sieht es aus, als + // hätte das Tippen nichts bewirkt. + if (isLinked) { + "Änderungen gehen direkt an die Box. Der angezeigte Stand kommt aus " + + "ihrer Antwort, nicht aus der Eingabe." + } else { + "Die Box ist gerade nicht verbunden. Die Änderung wird gemerkt und " + + "geht raus, sobald sie wieder erreichbar ist." + }, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(horizontal = 16.dp, vertical = 4.dp), + ) + } +} + +@Composable +private fun TargetStepper( + title: String, + value: Int?, + state: AlpicoolState, + enabled: Boolean, + onChange: (Int) -> Unit, +) { + val range = state.targetRange + val current = value ?: range.first + + Row( + modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text(title, modifier = Modifier.weight(1f)) + Text( + value?.let { "$it ${state.unitSymbol}" } ?: "–", + style = MaterialTheme.typography.titleMedium, + ) + IconButton( + onClick = { onChange((current - 1).coerceIn(range.first, range.last)) }, + enabled = enabled && value != null && current > range.first, + ) { Icon(Icons.Filled.Remove, contentDescription = "kälter") } + IconButton( + onClick = { onChange((current + 1).coerceIn(range.first, range.last)) }, + enabled = enabled && value != null && current < range.last, + ) { Icon(Icons.Filled.Add, contentDescription = "wärmer") } + } +} + +@Composable +fun SectionHeader(title: String) { + Text( + title, + style = MaterialTheme.typography.titleSmall, + color = MaterialTheme.colorScheme.primary, + modifier = Modifier.padding(start = 16.dp, top = 16.dp, bottom = 4.dp), + ) +} diff --git a/Android/app/src/main/kotlin/de/fritob/campermonitor/ui/Icons.kt b/Android/app/src/main/kotlin/de/fritob/campermonitor/ui/Icons.kt new file mode 100644 index 0000000..ff54198 --- /dev/null +++ b/Android/app/src/main/kotlin/de/fritob/campermonitor/ui/Icons.kt @@ -0,0 +1,53 @@ +package de.fritob.campermonitor.ui + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.AcUnit +import androidx.compose.material.icons.filled.Agriculture +import androidx.compose.material.icons.filled.AirportShuttle +import androidx.compose.material.icons.filled.Battery5Bar +import androidx.compose.material.icons.filled.BatteryChargingFull +import androidx.compose.material.icons.filled.Bolt +import androidx.compose.material.icons.filled.Cabin +import androidx.compose.material.icons.filled.DirectionsBoat +import androidx.compose.material.icons.filled.DirectionsBus +import androidx.compose.material.icons.filled.DirectionsCar +import androidx.compose.material.icons.filled.LocalShipping +import androidx.compose.material.icons.filled.Landscape +import androidx.compose.material.icons.filled.Speed +import androidx.compose.material.icons.filled.Straighten +import androidx.compose.material.icons.filled.WbSunny +import androidx.compose.ui.graphics.vector.ImageVector +import de.fritob.campermonitor.protocol.DeviceRole + +/** + * Die Entsprechungen der SF-Symbole aus der iOS-Fassung. + * + * Material bringt keine deckungsgleichen Symbole mit; ausgewählt ist jeweils + * das, was dieselbe Sache meint – nicht das, was am ähnlichsten aussieht. + */ +fun roleIcon(role: DeviceRole): ImageVector = when (role) { + DeviceRole.CHARGE_BOOSTER -> Icons.Filled.Bolt + DeviceRole.SOLAR_CHARGER -> Icons.Filled.WbSunny + DeviceRole.BATTERY_MONITOR -> Icons.Filled.Speed + DeviceRole.BMS -> Icons.Filled.BatteryChargingFull + DeviceRole.FRIDGE -> Icons.Filled.AcUnit + DeviceRole.LEVELING -> Icons.Filled.Straighten +} + +/** Symbol eines Fahrzeugprofils. */ +fun profileIcon(symbol: String): ImageVector = when (symbol) { + "box_truck" -> Icons.Filled.LocalShipping + "pickup" -> Icons.Filled.Agriculture + "bus" -> Icons.Filled.DirectionsBus + "double_decker" -> Icons.Filled.AirportShuttle + "car" -> Icons.Filled.DirectionsCar + "car_side" -> Icons.Filled.DirectionsCar + "tent" -> Icons.Filled.Cabin + "sailboat" -> Icons.Filled.DirectionsBoat + "lodge" -> Icons.Filled.Cabin + "mountains" -> Icons.Filled.Landscape + else -> Icons.Filled.LocalShipping +} + +/** Für die Anzeige des Batteriestands in der Fahrzeugauswahl. */ +val batteryIcon: ImageVector = Icons.Filled.Battery5Bar diff --git a/Android/app/src/main/kotlin/de/fritob/campermonitor/ui/LevelSetupScreen.kt b/Android/app/src/main/kotlin/de/fritob/campermonitor/ui/LevelSetupScreen.kt new file mode 100644 index 0000000..9513084 --- /dev/null +++ b/Android/app/src/main/kotlin/de/fritob/campermonitor/ui/LevelSetupScreen.kt @@ -0,0 +1,180 @@ +package de.fritob.campermonitor.ui + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.filled.GpsFixed +import androidx.compose.material.icons.filled.Refresh +import androidx.compose.material.icons.filled.SwapHoriz +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import de.fritob.campermonitor.bluetooth.BluetoothManager +import de.fritob.campermonitor.protocol.ConfiguredDevice +import de.fritob.campermonitor.protocol.LevelState +import de.fritob.campermonitor.store.DeviceStore + +/** + * 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. + */ +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun LevelSetupScreen( + device: ConfiguredDevice, + store: DeviceStore, + bluetooth: BluetoothManager, + onBack: () -> Unit, + onOpenSensorSetup: () -> Unit, +) { + val state = bluetooth.levelStates[device.id] ?: LevelState() + val current = store.devices.firstOrNull { it.id == device.id } ?: device + var showResetConfirmation by remember { mutableStateOf(false) } + + Scaffold( + topBar = { + TopAppBar( + title = { Text("Neigungsmesser einrichten") }, + navigationIcon = { + IconButton(onClick = onBack) { + Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Zurück") + } + }, + ) + }, + ) { padding -> + Column( + modifier = Modifier + .fillMaxSize() + .padding(padding) + .verticalScroll(rememberScrollState()) + .padding(16.dp), + verticalArrangement = Arrangement.spacedBy(20.dp), + ) { + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceEvenly) { + Reading("Längs", state.pitch, Modifier.weight(1f)) + Reading("Quer", state.roll, Modifier.weight(1f)) + } + + Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { + Text("Schritt 1 – Einbaulage", style = MaterialTheme.typography.titleSmall) + OutlinedButton( + onClick = onOpenSensorSetup, + enabled = state.hasReading, + modifier = Modifier.fillMaxWidth(), + ) { + Icon(Icons.Filled.SwapHoriz, contentDescription = null) + Text( + "Einbaulage bestimmen", + modifier = Modifier.weight(1f).padding(start = 8.dp), + ) + Text( + current.sensorOrientation.summary, + style = MaterialTheme.typography.labelSmall, + ) + } + 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.", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + + Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { + Text("Schritt 2 – Nullpunkt", style = MaterialTheme.typography.titleSmall) + Button( + onClick = { bluetooth.calibrateLevel(device.id) }, + enabled = state.hasReading, + modifier = Modifier.fillMaxWidth(), + ) { + Icon(Icons.Filled.GpsFixed, contentDescription = null) + Text("Aktuelle Lage als eben übernehmen", modifier = Modifier.padding(start = 8.dp)) + } + if (!state.isKnownUncalibrated) { + OutlinedButton( + onClick = { showResetConfirmation = true }, + modifier = Modifier.fillMaxWidth(), + colors = ButtonDefaults.outlinedButtonColors( + contentColor = MaterialTheme.colorScheme.error, + ), + ) { + Icon(Icons.Filled.Refresh, contentDescription = null) + Text("Nullpunkt verwerfen", modifier = Modifier.padding(start = 8.dp)) + } + } + Text( + calibrationHint(state), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } + } + + if (showResetConfirmation) { + AlertDialog( + onDismissRequest = { showResetConfirmation = false }, + title = { Text("Nullpunkt verwerfen?") }, + text = { Text("Die Anzeige zeigt danach wieder die Lage des Sensors.") }, + confirmButton = { + TextButton(onClick = { + bluetooth.resetLevelCalibration(device.id) + showResetConfirmation = false + }) { Text("Verwerfen", color = Color.Unspecified) } + }, + dismissButton = { + TextButton(onClick = { showResetConfirmation = false }) { Text("Abbrechen") } + }, + ) + } +} + +private fun calibrationHint(state: LevelState): String { + val pitchOffset = state.pitchOffset + val rollOffset = state.rollOffset + if (state.isCalibrated && pitchOffset != null && rollOffset != null) { + return "Der Nullpunkt liegt bei %.1f° längs und %.1f° quer. Zum Neusetzen das " + .format(pitchOffset, rollOffset) + "Fahrzeug eben stellen und dann tippen." + } + 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." +} diff --git a/Android/app/src/main/kotlin/de/fritob/campermonitor/ui/LevelViews.kt b/Android/app/src/main/kotlin/de/fritob/campermonitor/ui/LevelViews.kt new file mode 100644 index 0000000..ecd8795 --- /dev/null +++ b/Android/app/src/main/kotlin/de/fritob/campermonitor/ui/LevelViews.kt @@ -0,0 +1,210 @@ +package de.fritob.campermonitor.ui + +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.rotate +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import de.fritob.campermonitor.R +import de.fritob.campermonitor.protocol.LevelState +import kotlin.math.abs +import kotlin.math.max + +/** + * Grafische Libelle: eine Blase, die zeigt, wohin das Fahrzeug hängt. + * + * Die Blase wandert dorthin, wo das Fahrzeug **höher** steht – so, wie sich + * eine echte Wasserwaage verhält. Wer sie mittig haben will, muss also die + * Gegenseite anheben. + */ +@Composable +fun LevelBubble(pitch: Double?, roll: Double?, range: Double = 6.0) { + val deviation = when { + pitch != null && roll != null -> max(abs(pitch), abs(roll)) + pitch != null -> abs(pitch) + roll != null -> abs(roll) + else -> null + } + val isLevel = pitch != null && roll != null && + abs(pitch) <= LevelState.LEVEL_TOLERANCE && abs(roll) <= LevelState.LEVEL_TOLERANCE + + // Grün nur, wenn es wirklich eben ist – sonst sähe "schief" wie "eben" aus. + val bubbleColor = when { + deviation == null -> Color.Gray + deviation <= LevelState.LEVEL_TOLERANCE -> Color(0xFF1F9E52) + deviation <= 2 -> Color(0xFFE08600) + else -> Color(0xFFD03B2F) + } + val outline = MaterialTheme.colorScheme.outlineVariant + val fill = MaterialTheme.colorScheme.surfaceVariant + + Canvas(modifier = Modifier.fillMaxWidth().aspectRatio(1f)) { + val side = minOf(size.width, size.height) + val radius = side / 2 + val bubble = side * 0.16f + // Die Blase darf den Rand nicht verlassen, auch bei starker Neigung. + val travel = radius - bubble / 2 - 4 + val centre = Offset(size.width / 2, size.height / 2) + + fun clamped(value: Double?): Float = + ((value ?: 0.0).coerceIn(-range, range) / range).toFloat() + + drawCircle(color = fill, radius = radius, center = centre) + drawCircle(color = outline, radius = radius, center = centre, style = Stroke(1f)) + + // Ringe als echter Massstab: der innere markiert die Toleranz, der + // mittlere zwei Grad. Ohne Massstab sagt die Blasenlage nichts darüber, + // wie weit es noch ist. + val toleranceRadius = max( + (LevelState.LEVEL_TOLERANCE / range * travel).toFloat(), + bubble * 0.6f, + ) + drawCircle( + color = if (isLevel) Color(0xFF1F9E52) else outline, + radius = toleranceRadius, center = centre, + style = Stroke(if (isLevel) 2f else 1f), + ) + drawCircle( + color = outline, radius = (2.0 / range * travel).toFloat(), center = centre, + style = Stroke(1f), + ) + drawLine(outline, Offset(centre.x - travel, centre.y), Offset(centre.x + travel, centre.y)) + drawLine(outline, Offset(centre.x, centre.y - travel), Offset(centre.x, centre.y + travel)) + + // Positiver Pitch heisst: das Heck steht höher, die Blase wandert also + // nach oben – in der Ansicht nach hinten. + drawCircle( + color = bubbleColor.copy(alpha = if (deviation == null) 0.25f else 1f), + radius = bubble / 2, + center = Offset( + centre.x + clamped(roll) * travel, + centre.y - clamped(pitch) * travel, + ), + ) + } +} + +/** + * Die Neigung am Fahrzeug selbst: Seitenansicht für längs, Heckansicht für + * quer. Aus dem Ursprungsprojekt übernommen. + */ +@Composable +fun VehicleTiltView(pitch: Double?, roll: Double?) { + Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { + TiltedVehicle( + title = "Längs", + value = pitch, + // Positiver Pitch heisst Heck höher; das Bild zeigt nach rechts das + // Heck, also dreht die Front nach unten. + degrees = ((pitch ?: 0.0) * EXAGGERATION).toFloat(), + drawable = R.drawable.vehicle_side, + leftLabel = "Front", rightLabel = "Heck", + ) + TiltedVehicle( + title = "Quer", + value = roll, + degrees = (-(roll ?: 0.0) * EXAGGERATION).toFloat(), + drawable = R.drawable.vehicle_rear, + leftLabel = "links", rightLabel = "rechts", + ) + Text( + "Neigung ${EXAGGERATION.toInt()}-fach überhöht dargestellt – sonst wäre " + + "sie kaum zu erkennen. Die Gradzahlen sind echt.", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } +} + +/** Ohne Überhöhung sind zwei Grad am Bild nicht zu sehen. */ +private const val EXAGGERATION = 3.0 + +@Composable +private fun TiltedVehicle( + title: String, + value: Double?, + degrees: Float, + drawable: Int, + leftLabel: String, + rightLabel: String, +) { + val tint = when { + value == null -> MaterialTheme.colorScheme.onSurfaceVariant + abs(value) <= LevelState.LEVEL_TOLERANCE -> Color(0xFF1F9E52) + abs(value) <= 2 -> Color(0xFFE08600) + else -> Color(0xFFD03B2F) + } + Column { + Row(modifier = Modifier.fillMaxWidth()) { + Text(title, style = MaterialTheme.typography.titleSmall, modifier = Modifier.weight(1f)) + Text( + value?.let { "%.1f°".format(it) } ?: "–", + style = MaterialTheme.typography.titleSmall, + fontWeight = FontWeight.SemiBold, + color = tint, + ) + } + Box( + modifier = Modifier.fillMaxWidth().height(120.dp), + contentAlignment = Alignment.Center, + ) { + Image( + painter = painterResource(drawable), + contentDescription = null, + colorFilter = ColorFilter.tint(tint), + modifier = Modifier.fillMaxWidth(0.8f).rotate(degrees), + ) + } + Row(modifier = Modifier.fillMaxWidth()) { + Text( + leftLabel, + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.weight(1f), + ) + Text( + rightLabel, + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } +} + +/** Ein Messwert mit Beschriftung, wie er unter der Libelle steht. */ +@Composable +fun Reading(title: String, value: Double?, modifier: Modifier = Modifier) { + Column( + modifier = modifier.padding(vertical = 4.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text( + value?.let { "%.1f°".format(it) } ?: "–", + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.SemiBold, + ) + Text( + title, + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } +} diff --git a/Android/app/src/main/kotlin/de/fritob/campermonitor/ui/Permissions.kt b/Android/app/src/main/kotlin/de/fritob/campermonitor/ui/Permissions.kt new file mode 100644 index 0000000..51776fd --- /dev/null +++ b/Android/app/src/main/kotlin/de/fritob/campermonitor/ui/Permissions.kt @@ -0,0 +1,103 @@ +package de.fritob.campermonitor.ui + +import android.Manifest +import android.content.Context +import android.content.pm.PackageManager +import android.os.Build +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Button +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.core.content.ContextCompat + +/** + * Welche Rechte für Bluetooth gebraucht werden. + * + * Mit Android 12 hat sich das Modell geändert: davor lief ein BLE-Scan über + * die Standortfreigabe, seither gibt es eigene Bluetooth-Rechte. Beide Wege + * müssen bedient werden, sonst startet die App auf der einen oder der anderen + * Version nicht. + */ +val bluetoothPermissions: Array + get() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + arrayOf(Manifest.permission.BLUETOOTH_SCAN, Manifest.permission.BLUETOOTH_CONNECT) + } else { + arrayOf(Manifest.permission.ACCESS_FINE_LOCATION) + } + +fun hasBluetoothPermissions(context: Context): Boolean = + bluetoothPermissions.all { + ContextCompat.checkSelfPermission(context, it) == PackageManager.PERMISSION_GRANTED + } + +/** + * Zeigt [content], sobald die Rechte da sind – sonst eine Erklärung mit Knopf. + * + * Ohne Erklärung wirkt eine App, die beim ersten Start nach Bluetooth fragt, + * schnell übergriffig. Hier steht, wozu. + */ +@Composable +fun WithBluetoothPermission(content: @Composable () -> Unit) { + val context = LocalContext.current + var granted by remember { mutableStateOf(hasBluetoothPermissions(context)) } + var asked by remember { mutableStateOf(false) } + + val launcher = rememberLauncherForActivityResult( + ActivityResultContracts.RequestMultiplePermissions() + ) { result -> + granted = result.values.all { it } + asked = true + } + + if (granted) { + content() + return + } + + Column( + modifier = Modifier.fillMaxSize().padding(32.dp), + verticalArrangement = Arrangement.spacedBy(16.dp, Alignment.CenterVertically), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text( + "Bluetooth wird gebraucht", + style = MaterialTheme.typography.headlineSmall, + textAlign = TextAlign.Center, + ) + Text( + "Die App liest die Werte deiner Geräte über Bluetooth – Ladebooster, " + + "Solarladeregler, Batterie, Kühlbox und Neigungsmesser. Ohne die " + + "Freigabe bleibt sie leer.", + style = MaterialTheme.typography.bodyMedium, + textAlign = TextAlign.Center, + ) + if (asked) { + Text( + "Die Freigabe wurde abgelehnt. Sie lässt sich in den " + + "Android-Einstellungen unter „Apps → Camper Monitor → " + + "Berechtigungen“ nachholen.", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.error, + textAlign = TextAlign.Center, + ) + } + Button(onClick = { launcher.launch(bluetoothPermissions) }) { + Text(if (asked) "Nochmal fragen" else "Bluetooth freigeben") + } + } +} diff --git a/Android/app/src/main/kotlin/de/fritob/campermonitor/ui/ProfilesScreen.kt b/Android/app/src/main/kotlin/de/fritob/campermonitor/ui/ProfilesScreen.kt new file mode 100644 index 0000000..24ecc88 --- /dev/null +++ b/Android/app/src/main/kotlin/de/fritob/campermonitor/ui/ProfilesScreen.kt @@ -0,0 +1,241 @@ +package de.fritob.campermonitor.ui + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.Check +import androidx.compose.material.icons.filled.Edit +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Button +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.FilterChip +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import de.fritob.campermonitor.bluetooth.BluetoothManager +import de.fritob.campermonitor.protocol.Profile +import de.fritob.campermonitor.store.DeviceStore + +/** + * Die Fahrzeugverwaltung. + * + * Antippen wählt aus, der Stift öffnet Name, Symbol und Masse. Beides + * getrennt, weil die Auswahl der häufige Fall ist und das Bearbeiten der + * seltene – ein Wisch-Menü hatte unter iOS niemand gefunden. + */ +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ProfilesScreen(store: DeviceStore, bluetooth: BluetoothManager, onBack: () -> Unit) { + var editing by remember { mutableStateOf(null) } + var pendingDeletion by remember { mutableStateOf(null) } + + Scaffold( + topBar = { + TopAppBar( + title = { Text("Fahrzeuge") }, + navigationIcon = { + IconButton(onClick = onBack) { + Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Zurück") + } + }, + actions = { + IconButton(onClick = { + editing = store.addProfile("Camper ${store.profiles.size + 1}") + }) { + Icon(Icons.Filled.Add, contentDescription = "Fahrzeug hinzufügen") + } + }, + ) + }, + ) { padding -> + LazyColumn(modifier = Modifier.fillMaxSize().padding(padding)) { + items(store.profiles, key = { it.id }) { profile -> + Row( + modifier = Modifier + .fillMaxWidth() + .clickable { + store.selectProfile(profile.id) + bluetooth.refreshConfiguration() + } + .padding(horizontal = 16.dp, vertical = 12.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon(profileIcon(profile.symbol), contentDescription = null) + Column(modifier = Modifier.weight(1f).padding(start = 12.dp)) { + Text(profile.name, style = MaterialTheme.typography.bodyLarge) + Text( + "${store.devices.count { it.profileID == profile.id }} Geräte" + + measuresText(profile), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + if (profile.id == store.activeProfile?.id) { + Icon( + Icons.Filled.Check, contentDescription = "ausgewählt", + tint = MaterialTheme.colorScheme.primary, + ) + } + IconButton(onClick = { editing = profile }) { + Icon(Icons.Filled.Edit, contentDescription = "Bearbeiten") + } + } + } + + item { + Text( + "Antippen wählt das Fahrzeug aus, der Stift öffnet Name, Symbol " + + "und Masse. Jedes Fahrzeug hat seine eigenen Geräte, und die " + + "App liest immer nur die des gewählten aus.", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(16.dp), + ) + } + } + } + + editing?.let { profile -> + ProfileEditDialog( + profile = profile, + canDelete = store.profiles.size > 1, + onDismiss = { editing = null }, + onSave = { + store.updateProfile(it) + bluetooth.refreshConfiguration() + editing = null + }, + onDelete = { + pendingDeletion = profile + editing = null + }, + ) + } + + pendingDeletion?.let { profile -> + AlertDialog( + onDismissRequest = { pendingDeletion = null }, + title = { Text("Fahrzeug entfernen?") }, + text = { Text("Die Geräte dieses Fahrzeugs werden mit gelöscht.") }, + confirmButton = { + TextButton(onClick = { + store.removeProfile(profile) + bluetooth.refreshConfiguration() + pendingDeletion = null + }) { Text("Entfernen") } + }, + dismissButton = { + TextButton(onClick = { pendingDeletion = null }) { Text("Abbrechen") } + }, + ) + } +} + +private fun measuresText(profile: Profile): String { + val track = profile.trackWidth + val base = profile.wheelbase + if (track == null && base == null) return "" + val parts = buildList { + track?.let { add("Spur %.2f m".format(it)) } + base?.let { add("Radstand %.2f m".format(it)) } + } + return " · " + parts.joinToString(", ") +} + +@Composable +private fun ProfileEditDialog( + profile: Profile, + canDelete: Boolean, + onDismiss: () -> Unit, + onSave: (Profile) -> Unit, + onDelete: () -> Unit, +) { + var name by remember { mutableStateOf(profile.name) } + var symbol by remember { mutableStateOf(profile.symbol) } + var track by remember { mutableStateOf(profile.trackWidth?.toString() ?: "") } + var base by remember { mutableStateOf(profile.wheelbase?.toString() ?: "") } + + AlertDialog( + onDismissRequest = onDismiss, + title = { Text("Fahrzeug") }, + text = { + Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { + OutlinedTextField( + value = name, + onValueChange = { name = it }, + label = { Text("Name") }, + singleLine = true, + ) + Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { + Profile.SYMBOLS.take(5).forEach { candidate -> + FilterChip( + selected = symbol == candidate, + onClick = { symbol = candidate }, + label = { Icon(profileIcon(candidate), contentDescription = null) }, + ) + } + } + OutlinedTextField( + value = track, + onValueChange = { track = it }, + label = { Text("Spurweite in Metern") }, + singleLine = true, + ) + OutlinedTextField( + value = base, + onValueChange = { base = it }, + label = { Text("Radstand in Metern") }, + singleLine = true, + ) + Text( + "Beide Masse braucht nur der Keilrechner im " + + "Ausrichtungs-Assistenten. Ohne sie bleibt der Rest nutzbar.", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + }, + confirmButton = { + Button(onClick = { + onSave( + profile.copy( + name = name.trim().ifEmpty { profile.name }, + symbol = symbol, + trackWidth = track.replace(',', '.').toDoubleOrNull(), + wheelbase = base.replace(',', '.').toDoubleOrNull(), + ) + ) + }) { Text("Sichern") } + }, + dismissButton = { + Row { + if (canDelete) { + TextButton(onClick = onDelete) { Text("Entfernen") } + } + TextButton(onClick = onDismiss) { Text("Abbrechen") } + } + }, + ) +} diff --git a/Android/app/src/main/kotlin/de/fritob/campermonitor/ui/SensorSetupScreen.kt b/Android/app/src/main/kotlin/de/fritob/campermonitor/ui/SensorSetupScreen.kt new file mode 100644 index 0000000..54f5d33 --- /dev/null +++ b/Android/app/src/main/kotlin/de/fritob/campermonitor/ui/SensorSetupScreen.kt @@ -0,0 +1,247 @@ +package de.fritob.campermonitor.ui + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.CheckCircle +import androidx.compose.material.icons.filled.HorizontalRule +import androidx.compose.material.icons.filled.SouthEast +import androidx.compose.material.icons.filled.SouthWest +import androidx.compose.material.icons.filled.SwapHoriz +import androidx.compose.material.icons.filled.Warning +import androidx.compose.material3.Button +import androidx.compose.material3.Card +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import de.fritob.campermonitor.bluetooth.BluetoothManager +import de.fritob.campermonitor.protocol.ConfiguredDevice +import de.fritob.campermonitor.protocol.LevelState +import de.fritob.campermonitor.protocol.OrientationDetection +import de.fritob.campermonitor.protocol.SensorOrientation +import de.fritob.campermonitor.store.DeviceStore + +/** + * 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. + */ +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun SensorSetupScreen( + device: ConfiguredDevice, + store: DeviceStore, + bluetooth: BluetoothManager, + onBack: () -> Unit, +) { + val state = bluetooth.levelStates[device.id] ?: LevelState() + val live = remember(state) { + val pitch = state.rawPitch + val roll = state.rawRoll + if (pitch != null && roll != null) OrientationDetection.Reading(pitch, roll) else null + } + + var step by remember { mutableStateOf(Step.INTRO) } + /** Ruhelage, auf die beide Kippbewegungen bezogen werden. */ + var reference by remember { mutableStateOf(OrientationDetection.Reading(0.0, 0.0)) } + var noseChange by remember { mutableStateOf(null) } + var result by remember { mutableStateOf(null) } + var failure by remember { mutableStateOf(null) } + + Scaffold( + topBar = { TopAppBar(title = { Text("Einbaulage") }) }, + ) { padding -> + Column( + modifier = Modifier.fillMaxSize().padding(padding).padding(24.dp), + verticalArrangement = Arrangement.spacedBy(24.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + when (step) { + Step.INTRO -> { + InstructionCard( + icon = Icons.Filled.SwapHoriz, + 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(state) + Button( + onClick = { + reference = live ?: OrientationDetection.Reading(0.0, 0.0) + step = Step.TILT_NOSE + }, + enabled = live != null, + ) { Text("Los geht's") } + } + + Step.TILT_NOSE -> { + InstructionCard( + icon = Icons.Filled.SouthEast, + 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\nDeutlich kippen, etwa " + + "eine Handbreit, und in dieser Lage halten. Dann weiter.", + ) + LiveReadout(state) + Button( + onClick = { + live?.let { noseChange = it - 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 = Step.SETTLE + }, + enabled = live != null, + ) { Text("Weiter") } + } + + Step.SETTLE -> { + InstructionCard( + icon = Icons.Filled.HorizontalRule, + 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(state) + Button( + onClick = { + reference = live ?: reference + step = Step.TILT_SIDE + }, + enabled = live != null, + ) { Text("Weiter") } + } + + Step.TILT_SIDE -> { + InstructionCard( + icon = Icons.Filled.SouthWest, + 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\nWieder " + + "deutlich kippen und in dieser Lage halten.", + ) + LiveReadout(state) + Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) { + OutlinedButton(onClick = { step = Step.SETTLE }) { Text("Zurück") } + Button( + onClick = { + val nose = noseChange + val side = live?.minus(reference) + if (nose != null && side != null) { + when (val r = OrientationDetection.orientation(nose, side)) { + is OrientationDetection.Result.Success -> { + result = r.orientation + step = Step.DONE + } + is OrientationDetection.Result.Error -> { + failure = r.failure + step = Step.FAILED + } + } + } + }, + enabled = live != null, + ) { Text("Fertig") } + } + } + + Step.DONE -> { + val orientation = result ?: SensorOrientation.IDENTITY + InstructionCard( + icon = Icons.Filled.CheckCircle, + 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 den Nullpunkt " + + "zu setzen.", + ) + Button(onClick = { + val updated = device.copy(sensorOrientation = orientation) + store.update(updated) + bluetooth.updateSensorOrientation(updated) + onBack() + }) { Text("Übernehmen") } + } + + Step.FAILED -> { + InstructionCard( + icon = Icons.Filled.Warning, + title = "Das hat nicht geklappt", + text = failure?.message ?: "", + ) + Button(onClick = { step = Step.INTRO }) { Text("Nochmal versuchen") } + } + } + + TextButton(onClick = onBack) { Text("Abbrechen") } + } + } +} + +private enum class Step { INTRO, TILT_NOSE, SETTLE, TILT_SIDE, DONE, FAILED } + +@Composable +private fun InstructionCard(icon: ImageVector, title: String, text: String) { + Card(modifier = Modifier.fillMaxWidth()) { + Column( + modifier = Modifier.fillMaxWidth().padding(16.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Icon( + icon, contentDescription = null, + tint = MaterialTheme.colorScheme.primary, + modifier = Modifier.size(44.dp), + ) + Text( + title, + style = MaterialTheme.typography.titleMedium, + textAlign = TextAlign.Center, + ) + Text( + text, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + textAlign = TextAlign.Center, + ) + } + } +} + +/** + * Was der Sensor gerade meldet – ohne Umrechnung, denn die wird hier ja erst + * bestimmt. Deshalb heissen die Achsen A und B statt längs und quer. + */ +@Composable +private fun LiveReadout(state: LevelState) { + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceEvenly) { + Reading("Achse A", state.rawPitch, Modifier.weight(1f)) + Reading("Achse B", state.rawRoll, Modifier.weight(1f)) + } +} diff --git a/Android/app/src/main/kotlin/de/fritob/campermonitor/ui/SettingsScreen.kt b/Android/app/src/main/kotlin/de/fritob/campermonitor/ui/SettingsScreen.kt new file mode 100644 index 0000000..36c2740 --- /dev/null +++ b/Android/app/src/main/kotlin/de/fritob/campermonitor/ui/SettingsScreen.kt @@ -0,0 +1,105 @@ +package de.fritob.campermonitor.ui + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Switch +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import de.fritob.campermonitor.store.DeviceStore + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun SettingsScreen(store: DeviceStore, onBack: () -> Unit) { + // Der Speicher meldet Änderungen selbst; die lokale Kopie hält nur den + // Schalter in Bewegung, während geschrieben wird. + var showDiagnostics by remember { mutableStateOf(store.showDiagnostics) } + + Scaffold( + topBar = { + TopAppBar( + title = { Text("Einstellungen") }, + navigationIcon = { + IconButton(onClick = onBack) { + Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Zurück") + } + }, + ) + }, + ) { padding -> + Column( + modifier = Modifier.fillMaxSize().padding(padding).verticalScroll(rememberScrollState()), + ) { + SettingRow( + title = "Diagnose einblenden", + subtitle = "Zeigt Protokoll, Verbindungsweg und Rohdaten in den " + + "Gerätedetails. Im Alltag stören sie; bei einem Fehler sind sie " + + "genau das, was weiterhilft.", + trailing = { + Switch( + checked = showDiagnostics, + onCheckedChange = { + showDiagnostics = it + store.showDiagnostics = it + }, + ) + }, + ) + + Text( + "Camper Monitor liest Victron-Geräte über ihr Advertisement mit " + + "(„Instant Readout“) und spricht Batterie, Kühlbox und " + + "Neigungsmesser direkt über Bluetooth an.", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(16.dp), + ) + } + } +} + +@Composable +fun SettingRow( + title: String, + subtitle: String? = null, + onClick: (() -> Unit)? = null, + trailing: @Composable (() -> Unit)? = null, +) { + androidx.compose.foundation.layout.Row( + modifier = Modifier + .fillMaxWidth() + .then(if (onClick != null) Modifier.clickable(onClick = onClick) else Modifier) + .padding(horizontal = 16.dp, vertical = 12.dp), + verticalAlignment = androidx.compose.ui.Alignment.CenterVertically, + ) { + Column(modifier = Modifier.weight(1f)) { + Text(title, style = MaterialTheme.typography.bodyLarge) + if (subtitle != null) { + Text( + subtitle, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } + trailing?.invoke() + } +} diff --git a/Android/app/src/main/kotlin/de/fritob/campermonitor/ui/Theme.kt b/Android/app/src/main/kotlin/de/fritob/campermonitor/ui/Theme.kt new file mode 100644 index 0000000..81a18a1 --- /dev/null +++ b/Android/app/src/main/kotlin/de/fritob/campermonitor/ui/Theme.kt @@ -0,0 +1,44 @@ +package de.fritob.campermonitor.ui + +import android.os.Build +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.dynamicDarkColorScheme +import androidx.compose.material3.dynamicLightColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext + +/** + * Das Grün der iOS-Fassung als Akzent – damit beide Apps als dieselbe erkennbar + * bleiben. Ab Android 12 gewinnt die Farbwelt des Systems, das ist dort die + * Erwartung. + */ +private val CamperGreen = Color(0xFF1F9E52) + +private val LightColors = lightColorScheme( + primary = CamperGreen, + secondary = CamperGreen, +) + +private val DarkColors = darkColorScheme( + primary = Color(0xFF54C77F), + secondary = Color(0xFF54C77F), +) + +@Composable +fun CamperTheme( + darkTheme: Boolean = isSystemInDarkTheme(), + content: @Composable () -> Unit, +) { + val context = LocalContext.current + val colors = when { + Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> + if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) + darkTheme -> DarkColors + else -> LightColors + } + MaterialTheme(colorScheme = colors, content = content) +} diff --git a/Android/app/src/main/kotlin/de/fritob/campermonitor/ui/VictronKeyScreen.kt b/Android/app/src/main/kotlin/de/fritob/campermonitor/ui/VictronKeyScreen.kt new file mode 100644 index 0000000..92d0a86 --- /dev/null +++ b/Android/app/src/main/kotlin/de/fritob/campermonitor/ui/VictronKeyScreen.kt @@ -0,0 +1,145 @@ +package de.fritob.campermonitor.ui + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material3.Button +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.unit.dp +import de.fritob.campermonitor.bluetooth.BluetoothManager +import de.fritob.campermonitor.protocol.ConfiguredDevice +import de.fritob.campermonitor.store.DeviceStore + +/** + * Eingabe des Victron-Verschlüsselungsschlüssels. + * + * Der Schlüssel wird einmal eingetragen und danach nie wieder angefasst – + * deshalb steht er hier und nicht in der Geräteübersicht. + */ +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun VictronKeyScreen( + device: ConfiguredDevice, + store: DeviceStore, + bluetooth: BluetoothManager, + onBack: () -> Unit, +) { + var keyInput by remember { mutableStateOf(store.victronKeyText(device.id) ?: "") } + val diagnostics = bluetooth.diagnostics[device.id] + + val entered = remember(keyInput) { + keyInput.filter { !it.isWhitespace() }.take(2) + .let { if (it.length == 2) it.toIntOrNull(16) else null } + } + val expected = diagnostics?.expectedKeyFirstByte + val agree = if (expected != null && entered != null) expected == entered else null + + Scaffold( + topBar = { + TopAppBar( + title = { Text("Verschlüsselung") }, + navigationIcon = { + IconButton(onClick = onBack) { + Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Zurück") + } + }, + ) + }, + ) { padding -> + Column( + modifier = Modifier + .fillMaxSize() + .padding(padding) + .verticalScroll(rememberScrollState()) + .padding(16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + OutlinedTextField( + value = keyInput, + onValueChange = { keyInput = it }, + label = { Text("32 Hex-Zeichen") }, + singleLine = true, + modifier = Modifier.fillMaxWidth(), + ) + Button( + onClick = { + store.setVictronKey(keyInput, device.id) + bluetooth.refreshConfiguration() + }, + enabled = keyInput.filter { !it.isWhitespace() }.length == 32, + ) { + Text("Schlüssel speichern") + } + Text( + "In VictronConnect: Gerät öffnen → Zahnrad → ⋮ → Produkt-Info → " + + "„Instant Readout“ einschalten → Verschlüsselungsdaten anzeigen. " + + "Der Schlüssel ist 16 Byte lang (32 Hex-Zeichen).", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + + // Das erste Byte sendet das Gerät unverschlüsselt mit. Stimmt es + // nicht mit dem eingetragenen überein, gehört der Schlüssel zu + // einem anderen Victron-Gerät – der häufigste Fehler überhaupt. + if (expected != null) { + Text("Erstes Schlüsselbyte", style = MaterialTheme.typography.titleSmall) + Row(modifier = Modifier.fillMaxWidth()) { + Text("Gerät sendet", modifier = Modifier.weight(1f)) + Text( + "0x%02X".format(expected), + fontFamily = FontFamily.Monospace, + color = if (agree == false) { + MaterialTheme.colorScheme.error + } else { + MaterialTheme.colorScheme.onSurface + }, + ) + } + Row(modifier = Modifier.fillMaxWidth()) { + Text("Eingetragen", modifier = Modifier.weight(1f)) + Text( + entered?.let { "0x%02X".format(it) } ?: "–", + fontFamily = FontFamily.Monospace, + color = if (agree == false) { + MaterialTheme.colorScheme.error + } else { + MaterialTheme.colorScheme.onSurfaceVariant + }, + ) + } + Text( + if (agree == false) { + "Die beiden Bytes müssen übereinstimmen. Tun sie das nicht, " + + "stammt der Schlüssel von einem anderen Victron-Gerät – in " + + "VictronConnect prüfen, ob wirklich dieses Gerät geöffnet war." + } else { + "Zum Vergleichen: dieses Byte sendet das Gerät unverschlüsselt mit." + }, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } + } +} diff --git a/Android/app/src/main/res/drawable-nodpi/vehicle_rear.png b/Android/app/src/main/res/drawable-nodpi/vehicle_rear.png new file mode 100644 index 0000000000000000000000000000000000000000..a71ab5e9e54ccdef8b88d2cada8085c13d59cf95 GIT binary patch literal 132935 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%0Du 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

_qA4szWmd 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 F{{bENnJfSR literal 0 HcmV?d00001 diff --git a/Android/app/src/main/res/drawable-nodpi/vehicle_side.png b/Android/app/src/main/res/drawable-nodpi/vehicle_side.png new file mode 100644 index 0000000000000000000000000000000000000000..9f1c333b4a88ef9bb519934a4d0865d6727ab1a8 GIT binary patch literal 258939 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