Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature: Add update API for generated specifications #27

Merged
merged 19 commits into from
Nov 3, 2023
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ import org.eclipse.kuksa.testapp.databroker.model.ConnectionInfo
import org.eclipse.kuksa.testapp.databroker.viewmodel.ConnectionViewModel
import org.eclipse.kuksa.testapp.databroker.viewmodel.ConnectionViewModel.*
import org.eclipse.kuksa.testapp.extension.compose.Headline
import org.eclipse.kuksa.testapp.extension.compose.rememberCountdown
import org.eclipse.kuksa.testapp.extension.compose.RememberCountdown
import org.eclipse.kuksa.testapp.extension.fetchFileName
import org.eclipse.kuksa.testapp.preferences.ConnectionInfoRepository

Expand Down Expand Up @@ -216,7 +216,7 @@ fun DataBrokerConnection(viewModel: ConnectionViewModel) {
onClick = { },
modifier = Modifier.requiredWidth(MinimumButtonWidth),
) {
val timeout by rememberCountdown(initialMillis = viewModel.connectionTimeoutMillis)
val timeout by RememberCountdown(initialMillis = viewModel.connectionTimeoutMillis)

@Suppress("MagicNumber") // To seconds
val timeoutSeconds = timeout / 1000
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive

@Composable
fun rememberCountdown(
fun RememberCountdown(
initialMillis: Long,
step: Long = 1000,
): MutableState<Long> {
Expand Down
4 changes: 3 additions & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ android.defaults.buildfeatures.buildconfig=true
android.enableJetifier=true
android.nonFinalResIds=false
android.nonTransitiveRClass=false

android.useAndroidX=true

kotlin.code.style=official

org.gradle.jvmargs=-Xmx2048m

# When using compose + ksp the incremental compiler should be disabled: https://issuetracker.google.com/issues/207185051
Expand Down
6 changes: 3 additions & 3 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
[versions]
activityKtx = "1.7.2"
activityKtx = "1.8.0"
androidGradlePlugin = "8.1.2" # Check with detekt table first: https://detekt.dev/docs/introduction/compatibility/
datastore = "1.0.0"
constraintlayoutCompose = "1.0.1"
datastorePreferences = "1.0.0"
kotlin = "1.9.0"
kotlinpoet = "1.14.2"
kotlinxSerializationJson = "1.6.0"
runtimeLivedata = "1.5.2"
runtimeLivedata = "1.5.3"
symbolProcessingApi = "1.9.10-1.0.13"
tomcatAnnotations = "6.0.53"
detekt = "1.23.1"
Expand All @@ -21,7 +21,7 @@ mockk = "1.13.7"
androidxLifecycle = "2.6.2"
kotlinxCoroutines = "1.7.3"
kotlinCompilerExtension = "1.5.1"
composeBom = "2023.09.02"
composeBom = "2023.10.00"
jvmTarget = "17"

[libraries]
Expand Down
44 changes: 29 additions & 15 deletions kuksa-sdk/src/main/kotlin/org/eclipse/kuksa/DataBrokerConnection.kt
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.eclipse.kuksa.extension.TAG
import org.eclipse.kuksa.extension.copy
import org.eclipse.kuksa.extension.createProperties
import org.eclipse.kuksa.extension.createPropertyDataPoints
import org.eclipse.kuksa.model.Property
import org.eclipse.kuksa.pattern.listener.MultiListener
import org.eclipse.kuksa.proto.v1.KuksaValV1
Expand Down Expand Up @@ -80,7 +82,7 @@ class DataBrokerConnection internal constructor(
* @throws DataBrokerException in case the connection to the DataBroker is no longer active
*/
fun subscribe(
properties: List<Property>,
properties: Collection<Property>,
propertyObserver: PropertyObserver,
) {
val asyncStub = VALGrpc.newStub(managedChannel)
Expand Down Expand Up @@ -141,14 +143,7 @@ class DataBrokerConnection internal constructor(
fields: List<Types.Field> = listOf(Types.Field.FIELD_VALUE),
observer: VssSpecificationObserver<T>,
) {
val vssPathToVssProperty = specification.heritage
.ifEmpty { setOf(specification) }
.filterIsInstance<VssProperty<*>>() // Only final leafs with a value can be observed
.groupBy { it.vssPath }
.mapValues { it.value.first() } // Always one result because the vssPath is unique
val leafProperties = vssPathToVssProperty.values
.map { Property(it.vssPath, fields) }
.toList()
val leafProperties = specification.createProperties(fields)

try {
Log.d(TAG, "Subscribing to the following properties: $leafProperties")
Expand All @@ -170,7 +165,7 @@ class DataBrokerConnection internal constructor(
initialSubscriptionUpdates[vssPath] = true
val isInitialSubscriptionComplete = initialSubscriptionUpdates.values.all { it }
if (isInitialSubscriptionComplete) {
Log.d(TAG, "Initial update for subscribed property complete: $vssPath - $updatedValue")
Log.d(TAG, "Update for subscribed property complete: $vssPath - $updatedValue")
observer.onSpecificationChanged(updatedVssSpecification)
}
}
Expand Down Expand Up @@ -251,21 +246,20 @@ class DataBrokerConnection internal constructor(
}

/**
* Updates the underlying property of the specified vssPath with the updatedProperty. Notifies the callback
* about (un)successful operation.
* Updates the underlying property of the specified vssPath with the updated property.
*
* @throws DataBrokerException in case the connection to the DataBroker is no longer active
*/
suspend fun update(
property: Property,
updatedDatapoint: Datapoint,
datapoint: Datapoint,
): SetResponse {
Log.d(TAG, "updateProperty() called with: updatedProperty = $property")
Log.d(TAG, "updateProperty() called with: updatedProperty = $property, datapoint: $datapoint")
return withContext(dispatcher) {
val blockingStub = VALGrpc.newBlockingStub(managedChannel)
val dataEntry = Types.DataEntry.newBuilder()
.setPath(property.vssPath)
.setValue(updatedDatapoint)
.setValue(datapoint)
.build()
val entryUpdate = KuksaValV1.EntryUpdate.newBuilder()
.setEntry(dataEntry)
Expand All @@ -283,6 +277,26 @@ class DataBrokerConnection internal constructor(
}
}

/**
* Only a [VssProperty] can be updated because they have an actual value. When provided with any parent
* [VssSpecification] then this [update] method will find all [VssProperty] children and updates them instead.
* Compared to [update] with only one [Property] and [Datapoint], here multiple [SetResponse] will be returned
* because a [VssSpecification] may consists of multiple values which may need to be updated.
*
* @throws DataBrokerException in case the connection to the DataBroker is no longer active
*/
suspend fun update(vssSpecification: VssSpecification): List<SetResponse> {
val responses = mutableListOf<SetResponse>()

val propertyDataPointPairs = vssSpecification.createPropertyDataPoints()
Chrylo marked this conversation as resolved.
Show resolved Hide resolved
propertyDataPointPairs.forEach { (property, dataPoint) ->
val response = update(property, dataPoint)
responses.add(response)
}

return responses
}

/**
* Disconnect from the DataBroker.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,64 +23,96 @@ import android.util.Log
import org.eclipse.kuksa.proto.v1.Types
import org.eclipse.kuksa.proto.v1.Types.BoolArray
import org.eclipse.kuksa.proto.v1.Types.Datapoint
import org.eclipse.kuksa.proto.v1.Types.Datapoint.ValueCase
import org.eclipse.kuksa.vsscore.model.VssProperty

private const val CSV_DELIMITER = ","

val Types.Metadata.valueType: Datapoint.ValueCase
/**
* Returns the converted VSS value types -> Protobuf data types.
*/
val Types.Metadata.valueType: ValueCase
get() = dataType.dataPointValueCase

fun Datapoint.ValueCase.createDatapoint(value: String): Datapoint {
/**
* Converts the [VssProperty.value] into a [Datapoint] object.
*/
val <T : Any> VssProperty<T>.datapoint: Datapoint
get() {
val stringValue = value.toString()
return when (value::class) {
String::class -> ValueCase.STRING.createDatapoint(stringValue)
Boolean::class -> ValueCase.BOOL.createDatapoint(stringValue)
Float::class -> ValueCase.FLOAT.createDatapoint(stringValue)
Double::class -> ValueCase.DOUBLE.createDatapoint(stringValue)
Int::class -> ValueCase.INT32.createDatapoint(stringValue)
Long::class -> ValueCase.INT64.createDatapoint(stringValue)
UInt::class -> ValueCase.UINT32.createDatapoint(stringValue)
Array<String>::class -> ValueCase.DOUBLE.createDatapoint(stringValue)
IntArray::class -> ValueCase.INT32_ARRAY.createDatapoint(stringValue)
BooleanArray::class -> ValueCase.BOOL_ARRAY.createDatapoint(stringValue)
LongArray::class -> ValueCase.INT64_ARRAY.createDatapoint(stringValue)

else -> throw IllegalArgumentException("Could not create datapoint for the value class: ${value::class}!")
Chrylo marked this conversation as resolved.
Show resolved Hide resolved
}
}

/**
* Creates a [Datapoint] object with a given [value] which is in [String] format. The [String] will be converted
* to the correct type for the [Datapoint.Builder].
*/
fun ValueCase.createDatapoint(value: String): Datapoint {
val datapointBuilder = Datapoint.newBuilder()

try {
when (this) {
Datapoint.ValueCase.VALUE_NOT_SET, // also explicitly handled on UI level
Datapoint.ValueCase.STRING,
ValueCase.VALUE_NOT_SET, // also explicitly handled on UI level
ValueCase.STRING,
-> datapointBuilder.string = value

Datapoint.ValueCase.UINT32 ->
ValueCase.UINT32 ->
datapointBuilder.uint32 = value.toInt()

Datapoint.ValueCase.INT32 ->
ValueCase.INT32 ->
datapointBuilder.int32 = value.toInt()

Datapoint.ValueCase.UINT64 ->
ValueCase.UINT64 ->
datapointBuilder.uint64 = value.toLong()

Datapoint.ValueCase.INT64 ->
ValueCase.INT64 ->
datapointBuilder.int64 = value.toLong()

Datapoint.ValueCase.FLOAT ->
ValueCase.FLOAT ->
datapointBuilder.float = value.toFloat()

Datapoint.ValueCase.DOUBLE ->
ValueCase.DOUBLE ->
datapointBuilder.double = value.toDouble()

Datapoint.ValueCase.BOOL ->
ValueCase.BOOL ->
datapointBuilder.bool = value.toBoolean()

Datapoint.ValueCase.STRING_ARRAY ->
ValueCase.STRING_ARRAY ->
datapointBuilder.stringArray = createStringArray(value)

Datapoint.ValueCase.UINT32_ARRAY ->
ValueCase.UINT32_ARRAY ->
datapointBuilder.uint32Array = createUInt32Array(value)

Datapoint.ValueCase.INT32_ARRAY ->
ValueCase.INT32_ARRAY ->
datapointBuilder.int32Array = createInt32Array(value)

Datapoint.ValueCase.UINT64_ARRAY ->
ValueCase.UINT64_ARRAY ->
datapointBuilder.uint64Array = createUInt64Array(value)

Datapoint.ValueCase.INT64_ARRAY ->
ValueCase.INT64_ARRAY ->
datapointBuilder.int64Array = createInt64Array(value)

Datapoint.ValueCase.FLOAT_ARRAY ->
ValueCase.FLOAT_ARRAY ->
datapointBuilder.floatArray = createFloatArray(value)

Datapoint.ValueCase.DOUBLE_ARRAY ->
ValueCase.DOUBLE_ARRAY ->
datapointBuilder.doubleArray = createDoubleArray(value)

Datapoint.ValueCase.BOOL_ARRAY ->
ValueCase.BOOL_ARRAY ->
datapointBuilder.boolArray = createBoolArray(value)
}
} catch (e: NumberFormatException) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import org.eclipse.kuksa.proto.v1.Types
import org.eclipse.kuksa.proto.v1.Types.Datapoint.ValueCase

/**
* Returns the converted VSS data types -> Protobuf data types
* Returns the converted VSS data types -> Protobuf data types.
*/
val Types.DataType.dataPointValueCase: ValueCase
get() {
Expand Down
Loading