Add device collective demo

This commit is contained in:
Alexander Nozik 2024-06-06 16:54:17 +03:00
parent c63c2db651
commit a2b5880da9
14 changed files with 333 additions and 48 deletions
controls-magix/src
commonTest/kotlin/space/kscience/controls/client
jvmTest/kotlin/space/kscience/controls/client
controls-visualisation-compose/src/commonMain/kotlin
demo
gradle
settings.gradle.kts

@ -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))

@ -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<Unit>()
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()
}
}

@ -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

@ -13,6 +13,7 @@ kscience {
commonMain {
implementation(projects.controlsVisualisationCompose)
implementation(projects.controlsConstructor)
implementation(projects.magix.magixRsocket)
}
jvmMain {
// implementation("io.ktor:ktor-server-cio")

@ -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<T>(
val origin: DeviceState<T>,
val interval: Duration,
) : DeviceState<T> {
override val value: T get() = origin.value
override val valueFlow: Flow<T> get() = origin.valueFlow.sample(interval)
override fun toString(): String = "DebounceDeviceState($value, interval=$interval)"
}
fun <T> DeviceState<T>.debounce(interval: Duration) = DebounceDeviceState(this, interval)

@ -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<Gmc>,
val velocity: MutableDeviceState<GmcVelocity>,
)
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<RemoteDeviceState>,
) : 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)
}
}
}

@ -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
}

@ -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>(::RemoteDeviceConfiguration)
}
interface RemoteDevice : Device {
suspend fun getPosition(): Gmc
suspend fun getVelocity(): GmcVelocity
suspend fun setVelocity(value: GmcVelocity)
companion object : DeviceSpec<RemoteDevice>() {
val position by property<Gmc>(
converter = MetaConverter.serializable(),
read = { getPosition() }
)
val velocity by mutableProperty<GmcVelocity>(
converter = MetaConverter.serializable(),
read = { getVelocity() },
write = { _, value -> setVelocity(value) }
)
}
}
class RemoteDeviceConstructor(
context: Context,
val configuration: RemoteDeviceConfiguration,
position: MutableDeviceState<Gmc>,
velocity: MutableDeviceState<GmcVelocity>,
) : 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
}
}

@ -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<RemoteDeviceState> = 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))
}
}

@ -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<RemoteDeviceId, RemoteDevice> = 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()
}
}
}

Binary file not shown.

After

(image error) Size: 5.0 KiB

@ -1,8 +0,0 @@
package space.kscience.controls.demo.map
import androidx.compose.ui.window.application
fun main() = application {
}

@ -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"

@ -87,5 +87,5 @@ include(
":demo:echo",
":demo:mks-pdr900",
":demo:constructor",
":demo:devices-on-map"
":demo:device-collective"
)