time sync implemantation. app is open in the background

This commit is contained in:
Gitea committed 2026-10-17 22:28:13 +02:00
1 parent f8337eef46
commit c53eefaf79
11 files changed
+218 -122

No files matched your search

+4
View File
@@ -39,11 +39,13 @@ dependencies {
implementation(platform(libs.androidx.compose.bom))
implementation(libs.androidx.activity.compose)
implementation(libs.androidx.compose.material3)
implementation(libs.androidx.compose.runtime)
implementation(libs.androidx.compose.ui)
implementation(libs.androidx.compose.ui.graphics)
implementation(libs.androidx.compose.ui.tooling.preview)
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.firebase.crashlytics.buildtools)
testImplementation(libs.junit)
androidTestImplementation(platform(libs.androidx.compose.bom))
androidTestImplementation(libs.androidx.compose.ui.test.junit4)
@@ -51,4 +53,6 @@ dependencies {
androidTestImplementation(libs.androidx.junit)
debugImplementation(libs.androidx.compose.ui.test.manifest)
debugImplementation(libs.androidx.compose.ui.tooling)
implementation("androidx.navigation:navigation-compose:2.10.2")
}
+6 -1
View File
@@ -6,6 +6,8 @@
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" android:usesPermissionFlags="neverForLocation" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE"/>
<application
android:allowBackup="true"
@@ -16,10 +18,13 @@
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.SmartWatchAndroid">
<service
android:name=".BluetoothForegroundService"
android:foregroundServiceType="connectedDevice"
android:exported="false" />
<activity
android:name=".MainActivity"
android:exported="true"
android:label="@string/app_name"
android:theme="@style/Theme.SmartWatchAndroid"
android:windowSoftInputMode="adjustResize">
<intent-filter>
@@ -29,8 +29,7 @@ import java.util.UUID
import kotlin.time.Duration.Companion.milliseconds
class BleManager(private val context: Context) {
val currentTime = LocalTime.now()
val currentDate = LocalDate.now()
val currentDate: LocalDate? = LocalDate.now()
val deviceUUIDs = DeviceUUIDs
private var bluetoothGatt: BluetoothGatt? = null
@@ -40,9 +39,31 @@ class BleManager(private val context: Context) {
var isConnected by mutableStateOf(false)
private set
@RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT)
fun init() {
bluetoothManager = context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
bluetoothAdapter = bluetoothManager?.adapter
checkCurrentConnectionState("10:BD:A3:8F:01:0E")
}
@RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT)
fun checkCurrentConnectionState(deviceAddress: String) {
if (bluetoothManager == null) return
try {
val device = bluetoothAdapter?.getRemoteDevice(deviceAddress)
if (device != null) {
val state = bluetoothManager!!.getConnectionState(device, BluetoothProfile.GATT)
if (state == BluetoothProfile.STATE_CONNECTED) {
isConnected = true
Log.d("ESP32_BLE", "App restarted: device still connected!")
} else {
isConnected = false
Log.d("ESP32_BLE", "App restarted: device not connected.")
}
}
} catch (e: Exception) {
Log.d("ESP32_BLE", "error while checking the status: ${e.message}")
}
}
@RequiresApi(Build.VERSION_CODES.S)
fun checkAndRequestBluetoothPermissions() {
@@ -52,132 +73,122 @@ class BleManager(private val context: Context) {
Manifest.permission.ACCESS_FINE_LOCATION
)
// Prüfen, ob die Berechtigungen bereits erteilt wurden
val missingPermissions = permissions.filter {
ActivityCompat.checkSelfPermission(context, it) != PackageManager.PERMISSION_GRANTED
}
if (missingPermissions.isNotEmpty()) {
// Wenn Berechtigungen fehlen, frage den Nutzer danach
ActivityCompat.requestPermissions(context as Activity, missingPermissions.toTypedArray(), 101)
val activity = context as? Activity
if (activity != null) {
ActivityCompat.requestPermissions(activity, missingPermissions.toTypedArray(), 101)
} else {
Log.d("ESP32_BLE", "Cannot request permissions: Context is not an Activity!")
}
}
}
fun connectToEsp32(deviceAddress: String) {
Log.d("ESP32_BLE", "1. connectToEsp32 gestartet mit Adresse: $deviceAddress")
Log.d("ESP32_BLE", "connectToEsp32 started with address: $deviceAddress")
if (bluetoothAdapter == null) {
Log.d("ESP32_BLE", "Fehler: Bluetooth wird von diesem Gerät nicht unterstützt!")
Log.d("ESP32_BLE", "error: Bluetooth is not supported!")
return
}
try {
val device = bluetoothAdapter!!.getRemoteDevice(deviceAddress)
Log.d("ESP32_BLE", "2. BluetoothDevice Objekt erfolgreich erstellt.")
val gattCallback = object : BluetoothGattCallback() {
@RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT)
override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) {
Log.d("ESP32_BLE", "3. onConnectionStateChange aufgerufen! Status: $status, NewState: $newState")
if (status == BluetoothGatt.GATT_SUCCESS) {
if (newState == BluetoothProfile.STATE_CONNECTED) {
Log.d("ESP32_BLE", "4. Verbunden! Starte discoverServices()...")
Log.d("ESP32_BLE", "Connected! Start discoverServices()...")
gatt.discoverServices()
} else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
isConnected = false
gatt.close()
bluetoothGatt = null
Log.d("ESP32_BLE", "5. Getrennt worden.")
Log.d("ESP32_BLE", "Disconnected.")
}
} else {
Log.d("ESP32_BLE", "Fehler in onConnectionStateChange! Status-Code: $status")
Log.d("ESP32_BLE", "error in onConnectionStateChange! Status-Code: $status")
isConnected = false
}
}
override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) {
Log.d("ESP32_BLE", "6. onServicesDiscovered aufgerufen mit Status: $status")
if (status == BluetoothGatt.GATT_SUCCESS) {
isConnected = true
writeToEsp32("Hallo ESP32", deviceUUIDs.SERVICE_UUID, deviceUUIDs.CHAR_DATE_UUID)
Log.d("ESP32_BLE", "7. Services erfolgreich gefunden! Verbindung komplett erfolgreich.")
Log.d("ESP32_BLE", "connected successfully.")
}
}
}
// Berechtigungsprüfung
if (ActivityCompat.checkSelfPermission(context, Manifest.permission.BLUETOOTH_CONNECT) == PackageManager.PERMISSION_GRANTED) {
Log.d("ESP32_BLE", "8. Berechtigung vorhanden, rufe connectGatt auf...")
bluetoothGatt = device.connectGatt(context, false, gattCallback)
} else {
Log.d("ESP32_BLE", "FEHLER: Keine BLUETOOTH_CONNECT Berechtigung erteilt!")
Log.d("ESP32_BLE", "error: no BLUETOOTH_CONNECT permission!")
}
} catch (e: Exception) {
Log.d("ESP32_BLE", "Exception gefangen: ${e.message}")
Log.d("ESP32_BLE", "Exception: ${e.message}")
}
}
@RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT)
fun disconnectEsp32() {
// Nur den Disconnect-Befehl senden.
// Das Schließen (close) überlassen wir dem Callback, wenn es wirklich so weit ist!
if (ActivityCompat.checkSelfPermission(context, Manifest.permission.BLUETOOTH_CONNECT) == PackageManager.PERMISSION_GRANTED) {
bluetoothGatt?.disconnect()
Log.d("ESP32_BLE","disconnectEsp32")
}
else {
Log.d("BLE_DEBUG", "Permission BLUETOOTH_CONNECT fehlt!")
Log.d("BLE_DEBUG", "Permission BLUETOOTH_CONNECT error!")
}
}
fun writeToEsp32(dataString: String, SERVICE_UUID: UUID?, CHARACTERISTIC_UUID: UUID?) {
// Prüfen, ob die Verbindung steht und bluetoothGatt nicht null ist
val gatt = bluetoothGatt
if (gatt == null) {
Log.d("ESP32_BLE", "Fehler: Kein GATT-Objekt vorhanden (nicht verbunden).")
Log.d("ESP32_BLE", "error: no GATT-Objekt existing (not connected).")
return
}
// 1. Service holen
val service = gatt.getService(SERVICE_UUID)
if (service == null) {
Log.d("ESP32_BLE", "Fehler: Service UUID nicht gefunden!")
Log.d("ESP32_BLE", "error: Service UUID not found!")
return
}
// 2. Characteristic holen
val characteristic = service.getCharacteristic(CHARACTERISTIC_UUID)
if (characteristic == null) {
Log.d("ESP32_BLE", "Fehler: Characteristic UUID nicht gefunden!")
Log.d("ESP32_BLE", "error: Characteristic UUID not found!")
return
}
// 3. Daten in Bytes umwandeln (z.B. einen String wie "LED_ON" in UTF-8 Bytes)
val bytes = dataString.toByteArray(Charsets.UTF_8)
characteristic.value = bytes
// 4. Schreibtyp festlegen ( WRITE_TYPE_DEFAULT wartet auf Antwort vom ESP32, WRITE_TYPE_NO_RESPONSE ist schneller )
characteristic.writeType = BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT
// 5. Berechtigung prüfen und Schreibvorgang starten
if (ActivityCompat.checkSelfPermission(context, Manifest.permission.BLUETOOTH_CONNECT) == PackageManager.PERMISSION_GRANTED) {
val success = gatt.writeCharacteristic(characteristic)
Log.d("ESP32_BLE", "Schreibvorgang gestartet: $success")
Log.d("ESP32_BLE", "started writing: $success")
}
}
fun syncTime() {
val currentTime = LocalTime.now()
val formatterTime = DateTimeFormatter.ofPattern("HH mm ss")
val formattedTime = currentTime.format(formatterTime)
val formatter = DateTimeFormatter.ofPattern("dd MM yyyy")
val formattedDate = currentDate.format(formatter)
val formattedDate = currentDate?.format(formatter)
Log.d("Time/Date","$formattedDate $formattedTime")
CoroutineScope(Dispatchers.IO).launch {
writeToEsp32(formattedTime, deviceUUIDs.SERVICE_UUID, deviceUUIDs.CHAR_TIME_UUID)
delay(150.milliseconds) // Kurze Pause für den BLE-Stack (z. B. 150 ms)
delay(150.milliseconds)
writeToEsp32("$formattedDate", deviceUUIDs.SERVICE_UUID, deviceUUIDs.CHAR_DATE_UUID)
}
@@ -0,0 +1,47 @@
package com.example.smart_watch_android
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.Service
import android.content.Intent
import android.os.IBinder
import androidx.core.app.NotificationCompat
class BluetoothForegroundService : Service() {
companion object {
const val CHANNEL_ID = "BluetoothServiceChannel"
const val NOTIFICATION_ID = 1
}
override fun onCreate() {
super.onCreate()
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
createNotificationChannel()
val notification: Notification = NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle("Bluetooth connection active")
.setContentText("the app keeps the connection in the background.")
.setSmallIcon(android.R.drawable.stat_sys_data_bluetooth)
.build()
startForeground(NOTIFICATION_ID, notification)
return START_STICKY
}
override fun onBind(intent: Intent?): IBinder? {
return null
}
private fun createNotificationChannel() {
val serviceChannel = NotificationChannel(
CHANNEL_ID,
"Bluetooth Service Channel",
NotificationManager.IMPORTANCE_LOW
)
val manager = getSystemService(NotificationManager::class.java)
manager?.createNotificationChannel(serviceChannel)
}
}
@@ -1,38 +1,44 @@
package com.example.smart_watch_android
import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels
import androidx.annotation.RequiresApi
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.ui.Modifier
import com.example.smart_watch_android.BLE.BleManager
import com.example.smart_watch_android.Screens.AppNavigation
class MainActivity : ComponentActivity() {
val bleManager = BleManager(context = this)
private val viewModel: MainViewModel by viewModels()
@RequiresApi(Build.VERSION_CODES.S)
@androidx.annotation.RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
bleManager.init()
bleManager.checkAndRequestBluetoothPermissions()
viewModel.initBleManager(this)
viewModel.bleManager.checkAndRequestBluetoothPermissions()
enableEdgeToEdge()
setContent {
MaterialTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
// Hier wird deine AppNavigation aufgerufen,
// die die Screens steuert!
AppNavigation(bleManager)
) {
AppNavigation(bleManager = viewModel.bleManager)
}
}
}
startBluetoothService()
}
private fun startBluetoothService() {
val serviceIntent = Intent(this, BluetoothForegroundService::class.java)
startForegroundService(serviceIntent)
}
}
@@ -0,0 +1,20 @@
package com.example.smart_watch_android
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import com.example.smart_watch_android.BLE.BleManager
class MainViewModel(
application: Application,
) : AndroidViewModel(application) {
lateinit var bleManager: BleManager
private set
fun initBleManager(activityContext: android.content.Context) {
if (!::bleManager.isInitialized) {
bleManager = BleManager(context = activityContext)
bleManager.init()
}
}
}
@@ -0,0 +1,60 @@
package com.example.smart_watch_android.Screens
import android.Manifest
import androidx.annotation.RequiresPermission
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.navigation.NavController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import com.example.smart_watch_android.BLE.BleManager
import com.example.smart_watch_android.MainViewModel
@RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT)
@Composable
fun AppNavigation(bleManager: BleManager) {
val navController = rememberNavController()
LaunchedEffect(bleManager.isConnected) {
if (bleManager.isConnected) {
navController.navigate("control_panel") {
popUpTo("connect") { inclusive = true }
}
} else {
navController.navigate("connect") {
popUpTo("control_panel") { inclusive = true }
}
}
}
val startDestination = if (bleManager.isConnected) {
"control_panel"
} else {
"connect"
}
NavHost(
navController = navController,
startDestination = startDestination
) {
// Connect Screen
composable("connect") {
ConnectScreen(
isConnected = bleManager.isConnected,
onConnectClicked = {
bleManager.connectToEsp32("10:BD:A3:8F:01:0E")
}
)
}
composable("control_panel") {
ControlPanelScreen(
bleManager = bleManager,
onDisconnectClicked = {
bleManager.disconnectEsp32()
}
)
}
}
}
@@ -2,6 +2,8 @@ package com.example.smart_watch_android.Screens
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Text
@@ -9,6 +11,8 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@Composable
fun ConnectScreen(isConnected: Boolean, onConnectClicked: () -> Unit) {
@@ -18,13 +22,14 @@ fun ConnectScreen(isConnected: Boolean, onConnectClicked: () -> Unit) {
) {
Button(
onClick = onConnectClicked,
colors = if (isConnected) {
ButtonDefaults.buttonColors(containerColor = Color(0xFF4CAF50)) // Grün
} else {
ButtonDefaults.buttonColors()
}
modifier = Modifier.size(300.dp),
shape = CircleShape,
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF5E18BE))
) {
Text(text = if (isConnected) "Verbunden" else "Connect")
Text(
text = "Connect",
fontSize = 30.sp
)
}
}
}
@@ -37,11 +37,10 @@ fun ControlPanelScreen(
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
// Eingabefeld für Text / Uhrzeit / Befehle
OutlinedTextField(
value = textInput,
onValueChange = { textInput = it },
label = { Text("Nachricht an ESP32") },
label = { Text("Massage to ESP32") },
modifier = Modifier.fillMaxWidth()
)
@@ -52,12 +51,14 @@ fun ControlPanelScreen(
onClick = {
if (textInput.isNotEmpty()) {
bleManager.writeToEsp32(
textInput, DeviceUUIDs.SERVICE_UUID, DeviceUUIDs.CHAR_DATE_UUID
textInput, DeviceUUIDs.SERVICE_UUID, DeviceUUIDs.CHAR_MESSAGE_UUID
)
textInput = "" // Feld danach leeren
textInput = ""
}
},
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF6C24B6)),
modifier = Modifier.fillMaxWidth()
) {
Text("Send")
}
@@ -68,6 +69,7 @@ fun ControlPanelScreen(
onClick = {
bleManager.syncTime()
},
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF50128F)),
modifier = Modifier.fillMaxWidth()
) {
Text("sync Time")
@@ -75,7 +77,6 @@ fun ControlPanelScreen(
Spacer(modifier = Modifier.height(64.dp))
// Disconnect Button (z.B. in Rot, um ihn abzusetzen)
Button(
onClick = onDisconnectClicked,
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFFE53935)),
@@ -1,67 +0,0 @@
package com.example.smart_watch_android
import android.Manifest
import androidx.annotation.RequiresPermission
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
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.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.OutlinedTextField
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.graphics.Color
import androidx.compose.ui.unit.dp
import com.example.smart_watch_android.BLE.BleManager
import com.example.smart_watch_android.BLE.DeviceUUIDs
import com.example.smart_watch_android.Screens.ConnectScreen
import com.example.smart_watch_android.Screens.ControlPanelScreen
enum class Screen {
CONNECT,
CONTROL_PANEL
}
@RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT)
@Composable
fun AppNavigation(BLEManager: BleManager) {
var currentScreen by remember { mutableStateOf(Screen.CONNECT) }
val bleManager = BLEManager
if (bleManager.isConnected && currentScreen == Screen.CONNECT) {
currentScreen = Screen.CONTROL_PANEL
} else if (!bleManager.isConnected && currentScreen == Screen.CONTROL_PANEL) {
currentScreen = Screen.CONNECT
}
// Je nach Zustand den entsprechenden Screen anzeigen
when (currentScreen) {
Screen.CONNECT -> {
ConnectScreen(
isConnected = bleManager.isConnected,
onConnectClicked = {
bleManager.connectToEsp32("10:BD:A3:8F:01:0E")
}
)
}
Screen.CONTROL_PANEL -> {
ControlPanelScreen(
bleManager,
onDisconnectClicked = {
bleManager.disconnectEsp32()
// Screen wechselt automatisch über den Check oben
}
)
}
}
}