diff --git a/data/src/main/java/com/github/umercodez/sensorspot/data/gpsdataprovider/GpsDataProvider.kt b/data/src/main/java/com/github/umercodez/sensorspot/data/gpsdataprovider/GpsDataProvider.kt index a9524ae..5a3a858 100644 --- a/data/src/main/java/com/github/umercodez/sensorspot/data/gpsdataprovider/GpsDataProvider.kt +++ b/data/src/main/java/com/github/umercodez/sensorspot/data/gpsdataprovider/GpsDataProvider.kt @@ -22,11 +22,14 @@ package com.github.umercodez.sensorspot.data.gpsdataprovider import kotlinx.coroutines.flow.SharedFlow import kotlinx.serialization.Serializable import kotlinx.serialization.json.Json +import org.json.JSONObject private val jsonConf = Json { encodeDefaults = true } + + @Serializable data class GpsData( val type: String = "android.gps", @@ -38,7 +41,19 @@ data class GpsData( val time: Long, val bearing: Float ){ - fun toJson() = jsonConf.encodeToString(this) + fun toJson(includeType: Boolean = true) :String { + val json = mapOf( + "type" to if (includeType) type else null, + "latitude" to latitude, + "longitude" to longitude, + "altitude" to altitude, + "accuracy" to accuracy, + "speed" to speed, + "time" to time, + "bearing" to bearing + ).filterValues { it != null } + return JSONObject(json).toString() + } } interface GpsDataProvider { diff --git a/data/src/main/java/com/github/umercodez/sensorspot/data/repositories/settings/Settings.kt b/data/src/main/java/com/github/umercodez/sensorspot/data/repositories/settings/Settings.kt index 03f085d..394269a 100644 --- a/data/src/main/java/com/github/umercodez/sensorspot/data/repositories/settings/Settings.kt +++ b/data/src/main/java/com/github/umercodez/sensorspot/data/repositories/settings/Settings.kt @@ -1,21 +1,21 @@ -/* - * This file is a part of SensorSpot (https://www.github.com/UmerCodez/SensorSpot) - * Copyright (C) 2025 Umer Farooq (umerfarooq2383@gmail.com) - * - * SensorSpot is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * SensorSpot is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with SensorSpot. If not, see . - * - */ +/* + * This file is a part of SensorSpot (https://www.github.com/UmerCodez/SensorSpot) + * Copyright (C) 2025 Umer Farooq (umerfarooq2383@gmail.com) + * + * SensorSpot is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * SensorSpot is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with SensorSpot. If not, see . + * + */ package com.github.umercodez.sensorspot.data.repositories.settings @@ -29,6 +29,7 @@ data class Settings( val brokerPort: Int, val qos: Int, val topic: String, + val dedicatedTopics: Boolean, val connectionTimeoutSecs: Int, val useCredentials: Boolean, val userName: String, diff --git a/data/src/main/java/com/github/umercodez/sensorspot/data/repositories/settings/SettingsRepositoryImp.kt b/data/src/main/java/com/github/umercodez/sensorspot/data/repositories/settings/SettingsRepositoryImp.kt index f0f2cf0..cfbff21 100644 --- a/data/src/main/java/com/github/umercodez/sensorspot/data/repositories/settings/SettingsRepositoryImp.kt +++ b/data/src/main/java/com/github/umercodez/sensorspot/data/repositories/settings/SettingsRepositoryImp.kt @@ -1,21 +1,21 @@ -/* - * This file is a part of SensorSpot (https://www.github.com/UmerCodez/SensorSpot) - * Copyright (C) 2025 Umer Farooq (umerfarooq2383@gmail.com) - * - * SensorSpot is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * SensorSpot is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with SensorSpot. If not, see . - * - */ +/* + * This file is a part of SensorSpot (https://www.github.com/UmerCodez/SensorSpot) + * Copyright (C) 2025 Umer Farooq (umerfarooq2383@gmail.com) + * + * SensorSpot is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * SensorSpot is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with SensorSpot. If not, see . + * + */ package com.github.umercodez.sensorspot.data.repositories.settings import android.content.Context @@ -53,6 +53,7 @@ class SettingsRepositoryImp( val USE_CREDENTIALS = booleanPreferencesKey("use_credentials") val QOS = intPreferencesKey("qos") val TOPIC = stringPreferencesKey("topic") + val DEDICATED_TOPICS = booleanPreferencesKey("dedicated_topics") val CONNECTION_TIMEOUT_SECS = intPreferencesKey("connection_timeout_secs") val SENSOR_SAMPLING_RATE = intPreferencesKey("sensor_sampling_rate") } @@ -69,6 +70,7 @@ class SettingsRepositoryImp( useCredentials = pref[Key.USE_CREDENTIALS] ?: MqttConfigDefaults.USE_CREDENTIALS, qos = pref[Key.QOS] ?: MqttConfigDefaults.QOS, topic = pref[Key.TOPIC] ?: MqttConfigDefaults.TOPIC, + dedicatedTopics = pref[Key.DEDICATED_TOPICS] ?: MqttConfigDefaults.DEDICATED_TOPICS, connectionTimeoutSecs = pref[Key.CONNECTION_TIMEOUT_SECS] ?: MqttConfigDefaults.CONNECTION_TIMEOUT_SECS, sensorSamplingRate = pref[Key.SENSOR_SAMPLING_RATE] @@ -94,6 +96,7 @@ class SettingsRepositoryImp( pref[Key.USE_CREDENTIALS] = settings.useCredentials pref[Key.QOS] = settings.qos pref[Key.TOPIC] = settings.topic + pref[Key.DEDICATED_TOPICS] = settings.dedicatedTopics pref[Key.CONNECTION_TIMEOUT_SECS] = settings.connectionTimeoutSecs } diff --git a/data/src/main/java/com/github/umercodez/sensorspot/data/sensoreventprovider/SensorEventProvider.kt b/data/src/main/java/com/github/umercodez/sensorspot/data/sensoreventprovider/SensorEventProvider.kt index c42fd33..a11d16a 100644 --- a/data/src/main/java/com/github/umercodez/sensorspot/data/sensoreventprovider/SensorEventProvider.kt +++ b/data/src/main/java/com/github/umercodez/sensorspot/data/sensoreventprovider/SensorEventProvider.kt @@ -19,16 +19,22 @@ package com.github.umercodez.sensorspot.data.sensoreventprovider import kotlinx.coroutines.flow.Flow -import kotlinx.serialization.Serializable -import kotlinx.serialization.json.Json +import org.json.JSONObject + -@Serializable data class SensorEvent( val type: String, val values: List, val timestamp: Long ){ - fun toJson() = Json.encodeToString(this) + fun toJson(includeType: Boolean = true): String { + val json = mapOf( + "type" to if (includeType) type else null, + "values" to values, + "timestamp" to timestamp + ).filterValues { it != null } + return JSONObject(json).toString() + } } interface SensorEventProvider { val events: Flow diff --git a/data/src/main/java/com/github/umercodez/sensorspot/data/sensorpublisher/MqttConfig.kt b/data/src/main/java/com/github/umercodez/sensorspot/data/sensorpublisher/MqttConfig.kt index 12c9984..477878a 100644 --- a/data/src/main/java/com/github/umercodez/sensorspot/data/sensorpublisher/MqttConfig.kt +++ b/data/src/main/java/com/github/umercodez/sensorspot/data/sensorpublisher/MqttConfig.kt @@ -26,6 +26,7 @@ object MqttConfigDefaults{ const val BROKER_PORT = 1883 const val QOS = 0 const val TOPIC = "android/sensor" + const val DEDICATED_TOPICS = false const val CONNECTION_TIMEOUT_SECS = 5 const val USE_CREDENTIALS = false const val USER_NAME = "" @@ -40,6 +41,7 @@ data class MqttConfig( val brokerPort: Int = MqttConfigDefaults.BROKER_PORT, val qos: Int = MqttConfigDefaults.QOS, val topic: String = MqttConfigDefaults.TOPIC, + val dedicatedTopics: Boolean = MqttConfigDefaults.DEDICATED_TOPICS, val connectionTimeoutSecs: Int = MqttConfigDefaults.CONNECTION_TIMEOUT_SECS, val useCredentials: Boolean = MqttConfigDefaults.USE_CREDENTIALS, val userName: String = MqttConfigDefaults.USER_NAME, @@ -54,6 +56,7 @@ data class MqttConfig( brokerPort = settings.brokerPort, qos = settings.qos, topic = settings.topic, + dedicatedTopics = settings.dedicatedTopics, connectionTimeoutSecs = settings.connectionTimeoutSecs, useCredentials = settings.useCredentials, userName = settings.userName, diff --git a/data/src/main/java/com/github/umercodez/sensorspot/data/sensorpublisher/SensorPublisher.kt b/data/src/main/java/com/github/umercodez/sensorspot/data/sensorpublisher/SensorPublisher.kt index bc9ee81..eb86c8b 100644 --- a/data/src/main/java/com/github/umercodez/sensorspot/data/sensorpublisher/SensorPublisher.kt +++ b/data/src/main/java/com/github/umercodez/sensorspot/data/sensorpublisher/SensorPublisher.kt @@ -1,266 +1,274 @@ -/* - * This file is a part of SensorSpot (https://www.github.com/UmerCodez/SensorSpot) - * Copyright (C) 2025 Umer Farooq (umerfarooq2383@gmail.com) - * - * SensorSpot is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * SensorSpot is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with SensorSpot. If not, see . - * - */ -package com.github.umercodez.sensorspot.data.sensorpublisher - -import android.util.Log -import com.github.umercodez.sensorspot.data.clock.Clock -import com.github.umercodez.sensorspot.data.clock.ElapsedTime -import com.github.umercodez.sensorspot.data.gpsdataprovider.GpsDataProvider -import com.github.umercodez.sensorspot.data.sensoreventprovider.SensorEventProvider -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.SharedFlow -import kotlinx.coroutines.flow.asSharedFlow -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import org.eclipse.paho.mqttv5.client.IMqttToken -import org.eclipse.paho.mqttv5.client.MqttActionListener -import org.eclipse.paho.mqttv5.client.MqttAsyncClient -import org.eclipse.paho.mqttv5.client.MqttCallback -import org.eclipse.paho.mqttv5.client.MqttConnectionOptions -import org.eclipse.paho.mqttv5.client.MqttDisconnectResponse -import org.eclipse.paho.mqttv5.client.persist.MemoryPersistence -import org.eclipse.paho.mqttv5.common.MqttException -import org.eclipse.paho.mqttv5.common.MqttMessage -import org.eclipse.paho.mqttv5.common.packet.MqttProperties -import java.net.SocketTimeoutException -import java.util.UUID - - -sealed interface MqttConnectionState{ - data object Connecting: MqttConnectionState - data object Connected: MqttConnectionState - data object Disconnected: MqttConnectionState - data class ConnectionError(val exception: Throwable? = null): MqttConnectionState - data object ConnectionTimeout: MqttConnectionState -} - -class SensorPublisher( - private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO, - private val scope: CoroutineScope, - private val sensorEventProvider: SensorEventProvider, - private val gpsDataProvider: GpsDataProvider -) : MqttCallback { - - private val tag = "SensorPublisher" - - - private var mqttAsyncClient: MqttAsyncClient? = null - private var memoryPersistence: MemoryPersistence? = null - private var connectionOptions: MqttConnectionOptions? = null - - private val clock = Clock() - val elapsedTime : SharedFlow get() = clock.time - var sensorSamplingRate: Int = 200000 - - var sensorIntTypes: List - set(sensorTypes) { - sensorEventProvider.stopProvidingEvents() - sensorEventProvider.provideEventsFor(sensorTypes, sensorSamplingRate) - } - get() = sensorIntTypes - - private val _mqttConnectionState = MutableSharedFlow(replay = 1) - val mqttConnectionState = _mqttConnectionState.asSharedFlow() - - - suspend fun connectAndPublish(mqttConfig: MqttConfig) = withContext(ioDispatcher){ - - scope.launch { - sensorEventProvider.events.collect{ sensorEvent -> - - try { - - if(mqttAsyncClient?.isConnected == true) { - val message = MqttMessage(sensorEvent.toJson().toByteArray()).apply { - qos = mqttConfig.qos - } - mqttAsyncClient?.publish(mqttConfig.topic, message) - } - - } catch (e: MqttException) { - e.printStackTrace() - } - } - } - - scope.launch { - gpsDataProvider.gpsData.collect { gpsData -> - - try { - - if(mqttAsyncClient?.isConnected == true) { - val message = MqttMessage(gpsData.toJson().toByteArray()).apply { - qos = mqttConfig.qos - } - mqttAsyncClient?.publish(mqttConfig.topic, message) - } - - } catch (e: MqttException) { - e.printStackTrace() - } - - } - } - - _mqttConnectionState.emit(MqttConnectionState.Connecting) - - val broker = if (mqttConfig.useSSL) { - if (mqttConfig.useWebsocket) - "wss://${mqttConfig.brokerAddress}:${mqttConfig.brokerPort}" - else - "ssl://${mqttConfig.brokerAddress}:${mqttConfig.brokerPort}" - } else { - if (mqttConfig.useWebsocket) - "ws://${mqttConfig.brokerAddress}:${mqttConfig.brokerPort}" - else - "tcp://${mqttConfig.brokerAddress}:${mqttConfig.brokerPort}" - } - memoryPersistence = MemoryPersistence() - connectionOptions = MqttConnectionOptions() - - try { - - mqttAsyncClient = MqttAsyncClient(broker, UUID.randomUUID().toString(), memoryPersistence) - - - connectionOptions?.apply { - isAutomaticReconnect = false - isCleanStart = false - connectionTimeout = mqttConfig.connectionTimeoutSecs - - - if(mqttConfig.useCredentials){ - userName = mqttConfig.userName - password = mqttConfig.password.toByteArray() - } - - } - - mqttAsyncClient?.setCallback(this@SensorPublisher) - mqttAsyncClient?.connect(connectionOptions,null,object: MqttActionListener { - override fun onSuccess(asyncActionToken: IMqttToken?) { - println(asyncActionToken) - } - - override fun onFailure(asyncActionToken: IMqttToken?, exception: Throwable?) { - - if(exception is SocketTimeoutException) { - scope.launch { - _mqttConnectionState.emit(MqttConnectionState.ConnectionTimeout) - } - } - - else { - scope.launch { - _mqttConnectionState.emit(MqttConnectionState.ConnectionError(exception)) - } - } - exception?.printStackTrace() - - } - - }) - - } catch (e: MqttException) { - e.printStackTrace() - scope.launch { - _mqttConnectionState.emit(MqttConnectionState.ConnectionError(e)) - } - } - - } - - override fun connectComplete(reconnect: Boolean, serverURI: String?) { - Log.d(tag, "connectComplete()") - clock.start() - scope.launch { - _mqttConnectionState.emit(MqttConnectionState.Connected) - } - sensorEventProvider.provideEventsFor(sensorIntTypes,sensorSamplingRate) - } - - override fun disconnected(disconnectResponse: MqttDisconnectResponse?) { - Log.d(tag, "disconnected()") - clock.reset() - scope.launch { - _mqttConnectionState.emit(MqttConnectionState.Disconnected) - } - sensorEventProvider.stopProvidingEvents() - disconnectResponse?.reasonString?.also { println(it) } - } - - override fun mqttErrorOccurred(exception: MqttException?) { - Log.d(tag, "mqttErrorOccurred()") - clock.reset() - scope.launch { - _mqttConnectionState.emit(MqttConnectionState.ConnectionError(exception)) - } - exception?.printStackTrace() - } - - override fun messageArrived(topic: String?, message: MqttMessage?) { - - } - - override fun deliveryComplete(token: IMqttToken?) { - - } - - override fun authPacketArrived(reasonCode: Int, properties: MqttProperties?) { - - } - - suspend fun disconnect() : Unit = withContext(ioDispatcher) { - try { - - mqttAsyncClient?.disconnect() - mqttAsyncClient?.close() - _mqttConnectionState.emit(MqttConnectionState.Disconnected) - sensorEventProvider.stopProvidingEvents() - gpsDataProvider.stopProvidingGpsData() - clock.reset() - - } catch (e: Exception) { - e.printStackTrace() - _mqttConnectionState.emit(MqttConnectionState.ConnectionError(e)) - } - } - - fun provideGpsData(){ - gpsDataProvider.startProvidingGpsData() - } - - fun stopProvidingGpsData(){ - gpsDataProvider.stopProvidingGpsData() - } - - fun cleanUp(){ - sensorEventProvider.stopProvidingEvents() - sensorEventProvider.cleanUp() - - gpsDataProvider.startProvidingGpsData() - gpsDataProvider.cleanUp() - } - -} - - +/* + * This file is a part of SensorSpot (https://www.github.com/UmerCodez/SensorSpot) + * Copyright (C) 2025 Umer Farooq (umerfarooq2383@gmail.com) + * + * SensorSpot is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * SensorSpot is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with SensorSpot. If not, see . + * + */ +package com.github.umercodez.sensorspot.data.sensorpublisher + +import android.util.Log +import com.github.umercodez.sensorspot.data.clock.Clock +import com.github.umercodez.sensorspot.data.clock.ElapsedTime +import com.github.umercodez.sensorspot.data.gpsdataprovider.GpsDataProvider +import com.github.umercodez.sensorspot.data.sensoreventprovider.SensorEventProvider +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import org.eclipse.paho.mqttv5.client.IMqttToken +import org.eclipse.paho.mqttv5.client.MqttActionListener +import org.eclipse.paho.mqttv5.client.MqttAsyncClient +import org.eclipse.paho.mqttv5.client.MqttCallback +import org.eclipse.paho.mqttv5.client.MqttConnectionOptions +import org.eclipse.paho.mqttv5.client.MqttDisconnectResponse +import org.eclipse.paho.mqttv5.client.persist.MemoryPersistence +import org.eclipse.paho.mqttv5.common.MqttException +import org.eclipse.paho.mqttv5.common.MqttMessage +import org.eclipse.paho.mqttv5.common.packet.MqttProperties +import java.net.SocketTimeoutException +import java.util.UUID +import kotlin.text.split + + +sealed interface MqttConnectionState{ + data object Connecting: MqttConnectionState + data object Connected: MqttConnectionState + data object Disconnected: MqttConnectionState + data class ConnectionError(val exception: Throwable? = null): MqttConnectionState + data object ConnectionTimeout: MqttConnectionState +} + +class SensorPublisher( + private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO, + private val scope: CoroutineScope, + private val sensorEventProvider: SensorEventProvider, + private val gpsDataProvider: GpsDataProvider +) : MqttCallback { + + private val tag = "SensorPublisher" + + + private var mqttAsyncClient: MqttAsyncClient? = null + private var memoryPersistence: MemoryPersistence? = null + private var connectionOptions: MqttConnectionOptions? = null + + private val clock = Clock() + val elapsedTime : SharedFlow get() = clock.time + var sensorSamplingRate: Int = 200000 + + var sensorIntTypes: List + set(sensorTypes) { + sensorEventProvider.stopProvidingEvents() + sensorEventProvider.provideEventsFor(sensorTypes, sensorSamplingRate) + } + get() = sensorIntTypes + + private val _mqttConnectionState = MutableSharedFlow(replay = 1) + val mqttConnectionState = _mqttConnectionState.asSharedFlow() + + + private fun getTopic(mqttConfig: MqttConfig, sensorType: String ) : String { + return if(mqttConfig.dedicatedTopics){ + "${mqttConfig.topic}/${ if (sensorType.contains(".")) sensorType.split('.').last() else sensorType}" + } else { + mqttConfig.topic + } + } + suspend fun connectAndPublish(mqttConfig: MqttConfig) = withContext(ioDispatcher){ + + scope.launch { + sensorEventProvider.events.collect{ sensorEvent -> + + try { + + if(mqttAsyncClient?.isConnected == true) { + val message = MqttMessage(sensorEvent.toJson(!mqttConfig.dedicatedTopics).toByteArray()).apply { + qos = mqttConfig.qos + } + mqttAsyncClient?.publish(getTopic(mqttConfig, sensorEvent.type), message) + } + + } catch (e: MqttException) { + e.printStackTrace() + } + } + } + + scope.launch { + gpsDataProvider.gpsData.collect { gpsData -> + + try { + + if(mqttAsyncClient?.isConnected == true) { + val message = MqttMessage(gpsData.toJson(!mqttConfig.dedicatedTopics).toByteArray()).apply { + qos = mqttConfig.qos + } + mqttAsyncClient?.publish(getTopic(mqttConfig, gpsData.type), message) + } + + } catch (e: MqttException) { + e.printStackTrace() + } + + } + } + + _mqttConnectionState.emit(MqttConnectionState.Connecting) + + val broker = if (mqttConfig.useSSL) { + if (mqttConfig.useWebsocket) + "wss://${mqttConfig.brokerAddress}:${mqttConfig.brokerPort}" + else + "ssl://${mqttConfig.brokerAddress}:${mqttConfig.brokerPort}" + } else { + if (mqttConfig.useWebsocket) + "ws://${mqttConfig.brokerAddress}:${mqttConfig.brokerPort}" + else + "tcp://${mqttConfig.brokerAddress}:${mqttConfig.brokerPort}" + } + memoryPersistence = MemoryPersistence() + connectionOptions = MqttConnectionOptions() + + try { + + mqttAsyncClient = MqttAsyncClient(broker, UUID.randomUUID().toString(), memoryPersistence) + + + connectionOptions?.apply { + isAutomaticReconnect = false + isCleanStart = false + connectionTimeout = mqttConfig.connectionTimeoutSecs + + + if(mqttConfig.useCredentials){ + userName = mqttConfig.userName + password = mqttConfig.password.toByteArray() + } + + } + + mqttAsyncClient?.setCallback(this@SensorPublisher) + mqttAsyncClient?.connect(connectionOptions,null,object: MqttActionListener { + override fun onSuccess(asyncActionToken: IMqttToken?) { + println(asyncActionToken) + } + + override fun onFailure(asyncActionToken: IMqttToken?, exception: Throwable?) { + + if(exception is SocketTimeoutException) { + scope.launch { + _mqttConnectionState.emit(MqttConnectionState.ConnectionTimeout) + } + } + + else { + scope.launch { + _mqttConnectionState.emit(MqttConnectionState.ConnectionError(exception)) + } + } + exception?.printStackTrace() + + } + + }) + + } catch (e: MqttException) { + e.printStackTrace() + scope.launch { + _mqttConnectionState.emit(MqttConnectionState.ConnectionError(e)) + } + } + + } + + override fun connectComplete(reconnect: Boolean, serverURI: String?) { + Log.d(tag, "connectComplete()") + clock.start() + scope.launch { + _mqttConnectionState.emit(MqttConnectionState.Connected) + } + sensorEventProvider.provideEventsFor(sensorIntTypes,sensorSamplingRate) + } + + override fun disconnected(disconnectResponse: MqttDisconnectResponse?) { + Log.d(tag, "disconnected()") + clock.reset() + scope.launch { + _mqttConnectionState.emit(MqttConnectionState.Disconnected) + } + sensorEventProvider.stopProvidingEvents() + disconnectResponse?.reasonString?.also { println(it) } + } + + override fun mqttErrorOccurred(exception: MqttException?) { + Log.d(tag, "mqttErrorOccurred()") + clock.reset() + scope.launch { + _mqttConnectionState.emit(MqttConnectionState.ConnectionError(exception)) + } + exception?.printStackTrace() + } + + override fun messageArrived(topic: String?, message: MqttMessage?) { + + } + + override fun deliveryComplete(token: IMqttToken?) { + + } + + override fun authPacketArrived(reasonCode: Int, properties: MqttProperties?) { + + } + + suspend fun disconnect() : Unit = withContext(ioDispatcher) { + try { + + mqttAsyncClient?.disconnect() + mqttAsyncClient?.close() + _mqttConnectionState.emit(MqttConnectionState.Disconnected) + sensorEventProvider.stopProvidingEvents() + gpsDataProvider.stopProvidingGpsData() + clock.reset() + + } catch (e: Exception) { + e.printStackTrace() + _mqttConnectionState.emit(MqttConnectionState.ConnectionError(e)) + } + } + + fun provideGpsData(){ + gpsDataProvider.startProvidingGpsData() + } + + fun stopProvidingGpsData(){ + gpsDataProvider.stopProvidingGpsData() + } + + fun cleanUp(){ + sensorEventProvider.stopProvidingEvents() + sensorEventProvider.cleanUp() + + gpsDataProvider.startProvidingGpsData() + gpsDataProvider.cleanUp() + } + +} + + diff --git a/ui/src/main/java/com/github/umercodez/sensorspot/ui/screens/sensors/SensorScreenViewModel.kt b/ui/src/main/java/com/github/umercodez/sensorspot/ui/screens/sensors/SensorScreenViewModel.kt index 6bc5c50..aae0e62 100644 --- a/ui/src/main/java/com/github/umercodez/sensorspot/ui/screens/sensors/SensorScreenViewModel.kt +++ b/ui/src/main/java/com/github/umercodez/sensorspot/ui/screens/sensors/SensorScreenViewModel.kt @@ -1,104 +1,116 @@ -/* - * This file is a part of SensorSpot (https://www.github.com/UmerCodez/SensorSpot) - * Copyright (C) 2025 Umer Farooq (umerfarooq2383@gmail.com) - * - * SensorSpot is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * SensorSpot is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with SensorSpot. If not, see . - * - */ -package com.github.umercodez.sensorspot.ui.screens.sensors - -import android.util.Log -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.github.umercodez.sensorspot.data.repositories.settings.sensor.SensorsRepository -import com.github.umercodez.sensorspot.data.utils.LocationPermissionUtil -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch -import javax.inject.Inject - - -@HiltViewModel -class SensorScreenViewModel @Inject constructor( - private val sensorsRepository: SensorsRepository, - private val locationPermissionUtil: LocationPermissionUtil -): ViewModel(){ - - private val _uiState = MutableStateFlow(SensorsScreenState()) - val uiState = _uiState.asStateFlow() - val tag = "SensorScreenViewModel" - - init { - - Log.d(tag, "init() ${hashCode()}") - - - viewModelScope.launch{ - _uiState.value.allSensors.clear() - _uiState.value.allSensors.addAll(sensorsRepository.getAvailableSensors()) - - sensorsRepository.getSelectedSensors().forEach { sensor -> - _uiState.value.sensorSelectionState[sensor] = true - } - - _uiState.update { - it.copy(locationPermissionGranted = locationPermissionUtil.isLocationPermissionGranted()) - } - - } - - } - - fun onEvent(event: SensorsScreenEvent) { - when (event) { - is SensorsScreenEvent.OnSensorItemCheckedChange -> { - if (event.checked) { - _uiState.value.sensorSelectionState[event.sensor] = true - } else { - _uiState.value.sensorSelectionState[event.sensor] = false - } - - viewModelScope.launch { - val selectedSensors = _uiState.value.sensorSelectionState.filterValues { it == true }.keys.toList() - sensorsRepository.saveSelectedSensors(selectedSensors) - } - } - - is SensorsScreenEvent.OnGpsItemCheckedChange -> { - viewModelScope.launch { - - val gpsChecked = event.checked && locationPermissionUtil.isLocationPermissionGranted() - - _uiState.update { - it.copy(gpsChecked = gpsChecked) - } - sensorsRepository.saveGpsSelectionState(gpsChecked) - } - } - is SensorsScreenEvent.OnGrantLocationPermissionClick -> { - - } - } - } - - fun onLocationPermissionStateChange() { - _uiState.update { - it.copy(locationPermissionGranted = locationPermissionUtil.isLocationPermissionGranted()) - } - - } - +/* + * This file is a part of SensorSpot (https://www.github.com/UmerCodez/SensorSpot) + * Copyright (C) 2025 Umer Farooq (umerfarooq2383@gmail.com) + * + * SensorSpot is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * SensorSpot is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with SensorSpot. If not, see . + * + */ +package com.github.umercodez.sensorspot.ui.screens.sensors + +import android.util.Log +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.github.umercodez.sensorspot.data.repositories.settings.SettingsRepository +import com.github.umercodez.sensorspot.data.repositories.settings.sensor.SensorsRepository +import com.github.umercodez.sensorspot.data.utils.LocationPermissionUtil +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import javax.inject.Inject + + +@HiltViewModel +class SensorScreenViewModel @Inject constructor( + private val sensorsRepository: SensorsRepository, + private val locationPermissionUtil: LocationPermissionUtil, + private val settingsRepository: SettingsRepository +): ViewModel(){ + + private val _uiState = MutableStateFlow(SensorsScreenState()) + val uiState = _uiState.asStateFlow() + val tag = "SensorScreenViewModel" + + init { + + Log.d(tag, "init() ${hashCode()}") + + + viewModelScope.launch{ + _uiState.value.allSensors.clear() + _uiState.value.allSensors.addAll(sensorsRepository.getAvailableSensors()) + + sensorsRepository.getSelectedSensors().forEach { sensor -> + _uiState.value.sensorSelectionState[sensor] = true + } + + _uiState.update { + it.copy(locationPermissionGranted = locationPermissionUtil.isLocationPermissionGranted()) + } + } + + viewModelScope.launch { + settingsRepository.settings.collect { settings -> + _uiState.update { + it.copy( + dedicatedTopics = settings.dedicatedTopics, + mqttTopic = settings.topic + ) + } + } + } + + } + + fun onEvent(event: SensorsScreenEvent) { + when (event) { + is SensorsScreenEvent.OnSensorItemCheckedChange -> { + if (event.checked) { + _uiState.value.sensorSelectionState[event.sensor] = true + } else { + _uiState.value.sensorSelectionState[event.sensor] = false + } + + viewModelScope.launch { + val selectedSensors = _uiState.value.sensorSelectionState.filterValues { it == true }.keys.toList() + sensorsRepository.saveSelectedSensors(selectedSensors) + } + } + + is SensorsScreenEvent.OnGpsItemCheckedChange -> { + viewModelScope.launch { + + val gpsChecked = event.checked && locationPermissionUtil.isLocationPermissionGranted() + + _uiState.update { + it.copy(gpsChecked = gpsChecked) + } + sensorsRepository.saveGpsSelectionState(gpsChecked) + } + } + is SensorsScreenEvent.OnGrantLocationPermissionClick -> { + + } + } + } + + fun onLocationPermissionStateChange() { + _uiState.update { + it.copy(locationPermissionGranted = locationPermissionUtil.isLocationPermissionGranted()) + } + + } + } \ No newline at end of file diff --git a/ui/src/main/java/com/github/umercodez/sensorspot/ui/screens/sensors/SensorsScreen.kt b/ui/src/main/java/com/github/umercodez/sensorspot/ui/screens/sensors/SensorsScreen.kt index e771af0..ea03059 100644 --- a/ui/src/main/java/com/github/umercodez/sensorspot/ui/screens/sensors/SensorsScreen.kt +++ b/ui/src/main/java/com/github/umercodez/sensorspot/ui/screens/sensors/SensorsScreen.kt @@ -1,118 +1,122 @@ -/* - * This file is a part of SensorSpot (https://www.github.com/UmerCodez/SensorSpot) - * Copyright (C) 2025 Umer Farooq (umerfarooq2383@gmail.com) - * - * SensorSpot is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * SensorSpot is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with SensorSpot. If not, see . - * - */ -package com.github.umercodez.sensorspot.ui.screens.sensors - -import android.Manifest -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.toMutableStateList -import androidx.compose.ui.Modifier -import androidx.compose.ui.tooling.preview.Preview -import androidx.hilt.navigation.compose.hiltViewModel -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.github.umercodez.sensorspot.data.repositories.settings.sensor.fakeSensors -import com.github.umercodez.sensorspot.ui.SensorSpotTheme -import com.github.umercodez.sensorspot.ui.screens.sensors.components.GpsItem -import com.github.umercodez.sensorspot.ui.screens.sensors.components.SensorItem -import com.google.accompanist.permissions.ExperimentalPermissionsApi -import com.google.accompanist.permissions.rememberPermissionState - - -@OptIn(ExperimentalPermissionsApi::class) -@Composable -fun SensorsScreen( - viewModel: SensorScreenViewModel = hiltViewModel() -) { - val state by viewModel.uiState.collectAsStateWithLifecycle() - val locationPermissionState = rememberPermissionState(Manifest.permission.ACCESS_FINE_LOCATION) - - LaunchedEffect(locationPermissionState.status) { - viewModel.onLocationPermissionStateChange() - } - - SensorsScreen( - state = state, - onEvent = { event -> - - if(event is SensorsScreenEvent.OnGrantLocationPermissionClick) { - locationPermissionState.launchPermissionRequest() - } - - viewModel.onEvent(event) - } - ) - -} - - -@Composable -fun SensorsScreen( - state: SensorsScreenState, - onEvent: (SensorsScreenEvent) -> Unit -) { - LazyColumn( - modifier = Modifier.fillMaxWidth(), - ) { - items( - items = state.allSensors, - key = { it.hashCode() } - ) { sensor -> - SensorItem( - modifier = Modifier.animateItem(), - sensor = sensor, - checked = state.sensorSelectionState[sensor] == true, - onCheckedChange = { - onEvent(SensorsScreenEvent.OnSensorItemCheckedChange(sensor, it)) - } - ) - } - item { - GpsItem( - checked = state.gpsChecked, - onCheckedChange = { - onEvent(SensorsScreenEvent.OnGpsItemCheckedChange(it)) - }, - locationPermissionGranted = state.locationPermissionGranted, - onGrantLocationPermissionClick = { - onEvent(SensorsScreenEvent.OnGrantLocationPermissionClick) - } - ) - } - } - -} - - -@Preview(showBackground = true) -@Composable -fun SensorsScreenContentPreview() { - SensorSpotTheme { - SensorsScreen( - state = SensorsScreenState( - allSensors = fakeSensors.toMutableStateList(), - ), - onEvent = {} - ) - } -} - +/* + * This file is a part of SensorSpot (https://www.github.com/UmerCodez/SensorSpot) + * Copyright (C) 2025 Umer Farooq (umerfarooq2383@gmail.com) + * + * SensorSpot is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * SensorSpot is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with SensorSpot. If not, see . + * + */ +package com.github.umercodez.sensorspot.ui.screens.sensors + +import android.Manifest +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.toMutableStateList +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.github.umercodez.sensorspot.data.repositories.settings.sensor.fakeSensors +import com.github.umercodez.sensorspot.ui.SensorSpotTheme +import com.github.umercodez.sensorspot.ui.screens.sensors.components.GpsItem +import com.github.umercodez.sensorspot.ui.screens.sensors.components.SensorItem +import com.google.accompanist.permissions.ExperimentalPermissionsApi +import com.google.accompanist.permissions.rememberPermissionState + + +@OptIn(ExperimentalPermissionsApi::class) +@Composable +fun SensorsScreen( + viewModel: SensorScreenViewModel = hiltViewModel() +) { + val state by viewModel.uiState.collectAsStateWithLifecycle() + val locationPermissionState = rememberPermissionState(Manifest.permission.ACCESS_FINE_LOCATION) + + LaunchedEffect(locationPermissionState.status) { + viewModel.onLocationPermissionStateChange() + } + + SensorsScreen( + state = state, + onEvent = { event -> + + if(event is SensorsScreenEvent.OnGrantLocationPermissionClick) { + locationPermissionState.launchPermissionRequest() + } + + viewModel.onEvent(event) + } + ) + +} + + +@Composable +fun SensorsScreen( + state: SensorsScreenState, + onEvent: (SensorsScreenEvent) -> Unit +) { + LazyColumn( + modifier = Modifier.fillMaxWidth(), + ) { + items( + items = state.allSensors, + key = { it.hashCode() } + ) { sensor -> + SensorItem( + modifier = Modifier.animateItem(), + sensor = sensor, + dedicatedTopics = state.dedicatedTopics, + mqttTopic = state.mqttTopic, + checked = state.sensorSelectionState[sensor] == true, + onCheckedChange = { + onEvent(SensorsScreenEvent.OnSensorItemCheckedChange(sensor, it)) + } + ) + } + item { + GpsItem( + checked = state.gpsChecked, + dedicatedTopics = state.dedicatedTopics, + mqttTopic = state.mqttTopic, + onCheckedChange = { + onEvent(SensorsScreenEvent.OnGpsItemCheckedChange(it)) + }, + locationPermissionGranted = state.locationPermissionGranted, + onGrantLocationPermissionClick = { + onEvent(SensorsScreenEvent.OnGrantLocationPermissionClick) + } + ) + } + } + +} + + +@Preview(showBackground = true) +@Composable +fun SensorsScreenContentPreview() { + SensorSpotTheme { + SensorsScreen( + state = SensorsScreenState( + allSensors = fakeSensors.toMutableStateList(), + ), + onEvent = {} + ) + } +} + diff --git a/ui/src/main/java/com/github/umercodez/sensorspot/ui/screens/sensors/SensorsScreenState.kt b/ui/src/main/java/com/github/umercodez/sensorspot/ui/screens/sensors/SensorsScreenState.kt index 80e38c7..ee6d53c 100644 --- a/ui/src/main/java/com/github/umercodez/sensorspot/ui/screens/sensors/SensorsScreenState.kt +++ b/ui/src/main/java/com/github/umercodez/sensorspot/ui/screens/sensors/SensorsScreenState.kt @@ -28,5 +28,7 @@ data class SensorsScreenState( val allSensors: SnapshotStateList = mutableStateListOf(), val sensorSelectionState: SnapshotStateMap = mutableStateMapOf(), val locationPermissionGranted: Boolean = false, - val gpsChecked: Boolean = false + val gpsChecked: Boolean = false, + val dedicatedTopics: Boolean = false, + val mqttTopic: String = "android/sensor", ) \ No newline at end of file diff --git a/ui/src/main/java/com/github/umercodez/sensorspot/ui/screens/sensors/components/GpsItem.kt b/ui/src/main/java/com/github/umercodez/sensorspot/ui/screens/sensors/components/GpsItem.kt index c04b07a..1880567 100644 --- a/ui/src/main/java/com/github/umercodez/sensorspot/ui/screens/sensors/components/GpsItem.kt +++ b/ui/src/main/java/com/github/umercodez/sensorspot/ui/screens/sensors/components/GpsItem.kt @@ -25,6 +25,8 @@ import com.github.umercodez.sensorspot.ui.SensorSpotTheme @Composable fun GpsItem( checked: Boolean, + dedicatedTopics: Boolean = false, + mqttTopic: String = "android/sensor", onCheckedChange: (Boolean) -> Unit, modifier: Modifier = Modifier, locationPermissionGranted: Boolean = false, @@ -56,7 +58,7 @@ fun GpsItem( }, supportingContent = { Text( - text = "type = android.gps", + text = if (dedicatedTopics) "topic = ${mqttTopic}/gps" else "type = android.gps", color = MaterialTheme.colorScheme.onSurface ) }, diff --git a/ui/src/main/java/com/github/umercodez/sensorspot/ui/screens/sensors/components/SensorItem.kt b/ui/src/main/java/com/github/umercodez/sensorspot/ui/screens/sensors/components/SensorItem.kt index ede9e9f..c86d6a5 100644 --- a/ui/src/main/java/com/github/umercodez/sensorspot/ui/screens/sensors/components/SensorItem.kt +++ b/ui/src/main/java/com/github/umercodez/sensorspot/ui/screens/sensors/components/SensorItem.kt @@ -1,125 +1,132 @@ -/* - * This file is a part of SensorSpot (https://www.github.com/UmerCodez/SensorSpot) - * Copyright (C) 2025 Umer Farooq (umerfarooq2383@gmail.com) - * - * SensorSpot is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * SensorSpot is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with SensorSpot. If not, see . - * - */ -package com.github.umercodez.sensorspot.ui.screens.sensors.components - -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.text.BasicText -import androidx.compose.foundation.text.TextAutoSize -import androidx.compose.material3.ListItem -import androidx.compose.material3.ListItemDefaults -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Surface -import androidx.compose.material3.Switch -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.draw.clip -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import com.github.umercodez.sensorspot.data.repositories.settings.sensor.DeviceSensor -import com.github.umercodez.sensorspot.data.repositories.settings.sensor.accelerometer -import com.github.umercodez.sensorspot.ui.SensorSpotTheme - -@Composable -fun SensorItem( - sensor: DeviceSensor, - modifier: Modifier = Modifier, - checked: Boolean = false, - onCheckedChange: (Boolean) -> Unit -) { - var showDetails by remember { mutableStateOf(false) } - Column( - modifier = modifier - .fillMaxWidth() - .padding(5.dp) - .clip(RoundedCornerShape(16.dp)) - .clickable { showDetails = !showDetails } - ) { - ListItem( - headlineContent = { Text(text = sensor.name) }, - supportingContent = { - BasicText( - text = "type = ${sensor.stringType}", - style = MaterialTheme.typography.bodyLarge.copy(color = MaterialTheme.colorScheme.onSurface), - overflow = TextOverflow.Ellipsis, - maxLines = 1, - autoSize = TextAutoSize.StepBased( - minFontSize = 8.sp, - maxFontSize = 12.sp, - ) - ) - }, - trailingContent = { - Switch( - checked = checked, - onCheckedChange = onCheckedChange - ) - }, - colors = ListItemDefaults.colors( - containerColor = MaterialTheme.colorScheme.surfaceContainer - ) - ) - AnimatedVisibility(showDetails) { - Column( - modifier = Modifier - .background(MaterialTheme.colorScheme.surfaceContainer) - .fillMaxWidth() - .padding(10.dp), - horizontalAlignment = Alignment.Start - ) { - Text("maxRange = ${sensor.maximumRange}") - Text("minDelay = ${sensor.minDelay}") - Text("maxDelay = ${sensor.maxDelay}") - Text("reportingMode = ${sensor.reportingModeString}") - Text("wakeUpSensor = ${sensor.isWakeUpSensor}") - Text("power = ${sensor.power}") - Text("resolution = ${sensor.resolution}") - Text("vendor = ${sensor.vendor}") - - } - } - } -} - -@Preview -@Composable -private fun SensorItemPreview() { - SensorSpotTheme { - Surface { - SensorItem( - sensor = accelerometer.copy(stringType = "android.sensor.accelerometer"), - checked = true, - onCheckedChange = {} - ) - } - } -} +/* + * This file is a part of SensorSpot (https://www.github.com/UmerCodez/SensorSpot) + * Copyright (C) 2025 Umer Farooq (umerfarooq2383@gmail.com) + * + * SensorSpot is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * SensorSpot is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with SensorSpot. If not, see . + * + */ +package com.github.umercodez.sensorspot.ui.screens.sensors.components + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.BasicText +import androidx.compose.foundation.text.TextAutoSize +import androidx.compose.material3.ListItem +import androidx.compose.material3.ListItemDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Switch +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.draw.clip +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.github.umercodez.sensorspot.data.repositories.settings.sensor.DeviceSensor +import com.github.umercodez.sensorspot.data.repositories.settings.sensor.accelerometer +import com.github.umercodez.sensorspot.ui.SensorSpotTheme + +@Composable +fun SensorItem( + sensor: DeviceSensor, + dedicatedTopics: Boolean = false, + mqttTopic: String = "android/sensor", + modifier: Modifier = Modifier, + checked: Boolean = false, + onCheckedChange: (Boolean) -> Unit +) { + var showDetails by remember { mutableStateOf(false) } + Column( + modifier = modifier + .fillMaxWidth() + .padding(5.dp) + .clip(RoundedCornerShape(16.dp)) + .clickable { showDetails = !showDetails } + ) { + ListItem( + headlineContent = { Text(text = sensor.name) }, + supportingContent = { + val subText = if (dedicatedTopics) { + "topic = ${mqttTopic}/${ if (sensor.stringType.contains(".")) sensor.stringType.split('.').last() else sensor.stringType}" + } else { + "type = ${sensor.stringType}" + } + BasicText( + text = subText, + style = MaterialTheme.typography.bodyLarge.copy(color = MaterialTheme.colorScheme.onSurface), + overflow = TextOverflow.Ellipsis, + maxLines = 1, + autoSize = TextAutoSize.StepBased( + minFontSize = 8.sp, + maxFontSize = 12.sp, + ) + ) + }, + trailingContent = { + Switch( + checked = checked, + onCheckedChange = onCheckedChange + ) + }, + colors = ListItemDefaults.colors( + containerColor = MaterialTheme.colorScheme.surfaceContainer + ) + ) + AnimatedVisibility(showDetails) { + Column( + modifier = Modifier + .background(MaterialTheme.colorScheme.surfaceContainer) + .fillMaxWidth() + .padding(10.dp), + horizontalAlignment = Alignment.Start + ) { + Text("maxRange = ${sensor.maximumRange}") + Text("minDelay = ${sensor.minDelay}") + Text("maxDelay = ${sensor.maxDelay}") + Text("reportingMode = ${sensor.reportingModeString}") + Text("wakeUpSensor = ${sensor.isWakeUpSensor}") + Text("power = ${sensor.power}") + Text("resolution = ${sensor.resolution}") + Text("vendor = ${sensor.vendor}") + + } + } + } +} + +@Preview +@Composable +private fun SensorItemPreview() { + SensorSpotTheme { + Surface { + SensorItem( + sensor = accelerometer.copy(stringType = "android.sensor.accelerometer"), + checked = true, + onCheckedChange = {} + ) + } + } +} diff --git a/ui/src/main/java/com/github/umercodez/sensorspot/ui/screens/settings/SettingsScreen.kt b/ui/src/main/java/com/github/umercodez/sensorspot/ui/screens/settings/SettingsScreen.kt index b270666..f0c7c44 100644 --- a/ui/src/main/java/com/github/umercodez/sensorspot/ui/screens/settings/SettingsScreen.kt +++ b/ui/src/main/java/com/github/umercodez/sensorspot/ui/screens/settings/SettingsScreen.kt @@ -95,6 +95,17 @@ fun SettingsScreen( onUpdateClick = { onEvent(SettingsScreenEvent.OnTopicChange(it)) } ) + ListItem( + headlineContent = { Text("Dedicated topics") }, + supportingContent = { Text("use different topic for each sensor") }, + trailingContent = { + Switch( + checked = state.mqttConfig.dedicatedTopics, + onCheckedChange = { onEvent(SettingsScreenEvent.OnDedicatedTopicsChange(it)) } + ) + } + ) + ListItem( headlineContent = { Text("Qos") }, trailingContent = { diff --git a/ui/src/main/java/com/github/umercodez/sensorspot/ui/screens/settings/SettingsScreenEvent.kt b/ui/src/main/java/com/github/umercodez/sensorspot/ui/screens/settings/SettingsScreenEvent.kt index 08cf96c..936afad 100644 --- a/ui/src/main/java/com/github/umercodez/sensorspot/ui/screens/settings/SettingsScreenEvent.kt +++ b/ui/src/main/java/com/github/umercodez/sensorspot/ui/screens/settings/SettingsScreenEvent.kt @@ -16,20 +16,21 @@ * along with SensorSpot. If not, see . * */ -package com.github.umercodez.sensorspot.ui.screens.settings - -sealed interface SettingsScreenEvent { - data class OnBrokerAddressChange(val brokerIp: String) : SettingsScreenEvent - data class OnBrokerPortChange(val brokerPort: Int) : SettingsScreenEvent - data class OnTopicChange(val topic: String) : SettingsScreenEvent - data class OnQosChange(val qos: Int) : SettingsScreenEvent - data class OnUseSSLChange(val useSSL: Boolean) : SettingsScreenEvent - data class OnUseWebsocketChange(val useWebsocket: Boolean) : SettingsScreenEvent - data class OnUseCredentialsChange(val useCredentials: Boolean) : SettingsScreenEvent - data class OnConnectionTimeoutChange(val connectionTimeout: Int) : SettingsScreenEvent - data class OnPasswordChange(val password: String) : SettingsScreenEvent - data class OnUserNameChange(val userName: String) : SettingsScreenEvent - data class OnSensorSamplingRateChange(val sensorSamplingRate: Int) : SettingsScreenEvent - - object OnBackClick: SettingsScreenEvent +package com.github.umercodez.sensorspot.ui.screens.settings + +sealed interface SettingsScreenEvent { + data class OnBrokerAddressChange(val brokerIp: String) : SettingsScreenEvent + data class OnBrokerPortChange(val brokerPort: Int) : SettingsScreenEvent + data class OnTopicChange(val topic: String) : SettingsScreenEvent + data class OnDedicatedTopicsChange(val dedicatedTopics: Boolean) : SettingsScreenEvent + data class OnQosChange(val qos: Int) : SettingsScreenEvent + data class OnUseSSLChange(val useSSL: Boolean) : SettingsScreenEvent + data class OnUseWebsocketChange(val useWebsocket: Boolean) : SettingsScreenEvent + data class OnUseCredentialsChange(val useCredentials: Boolean) : SettingsScreenEvent + data class OnConnectionTimeoutChange(val connectionTimeout: Int) : SettingsScreenEvent + data class OnPasswordChange(val password: String) : SettingsScreenEvent + data class OnUserNameChange(val userName: String) : SettingsScreenEvent + data class OnSensorSamplingRateChange(val sensorSamplingRate: Int) : SettingsScreenEvent + + object OnBackClick: SettingsScreenEvent } \ No newline at end of file diff --git a/ui/src/main/java/com/github/umercodez/sensorspot/ui/screens/settings/SettingsScreenViewModel.kt b/ui/src/main/java/com/github/umercodez/sensorspot/ui/screens/settings/SettingsScreenViewModel.kt index 9bc5a62..8c312d8 100644 --- a/ui/src/main/java/com/github/umercodez/sensorspot/ui/screens/settings/SettingsScreenViewModel.kt +++ b/ui/src/main/java/com/github/umercodez/sensorspot/ui/screens/settings/SettingsScreenViewModel.kt @@ -86,6 +86,13 @@ class SettingsScreenViewModel @Inject constructor( } } } + is SettingsScreenEvent.OnDedicatedTopicsChange -> { + viewModelScope.launch { + settingsRepository.updateSettings { + it.copy(dedicatedTopics = event.dedicatedTopics) + } + } + } is SettingsScreenEvent.OnUseCredentialsChange -> { viewModelScope.launch { settingsRepository.updateSettings {