Add device collective demo
This commit is contained in:
parent
c63c2db651
commit
a2b5880da9
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
24
demo/device-collective/src/jvmMain/kotlin/GmcVelocity.kt
Normal file
24
demo/device-collective/src/jvmMain/kotlin/GmcVelocity.kt
Normal 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
|
||||
}
|
64
demo/device-collective/src/jvmMain/kotlin/RemoteDevice.kt
Normal file
64
demo/device-collective/src/jvmMain/kotlin/RemoteDevice.kt
Normal 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
|
||||
}
|
||||
}
|
50
demo/device-collective/src/jvmMain/kotlin/debugModel.kt
Normal file
50
demo/device-collective/src/jvmMain/kotlin/debugModel.kt
Normal 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))
|
||||
}
|
||||
}
|
91
demo/device-collective/src/jvmMain/kotlin/main.kt
Normal file
91
demo/device-collective/src/jvmMain/kotlin/main.kt
Normal 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()
|
||||
}
|
||||
}
|
||||
}
|
BIN
demo/device-collective/src/jvmMain/resources/SPC-logo.png
Normal file
BIN
demo/device-collective/src/jvmMain/resources/SPC-logo.png
Normal file
Binary file not shown.
After Width: | Height: | 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"
|
||||
)
|
||||
|
Loading…
Reference in New Issue
Block a user