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

View File

@ -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,8 +20,8 @@ class MagixLoopTest {
@Test
fun realDeviceHub() = runTest {
withContext(Dispatchers.Default) {
val context = Context {
coroutineContext(Dispatchers.Default)
plugin(DeviceManager)
}
@ -33,11 +33,14 @@ class MagixLoopTest {
deviceManager.launchMagixService(deviceEndpoint, "device")
launch {
delay(50)
val trigger = CompletableDeferred<Unit>()
context.launch {
repeat(10) {
deviceManager.install("test[$it]", TestDevice)
}
delay(100)
trigger.complete(Unit)
}
val clientEndpoint = MagixEndpoint.rSocketWithWebSockets("localhost")
@ -45,11 +48,9 @@ class MagixLoopTest {
val remoteHub = clientEndpoint.remoteDeviceHub(context, "client", "device")
assertEquals(0, remoteHub.devices.size)
delay(60)
clientEndpoint.requestDeviceUpdate("client", "device")
delay(60)
trigger.join()
assertEquals(10, remoteHub.devices.size)
server.stop()
}
}
}

View File

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

View File

@ -13,6 +13,7 @@ kscience {
commonMain {
implementation(projects.controlsVisualisationCompose)
implementation(projects.controlsConstructor)
implementation(projects.magix.magixRsocket)
}
jvmMain {
// 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]
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"

View File

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