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 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))
|
||||||
|
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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
|
||||||
|
@ -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")
|
@ -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]
|
[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"
|
||||||
|
|
||||||
|
@ -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"
|
||||||
)
|
)
|
||||||
|
Loading…
Reference in New Issue
Block a user