Android: Bluetooth-Schicht und Ablage

Damit steht der erste Teil vollständig: Protokoll, Funk und Ablage
übersetzen sich, die 50 Prüfungen laufen.

Die Funkschicht folgt der iOS-Fassung samt allem, was wir uns dort erst
erarbeiten mussten. Ein eigener Thread für alles, was Bluetooth anfasst;
Android ruft Scan- und GATT-Rückmeldungen auf Binder-Threads auf. Die
Wartesperre nach Fehlschlägen, die zwischen einem Abbruch einer stehenden
Verbindung und einem echten Fehlschlag unterscheidet. Stellbefehle, die
aufgehoben statt verworfen werden, wenn die Kühlbox gerade weg ist. Die
Zwanzig-Byte-Grenze beim Schreiben an die Kühlbox. Die stabile Reihenfolge
der Kandidaten, damit nicht der Zufall die Schreibart bestimmt.

Vier Dinge sind auf Android anders und im Quelltext vermerkt:

Die Herstellerdaten kommen ohne die zwei Bytes der Company-ID - die steht
im Schlüssel der Tabelle. Sie werden wieder davorgesetzt, damit derselbe
Rahmen ankommt wie unter iOS und dieselben Prüfungen gelten.

Notify muss über einen Deskriptor eingeschaltet werden; das Einschalten auf
unserer Seite allein genügt nicht.

Geräte werden über ihre MAC-Adresse angesprochen statt über eine
systemvergebene Kennung - iOS gibt die Adresse gar nicht heraus.

Die Victron-Schlüssel liegen in app-privater Ablage statt im
Schlüsselbund. Das schützt gegen andere Apps, ist aber nicht
hardwaregestützt. Ohne zusätzliche Bibliothek gibt es unter Android kein
gleichwertiges Gegenstück.

Als Nächstes die Oberfläche.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
BiasF
2026-08-31 10:28:53 +02:00
co-authored by Claude Opus 5
parent 0cf8448f34
commit 866fce9711
15 changed files with 2209 additions and 0 deletions
@@ -0,0 +1,85 @@
package de.fritob.campermonitor.protocol
import java.util.UUID
/**
* Welche Rolle ein Gerät im Camper spielt. Bestimmt Symbol, Sortierung und
* welche Kennzahl als "Hauptwert" auf der Kachel gross dargestellt wird.
*/
enum class DeviceRole(val title: String) {
CHARGE_BOOSTER("Ladebooster"),
SOLAR_CHARGER("Solarladeregler"),
BATTERY_MONITOR("Batteriemonitor"),
BMS("Batterie / BMS"),
FRIDGE("Kühlbox"),
LEVELING("Nivellierung");
/**
* Victron-Geräte werden passiv über das Advertisement gelesen, BMS,
* Kühlbox und Neigungsmesser brauchen eine echte GATT-Verbindung.
*/
val transport: DeviceTransport
get() = when (this) {
BMS, FRIDGE, LEVELING -> DeviceTransport.CONNECT
else -> DeviceTransport.ADVERTISEMENT
}
}
enum class DeviceTransport {
/** Passives Mitlesen der BLE-Werbedaten (Victron Instant Readout). */
ADVERTISEMENT,
/** Verbindungsaufbau, Kommando schreiben, Antwort per Notify lesen. */
CONNECT,
}
/**
* Ein eingerichtetes Gerät.
*
* Die Bluetooth-Adresse ist unter Android ein Text („AA:BB:CC:DD:EE:FF") und
* nicht wie unter iOS eine UUID iOS gibt die Hardware-Adresse gar nicht
* heraus, Android schon.
*/
data class ConfiguredDevice(
val id: UUID = UUID.randomUUID(),
val name: String,
val role: DeviceRole,
val profileID: UUID,
/** MAC-Adresse des Geräts. */
val address: String,
/** Wie das Gerät sich selbst nennt, für die Wiedererkennung. */
val advertisedName: String? = null,
val fridgeZoneMode: FridgeZoneMode = FridgeZoneMode.AUTOMATIC,
val sensorOrientation: SensorOrientation = SensorOrientation.IDENTITY,
)
/**
* Ein Fahrzeug. Jedes Profil hat seinen eigenen Satz Geräte; die App zeigt und
* funkt immer nur für das gerade gewählte.
*/
data class Profile(
val id: UUID = UUID.randomUUID(),
val name: String,
/** Symbolname für die Auswahl im Dashboard. */
val symbol: String = "box_truck",
/** Spurweite in Metern für die Berechnung der Auffahrkeile quer. */
val trackWidth: Double? = null,
/** Radstand in Metern dasselbe längs. */
val wheelbase: Double? = null,
) {
companion object {
/**
* Profil, dem Geräte aus der Zeit vor der Profilverwaltung zugeordnet
* werden. Feste Kennung, damit die Zuordnung beim Update erhalten bleibt.
*/
val DEFAULT_ID: UUID = UUID.fromString("00000000-0000-0000-0000-00000000c001")
fun initial() = Profile(id = DEFAULT_ID, name = "Mein Camper")
/** Auswahl für die Profilbearbeitung. */
val SYMBOLS = listOf(
"box_truck", "pickup", "bus", "double_decker",
"car", "car_side", "tent", "sailboat", "lodge", "mountains",
)
}
}