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

View File

@ -93,7 +93,7 @@ internal class RemoteDeviceConnect {
val virtualMagixEndpoint = VirtualMagixEndpoint(deviceManager) 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)) assertContains(0.0..1.0, remoteDevice.read(TestDevice.value))

View File

@ -1,10 +1,10 @@
package space.kscience.controls.client package space.kscience.controls.client
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.withContext
import space.kscience.controls.client.RemoteDeviceConnect.TestDevice import space.kscience.controls.client.RemoteDeviceConnect.TestDevice
import space.kscience.controls.manager.DeviceManager import space.kscience.controls.manager.DeviceManager
import space.kscience.controls.manager.install import space.kscience.controls.manager.install
@ -20,36 +20,37 @@ class MagixLoopTest {
@Test @Test
fun realDeviceHub() = runTest { fun realDeviceHub() = runTest {
withContext(Dispatchers.Default) { val context = Context {
val context = Context { coroutineContext(Dispatchers.Default)
plugin(DeviceManager) 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 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()
} }
} }

View File

@ -1,3 +1,5 @@
@file:OptIn(FlowPreview::class)
package space.kscience.controls.compose package space.kscience.controls.compose
import androidx.compose.runtime.* 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.style.LineStyle
import io.github.koalaplot.core.xygraph.DefaultPoint import io.github.koalaplot.core.xygraph.DefaultPoint
import io.github.koalaplot.core.xygraph.XYGraphScope import io.github.koalaplot.core.xygraph.XYGraphScope
import kotlinx.coroutines.Job import kotlinx.coroutines.*
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.datetime.Clock import kotlinx.datetime.Clock
import kotlinx.datetime.Instant import kotlinx.datetime.Instant
import space.kscience.controls.api.Device import space.kscience.controls.api.Device

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

Width:  |  Height:  |  Size: 5.0 KiB

View File

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

View File

@ -1,6 +1,6 @@
[versions] [versions]
dataforge = "0.8.0" dataforge = "0.9.0"
rsocket = "0.15.4" rsocket = "0.15.4"
xodus = "2.0.1" xodus = "2.0.1"
@ -10,7 +10,7 @@ fazecast = "2.10.3"
tornadofx = "1.7.20" tornadofx = "1.7.20"
plotlykt = "0.7.0" plotlykt = "0.7.2"
logback = "1.2.11" logback = "1.2.11"
@ -29,7 +29,7 @@ pi4j-ktx = "2.4.0"
plc4j = "0.12.0" plc4j = "0.12.0"
visionforge = "0.4.1" visionforge = "0.4.2"
versions = "0.51.0" versions = "0.51.0"

View File

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