diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceState.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceState.kt index 846a37b..5b547f7 100644 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceState.kt +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceState.kt @@ -74,15 +74,16 @@ public fun DeviceState.Companion.map( public fun DeviceState.map(mapper: (T) -> R): DeviceStateWithDependencies = DeviceState.map(this, mapper) -public fun DeviceState>.values(): DeviceState = object : DeviceState { - override val value: Double - get() = this@values.value.value +public fun DeviceState>.values(): DeviceState = + object : DeviceState { + override val value: Double + get() = this@values.value.value - override val valueFlow: Flow - get() = this@values.valueFlow.map { it.value } + override val valueFlow: Flow + get() = this@values.valueFlow.map { it.value } - override fun toString(): String = this@values.toString() -} + override fun toString(): String = this@values.toString() + } /** * Combine two device states into one read-only [DeviceState]. Only the latest value of each state is used. diff --git a/controls-core/src/commonMain/kotlin/space/kscience/controls/misc/converters.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/misc/converters.kt index 4297d20..c200758 100644 --- a/controls-core/src/commonMain/kotlin/space/kscience/controls/misc/converters.kt +++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/misc/converters.kt @@ -41,13 +41,22 @@ private object InstantConverter : MetaConverter { public val MetaConverter.Companion.instant: MetaConverter get() = InstantConverter private object DoubleRangeConverter : MetaConverter> { - override fun readOrNull(source: Meta): ClosedFloatingPointRange? = source.value?.doubleArray?.let { (start, end)-> - start..end - } + override fun readOrNull(source: Meta): ClosedFloatingPointRange? = + source.value?.doubleArray?.let { (start, end) -> + start..end + } override fun convert( obj: ClosedFloatingPointRange, ): Meta = Meta(doubleArrayOf(obj.start, obj.endInclusive).asValue()) } -public val MetaConverter.Companion.doubleRange: MetaConverter> get() = DoubleRangeConverter \ No newline at end of file +public val MetaConverter.Companion.doubleRange: MetaConverter> get() = DoubleRangeConverter + +private object StringListConverter : MetaConverter> { + override fun convert(obj: List): Meta = Meta(obj.map { it.asValue() }.asValue()) + + override fun readOrNull(source: Meta): List? = source.stringList ?: source["@jsonArray"]?.stringList +} + +public val MetaConverter.Companion.stringList: MetaConverter> get() = StringListConverter diff --git a/controls-magix/src/commonMain/kotlin/space/kscience/controls/client/clientPropertyAccess.kt b/controls-magix/src/commonMain/kotlin/space/kscience/controls/client/clientPropertyAccess.kt index 20c59e8..10b1196 100644 --- a/controls-magix/src/commonMain/kotlin/space/kscience/controls/client/clientPropertyAccess.kt +++ b/controls-magix/src/commonMain/kotlin/space/kscience/controls/client/clientPropertyAccess.kt @@ -19,10 +19,13 @@ import space.kscience.dataforge.meta.Meta public suspend fun DeviceClient.read(propertySpec: DevicePropertySpec<*, T>): T = propertySpec.converter.readOrNull(readProperty(propertySpec.name)) ?: error("Property read result is not valid") - public suspend fun DeviceClient.request(propertySpec: DevicePropertySpec<*, T>): T = propertySpec.converter.read(getOrReadProperty(propertySpec.name)) +public fun DeviceClient.getCached(propertySpec: DevicePropertySpec<*, T>): T? = + getProperty(propertySpec.name)?.let { propertySpec.converter.read(it) } + + public suspend fun DeviceClient.write(propertySpec: MutableDevicePropertySpec<*, T>, value: T) { writeProperty(propertySpec.name, propertySpec.converter.convert(value)) } diff --git a/demo/device-collective/src/jvmMain/kotlin/CollectiveDevice.kt b/demo/device-collective/src/jvmMain/kotlin/CollectiveDevice.kt index 9fce04d..941068c 100644 --- a/demo/device-collective/src/jvmMain/kotlin/CollectiveDevice.kt +++ b/demo/device-collective/src/jvmMain/kotlin/CollectiveDevice.kt @@ -3,12 +3,10 @@ package space.kscience.controls.demo.collective import space.kscience.controls.api.Device -import space.kscience.controls.constructor.DeviceConstructor -import space.kscience.controls.constructor.MutableDeviceState -import space.kscience.controls.constructor.registerAsProperty +import space.kscience.controls.constructor.* +import space.kscience.controls.misc.stringList import space.kscience.controls.peer.PeerConnection import space.kscience.controls.spec.DeviceSpec -import space.kscience.controls.spec.unit import space.kscience.dataforge.context.Context import space.kscience.dataforge.meta.MetaConverter import space.kscience.dataforge.meta.Scheme @@ -56,9 +54,16 @@ interface CollectiveDevice : Device { write = { _, value -> setVelocity(value) } ) - val listVisible by action(MetaConverter.unit, MetaConverter.valueList { it.string }) { - listVisible().toList() - } + val visibleNeighbors by property( + MetaConverter.stringList, + read = { + listVisible().toList() + } + ) + +// val listVisible by action(MetaConverter.unit, MetaConverter.valueList { it.string }) { +// listVisible().toList() +// } } } @@ -74,8 +79,28 @@ class CollectiveDeviceConstructor( override val id: CollectiveDeviceId get() = configuration.deviceId - val position = registerAsProperty(CollectiveDevice.position, position.sample(configuration.reportInterval.milliseconds)) - val velocity = registerAsProperty(CollectiveDevice.velocity, velocity.sample(configuration.reportInterval.milliseconds)) + val position = registerAsProperty( + CollectiveDevice.position, + position.sample(configuration.reportInterval.milliseconds) + ) + + val velocity = registerAsProperty( + CollectiveDevice.velocity, + velocity.sample(configuration.reportInterval.milliseconds) + ) + + private val _visibleNeighbors: MutableDeviceState> = stateOf(emptyList()) + + val visibleNeighbors = registerAsProperty( + CollectiveDevice.visibleNeighbors, + _visibleNeighbors.map { it.toList() } + ) + + init { + position.onNext { + _visibleNeighbors.value = observation.invoke().keys + } + } override suspend fun getPosition(): Gmc = position.value diff --git a/demo/device-collective/src/jvmMain/kotlin/DeviceCollectiveModel.kt b/demo/device-collective/src/jvmMain/kotlin/DeviceCollectiveModel.kt index 2c325b4..d8c64dd 100644 --- a/demo/device-collective/src/jvmMain/kotlin/DeviceCollectiveModel.kt +++ b/demo/device-collective/src/jvmMain/kotlin/DeviceCollectiveModel.kt @@ -20,8 +20,6 @@ import space.kscience.magix.api.MagixEndpoint import space.kscience.magix.rsocket.rSocketWithWebSockets import space.kscience.magix.server.startMagixServer import space.kscience.maps.coordinates.* -import kotlin.time.Duration -import kotlin.time.Duration.Companion.milliseconds internal data class CollectiveDeviceState( @@ -46,9 +44,8 @@ internal fun VirtualDeviceState( internal class DeviceCollectiveModel( context: Context, val deviceStates: Collection, - val visibilityRange: Distance = 0.4.kilometers, + val visibilityRange: Distance = 1.kilometers, val radioRange: Distance = 5.kilometers, - val reportInterval: Duration = 1000.milliseconds ) : ModelConstructor(context), PeerConnection { /** @@ -107,6 +104,7 @@ internal fun CoroutineScope.launchCollectiveMagixServer( val server = startMagixServer( // RSocketMagixFlowPlugin() ) + val deviceEndpoint = MagixEndpoint.rSocketWithWebSockets("localhost") collectiveModel.devices.forEach { (id, device) -> val deviceContext = collectiveModel.context.buildContext(id.parseAsName()) { @@ -116,7 +114,7 @@ internal fun CoroutineScope.launchCollectiveMagixServer( deviceContext.install(id, device) - val deviceEndpoint = MagixEndpoint.rSocketWithWebSockets("localhost") +// val deviceEndpoint = MagixEndpoint.rSocketWithWebSockets("localhost") deviceContext.request(DeviceManager).launchMagixService(deviceEndpoint, id) } diff --git a/demo/device-collective/src/jvmMain/kotlin/main.kt b/demo/device-collective/src/jvmMain/kotlin/main.kt index 9aff323..1292a0e 100644 --- a/demo/device-collective/src/jvmMain/kotlin/main.kt +++ b/demo/device-collective/src/jvmMain/kotlin/main.kt @@ -10,6 +10,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.material.Card import androidx.compose.material.Checkbox import androidx.compose.material.Text +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.* import androidx.compose.ui.Alignment @@ -24,9 +25,12 @@ import androidx.compose.ui.window.Window import androidx.compose.ui.window.application import io.ktor.client.HttpClient import io.ktor.client.engine.cio.CIO -import kotlinx.coroutines.* +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import org.jetbrains.compose.splitpane.ExperimentalSplitPaneApi import org.jetbrains.compose.splitpane.HorizontalSplitPane import org.jetbrains.compose.splitpane.rememberSplitPaneState @@ -47,7 +51,6 @@ import space.kscience.maps.coordinates.Gmc import space.kscience.maps.coordinates.meters import space.kscience.maps.features.* import java.nio.file.Path -import kotlin.time.Duration.Companion.seconds @Composable @@ -55,6 +58,8 @@ fun rememberContext(name: String, contextBuilder: ContextBuilder.() -> Unit = {} Context(name, contextBuilder) } +private val gmcMetaConverter = MetaConverter.serializable() + @Composable fun App() { val scope = rememberCoroutineScope() @@ -82,8 +87,7 @@ fun App() { val magixClient = MagixEndpoint.rSocketWithWebSockets("localhost") client.complete(magixClient) - - collectiveModel.roster.forEach { (id, config) -> + collectiveModel.roster.forEach { (id, config) -> devices[id] = magixClient.remoteDevice(parentContext, "listener", id, id.parseAsName()) } } @@ -129,23 +133,14 @@ fun App() { client.await().subscribe(DeviceManager.magixFormat).onEach { (magixMessage, deviceMessage) -> if (deviceMessage is PropertyChangedMessage && deviceMessage.property == "position") { val id = magixMessage.sourceEndpoint - val position = MetaConverter.serializable().read(deviceMessage.value) + val position = gmcMetaConverter.read(deviceMessage.value) val activeDevice = selectedDeviceId?.let { devices[it] } - suspend fun DeviceClient.idIsVisible() = try { - withTimeout(1.seconds) { - id in execute(CollectiveDevice.listVisible) - } - } catch (ex: Exception) { - ex.printStackTrace() - true - } - if ( activeDevice == null || id == selectedDeviceId || !showOnlyVisible || - activeDevice.idIsVisible() + id in activeDevice.request(CollectiveDevice.visibleNeighbors) ) { rectangle( position, @@ -168,24 +163,29 @@ fun App() { Column( modifier = Modifier.verticalScroll(rememberScrollState()) ) { - devices.forEach { (id, _) -> + collectiveModel.roster.forEach { (id, _) -> Card( elevation = 16.dp, modifier = Modifier.padding(8.dp).onClick { selectedDeviceId = id }.conditional(id == selectedDeviceId) { border(2.dp, Color.Blue) - } + }, ) { Column( modifier = Modifier.padding(8.dp) ) { - Text( - text = id, - fontSize = 16.sp, - fontWeight = FontWeight.Bold, - modifier = Modifier.padding(10.dp).fillMaxWidth() - ) + Row(verticalAlignment = Alignment.CenterVertically) { + if (devices[id] == null) { + CircularProgressIndicator() + } + Text( + text = id, + fontSize = 16.sp, + fontWeight = FontWeight.Bold, + modifier = Modifier.padding(10.dp).fillMaxWidth(), + ) + } if (id == selectedDeviceId) { roster[id]?.let { Text("Meta:", color = Color.Blue, fontWeight = FontWeight.Bold) @@ -195,14 +195,11 @@ fun App() { } currentPosition?.let { currentPosition -> - Text("Широта: ${String.format("%.3f", currentPosition.latitude.toDegrees().value)}") Text( - "Долгота: ${ - String.format( - "%.3f", - currentPosition.longitude.toDegrees().value - ) - }" + "Широта: ${String.format("%.3f", currentPosition.latitude.toDegrees().value)}" + ) + Text( + "Долгота: ${String.format("%.3f", currentPosition.longitude.toDegrees().value)}" ) currentPosition.elevation?.let { Text("Высота: ${String.format("%.1f", it.meters)} м")