diff --git a/controls-magix/src/commonTest/kotlin/space/kscience/controls/client/RemoteDeviceConnect.kt b/controls-magix/src/commonTest/kotlin/space/kscience/controls/client/RemoteDeviceConnect.kt index 3c707b2..18a3dfc 100644 --- a/controls-magix/src/commonTest/kotlin/space/kscience/controls/client/RemoteDeviceConnect.kt +++ b/controls-magix/src/commonTest/kotlin/space/kscience/controls/client/RemoteDeviceConnect.kt @@ -93,7 +93,7 @@ internal class RemoteDeviceConnect { val virtualMagixEndpoint = VirtualMagixEndpoint(deviceManager) - val remoteDevice = virtualMagixEndpoint.remoteDevice(context, "client", "device", "test".asName()) + val remoteDevice: DeviceClient = virtualMagixEndpoint.remoteDevice(context, "client", "device", "test".asName()) assertContains(0.0..1.0, remoteDevice.read(TestDevice.value)) diff --git a/controls-magix/src/jvmTest/kotlin/space/kscience/controls/client/MagixLoopTest.kt b/controls-magix/src/jvmTest/kotlin/space/kscience/controls/client/MagixLoopTest.kt index b08066b..31f73fc 100644 --- a/controls-magix/src/jvmTest/kotlin/space/kscience/controls/client/MagixLoopTest.kt +++ b/controls-magix/src/jvmTest/kotlin/space/kscience/controls/client/MagixLoopTest.kt @@ -1,10 +1,10 @@ package space.kscience.controls.client +import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.test.runTest -import kotlinx.coroutines.withContext import space.kscience.controls.client.RemoteDeviceConnect.TestDevice import space.kscience.controls.manager.DeviceManager import space.kscience.controls.manager.install @@ -20,36 +20,37 @@ class MagixLoopTest { @Test fun realDeviceHub() = runTest { - withContext(Dispatchers.Default) { - val context = Context { - plugin(DeviceManager) - } - - val server = context.startMagixServer() - - val deviceManager = context.request(DeviceManager) - - val deviceEndpoint = MagixEndpoint.rSocketWithWebSockets("localhost") - - deviceManager.launchMagixService(deviceEndpoint, "device") - - launch { - delay(50) - repeat(10) { - deviceManager.install("test[$it]", TestDevice) - } - } - - val clientEndpoint = MagixEndpoint.rSocketWithWebSockets("localhost") - - val remoteHub = clientEndpoint.remoteDeviceHub(context, "client", "device") - - assertEquals(0, remoteHub.devices.size) - delay(60) - clientEndpoint.requestDeviceUpdate("client", "device") - delay(60) - assertEquals(10, remoteHub.devices.size) - server.stop() + val context = Context { + coroutineContext(Dispatchers.Default) + plugin(DeviceManager) } + + val server = context.startMagixServer() + + val deviceManager = context.request(DeviceManager) + + val deviceEndpoint = MagixEndpoint.rSocketWithWebSockets("localhost") + + deviceManager.launchMagixService(deviceEndpoint, "device") + + val trigger = CompletableDeferred() + + context.launch { + repeat(10) { + deviceManager.install("test[$it]", TestDevice) + } + delay(100) + trigger.complete(Unit) + } + + val clientEndpoint = MagixEndpoint.rSocketWithWebSockets("localhost") + + val remoteHub = clientEndpoint.remoteDeviceHub(context, "client", "device") + + assertEquals(0, remoteHub.devices.size) + clientEndpoint.requestDeviceUpdate("client", "device") + trigger.join() + assertEquals(10, remoteHub.devices.size) + server.stop() } } \ No newline at end of file diff --git a/controls-visualisation-compose/src/commonMain/kotlin/koalaPlots.kt b/controls-visualisation-compose/src/commonMain/kotlin/koalaPlots.kt index 855de10..87a9fca 100644 --- a/controls-visualisation-compose/src/commonMain/kotlin/koalaPlots.kt +++ b/controls-visualisation-compose/src/commonMain/kotlin/koalaPlots.kt @@ -1,3 +1,5 @@ +@file:OptIn(FlowPreview::class) + package space.kscience.controls.compose import androidx.compose.runtime.* @@ -6,11 +8,8 @@ import io.github.koalaplot.core.line.LinePlot import io.github.koalaplot.core.style.LineStyle import io.github.koalaplot.core.xygraph.DefaultPoint import io.github.koalaplot.core.xygraph.XYGraphScope -import kotlinx.coroutines.Job -import kotlinx.coroutines.delay +import kotlinx.coroutines.* import kotlinx.coroutines.flow.* -import kotlinx.coroutines.isActive -import kotlinx.coroutines.launch import kotlinx.datetime.Clock import kotlinx.datetime.Instant import space.kscience.controls.api.Device diff --git a/demo/devices-on-map/build.gradle.kts b/demo/device-collective/build.gradle.kts similarity index 94% rename from demo/devices-on-map/build.gradle.kts rename to demo/device-collective/build.gradle.kts index b81dc12..5520265 100644 --- a/demo/devices-on-map/build.gradle.kts +++ b/demo/device-collective/build.gradle.kts @@ -13,6 +13,7 @@ kscience { commonMain { implementation(projects.controlsVisualisationCompose) implementation(projects.controlsConstructor) + implementation(projects.magix.magixRsocket) } jvmMain { // implementation("io.ktor:ktor-server-cio") diff --git a/demo/device-collective/src/jvmMain/kotlin/DebounceDeviceState.kt b/demo/device-collective/src/jvmMain/kotlin/DebounceDeviceState.kt new file mode 100644 index 0000000..b507cd6 --- /dev/null +++ b/demo/device-collective/src/jvmMain/kotlin/DebounceDeviceState.kt @@ -0,0 +1,20 @@ +package space.kscience.controls.demo.map + +import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.sample +import space.kscience.controls.constructor.DeviceState +import kotlin.time.Duration + +@OptIn(FlowPreview::class) +class DebounceDeviceState( + val origin: DeviceState, + val interval: Duration, +) : DeviceState { + override val value: T get() = origin.value + override val valueFlow: Flow get() = origin.valueFlow.sample(interval) + + override fun toString(): String = "DebounceDeviceState($value, interval=$interval)" +} + +fun DeviceState.debounce(interval: Duration) = DebounceDeviceState(this, interval) \ No newline at end of file diff --git a/demo/device-collective/src/jvmMain/kotlin/DeviceCollectiveModel.kt b/demo/device-collective/src/jvmMain/kotlin/DeviceCollectiveModel.kt new file mode 100644 index 0000000..fdf13a1 --- /dev/null +++ b/demo/device-collective/src/jvmMain/kotlin/DeviceCollectiveModel.kt @@ -0,0 +1,43 @@ +package space.kscience.controls.demo.map + +import space.kscience.controls.constructor.ModelConstructor +import space.kscience.controls.constructor.MutableDeviceState +import space.kscience.controls.constructor.onTimer +import space.kscience.dataforge.context.Context +import space.kscience.maps.coordinates.Gmc + + +typealias RemoteDeviceId = String + + +data class RemoteDeviceState( + val id: RemoteDeviceId, + val configuration: RemoteDeviceConfiguration, + val position: MutableDeviceState, + val velocity: MutableDeviceState, +) + +public fun RemoteDeviceState( + id: RemoteDeviceId, + position: Gmc, + configuration: RemoteDeviceConfiguration.() -> Unit = {}, +) = RemoteDeviceState( + id, + RemoteDeviceConfiguration(configuration), + MutableDeviceState(position), + MutableDeviceState(GmcVelocity.zero) +) + + +class DeviceCollectiveModel( + context: Context, + val deviceStates: Collection, +) : ModelConstructor(context) { + + private val movement = onTimer { prev, next -> + val delta = (next - prev) + deviceStates.forEach { state -> + state.position.value = state.position.value.moveWith(state.velocity.value, delta) + } + } +} \ No newline at end of file diff --git a/demo/device-collective/src/jvmMain/kotlin/GmcVelocity.kt b/demo/device-collective/src/jvmMain/kotlin/GmcVelocity.kt new file mode 100644 index 0000000..bea840d --- /dev/null +++ b/demo/device-collective/src/jvmMain/kotlin/GmcVelocity.kt @@ -0,0 +1,24 @@ +package space.kscience.controls.demo.map + +import kotlinx.serialization.Serializable +import space.kscience.kmath.geometry.Angle +import space.kscience.maps.coordinates.* +import kotlin.time.Duration +import kotlin.time.DurationUnit + +@Serializable +data class GmcVelocity(val bearing: Angle, val velocity: Distance, val elevation: Distance = 0.kilometers){ + companion object{ + val zero = GmcVelocity(Angle.zero, 0.kilometers) + } +} + + +fun Gmc.moveWith(velocity: GmcVelocity, duration: Duration): Gmc { + val seconds = duration.toDouble(DurationUnit.SECONDS) + + return GeoEllipsoid.WGS84.curveInDirection( + GmcPose(this, velocity.bearing), + velocity.velocity * seconds, + ).backward.coordinates +} \ No newline at end of file diff --git a/demo/device-collective/src/jvmMain/kotlin/RemoteDevice.kt b/demo/device-collective/src/jvmMain/kotlin/RemoteDevice.kt new file mode 100644 index 0000000..19a4e5b --- /dev/null +++ b/demo/device-collective/src/jvmMain/kotlin/RemoteDevice.kt @@ -0,0 +1,64 @@ +@file:OptIn(DFExperimental::class) + +package space.kscience.controls.demo.map + +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.spec.DeviceSpec +import space.kscience.dataforge.context.Context +import space.kscience.dataforge.meta.MetaConverter +import space.kscience.dataforge.meta.Scheme +import space.kscience.dataforge.meta.SchemeSpec +import space.kscience.dataforge.misc.DFExperimental +import space.kscience.maps.coordinates.Gmc +import kotlin.time.Duration.Companion.milliseconds + +class RemoteDeviceConfiguration : Scheme() { + companion object : SchemeSpec(::RemoteDeviceConfiguration) +} + + +interface RemoteDevice : Device { + + suspend fun getPosition(): Gmc + + suspend fun getVelocity(): GmcVelocity + + suspend fun setVelocity(value: GmcVelocity) + + + companion object : DeviceSpec() { + val position by property( + converter = MetaConverter.serializable(), + read = { getPosition() } + ) + + val velocity by mutableProperty( + converter = MetaConverter.serializable(), + read = { getVelocity() }, + write = { _, value -> setVelocity(value) } + ) + } +} + + +class RemoteDeviceConstructor( + context: Context, + val configuration: RemoteDeviceConfiguration, + position: MutableDeviceState, + velocity: MutableDeviceState, +) : DeviceConstructor(context, configuration.meta), RemoteDevice { + + val position = registerAsProperty(RemoteDevice.position, position.debounce(500.milliseconds)) + val velocity = registerAsProperty(RemoteDevice.velocity, velocity) + + override suspend fun getPosition(): Gmc = position.value + + override suspend fun getVelocity(): GmcVelocity = velocity.value + + override suspend fun setVelocity(value: GmcVelocity) { + velocity.value = value + } +} \ No newline at end of file diff --git a/demo/device-collective/src/jvmMain/kotlin/debugModel.kt b/demo/device-collective/src/jvmMain/kotlin/debugModel.kt new file mode 100644 index 0000000..e352fcc --- /dev/null +++ b/demo/device-collective/src/jvmMain/kotlin/debugModel.kt @@ -0,0 +1,50 @@ +package space.kscience.controls.demo.map + +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import space.kscience.controls.spec.write +import space.kscience.dataforge.context.Context +import space.kscience.kmath.geometry.degrees +import space.kscience.kmath.geometry.radians +import space.kscience.maps.coordinates.Gmc +import space.kscience.maps.coordinates.kilometers +import kotlin.math.PI +import kotlin.random.Random + +private val deviceVelocity = 0.1.kilometers + +private val center = Gmc.ofDegrees(55.925, 37.514) +private val radius = 0.01.degrees + + +internal fun generateModel(context: Context): DeviceCollectiveModel { + val devices: List = buildList { + repeat(100) { + add( + RemoteDeviceState( + "device[$it]", + Gmc( + center.latitude + radius * Random.nextDouble(), + center.longitude + radius * Random.nextDouble() + ) + ) + ) + } + } + + val model = DeviceCollectiveModel(context, devices) + + return model +} + +fun RemoteDevice.moveInCircles(): Job = launch { + var bearing = Random.nextDouble(-PI, PI).radians + write(RemoteDevice.velocity, GmcVelocity(bearing, deviceVelocity)) + while (isActive) { + delay(500) + bearing += 5.degrees + write(RemoteDevice.velocity, GmcVelocity(bearing, deviceVelocity)) + } +} \ No newline at end of file diff --git a/demo/device-collective/src/jvmMain/kotlin/main.kt b/demo/device-collective/src/jvmMain/kotlin/main.kt new file mode 100644 index 0000000..0c7f7ad --- /dev/null +++ b/demo/device-collective/src/jvmMain/kotlin/main.kt @@ -0,0 +1,91 @@ +package space.kscience.controls.demo.map + +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +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.flow.launchIn +import kotlinx.coroutines.flow.onEach +import space.kscience.controls.manager.DeviceManager +import space.kscience.controls.spec.propertyFlow +import space.kscience.dataforge.context.Context +import space.kscience.dataforge.context.request +import space.kscience.maps.compose.MapView +import space.kscience.maps.compose.OpenStreetMapTileProvider +import space.kscience.maps.features.ViewConfig +import space.kscience.maps.features.circle +import space.kscience.maps.features.color +import space.kscience.maps.features.rectangle +import java.nio.file.Path + + +@Composable +fun rememberDeviceManager(): DeviceManager = remember { + val context = Context { + plugin(DeviceManager) + } + + context.request(DeviceManager) +} + + +@Composable +fun App() { + val scope = rememberCoroutineScope() + + + val deviceManager = rememberDeviceManager() + + + val collectiveModel = remember { + generateModel(deviceManager.context) + } + + val devices: Map = remember { + collectiveModel.deviceStates.associate { + val device = RemoteDeviceConstructor(deviceManager.context, it.configuration, it.position, it.velocity) + device.moveInCircles() + it.id to device + } + } + + val mapTileProvider = remember { + OpenStreetMapTileProvider( + client = HttpClient(CIO), + cacheDirectory = Path.of("mapCache") + ) + } + + MapView( + mapTileProvider = mapTileProvider, + config = ViewConfig() + ) { + collectiveModel.deviceStates.forEach { device -> + circle(device.position.value, id = device.id + ".position").color(Color.Red) + device.position.valueFlow.onEach { + circle(device.position.value, id = device.id + ".position").color(Color.Red) + }.launchIn(scope) + } + + devices.forEach { (id, device) -> + device.propertyFlow(RemoteDevice.position).onEach { position -> + rectangle(position, id = id).color(Color.Blue) + }.launchIn(scope) + } + } +} + + +fun main() = application { + Window(onCloseRequest = ::exitApplication, title = "Maps-kt demo", icon = painterResource("SPC-logo.png")) { + MaterialTheme { + App() + } + } +} \ No newline at end of file diff --git a/demo/device-collective/src/jvmMain/resources/SPC-logo.png b/demo/device-collective/src/jvmMain/resources/SPC-logo.png new file mode 100644 index 0000000..953de16 Binary files /dev/null and b/demo/device-collective/src/jvmMain/resources/SPC-logo.png differ diff --git a/demo/devices-on-map/src/jvmMain/kotlin/main.kt b/demo/devices-on-map/src/jvmMain/kotlin/main.kt deleted file mode 100644 index 648610e..0000000 --- a/demo/devices-on-map/src/jvmMain/kotlin/main.kt +++ /dev/null @@ -1,8 +0,0 @@ -package space.kscience.controls.demo.map - -import androidx.compose.ui.window.application - - -fun main() = application { - -} \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 102c441..7c5a861 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,6 +1,6 @@ [versions] -dataforge = "0.8.0" +dataforge = "0.9.0" rsocket = "0.15.4" xodus = "2.0.1" @@ -10,7 +10,7 @@ fazecast = "2.10.3" tornadofx = "1.7.20" -plotlykt = "0.7.0" +plotlykt = "0.7.2" logback = "1.2.11" @@ -29,7 +29,7 @@ pi4j-ktx = "2.4.0" plc4j = "0.12.0" -visionforge = "0.4.1" +visionforge = "0.4.2" versions = "0.51.0" diff --git a/settings.gradle.kts b/settings.gradle.kts index 7aebf36..7e84c71 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -87,5 +87,5 @@ include( ":demo:echo", ":demo:mks-pdr900", ":demo:constructor", - ":demo:devices-on-map" + ":demo:device-collective" )