From a2b5880da99f6718afcff5a54a7f3cd9e11db767 Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Thu, 6 Jun 2024 16:54:17 +0300 Subject: [PATCH 01/10] Add device collective demo --- .../controls/client/RemoteDeviceConnect.kt | 2 +- .../kscience/controls/client/MagixLoopTest.kt | 63 ++++++------ .../src/commonMain/kotlin/koalaPlots.kt | 7 +- .../build.gradle.kts | 1 + .../src/jvmMain/kotlin/DebounceDeviceState.kt | 20 ++++ .../jvmMain/kotlin/DeviceCollectiveModel.kt | 43 +++++++++ .../src/jvmMain/kotlin/GmcVelocity.kt | 24 +++++ .../src/jvmMain/kotlin/RemoteDevice.kt | 64 ++++++++++++ .../src/jvmMain/kotlin/debugModel.kt | 50 ++++++++++ .../src/jvmMain/kotlin/main.kt | 91 ++++++++++++++++++ .../src/jvmMain/resources/SPC-logo.png | Bin 0 -> 5166 bytes .../devices-on-map/src/jvmMain/kotlin/main.kt | 8 -- gradle/libs.versions.toml | 6 +- settings.gradle.kts | 2 +- 14 files changed, 333 insertions(+), 48 deletions(-) rename demo/{devices-on-map => device-collective}/build.gradle.kts (94%) create mode 100644 demo/device-collective/src/jvmMain/kotlin/DebounceDeviceState.kt create mode 100644 demo/device-collective/src/jvmMain/kotlin/DeviceCollectiveModel.kt create mode 100644 demo/device-collective/src/jvmMain/kotlin/GmcVelocity.kt create mode 100644 demo/device-collective/src/jvmMain/kotlin/RemoteDevice.kt create mode 100644 demo/device-collective/src/jvmMain/kotlin/debugModel.kt create mode 100644 demo/device-collective/src/jvmMain/kotlin/main.kt create mode 100644 demo/device-collective/src/jvmMain/resources/SPC-logo.png delete mode 100644 demo/devices-on-map/src/jvmMain/kotlin/main.kt diff --git a/controls-magix/src/commonTest/kotlin/space/kscience/controls/client/RemoteDeviceConnect.kt b/controls-magix/src/commonTest/kotlin/space/kscience/controls/client/RemoteDeviceConnect.kt index 3c707b2..18a3dfc 100644 --- a/controls-magix/src/commonTest/kotlin/space/kscience/controls/client/RemoteDeviceConnect.kt +++ b/controls-magix/src/commonTest/kotlin/space/kscience/controls/client/RemoteDeviceConnect.kt @@ -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)) diff --git a/controls-magix/src/jvmTest/kotlin/space/kscience/controls/client/MagixLoopTest.kt b/controls-magix/src/jvmTest/kotlin/space/kscience/controls/client/MagixLoopTest.kt index b08066b..31f73fc 100644 --- a/controls-magix/src/jvmTest/kotlin/space/kscience/controls/client/MagixLoopTest.kt +++ b/controls-magix/src/jvmTest/kotlin/space/kscience/controls/client/MagixLoopTest.kt @@ -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() + + 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() } } \ No newline at end of file diff --git a/controls-visualisation-compose/src/commonMain/kotlin/koalaPlots.kt b/controls-visualisation-compose/src/commonMain/kotlin/koalaPlots.kt index 855de10..87a9fca 100644 --- a/controls-visualisation-compose/src/commonMain/kotlin/koalaPlots.kt +++ b/controls-visualisation-compose/src/commonMain/kotlin/koalaPlots.kt @@ -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 diff --git a/demo/devices-on-map/build.gradle.kts b/demo/device-collective/build.gradle.kts similarity index 94% rename from demo/devices-on-map/build.gradle.kts rename to demo/device-collective/build.gradle.kts index b81dc12..5520265 100644 --- a/demo/devices-on-map/build.gradle.kts +++ b/demo/device-collective/build.gradle.kts @@ -13,6 +13,7 @@ kscience { commonMain { implementation(projects.controlsVisualisationCompose) implementation(projects.controlsConstructor) + implementation(projects.magix.magixRsocket) } jvmMain { // implementation("io.ktor:ktor-server-cio") diff --git a/demo/device-collective/src/jvmMain/kotlin/DebounceDeviceState.kt b/demo/device-collective/src/jvmMain/kotlin/DebounceDeviceState.kt new file mode 100644 index 0000000..b507cd6 --- /dev/null +++ b/demo/device-collective/src/jvmMain/kotlin/DebounceDeviceState.kt @@ -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( + val origin: DeviceState, + val interval: Duration, +) : DeviceState { + override val value: T get() = origin.value + override val valueFlow: Flow get() = origin.valueFlow.sample(interval) + + override fun toString(): String = "DebounceDeviceState($value, interval=$interval)" +} + +fun DeviceState.debounce(interval: Duration) = DebounceDeviceState(this, interval) \ No newline at end of file diff --git a/demo/device-collective/src/jvmMain/kotlin/DeviceCollectiveModel.kt b/demo/device-collective/src/jvmMain/kotlin/DeviceCollectiveModel.kt new file mode 100644 index 0000000..fdf13a1 --- /dev/null +++ b/demo/device-collective/src/jvmMain/kotlin/DeviceCollectiveModel.kt @@ -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, + val velocity: MutableDeviceState, +) + +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, +) : 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) + } + } +} \ No newline at end of file diff --git a/demo/device-collective/src/jvmMain/kotlin/GmcVelocity.kt b/demo/device-collective/src/jvmMain/kotlin/GmcVelocity.kt new file mode 100644 index 0000000..bea840d --- /dev/null +++ b/demo/device-collective/src/jvmMain/kotlin/GmcVelocity.kt @@ -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 +} \ No newline at end of file diff --git a/demo/device-collective/src/jvmMain/kotlin/RemoteDevice.kt b/demo/device-collective/src/jvmMain/kotlin/RemoteDevice.kt new file mode 100644 index 0000000..19a4e5b --- /dev/null +++ b/demo/device-collective/src/jvmMain/kotlin/RemoteDevice.kt @@ -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) +} + + +interface RemoteDevice : Device { + + suspend fun getPosition(): Gmc + + suspend fun getVelocity(): GmcVelocity + + suspend fun setVelocity(value: GmcVelocity) + + + companion object : DeviceSpec() { + val position by property( + converter = MetaConverter.serializable(), + read = { getPosition() } + ) + + val velocity by mutableProperty( + converter = MetaConverter.serializable(), + read = { getVelocity() }, + write = { _, value -> setVelocity(value) } + ) + } +} + + +class RemoteDeviceConstructor( + context: Context, + val configuration: RemoteDeviceConfiguration, + position: MutableDeviceState, + velocity: MutableDeviceState, +) : 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 + } +} \ No newline at end of file diff --git a/demo/device-collective/src/jvmMain/kotlin/debugModel.kt b/demo/device-collective/src/jvmMain/kotlin/debugModel.kt new file mode 100644 index 0000000..e352fcc --- /dev/null +++ b/demo/device-collective/src/jvmMain/kotlin/debugModel.kt @@ -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 = 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)) + } +} \ No newline at end of file diff --git a/demo/device-collective/src/jvmMain/kotlin/main.kt b/demo/device-collective/src/jvmMain/kotlin/main.kt new file mode 100644 index 0000000..0c7f7ad --- /dev/null +++ b/demo/device-collective/src/jvmMain/kotlin/main.kt @@ -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 = 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() + } + } +} \ No newline at end of file diff --git a/demo/device-collective/src/jvmMain/resources/SPC-logo.png b/demo/device-collective/src/jvmMain/resources/SPC-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..953de16d4876765010935ffc20cbd1d37351dbb6 GIT binary patch literal 5166 zcmeHLdpML^7ax~0lFVx=62pw5QfYE6*WP)N%B7S`H#Lr1i6&$ya-6ANxji~v6pdRK zCATA82z8E2DWpUZP9{x_>tI|GzP+dDdA|R?|Gz)xnf0!{e!sQXdiUOI?Kj%d-bQ|& z$~*#rAaA=F+d&{mq{AO^F3>#7lKUI{$sXP87Dynh)r3C@_6>{UK)EEy$~DL-z$+-^ z;4x1^NJz+9U;o2_9tV$lt_?Wmo!n=tLLiV!Y_Ux{*(tqk6ZP%E;nl*B){=W&vFm>O zJ91vs!s|CI69ty82WVs^gNEgin?5{YYp_;4ti`nt>{(PuT~$Y2*EnB+vFXazxe}V0 zuV`J%gpny$_G{+sPU|gIA3o1;=r1YlpO`c=rJQ;aT-x`kZ0IFFa9_s<^38fvrZFMy zNrgayFnpOP;b_evtk6UW7Z56;1}70bF=FI;nzZE-L^3V*|HJ>CmMiH3iFKiajy;dx zs8K6b9}I? zz;9en_{AfJ*LOl4FX=0@=qnqY8WWj~R-f1A-knN}T!t_|9GUnlhxg=tUi5}Sce0FH zen3ybL@wR8^NU)Ide~S)Qu)7^lFA3b@4)u&oi3vl0|NtDrRM9xw6>*&a?0(iK5+A{ zQmP*d^Nl+P9u-UsU#k|Cs=Gg4mRst->16*|m&&5%H8`KUho0N7aQ7OK?sYt@@&&Kf zMs0Dx2Eo8PkI{YEdXCbG;r&vq1|!{=yi?uh*q^U>XY(y}qe+>cBPU0q|8!v6s75^7^w!DxQ=*M} z!q&B=rfVO5DiK!fa6c3&T!#AHdp}W!>)D@j@6qG!dh29Z1APyT z)h}}p<{w82-(C{_{%3p~uVegDk}(SvrEDL1QPLOZF!d&s)f+5#=awZunxtvy+gZVG zcAxZ28Eoe&YtBB>g2`OpB#B1Y_*|ebEU2=M`NEIZ}lgqP7d32Iw-W4 zht)h}pA?=q7I_SH-Z`9?$CbRM@0+Z4+$1whx_97Pb6vb#Ior9Xi0}W_sjBwgZ6{t* z=7N}<2QBL()=%GbEjqMIwCu}5ZEDlu!B6R9&tx28L)0gllGdf0q)x^*X0-OjDsYc` zeF*IMMg-qi_`OkDogd$(Crd<#UIGJ1c%%nBN_qjWTOy7<$Fcc+@#p&5o3wRmN( z;dSaxd#HXZmdd`RU)8kd2%;4Jwa2)}p;fecq5j2sj%((%{ldYqDi^*;FqC@tIfEfo z3OLF5KJw9qci$vpyP3>xJC1AZt5=RE`JJbtzn1juG`mrtWuHVkb^O_3-tAkLEA3+X zvba(?%h9`DZ}044z38<_$V@QRH+=uwF;Pm@)pZ9^)%By1t_4ib5BUR^1RGv~<)C6V z_Ua$jWRw+pVO>bwYHF1HGW706QkN{^r;ISKaaK6s2R>oANHN2d8kLApcR-DX17|ZC zDpV0BCN`PGaAYaxi=7RHhILc1e1BTSTm)Jak*S7BknZjkV!?K?Z7{9ET8vgAQ>{!D^$h2=5N7)GS44)>_bD^^@QeaEULZV0&`RaeLp>vy}MmfQ1>cf1JkS5vs~2=xHqw7*NP1QB|zO8X(v};XawFVhaUB zAZ!(D6san9Vhs=i1}wW9Y$&U+Zq`7GoA9MxeN~)AX-?R@>o_UrU+ltYadX{qT806J zH-RW!0`HwBHmrgX-20u_un`LS*N6>U#KIL~!)~!~k=Squy%3UrD%M6Snv|oBg-$Va z>@?A!D3}d98X9y)EKt#)b7DaQ4T^`tTMV~{+08glY;c2AI}z$e@m%N8Fg{EZy&c2% z!+bMMqe-PyknajMLJbqgEYv`U#WWsvoG;5^E~@~~9>^2B2+3%M`Bs*rVf|vF7L17E z)+nxQN0TlGQ$#C#5Gq>&!mR%wKHUo6Zb1P?Y_f#8ild_Wiy@QW3M49+l=1jcpjTy} zj&p^)TtE&NjloHG&yu=ouN9_}=ro$Ntk@}sxam9$%Z(0b0&_n7X3jE#=x*i?see2|-jC5MH z2Yzqm^-qsnH~vj`vJTWis_UkaRhmo$u8OL)3~EUtk(=tmOGXuQ$@3s zg1}I;QJvPv@>nu6A%P4c$QKe{MM5B#6r4&7Ze-6+u-J~XG3ys}AFYvoan`H9V)_DK zbk@xYEKBq1!dc$Mh#*qy7ZHQAJNNl7C@FpMVa6LG7CLqgd9pYoQ1Y*EC`q4Ut1>=CjbN zTRAE>>t~@}is^HaH1Wm&c^bkU8N^qDAFnOT4a<^{_UZlkv_Z>R;Y-PtiJp~~-36c| z2*(R8RGe;YyUl&gmQ!k@3{8om1WR&`3ML$IVC80(If`Yyt9{VEB$YdQL$D-n1oRoK z8ei|KL2oVu6?)g+*{TH)lmi*B-879oHe>zlB0~NBkuZ8giN! z0%FVH%Q|uiU9yfKwtt9;gg6d-(2I7I5^caFFKAnaN;dALH9CgRPlN+pf(HarE8CY-(G58rMhPZZWSTQ)g!rl+E>t)Qf zEM?)O+cZ(NHyo0@ktlz1`hunPGj2zt z9joJPaEKX{&Qgb)Y!(Lg7}*1%!R(q!2$T>(V#CaoLixjyY6>_01xiikd% z+G(cR8ckkI5y3|j#G=t;#W}!%({@!yq7V{@jmF?;*n&{-)ku^yG_giG2h5;}^2&PI zryJP|fcTDN56*O$ad@U1KpfKpCq0HsjI?ws=ifPEfIKV@U^&6^K$K9Cm0;?F%JV}* zriR%;HwB>bT!JtUD^Pi$Ls)rCAa|?*lHnpIQlMc0M6g8Z`(X)#-arYvCMIH&L79U@ zpv-y0)w&(7c}NCXEGQxwqLI|12ZZIaBv57VwM-R` z#OH;7+$OPwq;YjK$7c;>y1IqqBc9sTZ5$spvBsX`vj_?;mK+~>vB1myAI4KiS61m!_pFW%sn~pFu;hTFZz;Jmj zFgNMLTBsihj=7p3%nHbOpbd1G2q20Qk*TktM#F})S*$Qcn5ANq-!MEF`ie>1OWi_=8P6iU3FMM%U+QD=aQ!|zSU6qyD(&UbO1gSlCR zsCxpOq)jy!b$FSnrZ$lWG^0s}gF2)XDqA~(x2)b9bOIc#q2O2<>vl4yOpmweWZ$yv z1~x_P=dX>YuD{_4)H@u_ZqOI&x~WvBo($VN$r}fE0kYPS!hig|cP6Ag;_fM+<&HHN z&^>Fk=qEnMCV}o;bbY-=X_vP!cQiu3@B8>Zczc-HmMsVSHceFW%>#G)mAAdPb1OFc zY#aM^&I3pErJv9;o?gwu6s9!#~ z9?{I%uUV`QCJE8DWR6P}P>GTl9w0yAASkxqoe^ zfIF*sNV}D=Hees3%}pDN4b|+Ag*#L%G~16Jh1YZ5w^!Yo3FegM@1Lq3elO39Kb*HmS;r+;^}*=eCOeLDNUL9wr>gOZ z)TF>CWpwY6vVvyH8yitepOJ!@VzaL6!Xc&A=f>p%qTSnr`?E?7*A-*g8QiY0rqaV~mI));3l`KIcOzM dG+}ahdgIi>+tx>p41sTb1Y0Y6EEhcx^&il Date: Fri, 7 Jun 2024 10:52:28 +0300 Subject: [PATCH 02/10] Add PeerConnection --- CHANGELOG.md | 1 + .../kscience/controls/api/DeviceMessage.kt | 10 ++++- .../kscience/controls/peer/PeerConnection.kt | 42 +++++++++++++++++++ 3 files changed, 51 insertions(+), 2 deletions(-) create mode 100644 controls-core/src/commonMain/kotlin/space/kscience/controls/peer/PeerConnection.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index 92b2765..ae8c47f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - PLC4X bindings - Shortcuts to access all Controls devices in a magix network. - `DeviceClient` properly evaluates lifecycle and logs +- `PeerConnection` API for direct device-device binary sharing ### Changed - Constructor properties return `DeviceState` in order to be able to subscribe to them diff --git a/controls-core/src/commonMain/kotlin/space/kscience/controls/api/DeviceMessage.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/api/DeviceMessage.kt index dda89de..f91beb5 100644 --- a/controls-core/src/commonMain/kotlin/space/kscience/controls/api/DeviceMessage.kt +++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/api/DeviceMessage.kt @@ -166,12 +166,18 @@ public data class ActionResultMessage( } /** - * Notifies listeners that a new binary with given [binaryID] is available. The binary itself could not be provided via [DeviceMessage] API. + * Notifies listeners that a new binary with given [contentId] and [contentMeta] is available. + * + * [contentMeta] includes public information that could be shared with loop subscribers. It should not contain sensitive data. + * + * The binary itself could not be provided via [DeviceMessage] API. + * [space.kscience.controls.peer.PeerConnection] must be used instead */ @Serializable @SerialName("binary.notification") public data class BinaryNotificationMessage( - val binaryID: String, + val contentId: String, + val contentMeta: Meta, override val sourceDevice: Name, override val targetDevice: Name? = null, override val comment: String? = null, diff --git a/controls-core/src/commonMain/kotlin/space/kscience/controls/peer/PeerConnection.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/peer/PeerConnection.kt new file mode 100644 index 0000000..14b49ef --- /dev/null +++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/peer/PeerConnection.kt @@ -0,0 +1,42 @@ +package space.kscience.controls.peer + +import space.kscience.dataforge.io.Envelope +import space.kscience.dataforge.meta.Meta +import space.kscience.dataforge.names.Name + +/** + * A manager that allows direct synchronous sending and receiving binary data + */ +public interface PeerConnection { + /** + * Receive an [Envelope] from a device with name [deviceName] on a given [address] with given [contentId]. + * + * The address depends on the specifics of given [PeerConnection]. For example, it could be a TCP/IP port or + * magix endpoint name. + * + * Depending on [PeerConnection] implementation, the resulting [Envelope] could be lazy loaded + * + * Additional metadata in [requestMeta] could be required for authentication. + */ + public suspend fun receive( + address: String, + deviceName: Name, + contentId: String, + requestMeta: Meta = Meta.EMPTY, + ): Envelope + + /** + * Send an [envelope] to a device with name [deviceName] on a given [address] + * + * The address depends on the specifics of given [PeerConnection]. For example, it could be a TCP/IP port or + * magix endpoint name. + * + * Additional metadata in [requestMeta] could be required for authentication. + */ + public suspend fun send( + address: String, + deviceName: Name, + envelope: Envelope, + requestMeta: Meta = Meta.EMPTY, + ) +} \ No newline at end of file From 13b80be8841761568ed17db6b23a2e9116591411 Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Fri, 7 Jun 2024 20:20:39 +0300 Subject: [PATCH 03/10] Implement visibility range for collective device --- .gitignore | 5 +- .../src/commonMain/kotlin/misc.kt | 12 ++ .../constructor/src/jvmMain/kotlin/Plotter.kt | 19 ++- demo/device-collective/build.gradle.kts | 2 +- .../{RemoteDevice.kt => CollectiveDevice.kt} | 32 ++-- .../jvmMain/kotlin/DeviceCollectiveModel.kt | 42 +++-- .../src/jvmMain/kotlin/GmcVelocity.kt | 2 +- ...nceDeviceState.kt => SampleDeviceState.kt} | 6 +- .../src/jvmMain/kotlin/debugModel.kt | 31 ++-- .../src/jvmMain/kotlin/main.kt | 146 +++++++++++++++--- 10 files changed, 226 insertions(+), 71 deletions(-) create mode 100644 controls-visualisation-compose/src/commonMain/kotlin/misc.kt rename demo/device-collective/src/jvmMain/kotlin/{RemoteDevice.kt => CollectiveDevice.kt} (60%) rename demo/device-collective/src/jvmMain/kotlin/{DebounceDeviceState.kt => SampleDeviceState.kt} (76%) diff --git a/.gitignore b/.gitignore index e688053..5fab474 100644 --- a/.gitignore +++ b/.gitignore @@ -9,4 +9,7 @@ out/ build/ -!gradle-wrapper.jar \ No newline at end of file + +!gradle-wrapper.jar + +/demo/device-collective/mapCache/ diff --git a/controls-visualisation-compose/src/commonMain/kotlin/misc.kt b/controls-visualisation-compose/src/commonMain/kotlin/misc.kt new file mode 100644 index 0000000..caf21e3 --- /dev/null +++ b/controls-visualisation-compose/src/commonMain/kotlin/misc.kt @@ -0,0 +1,12 @@ +package space.kscience.controls.compose + +import androidx.compose.ui.Modifier + +public inline fun Modifier.conditional( + condition: Boolean, + modifier: Modifier.() -> Modifier, +): Modifier = if (condition) { + then(modifier(Modifier)) +} else { + this +} \ No newline at end of file diff --git a/demo/constructor/src/jvmMain/kotlin/Plotter.kt b/demo/constructor/src/jvmMain/kotlin/Plotter.kt index dfded59..40f81f9 100644 --- a/demo/constructor/src/jvmMain/kotlin/Plotter.kt +++ b/demo/constructor/src/jvmMain/kotlin/Plotter.kt @@ -15,6 +15,7 @@ import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.isActive import space.kscience.controls.constructor.* +import space.kscience.controls.constructor.devices.LimitSwitch import space.kscience.controls.constructor.devices.StepDrive import space.kscience.controls.constructor.devices.angle import space.kscience.controls.constructor.models.Leadscrew @@ -31,10 +32,18 @@ private class Plotter( context: Context, xDrive: StepDrive, yDrive: StepDrive, + xStartLimit: LimitSwitch, + xEndLimit: LimitSwitch, + yStartLimit: LimitSwitch, + yEndLimit: LimitSwitch, val paint: suspend (Color) -> Unit, ) : DeviceConstructor(context) { val xDrive by device(xDrive) val yDrive by device(yDrive) + val xStartLimit by device(xStartLimit) + val xEndLimit by device(xEndLimit) + val yStartLimit by device(yStartLimit) + val yEndLimit by device(yEndLimit) public fun moveToXY(x: Number, y: Number) { xDrive.target.value = x.toLong() @@ -108,7 +117,15 @@ private class PlotterModel( val xy: DeviceState> = combineState(x, y) { x, y -> XY(x, y) } - val plotter = Plotter(context, xDrive, yDrive) { color -> + val plotter = Plotter( + context = context, + xDrive = xDrive, + yDrive = yDrive, + xStartLimit = LimitSwitch(context,x.atStart), + xEndLimit = LimitSwitch(context,x.atEnd), + yStartLimit = LimitSwitch(context,x.atStart), + yEndLimit = LimitSwitch(context,x.atEnd), + ) { color -> println("Point X: ${x.value.value}, Y: ${y.value.value}, color: $color") callback(PlotterPoint(x.value, y.value, color)) } diff --git a/demo/device-collective/build.gradle.kts b/demo/device-collective/build.gradle.kts index 5520265..8bb0597 100644 --- a/demo/device-collective/build.gradle.kts +++ b/demo/device-collective/build.gradle.kts @@ -37,6 +37,6 @@ kotlin.explicitApi = ExplicitApiMode.Disabled compose.desktop { application { - mainClass = "space.kscience.controls.demo.map.MainKt" + mainClass = "space.kscience.controls.demo.collective.MainKt" } } \ No newline at end of file diff --git a/demo/device-collective/src/jvmMain/kotlin/RemoteDevice.kt b/demo/device-collective/src/jvmMain/kotlin/CollectiveDevice.kt similarity index 60% rename from demo/device-collective/src/jvmMain/kotlin/RemoteDevice.kt rename to demo/device-collective/src/jvmMain/kotlin/CollectiveDevice.kt index 19a4e5b..050794c 100644 --- a/demo/device-collective/src/jvmMain/kotlin/RemoteDevice.kt +++ b/demo/device-collective/src/jvmMain/kotlin/CollectiveDevice.kt @@ -1,6 +1,6 @@ @file:OptIn(DFExperimental::class) -package space.kscience.controls.demo.map +package space.kscience.controls.demo.collective import space.kscience.controls.api.Device import space.kscience.controls.constructor.DeviceConstructor @@ -10,17 +10,20 @@ 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.meta.string 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) +class CollectiveDeviceConfiguration(deviceId: DeviceId) : Scheme() { + var deviceId by string(deviceId) + var description by string() } -interface RemoteDevice : Device { +interface CollectiveDevice : Device { + + public val id: DeviceId suspend fun getPosition(): Gmc @@ -28,8 +31,10 @@ interface RemoteDevice : Device { suspend fun setVelocity(value: GmcVelocity) + suspend fun listVisible(): Collection - companion object : DeviceSpec() { + + companion object : DeviceSpec() { val position by property( converter = MetaConverter.serializable(), read = { getPosition() } @@ -44,15 +49,18 @@ interface RemoteDevice : Device { } -class RemoteDeviceConstructor( +class CollectiveDeviceConstructor( context: Context, - val configuration: RemoteDeviceConfiguration, + val configuration: CollectiveDeviceConfiguration, position: MutableDeviceState, velocity: MutableDeviceState, -) : DeviceConstructor(context, configuration.meta), RemoteDevice { + private val listVisible: suspend () -> Collection, +) : DeviceConstructor(context, configuration.meta), CollectiveDevice { - val position = registerAsProperty(RemoteDevice.position, position.debounce(500.milliseconds)) - val velocity = registerAsProperty(RemoteDevice.velocity, velocity) + override val id: DeviceId get() = configuration.deviceId + + val position = registerAsProperty(CollectiveDevice.position, position.sample(500.milliseconds)) + val velocity = registerAsProperty(CollectiveDevice.velocity, velocity) override suspend fun getPosition(): Gmc = position.value @@ -61,4 +69,6 @@ class RemoteDeviceConstructor( override suspend fun setVelocity(value: GmcVelocity) { velocity.value = value } + + override suspend fun listVisible(): Collection = listVisible.invoke() } \ No newline at end of file diff --git a/demo/device-collective/src/jvmMain/kotlin/DeviceCollectiveModel.kt b/demo/device-collective/src/jvmMain/kotlin/DeviceCollectiveModel.kt index fdf13a1..4abedea 100644 --- a/demo/device-collective/src/jvmMain/kotlin/DeviceCollectiveModel.kt +++ b/demo/device-collective/src/jvmMain/kotlin/DeviceCollectiveModel.kt @@ -1,43 +1,59 @@ -package space.kscience.controls.demo.map +package space.kscience.controls.demo.collective 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 +import space.kscience.maps.coordinates.* -typealias RemoteDeviceId = String +typealias DeviceId = String -data class RemoteDeviceState( - val id: RemoteDeviceId, - val configuration: RemoteDeviceConfiguration, +internal data class VirtualDeviceState( + val id: DeviceId, + val configuration: CollectiveDeviceConfiguration, val position: MutableDeviceState, val velocity: MutableDeviceState, ) -public fun RemoteDeviceState( - id: RemoteDeviceId, +internal fun VirtualDeviceState( + id: DeviceId, position: Gmc, - configuration: RemoteDeviceConfiguration.() -> Unit = {}, -) = RemoteDeviceState( + configuration: CollectiveDeviceConfiguration.() -> Unit = {}, +) = VirtualDeviceState( id, - RemoteDeviceConfiguration(configuration), + CollectiveDeviceConfiguration(id).apply(configuration), MutableDeviceState(position), MutableDeviceState(GmcVelocity.zero) ) -class DeviceCollectiveModel( +internal class DeviceCollectiveModel( context: Context, - val deviceStates: Collection, + val deviceStates: Collection, + val visibilityRange: Distance, ) : ModelConstructor(context) { + /** + * Propagate movement + */ private val movement = onTimer { prev, next -> val delta = (next - prev) deviceStates.forEach { state -> state.position.value = state.position.value.moveWith(state.velocity.value, delta) } } + + suspend fun locateVisible(id: DeviceId): Map { + val coordinatesSnapshot = deviceStates.associate { it.id to it.position.value } + + val selected = coordinatesSnapshot[id] ?: error("Can't find device with id $id") + + val allCurves = coordinatesSnapshot + .filterKeys { it != id } + .mapValues { GeoEllipsoid.WGS84.curveBetween(selected, it.value) } + + return allCurves.filterValues { it.distance in 0.kilometers..visibilityRange } + } } \ No newline at end of file diff --git a/demo/device-collective/src/jvmMain/kotlin/GmcVelocity.kt b/demo/device-collective/src/jvmMain/kotlin/GmcVelocity.kt index bea840d..9d356c6 100644 --- a/demo/device-collective/src/jvmMain/kotlin/GmcVelocity.kt +++ b/demo/device-collective/src/jvmMain/kotlin/GmcVelocity.kt @@ -1,4 +1,4 @@ -package space.kscience.controls.demo.map +package space.kscience.controls.demo.collective import kotlinx.serialization.Serializable import space.kscience.kmath.geometry.Angle diff --git a/demo/device-collective/src/jvmMain/kotlin/DebounceDeviceState.kt b/demo/device-collective/src/jvmMain/kotlin/SampleDeviceState.kt similarity index 76% rename from demo/device-collective/src/jvmMain/kotlin/DebounceDeviceState.kt rename to demo/device-collective/src/jvmMain/kotlin/SampleDeviceState.kt index b507cd6..a2ef06c 100644 --- a/demo/device-collective/src/jvmMain/kotlin/DebounceDeviceState.kt +++ b/demo/device-collective/src/jvmMain/kotlin/SampleDeviceState.kt @@ -1,4 +1,4 @@ -package space.kscience.controls.demo.map +package space.kscience.controls.demo.collective import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.Flow @@ -7,7 +7,7 @@ import space.kscience.controls.constructor.DeviceState import kotlin.time.Duration @OptIn(FlowPreview::class) -class DebounceDeviceState( +class SampleDeviceState( val origin: DeviceState, val interval: Duration, ) : DeviceState { @@ -17,4 +17,4 @@ class DebounceDeviceState( override fun toString(): String = "DebounceDeviceState($value, interval=$interval)" } -fun DeviceState.debounce(interval: Duration) = DebounceDeviceState(this, interval) \ No newline at end of file +fun DeviceState.sample(interval: Duration) = SampleDeviceState(this, interval) \ No newline at end of file diff --git a/demo/device-collective/src/jvmMain/kotlin/debugModel.kt b/demo/device-collective/src/jvmMain/kotlin/debugModel.kt index e352fcc..96c9ca8 100644 --- a/demo/device-collective/src/jvmMain/kotlin/debugModel.kt +++ b/demo/device-collective/src/jvmMain/kotlin/debugModel.kt @@ -1,4 +1,4 @@ -package space.kscience.controls.demo.map +package space.kscience.controls.demo.collective import kotlinx.coroutines.Job import kotlinx.coroutines.delay @@ -20,31 +20,32 @@ private val radius = 0.01.degrees internal fun generateModel(context: Context): DeviceCollectiveModel { - val devices: List = buildList { - repeat(100) { - add( - RemoteDeviceState( - "device[$it]", - Gmc( - center.latitude + radius * Random.nextDouble(), - center.longitude + radius * Random.nextDouble() - ) - ) + val devices: List = List(100) { index -> + val id = "device[$index]" + + VirtualDeviceState( + id = id, + Gmc( + center.latitude + radius * Random.nextDouble(), + center.longitude + radius * Random.nextDouble() ) + ) { + deviceId = id + description = "Virtual remote device $id" } } - val model = DeviceCollectiveModel(context, devices) + val model = DeviceCollectiveModel(context, devices, 0.2.kilometers) return model } -fun RemoteDevice.moveInCircles(): Job = launch { +fun CollectiveDevice.moveInCircles(): Job = launch { var bearing = Random.nextDouble(-PI, PI).radians - write(RemoteDevice.velocity, GmcVelocity(bearing, deviceVelocity)) + write(CollectiveDevice.velocity, GmcVelocity(bearing, deviceVelocity)) while (isActive) { delay(500) bearing += 5.degrees - write(RemoteDevice.velocity, GmcVelocity(bearing, deviceVelocity)) + write(CollectiveDevice.velocity, GmcVelocity(bearing, deviceVelocity)) } } \ No newline at end of file diff --git a/demo/device-collective/src/jvmMain/kotlin/main.kt b/demo/device-collective/src/jvmMain/kotlin/main.kt index 0c7f7ad..1db8621 100644 --- a/demo/device-collective/src/jvmMain/kotlin/main.kt +++ b/demo/device-collective/src/jvmMain/kotlin/main.kt @@ -1,27 +1,42 @@ -package space.kscience.controls.demo.map +@file:OptIn(ExperimentalFoundationApi::class, ExperimentalSplitPaneApi::class) +package space.kscience.controls.demo.collective + +import androidx.compose.foundation.* +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material.Card +import androidx.compose.material.Checkbox +import androidx.compose.material.Text import androidx.compose.material3.MaterialTheme -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp 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 org.jetbrains.compose.splitpane.ExperimentalSplitPaneApi +import org.jetbrains.compose.splitpane.HorizontalSplitPane +import org.jetbrains.compose.splitpane.rememberSplitPaneState +import space.kscience.controls.compose.conditional import space.kscience.controls.manager.DeviceManager -import space.kscience.controls.spec.propertyFlow +import space.kscience.controls.spec.useProperty 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 space.kscience.maps.features.* import java.nio.file.Path @@ -34,7 +49,6 @@ fun rememberDeviceManager(): DeviceManager = remember { context.request(DeviceManager) } - @Composable fun App() { val scope = rememberCoroutineScope() @@ -47,14 +61,24 @@ fun App() { generateModel(deviceManager.context) } - val devices: Map = remember { + val devices: Map = remember { collectiveModel.deviceStates.associate { - val device = RemoteDeviceConstructor(deviceManager.context, it.configuration, it.position, it.velocity) + val device = CollectiveDeviceConstructor( + context = deviceManager.context, + configuration = it.configuration, + position = it.position, + velocity = it.velocity + ) { + collectiveModel.locateVisible(it.id).keys + } device.moveInCircles() it.id to device } } + var selectedDeviceId by remember { mutableStateOf(null) } + var showOnlyVisible by remember { mutableStateOf(false) } + val mapTileProvider = remember { OpenStreetMapTileProvider( client = HttpClient(CIO), @@ -62,21 +86,93 @@ fun App() { ) } - MapView( - mapTileProvider = mapTileProvider, - config = ViewConfig() + HorizontalSplitPane( + splitPaneState = rememberSplitPaneState(0.9f) ) { - 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) - } + first(400.dp) { + 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", size = 3.dp) + .color(Color.Red) + .modifyAttribute(AlphaAttribute, 0.5f) // does not work right now + }.launchIn(scope) + } - devices.forEach { (id, device) -> - device.propertyFlow(RemoteDevice.position).onEach { position -> - rectangle(position, id = id).color(Color.Blue) - }.launchIn(scope) + devices.forEach { (id, device) -> + device.useProperty(CollectiveDevice.position, scope = scope) { position -> + + val activeDevice = selectedDeviceId?.let { devices[it] } + + if (activeDevice == null || id == selectedDeviceId || !showOnlyVisible || id in activeDevice.listVisible()) { + rectangle( + position, + id = id, + size = if (selectedDeviceId == id) DpSize(10.dp, 10.dp) else DpSize(5.dp, 5.dp) + ).color(if (selectedDeviceId == id) Color.Magenta else Color.Blue) + .modifyAttribute(AlphaAttribute, if (selectedDeviceId == id) 1f else 0.5f) + .onClick { selectedDeviceId = id } + } else { + removeFeature(id) + } + + } + } + } + } + second(200.dp) { + Column { + selectedDeviceId?.let { id -> + Column( + modifier = Modifier + .padding(8.dp) + .border(2.dp, Color.DarkGray) + ) { + Card( + elevation = 16.dp, + ) { + Text( + text = "Выбран: $id", + fontSize = 16.sp, + fontWeight = FontWeight.Bold, + modifier = Modifier.padding(10.dp).fillMaxWidth() + ) + } + devices[id]?.let { + Text(it.meta.toString(), Modifier.padding(10.dp)) + } + Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(10.dp).fillMaxWidth()) { + Text("Показать только видимые") + Checkbox(showOnlyVisible, { showOnlyVisible = it }) + } + } + } + Column( + modifier = Modifier.verticalScroll(rememberScrollState()) + ) { + devices.forEach { (id, device) -> + Card( + elevation = 16.dp, + modifier = Modifier.padding(8.dp).onClick { + selectedDeviceId = id + }.conditional(id == selectedDeviceId) { + border(2.dp, Color.Blue) + } + ) { + Text( + text = id, + fontSize = 16.sp, + fontWeight = FontWeight.Bold, + modifier = Modifier.padding(10.dp).fillMaxWidth() + ) + } + } + } + } } } } From e9bde6867466d4fcea57b20a11c46d25856abd60 Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Sun, 9 Jun 2024 15:09:43 +0300 Subject: [PATCH 04/10] [WIP] remote communication for CollectiveDevice --- .../kscience/controls/peer/PeerConnection.kt | 7 +- .../controls/server/deviceWebServer.kt | 2 + demo/device-collective/build.gradle.kts | 2 + .../src/jvmMain/kotlin/CollectiveDevice.kt | 33 ++- .../jvmMain/kotlin/DeviceCollectiveModel.kt | 86 ++++++- .../src/jvmMain/kotlin/SampleDeviceState.kt | 19 +- .../src/jvmMain/kotlin/debugModel.kt | 13 +- .../src/jvmMain/kotlin/main.kt | 218 +++++++++++------- .../kscience/controls/demo/MassDevice.kt | 3 +- .../kscience/magix/api/MagixFlowPlugin.kt | 13 +- .../kscience/magix/server/magixModule.kt | 57 +++-- .../space/kscience/magix/server/server.kt | 2 +- 12 files changed, 312 insertions(+), 143 deletions(-) diff --git a/controls-core/src/commonMain/kotlin/space/kscience/controls/peer/PeerConnection.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/peer/PeerConnection.kt index 14b49ef..a831207 100644 --- a/controls-core/src/commonMain/kotlin/space/kscience/controls/peer/PeerConnection.kt +++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/peer/PeerConnection.kt @@ -2,14 +2,13 @@ package space.kscience.controls.peer import space.kscience.dataforge.io.Envelope import space.kscience.dataforge.meta.Meta -import space.kscience.dataforge.names.Name /** * A manager that allows direct synchronous sending and receiving binary data */ public interface PeerConnection { /** - * Receive an [Envelope] from a device with name [deviceName] on a given [address] with given [contentId]. + * Receive an [Envelope] from a device on a given [address] with given [contentId]. * * The address depends on the specifics of given [PeerConnection]. For example, it could be a TCP/IP port or * magix endpoint name. @@ -20,13 +19,12 @@ public interface PeerConnection { */ public suspend fun receive( address: String, - deviceName: Name, contentId: String, requestMeta: Meta = Meta.EMPTY, ): Envelope /** - * Send an [envelope] to a device with name [deviceName] on a given [address] + * Send an [envelope] to a device on a given [address] * * The address depends on the specifics of given [PeerConnection]. For example, it could be a TCP/IP port or * magix endpoint name. @@ -35,7 +33,6 @@ public interface PeerConnection { */ public suspend fun send( address: String, - deviceName: Name, envelope: Envelope, requestMeta: Meta = Meta.EMPTY, ) diff --git a/controls-server/src/jvmMain/kotlin/space/kscience/controls/server/deviceWebServer.kt b/controls-server/src/jvmMain/kotlin/space/kscience/controls/server/deviceWebServer.kt index 4f37322..8fd104e 100644 --- a/controls-server/src/jvmMain/kotlin/space/kscience/controls/server/deviceWebServer.kt +++ b/controls-server/src/jvmMain/kotlin/space/kscience/controls/server/deviceWebServer.kt @@ -38,6 +38,7 @@ import space.kscience.dataforge.names.get import space.kscience.magix.api.MagixEndpoint import space.kscience.magix.api.MagixFlowPlugin import space.kscience.magix.api.MagixMessage +import space.kscience.magix.api.start import space.kscience.magix.server.magixModule @@ -215,5 +216,6 @@ public fun Application.deviceManagerModule( plugins.forEach { it.start(this, magixFlow) } + magixModule(magixFlow) } \ No newline at end of file diff --git a/demo/device-collective/build.gradle.kts b/demo/device-collective/build.gradle.kts index 8bb0597..3290a0d 100644 --- a/demo/device-collective/build.gradle.kts +++ b/demo/device-collective/build.gradle.kts @@ -13,7 +13,9 @@ kscience { commonMain { implementation(projects.controlsVisualisationCompose) implementation(projects.controlsConstructor) + implementation(projects.magix.magixServer) implementation(projects.magix.magixRsocket) + implementation(projects.controlsMagix) } jvmMain { // implementation("io.ktor:ktor-server-cio") diff --git a/demo/device-collective/src/jvmMain/kotlin/CollectiveDevice.kt b/demo/device-collective/src/jvmMain/kotlin/CollectiveDevice.kt index 050794c..9fce04d 100644 --- a/demo/device-collective/src/jvmMain/kotlin/CollectiveDevice.kt +++ b/demo/device-collective/src/jvmMain/kotlin/CollectiveDevice.kt @@ -6,24 +6,35 @@ 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.peer.PeerConnection import space.kscience.controls.spec.DeviceSpec +import space.kscience.controls.spec.unit import space.kscience.dataforge.context.Context import space.kscience.dataforge.meta.MetaConverter import space.kscience.dataforge.meta.Scheme +import space.kscience.dataforge.meta.int import space.kscience.dataforge.meta.string import space.kscience.dataforge.misc.DFExperimental import space.kscience.maps.coordinates.Gmc +import space.kscience.maps.coordinates.GmcCurve import kotlin.time.Duration.Companion.milliseconds -class CollectiveDeviceConfiguration(deviceId: DeviceId) : Scheme() { +typealias CollectiveDeviceId = String + +class CollectiveDeviceConfiguration(deviceId: CollectiveDeviceId) : Scheme() { var deviceId by string(deviceId) var description by string() + var reportInterval by int(500) + var radioFrequency by string(default = "169 MHz") } +typealias CollectiveDeviceRoster = Map interface CollectiveDevice : Device { - public val id: DeviceId + public val id: CollectiveDeviceId + + public val peerConnection: PeerConnection suspend fun getPosition(): Gmc @@ -31,8 +42,7 @@ interface CollectiveDevice : Device { suspend fun setVelocity(value: GmcVelocity) - suspend fun listVisible(): Collection - + suspend fun listVisible(): Collection companion object : DeviceSpec() { val position by property( @@ -45,6 +55,10 @@ interface CollectiveDevice : Device { read = { getVelocity() }, write = { _, value -> setVelocity(value) } ) + + val listVisible by action(MetaConverter.unit, MetaConverter.valueList { it.string }) { + listVisible().toList() + } } } @@ -54,13 +68,14 @@ class CollectiveDeviceConstructor( val configuration: CollectiveDeviceConfiguration, position: MutableDeviceState, velocity: MutableDeviceState, - private val listVisible: suspend () -> Collection, + override val peerConnection: PeerConnection, + private val observation: suspend () -> Map, ) : DeviceConstructor(context, configuration.meta), CollectiveDevice { - override val id: DeviceId get() = configuration.deviceId + override val id: CollectiveDeviceId get() = configuration.deviceId - val position = registerAsProperty(CollectiveDevice.position, position.sample(500.milliseconds)) - val velocity = registerAsProperty(CollectiveDevice.velocity, velocity) + val position = registerAsProperty(CollectiveDevice.position, position.sample(configuration.reportInterval.milliseconds)) + val velocity = registerAsProperty(CollectiveDevice.velocity, velocity.sample(configuration.reportInterval.milliseconds)) override suspend fun getPosition(): Gmc = position.value @@ -70,5 +85,5 @@ class CollectiveDeviceConstructor( velocity.value = value } - override suspend fun listVisible(): Collection = listVisible.invoke() + override suspend fun listVisible(): Collection = observation.invoke().keys } \ No newline at end of file diff --git a/demo/device-collective/src/jvmMain/kotlin/DeviceCollectiveModel.kt b/demo/device-collective/src/jvmMain/kotlin/DeviceCollectiveModel.kt index 4abedea..2c325b4 100644 --- a/demo/device-collective/src/jvmMain/kotlin/DeviceCollectiveModel.kt +++ b/demo/device-collective/src/jvmMain/kotlin/DeviceCollectiveModel.kt @@ -1,27 +1,41 @@ package space.kscience.controls.demo.collective +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch +import space.kscience.controls.client.launchMagixService import space.kscience.controls.constructor.ModelConstructor import space.kscience.controls.constructor.MutableDeviceState import space.kscience.controls.constructor.onTimer +import space.kscience.controls.manager.DeviceManager +import space.kscience.controls.manager.install +import space.kscience.controls.peer.PeerConnection import space.kscience.dataforge.context.Context +import space.kscience.dataforge.context.request +import space.kscience.dataforge.io.Envelope +import space.kscience.dataforge.meta.Meta +import space.kscience.dataforge.names.parseAsName +import space.kscience.magix.api.MagixEndpoint +import space.kscience.magix.rsocket.rSocketWithWebSockets +import space.kscience.magix.server.startMagixServer import space.kscience.maps.coordinates.* +import kotlin.time.Duration +import kotlin.time.Duration.Companion.milliseconds -typealias DeviceId = String - - -internal data class VirtualDeviceState( - val id: DeviceId, +internal data class CollectiveDeviceState( + val id: CollectiveDeviceId, val configuration: CollectiveDeviceConfiguration, val position: MutableDeviceState, val velocity: MutableDeviceState, ) internal fun VirtualDeviceState( - id: DeviceId, + id: CollectiveDeviceId, position: Gmc, configuration: CollectiveDeviceConfiguration.() -> Unit = {}, -) = VirtualDeviceState( +) = CollectiveDeviceState( id, CollectiveDeviceConfiguration(id).apply(configuration), MutableDeviceState(position), @@ -31,9 +45,11 @@ internal fun VirtualDeviceState( internal class DeviceCollectiveModel( context: Context, - val deviceStates: Collection, - val visibilityRange: Distance, -) : ModelConstructor(context) { + val deviceStates: Collection, + val visibilityRange: Distance = 0.4.kilometers, + val radioRange: Distance = 5.kilometers, + val reportInterval: Duration = 1000.milliseconds +) : ModelConstructor(context), PeerConnection { /** * Propagate movement @@ -45,7 +61,7 @@ internal class DeviceCollectiveModel( } } - suspend fun locateVisible(id: DeviceId): Map { + private fun locateVisible(id: CollectiveDeviceId): Map { val coordinatesSnapshot = deviceStates.associate { it.id to it.position.value } val selected = coordinatesSnapshot[id] ?: error("Can't find device with id $id") @@ -56,4 +72,52 @@ internal class DeviceCollectiveModel( return allCurves.filterValues { it.distance in 0.kilometers..visibilityRange } } + + val devices = deviceStates.associate { + val device = CollectiveDeviceConstructor( + context = context, + configuration = it.configuration, + position = it.position, + velocity = it.velocity, + peerConnection = this, + ) { + locateVisible(it.id) + } + //start movement program + device.moveInCircles() + it.id to device + } + + val roster = deviceStates.associate { it.id to it.configuration } + + override suspend fun receive(address: String, contentId: String, requestMeta: Meta): Envelope { + TODO("Not yet implemented") + } + + override suspend fun send(address: String, envelope: Envelope, requestMeta: Meta) { +// devices.values.filter { it.configuration.radioFrequency == address }.forEach { device -> +// ``` +// } + } +} + +internal fun CoroutineScope.launchCollectiveMagixServer( + collectiveModel: DeviceCollectiveModel, +): Job = launch(Dispatchers.IO) { + val server = startMagixServer( +// RSocketMagixFlowPlugin() + ) + + collectiveModel.devices.forEach { (id, device) -> + val deviceContext = collectiveModel.context.buildContext(id.parseAsName()) { + coroutineContext(coroutineContext) + plugin(DeviceManager) + } + + deviceContext.install(id, device) + + val deviceEndpoint = MagixEndpoint.rSocketWithWebSockets("localhost") + + deviceContext.request(DeviceManager).launchMagixService(deviceEndpoint, id) + } } \ No newline at end of file diff --git a/demo/device-collective/src/jvmMain/kotlin/SampleDeviceState.kt b/demo/device-collective/src/jvmMain/kotlin/SampleDeviceState.kt index a2ef06c..499c903 100644 --- a/demo/device-collective/src/jvmMain/kotlin/SampleDeviceState.kt +++ b/demo/device-collective/src/jvmMain/kotlin/SampleDeviceState.kt @@ -4,6 +4,7 @@ import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.sample import space.kscience.controls.constructor.DeviceState +import space.kscience.controls.constructor.MutableDeviceState import kotlin.time.Duration @OptIn(FlowPreview::class) @@ -11,10 +12,24 @@ class SampleDeviceState( val origin: DeviceState, val interval: Duration, ) : DeviceState { - override val value: T get() = origin.value + override val value: T by origin::value override val valueFlow: Flow get() = origin.valueFlow.sample(interval) override fun toString(): String = "DebounceDeviceState($value, interval=$interval)" } -fun DeviceState.sample(interval: Duration) = SampleDeviceState(this, interval) \ No newline at end of file + +fun DeviceState.sample(interval: Duration) = SampleDeviceState(this, interval) + +@OptIn(FlowPreview::class) +class MutableSampleDeviceState( + val origin: MutableDeviceState, + val interval: Duration, +) : MutableDeviceState { + override var value: T by origin::value + override val valueFlow: Flow get() = origin.valueFlow.sample(interval) + + override fun toString(): String = "DebounceDeviceState($value, interval=$interval)" +} + +fun MutableDeviceState.sample(interval: Duration) = MutableSampleDeviceState(this, interval) \ No newline at end of file diff --git a/demo/device-collective/src/jvmMain/kotlin/debugModel.kt b/demo/device-collective/src/jvmMain/kotlin/debugModel.kt index 96c9ca8..d0ec384 100644 --- a/demo/device-collective/src/jvmMain/kotlin/debugModel.kt +++ b/demo/device-collective/src/jvmMain/kotlin/debugModel.kt @@ -12,6 +12,8 @@ import space.kscience.maps.coordinates.Gmc import space.kscience.maps.coordinates.kilometers import kotlin.math.PI import kotlin.random.Random +import kotlin.time.Duration +import kotlin.time.Duration.Companion.milliseconds private val deviceVelocity = 0.1.kilometers @@ -19,8 +21,13 @@ private val center = Gmc.ofDegrees(55.925, 37.514) private val radius = 0.01.degrees -internal fun generateModel(context: Context): DeviceCollectiveModel { - val devices: List = List(100) { index -> +internal fun generateModel( + context: Context, + size: Int = 50, + reportInterval: Duration = 500.milliseconds, + additionalConfiguration: CollectiveDeviceConfiguration.() -> Unit = {}, +): DeviceCollectiveModel { + val devices: List = List(size) { index -> val id = "device[$index]" VirtualDeviceState( @@ -32,6 +39,8 @@ internal fun generateModel(context: Context): DeviceCollectiveModel { ) { deviceId = id description = "Virtual remote device $id" + this.reportInterval = reportInterval.inWholeMilliseconds.toInt() + additionalConfiguration() } } diff --git a/demo/device-collective/src/jvmMain/kotlin/main.kt b/demo/device-collective/src/jvmMain/kotlin/main.kt index 1db8621..9aff323 100644 --- a/demo/device-collective/src/jvmMain/kotlin/main.kt +++ b/demo/device-collective/src/jvmMain/kotlin/main.kt @@ -24,74 +24,95 @@ 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.* import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import org.jetbrains.compose.splitpane.ExperimentalSplitPaneApi import org.jetbrains.compose.splitpane.HorizontalSplitPane import org.jetbrains.compose.splitpane.rememberSplitPaneState +import space.kscience.controls.api.PropertyChangedMessage +import space.kscience.controls.client.* import space.kscience.controls.compose.conditional import space.kscience.controls.manager.DeviceManager -import space.kscience.controls.spec.useProperty import space.kscience.dataforge.context.Context -import space.kscience.dataforge.context.request +import space.kscience.dataforge.context.ContextBuilder +import space.kscience.dataforge.meta.MetaConverter +import space.kscience.dataforge.names.parseAsName +import space.kscience.magix.api.MagixEndpoint +import space.kscience.magix.api.subscribe +import space.kscience.magix.rsocket.rSocketWithWebSockets import space.kscience.maps.compose.MapView import space.kscience.maps.compose.OpenStreetMapTileProvider +import space.kscience.maps.coordinates.Gmc +import space.kscience.maps.coordinates.meters import space.kscience.maps.features.* import java.nio.file.Path +import kotlin.time.Duration.Companion.seconds @Composable -fun rememberDeviceManager(): DeviceManager = remember { - val context = Context { - plugin(DeviceManager) - } - - context.request(DeviceManager) +fun rememberContext(name: String, contextBuilder: ContextBuilder.() -> Unit = {}): Context = remember { + Context(name, contextBuilder) } @Composable fun App() { val scope = rememberCoroutineScope() - - val deviceManager = rememberDeviceManager() - - - val collectiveModel = remember { - generateModel(deviceManager.context) + val parentContext = rememberContext("Parent") { + plugin(DeviceManager) } - val devices: Map = remember { - collectiveModel.deviceStates.associate { - val device = CollectiveDeviceConstructor( - context = deviceManager.context, - configuration = it.configuration, - position = it.position, - velocity = it.velocity - ) { - collectiveModel.locateVisible(it.id).keys + val collectiveModel = remember { + generateModel(parentContext, 60) + } + + val roster = remember { + collectiveModel.roster + } + + val client = remember { CompletableDeferred() } + + val devices = remember { mutableStateMapOf() } + + + LaunchedEffect(collectiveModel) { + launchCollectiveMagixServer(collectiveModel) + withContext(Dispatchers.IO) { + val magixClient = MagixEndpoint.rSocketWithWebSockets("localhost") + + client.complete(magixClient) + + collectiveModel.roster.forEach { (id, config) -> + devices[id] = magixClient.remoteDevice(parentContext, "listener", id, id.parseAsName()) } - device.moveInCircles() - it.id to device + } + + } + + var selectedDeviceId by remember { mutableStateOf(null) } + + var currentPosition by remember { mutableStateOf(null) } + + LaunchedEffect(selectedDeviceId, devices) { + selectedDeviceId?.let { devices[it] }?.propertyFlow(CollectiveDevice.position)?.collect { + currentPosition = it } } - var selectedDeviceId by remember { mutableStateOf(null) } var showOnlyVisible by remember { mutableStateOf(false) } - val mapTileProvider = remember { - OpenStreetMapTileProvider( - client = HttpClient(CIO), - cacheDirectory = Path.of("mapCache") - ) - } - HorizontalSplitPane( splitPaneState = rememberSplitPaneState(0.9f) ) { first(400.dp) { MapView( - mapTileProvider = mapTileProvider, + mapTileProvider = remember { + OpenStreetMapTileProvider( + client = HttpClient(CIO), + cacheDirectory = Path.of("mapCache") + ) + }, config = ViewConfig() ) { collectiveModel.deviceStates.forEach { device -> @@ -103,65 +124,61 @@ fun App() { }.launchIn(scope) } - devices.forEach { (id, device) -> - device.useProperty(CollectiveDevice.position, scope = scope) { position -> + scope.launch { - val activeDevice = selectedDeviceId?.let { devices[it] } + client.await().subscribe(DeviceManager.magixFormat).onEach { (magixMessage, deviceMessage) -> + if (deviceMessage is PropertyChangedMessage && deviceMessage.property == "position") { + val id = magixMessage.sourceEndpoint + val position = MetaConverter.serializable().read(deviceMessage.value) + val activeDevice = selectedDeviceId?.let { devices[it] } - if (activeDevice == null || id == selectedDeviceId || !showOnlyVisible || id in activeDevice.listVisible()) { - rectangle( - position, - id = id, - size = if (selectedDeviceId == id) DpSize(10.dp, 10.dp) else DpSize(5.dp, 5.dp) - ).color(if (selectedDeviceId == id) Color.Magenta else Color.Blue) - .modifyAttribute(AlphaAttribute, if (selectedDeviceId == id) 1f else 0.5f) - .onClick { selectedDeviceId = id } - } else { - removeFeature(id) + suspend fun DeviceClient.idIsVisible() = try { + withTimeout(1.seconds) { + id in execute(CollectiveDevice.listVisible) + } + } catch (ex: Exception) { + ex.printStackTrace() + true + } + + if ( + activeDevice == null || + id == selectedDeviceId || + !showOnlyVisible || + activeDevice.idIsVisible() + ) { + rectangle( + position, + id = id, + size = if (selectedDeviceId == id) DpSize(10.dp, 10.dp) else DpSize(5.dp, 5.dp) + ).color(if (selectedDeviceId == id) Color.Magenta else Color.Blue) + .modifyAttribute(AlphaAttribute, if (selectedDeviceId == id) 1f else 0.5f) + .onClick { selectedDeviceId = id } + } else { + removeFeature(id) + } } + }.launchIn(scope) - } } } } second(200.dp) { - Column { - selectedDeviceId?.let { id -> - Column( - modifier = Modifier - .padding(8.dp) - .border(2.dp, Color.DarkGray) + + Column( + modifier = Modifier.verticalScroll(rememberScrollState()) + ) { + devices.forEach { (id, _) -> + Card( + elevation = 16.dp, + modifier = Modifier.padding(8.dp).onClick { + selectedDeviceId = id + }.conditional(id == selectedDeviceId) { + border(2.dp, Color.Blue) + } ) { - Card( - elevation = 16.dp, - ) { - Text( - text = "Выбран: $id", - fontSize = 16.sp, - fontWeight = FontWeight.Bold, - modifier = Modifier.padding(10.dp).fillMaxWidth() - ) - } - devices[id]?.let { - Text(it.meta.toString(), Modifier.padding(10.dp)) - } - Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(10.dp).fillMaxWidth()) { - Text("Показать только видимые") - Checkbox(showOnlyVisible, { showOnlyVisible = it }) - } - } - } - Column( - modifier = Modifier.verticalScroll(rememberScrollState()) - ) { - devices.forEach { (id, device) -> - Card( - elevation = 16.dp, - modifier = Modifier.padding(8.dp).onClick { - selectedDeviceId = id - }.conditional(id == selectedDeviceId) { - border(2.dp, Color.Blue) - } + Column( + modifier = Modifier.padding(8.dp) ) { Text( text = id, @@ -169,16 +186,49 @@ fun App() { fontWeight = FontWeight.Bold, modifier = Modifier.padding(10.dp).fillMaxWidth() ) + if (id == selectedDeviceId) { + roster[id]?.let { + Text("Meta:", color = Color.Blue, fontWeight = FontWeight.Bold) + Card(elevation = 16.dp, modifier = Modifier.fillMaxWidth().padding(8.dp)) { + Text(it.toString()) + } + } + + currentPosition?.let { currentPosition -> + Text("Широта: ${String.format("%.3f", currentPosition.latitude.toDegrees().value)}") + Text( + "Долгота: ${ + String.format( + "%.3f", + currentPosition.longitude.toDegrees().value + ) + }" + ) + currentPosition.elevation?.let { + Text("Высота: ${String.format("%.1f", it.meters)} м") + } + } + + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth() + ) { + Text("Показать только видимые") + Checkbox(showOnlyVisible, { showOnlyVisible = it }) + } + } } } } } + } } } fun main() = application { +// System.setProperty(IO_PARALLELISM_PROPERTY_NAME, 300.toString()) Window(onCloseRequest = ::exitApplication, title = "Maps-kt demo", icon = painterResource("SPC-logo.png")) { MaterialTheme { App() diff --git a/demo/many-devices/src/main/kotlin/space/kscience/controls/demo/MassDevice.kt b/demo/many-devices/src/main/kotlin/space/kscience/controls/demo/MassDevice.kt index 5eafabf..53c4c7f 100644 --- a/demo/many-devices/src/main/kotlin/space/kscience/controls/demo/MassDevice.kt +++ b/demo/many-devices/src/main/kotlin/space/kscience/controls/demo/MassDevice.kt @@ -83,8 +83,7 @@ suspend fun main() { val endpointId = "device$it" val deviceEndpoint = MagixEndpoint.rSocketStreamWithWebSockets("localhost") - deviceManager.launchMagixService(deviceEndpoint, endpointId, Dispatchers.IO) - + deviceManager.launchMagixService(deviceEndpoint, endpointId) } val trace = Bar { diff --git a/magix/magix-api/src/commonMain/kotlin/space/kscience/magix/api/MagixFlowPlugin.kt b/magix/magix-api/src/commonMain/kotlin/space/kscience/magix/api/MagixFlowPlugin.kt index 83c95cc..1a3dd75 100644 --- a/magix/magix-api/src/commonMain/kotlin/space/kscience/magix/api/MagixFlowPlugin.kt +++ b/magix/magix-api/src/commonMain/kotlin/space/kscience/magix/api/MagixFlowPlugin.kt @@ -21,9 +21,10 @@ public fun interface MagixFlowPlugin { sendMessage: suspend (MagixMessage) -> Unit, ): Job - /** - * Use the same [MutableSharedFlow] to send and receive messages. Could be a bottleneck in case of many plugins. - */ - public fun start(scope: CoroutineScope, magixFlow: MutableSharedFlow): Job = - start(scope, magixFlow) { magixFlow.emit(it) } -} \ No newline at end of file +} + +/** + * Use the same [MutableSharedFlow] to send and receive messages. Could be a bottleneck in case of many plugins. + */ +public fun MagixFlowPlugin.start(scope: CoroutineScope, magixFlow: MutableSharedFlow): Job = + start(scope, magixFlow) { magixFlow.emit(it) } \ No newline at end of file diff --git a/magix/magix-server/src/jvmMain/kotlin/space/kscience/magix/server/magixModule.kt b/magix/magix-server/src/jvmMain/kotlin/space/kscience/magix/server/magixModule.kt index dc197ad..a1edec5 100644 --- a/magix/magix-server/src/jvmMain/kotlin/space/kscience/magix/server/magixModule.kt +++ b/magix/magix-server/src/jvmMain/kotlin/space/kscience/magix/server/magixModule.kt @@ -10,7 +10,9 @@ import io.ktor.server.util.getValue import io.ktor.server.websocket.WebSockets import io.rsocket.kotlin.ktor.server.RSocketSupport import io.rsocket.kotlin.ktor.server.rSocket +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.map import kotlinx.html.* import kotlinx.serialization.encodeToString @@ -42,7 +44,11 @@ private fun ApplicationCall.buildFilter(): MagixMessageFilter { /** * Attach magix http/sse and websocket-based rsocket event loop + statistics page to existing [MutableSharedFlow] */ -public fun Application.magixModule(magixFlow: MutableSharedFlow, route: String = "/") { +public fun Application.magixModule( + magixFlow: Flow, + send: suspend (MagixMessage) -> Unit, + route: String = "/", +) { if (pluginOrNull(WebSockets) == null) { install(WebSockets) } @@ -62,27 +68,31 @@ public fun Application.magixModule(magixFlow: MutableSharedFlow, r routing { route(route) { - install(ContentNegotiation){ + install(ContentNegotiation) { json() } - get("state") { - call.respondHtml { - head { - meta { - httpEquiv = "refresh" - content = "2" + if (magixFlow is SharedFlow) { + get("state") { + call.respondHtml { + head { + meta { + httpEquiv = "refresh" + content = "2" + } } - } - body { - h1 { +"Magix loop statistics" } - h2 { +"Number of subscribers: ${magixFlow.subscriptionCount.value}" } - h3 { +"Replay cache size: ${magixFlow.replayCache.size}" } - h3 { +"Replay cache:" } - ol { - magixFlow.replayCache.forEach { message -> - li { - code { - +magixJson.encodeToString(message) + body { + h1 { +"Magix loop statistics" } + if (magixFlow is MutableSharedFlow) { + h2 { +"Number of subscribers: ${magixFlow.subscriptionCount.value}" } + } + h3 { +"Replay cache size: ${magixFlow.replayCache.size}" } + h3 { +"Replay cache:" } + ol { + magixFlow.replayCache.forEach { message -> + li { + code { + +magixJson.encodeToString(message) + } } } } @@ -102,17 +112,22 @@ public fun Application.magixModule(magixFlow: MutableSharedFlow, r } post("broadcast") { val message = call.receive() - magixFlow.emit(message) + send(message) } //rSocket WS server. Filter from Payload rSocket( "rsocket", - acceptor = RSocketMagixFlowPlugin.acceptor(application, magixFlow) { magixFlow.emit(it) } + acceptor = RSocketMagixFlowPlugin.acceptor(application, magixFlow) { send(it) } ) } } } +public fun Application.magixModule( + magixFlow: MutableSharedFlow, + route: String = "/", +): Unit = magixModule(magixFlow, { magixFlow.emit(it) }, route) + /** * Create a new loop [MutableSharedFlow] with given [buffer] and setup magix module based on it */ diff --git a/magix/magix-server/src/jvmMain/kotlin/space/kscience/magix/server/server.kt b/magix/magix-server/src/jvmMain/kotlin/space/kscience/magix/server/server.kt index 2396e25..aa26bee 100644 --- a/magix/magix-server/src/jvmMain/kotlin/space/kscience/magix/server/server.kt +++ b/magix/magix-server/src/jvmMain/kotlin/space/kscience/magix/server/server.kt @@ -9,6 +9,7 @@ import kotlinx.coroutines.flow.MutableSharedFlow import space.kscience.magix.api.MagixEndpoint.Companion.DEFAULT_MAGIX_HTTP_PORT import space.kscience.magix.api.MagixFlowPlugin import space.kscience.magix.api.MagixMessage +import space.kscience.magix.api.start /** @@ -22,7 +23,6 @@ public fun CoroutineScope.startMagixServer( val magixFlow = MutableSharedFlow( replay = buffer, - extraBufferCapacity = buffer, onBufferOverflow = BufferOverflow.DROP_OLDEST ) From c55ce2cf9a393f43829d792de9f14dd93027353f Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Sun, 9 Jun 2024 20:51:12 +0300 Subject: [PATCH 05/10] Fix visibility range for collective --- .../controls/constructor/DeviceState.kt | 15 ++--- .../kscience/controls/misc/converters.kt | 17 ++++-- .../controls/client/clientPropertyAccess.kt | 5 +- .../src/jvmMain/kotlin/CollectiveDevice.kt | 43 +++++++++++--- .../jvmMain/kotlin/DeviceCollectiveModel.kt | 8 +-- .../src/jvmMain/kotlin/main.kt | 57 +++++++++---------- 6 files changed, 89 insertions(+), 56 deletions(-) diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceState.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceState.kt index 846a37b..5b547f7 100644 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceState.kt +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceState.kt @@ -74,15 +74,16 @@ public fun DeviceState.Companion.map( public fun DeviceState.map(mapper: (T) -> R): DeviceStateWithDependencies = DeviceState.map(this, mapper) -public fun DeviceState>.values(): DeviceState = object : DeviceState { - override val value: Double - get() = this@values.value.value +public fun DeviceState>.values(): DeviceState = + object : DeviceState { + override val value: Double + get() = this@values.value.value - override val valueFlow: Flow - get() = this@values.valueFlow.map { it.value } + override val valueFlow: Flow + get() = this@values.valueFlow.map { it.value } - override fun toString(): String = this@values.toString() -} + override fun toString(): String = this@values.toString() + } /** * Combine two device states into one read-only [DeviceState]. Only the latest value of each state is used. diff --git a/controls-core/src/commonMain/kotlin/space/kscience/controls/misc/converters.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/misc/converters.kt index 4297d20..c200758 100644 --- a/controls-core/src/commonMain/kotlin/space/kscience/controls/misc/converters.kt +++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/misc/converters.kt @@ -41,13 +41,22 @@ private object InstantConverter : MetaConverter { public val MetaConverter.Companion.instant: MetaConverter get() = InstantConverter private object DoubleRangeConverter : MetaConverter> { - override fun readOrNull(source: Meta): ClosedFloatingPointRange? = source.value?.doubleArray?.let { (start, end)-> - start..end - } + override fun readOrNull(source: Meta): ClosedFloatingPointRange? = + source.value?.doubleArray?.let { (start, end) -> + start..end + } override fun convert( obj: ClosedFloatingPointRange, ): Meta = Meta(doubleArrayOf(obj.start, obj.endInclusive).asValue()) } -public val MetaConverter.Companion.doubleRange: MetaConverter> get() = DoubleRangeConverter \ No newline at end of file +public val MetaConverter.Companion.doubleRange: MetaConverter> get() = DoubleRangeConverter + +private object StringListConverter : MetaConverter> { + override fun convert(obj: List): Meta = Meta(obj.map { it.asValue() }.asValue()) + + override fun readOrNull(source: Meta): List? = source.stringList ?: source["@jsonArray"]?.stringList +} + +public val MetaConverter.Companion.stringList: MetaConverter> get() = StringListConverter diff --git a/controls-magix/src/commonMain/kotlin/space/kscience/controls/client/clientPropertyAccess.kt b/controls-magix/src/commonMain/kotlin/space/kscience/controls/client/clientPropertyAccess.kt index 20c59e8..10b1196 100644 --- a/controls-magix/src/commonMain/kotlin/space/kscience/controls/client/clientPropertyAccess.kt +++ b/controls-magix/src/commonMain/kotlin/space/kscience/controls/client/clientPropertyAccess.kt @@ -19,10 +19,13 @@ import space.kscience.dataforge.meta.Meta public suspend fun DeviceClient.read(propertySpec: DevicePropertySpec<*, T>): T = propertySpec.converter.readOrNull(readProperty(propertySpec.name)) ?: error("Property read result is not valid") - public suspend fun DeviceClient.request(propertySpec: DevicePropertySpec<*, T>): T = propertySpec.converter.read(getOrReadProperty(propertySpec.name)) +public fun DeviceClient.getCached(propertySpec: DevicePropertySpec<*, T>): T? = + getProperty(propertySpec.name)?.let { propertySpec.converter.read(it) } + + public suspend fun DeviceClient.write(propertySpec: MutableDevicePropertySpec<*, T>, value: T) { writeProperty(propertySpec.name, propertySpec.converter.convert(value)) } diff --git a/demo/device-collective/src/jvmMain/kotlin/CollectiveDevice.kt b/demo/device-collective/src/jvmMain/kotlin/CollectiveDevice.kt index 9fce04d..941068c 100644 --- a/demo/device-collective/src/jvmMain/kotlin/CollectiveDevice.kt +++ b/demo/device-collective/src/jvmMain/kotlin/CollectiveDevice.kt @@ -3,12 +3,10 @@ package space.kscience.controls.demo.collective 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.constructor.* +import space.kscience.controls.misc.stringList import space.kscience.controls.peer.PeerConnection import space.kscience.controls.spec.DeviceSpec -import space.kscience.controls.spec.unit import space.kscience.dataforge.context.Context import space.kscience.dataforge.meta.MetaConverter import space.kscience.dataforge.meta.Scheme @@ -56,9 +54,16 @@ interface CollectiveDevice : Device { write = { _, value -> setVelocity(value) } ) - val listVisible by action(MetaConverter.unit, MetaConverter.valueList { it.string }) { - listVisible().toList() - } + val visibleNeighbors by property( + MetaConverter.stringList, + read = { + listVisible().toList() + } + ) + +// val listVisible by action(MetaConverter.unit, MetaConverter.valueList { it.string }) { +// listVisible().toList() +// } } } @@ -74,8 +79,28 @@ class CollectiveDeviceConstructor( override val id: CollectiveDeviceId get() = configuration.deviceId - val position = registerAsProperty(CollectiveDevice.position, position.sample(configuration.reportInterval.milliseconds)) - val velocity = registerAsProperty(CollectiveDevice.velocity, velocity.sample(configuration.reportInterval.milliseconds)) + val position = registerAsProperty( + CollectiveDevice.position, + position.sample(configuration.reportInterval.milliseconds) + ) + + val velocity = registerAsProperty( + CollectiveDevice.velocity, + velocity.sample(configuration.reportInterval.milliseconds) + ) + + private val _visibleNeighbors: MutableDeviceState> = stateOf(emptyList()) + + val visibleNeighbors = registerAsProperty( + CollectiveDevice.visibleNeighbors, + _visibleNeighbors.map { it.toList() } + ) + + init { + position.onNext { + _visibleNeighbors.value = observation.invoke().keys + } + } override suspend fun getPosition(): Gmc = position.value diff --git a/demo/device-collective/src/jvmMain/kotlin/DeviceCollectiveModel.kt b/demo/device-collective/src/jvmMain/kotlin/DeviceCollectiveModel.kt index 2c325b4..d8c64dd 100644 --- a/demo/device-collective/src/jvmMain/kotlin/DeviceCollectiveModel.kt +++ b/demo/device-collective/src/jvmMain/kotlin/DeviceCollectiveModel.kt @@ -20,8 +20,6 @@ import space.kscience.magix.api.MagixEndpoint import space.kscience.magix.rsocket.rSocketWithWebSockets import space.kscience.magix.server.startMagixServer import space.kscience.maps.coordinates.* -import kotlin.time.Duration -import kotlin.time.Duration.Companion.milliseconds internal data class CollectiveDeviceState( @@ -46,9 +44,8 @@ internal fun VirtualDeviceState( internal class DeviceCollectiveModel( context: Context, val deviceStates: Collection, - val visibilityRange: Distance = 0.4.kilometers, + val visibilityRange: Distance = 1.kilometers, val radioRange: Distance = 5.kilometers, - val reportInterval: Duration = 1000.milliseconds ) : ModelConstructor(context), PeerConnection { /** @@ -107,6 +104,7 @@ internal fun CoroutineScope.launchCollectiveMagixServer( val server = startMagixServer( // RSocketMagixFlowPlugin() ) + val deviceEndpoint = MagixEndpoint.rSocketWithWebSockets("localhost") collectiveModel.devices.forEach { (id, device) -> val deviceContext = collectiveModel.context.buildContext(id.parseAsName()) { @@ -116,7 +114,7 @@ internal fun CoroutineScope.launchCollectiveMagixServer( deviceContext.install(id, device) - val deviceEndpoint = MagixEndpoint.rSocketWithWebSockets("localhost") +// val deviceEndpoint = MagixEndpoint.rSocketWithWebSockets("localhost") deviceContext.request(DeviceManager).launchMagixService(deviceEndpoint, id) } diff --git a/demo/device-collective/src/jvmMain/kotlin/main.kt b/demo/device-collective/src/jvmMain/kotlin/main.kt index 9aff323..1292a0e 100644 --- a/demo/device-collective/src/jvmMain/kotlin/main.kt +++ b/demo/device-collective/src/jvmMain/kotlin/main.kt @@ -10,6 +10,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.material.Card import androidx.compose.material.Checkbox import androidx.compose.material.Text +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.* import androidx.compose.ui.Alignment @@ -24,9 +25,12 @@ 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.* +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import org.jetbrains.compose.splitpane.ExperimentalSplitPaneApi import org.jetbrains.compose.splitpane.HorizontalSplitPane import org.jetbrains.compose.splitpane.rememberSplitPaneState @@ -47,7 +51,6 @@ import space.kscience.maps.coordinates.Gmc import space.kscience.maps.coordinates.meters import space.kscience.maps.features.* import java.nio.file.Path -import kotlin.time.Duration.Companion.seconds @Composable @@ -55,6 +58,8 @@ fun rememberContext(name: String, contextBuilder: ContextBuilder.() -> Unit = {} Context(name, contextBuilder) } +private val gmcMetaConverter = MetaConverter.serializable() + @Composable fun App() { val scope = rememberCoroutineScope() @@ -82,8 +87,7 @@ fun App() { val magixClient = MagixEndpoint.rSocketWithWebSockets("localhost") client.complete(magixClient) - - collectiveModel.roster.forEach { (id, config) -> + collectiveModel.roster.forEach { (id, config) -> devices[id] = magixClient.remoteDevice(parentContext, "listener", id, id.parseAsName()) } } @@ -129,23 +133,14 @@ fun App() { client.await().subscribe(DeviceManager.magixFormat).onEach { (magixMessage, deviceMessage) -> if (deviceMessage is PropertyChangedMessage && deviceMessage.property == "position") { val id = magixMessage.sourceEndpoint - val position = MetaConverter.serializable().read(deviceMessage.value) + val position = gmcMetaConverter.read(deviceMessage.value) val activeDevice = selectedDeviceId?.let { devices[it] } - suspend fun DeviceClient.idIsVisible() = try { - withTimeout(1.seconds) { - id in execute(CollectiveDevice.listVisible) - } - } catch (ex: Exception) { - ex.printStackTrace() - true - } - if ( activeDevice == null || id == selectedDeviceId || !showOnlyVisible || - activeDevice.idIsVisible() + id in activeDevice.request(CollectiveDevice.visibleNeighbors) ) { rectangle( position, @@ -168,24 +163,29 @@ fun App() { Column( modifier = Modifier.verticalScroll(rememberScrollState()) ) { - devices.forEach { (id, _) -> + collectiveModel.roster.forEach { (id, _) -> Card( elevation = 16.dp, modifier = Modifier.padding(8.dp).onClick { selectedDeviceId = id }.conditional(id == selectedDeviceId) { border(2.dp, Color.Blue) - } + }, ) { Column( modifier = Modifier.padding(8.dp) ) { - Text( - text = id, - fontSize = 16.sp, - fontWeight = FontWeight.Bold, - modifier = Modifier.padding(10.dp).fillMaxWidth() - ) + Row(verticalAlignment = Alignment.CenterVertically) { + if (devices[id] == null) { + CircularProgressIndicator() + } + Text( + text = id, + fontSize = 16.sp, + fontWeight = FontWeight.Bold, + modifier = Modifier.padding(10.dp).fillMaxWidth(), + ) + } if (id == selectedDeviceId) { roster[id]?.let { Text("Meta:", color = Color.Blue, fontWeight = FontWeight.Bold) @@ -195,14 +195,11 @@ fun App() { } currentPosition?.let { currentPosition -> - Text("Широта: ${String.format("%.3f", currentPosition.latitude.toDegrees().value)}") Text( - "Долгота: ${ - String.format( - "%.3f", - currentPosition.longitude.toDegrees().value - ) - }" + "Широта: ${String.format("%.3f", currentPosition.latitude.toDegrees().value)}" + ) + Text( + "Долгота: ${String.format("%.3f", currentPosition.longitude.toDegrees().value)}" ) currentPosition.elevation?.let { Text("Высота: ${String.format("%.1f", it.meters)} м") From 60a693b1b3eaebc0ca9a2e43dbd2b7cbd096a418 Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Sun, 9 Jun 2024 21:12:18 +0300 Subject: [PATCH 06/10] Fix visibility range for collective --- .../src/jvmMain/kotlin/DeviceCollectiveModel.kt | 2 +- .../src/jvmMain/kotlin/debugModel.kt | 2 +- demo/device-collective/src/jvmMain/kotlin/main.kt | 11 ++++++++--- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/demo/device-collective/src/jvmMain/kotlin/DeviceCollectiveModel.kt b/demo/device-collective/src/jvmMain/kotlin/DeviceCollectiveModel.kt index d8c64dd..510ab3e 100644 --- a/demo/device-collective/src/jvmMain/kotlin/DeviceCollectiveModel.kt +++ b/demo/device-collective/src/jvmMain/kotlin/DeviceCollectiveModel.kt @@ -44,7 +44,7 @@ internal fun VirtualDeviceState( internal class DeviceCollectiveModel( context: Context, val deviceStates: Collection, - val visibilityRange: Distance = 1.kilometers, + val visibilityRange: Distance = 0.5.kilometers, val radioRange: Distance = 5.kilometers, ) : ModelConstructor(context), PeerConnection { diff --git a/demo/device-collective/src/jvmMain/kotlin/debugModel.kt b/demo/device-collective/src/jvmMain/kotlin/debugModel.kt index d0ec384..cdca5cd 100644 --- a/demo/device-collective/src/jvmMain/kotlin/debugModel.kt +++ b/demo/device-collective/src/jvmMain/kotlin/debugModel.kt @@ -44,7 +44,7 @@ internal fun generateModel( } } - val model = DeviceCollectiveModel(context, devices, 0.2.kilometers) + val model = DeviceCollectiveModel(context, devices) return model } diff --git a/demo/device-collective/src/jvmMain/kotlin/main.kt b/demo/device-collective/src/jvmMain/kotlin/main.kt index 1292a0e..b05f038 100644 --- a/demo/device-collective/src/jvmMain/kotlin/main.kt +++ b/demo/device-collective/src/jvmMain/kotlin/main.kt @@ -51,6 +51,7 @@ import space.kscience.maps.coordinates.Gmc import space.kscience.maps.coordinates.meters import space.kscience.maps.features.* import java.nio.file.Path +import kotlin.time.Duration.Companion.seconds @Composable @@ -69,7 +70,7 @@ fun App() { } val collectiveModel = remember { - generateModel(parentContext, 60) + generateModel(parentContext, 100, reportInterval = 1.seconds) } val roster = remember { @@ -83,12 +84,16 @@ fun App() { LaunchedEffect(collectiveModel) { launchCollectiveMagixServer(collectiveModel) + withContext(Dispatchers.IO) { val magixClient = MagixEndpoint.rSocketWithWebSockets("localhost") client.complete(magixClient) - collectiveModel.roster.forEach { (id, config) -> - devices[id] = magixClient.remoteDevice(parentContext, "listener", id, id.parseAsName()) + + collectiveModel.roster.forEach { (id, config) -> + scope.launch { + devices[id] = magixClient.remoteDevice(parentContext, "listener", id, id.parseAsName()) + } } } From a5bb42706b1048854bf1223b4bd34f2e2172800e Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Wed, 12 Jun 2024 11:56:27 +0300 Subject: [PATCH 07/10] Change visualization for collective --- .../kscience/controls/peer/PeerConnection.kt | 2 +- .../src/jvmMain/kotlin/CollectiveDevice.kt | 4 +- ...eDeviceState.kt => DebounceDeviceState.kt} | 11 +-- .../jvmMain/kotlin/DeviceCollectiveModel.kt | 39 +++++--- .../src/jvmMain/kotlin/debugModel.kt | 10 +-- .../src/jvmMain/kotlin/main.kt | 88 +++++++++++++------ 6 files changed, 102 insertions(+), 52 deletions(-) rename demo/device-collective/src/jvmMain/kotlin/{SampleDeviceState.kt => DebounceDeviceState.kt} (70%) diff --git a/controls-core/src/commonMain/kotlin/space/kscience/controls/peer/PeerConnection.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/peer/PeerConnection.kt index a831207..55624b7 100644 --- a/controls-core/src/commonMain/kotlin/space/kscience/controls/peer/PeerConnection.kt +++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/peer/PeerConnection.kt @@ -21,7 +21,7 @@ public interface PeerConnection { address: String, contentId: String, requestMeta: Meta = Meta.EMPTY, - ): Envelope + ): Envelope? /** * Send an [envelope] to a device on a given [address] diff --git a/demo/device-collective/src/jvmMain/kotlin/CollectiveDevice.kt b/demo/device-collective/src/jvmMain/kotlin/CollectiveDevice.kt index 941068c..82170b9 100644 --- a/demo/device-collective/src/jvmMain/kotlin/CollectiveDevice.kt +++ b/demo/device-collective/src/jvmMain/kotlin/CollectiveDevice.kt @@ -81,12 +81,12 @@ class CollectiveDeviceConstructor( val position = registerAsProperty( CollectiveDevice.position, - position.sample(configuration.reportInterval.milliseconds) + position.debounce(configuration.reportInterval.milliseconds) ) val velocity = registerAsProperty( CollectiveDevice.velocity, - velocity.sample(configuration.reportInterval.milliseconds) + velocity.debounce(configuration.reportInterval.milliseconds) ) private val _visibleNeighbors: MutableDeviceState> = stateOf(emptyList()) diff --git a/demo/device-collective/src/jvmMain/kotlin/SampleDeviceState.kt b/demo/device-collective/src/jvmMain/kotlin/DebounceDeviceState.kt similarity index 70% rename from demo/device-collective/src/jvmMain/kotlin/SampleDeviceState.kt rename to demo/device-collective/src/jvmMain/kotlin/DebounceDeviceState.kt index 499c903..ac699e6 100644 --- a/demo/device-collective/src/jvmMain/kotlin/SampleDeviceState.kt +++ b/demo/device-collective/src/jvmMain/kotlin/DebounceDeviceState.kt @@ -2,27 +2,28 @@ package space.kscience.controls.demo.collective import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.sample import space.kscience.controls.constructor.DeviceState import space.kscience.controls.constructor.MutableDeviceState import kotlin.time.Duration @OptIn(FlowPreview::class) -class SampleDeviceState( +class DebounceDeviceState( val origin: DeviceState, val interval: Duration, ) : DeviceState { override val value: T by origin::value - override val valueFlow: Flow get() = origin.valueFlow.sample(interval) + override val valueFlow: Flow get() = origin.valueFlow.debounce(interval) override fun toString(): String = "DebounceDeviceState($value, interval=$interval)" } -fun DeviceState.sample(interval: Duration) = SampleDeviceState(this, interval) +fun DeviceState.debounce(interval: Duration) = DebounceDeviceState(this, interval) @OptIn(FlowPreview::class) -class MutableSampleDeviceState( +class MutableDebounceDeviceState( val origin: MutableDeviceState, val interval: Duration, ) : MutableDeviceState { @@ -32,4 +33,4 @@ class MutableSampleDeviceState( override fun toString(): String = "DebounceDeviceState($value, interval=$interval)" } -fun MutableDeviceState.sample(interval: Duration) = MutableSampleDeviceState(this, interval) \ No newline at end of file +fun MutableDeviceState.debounce(interval: Duration) = MutableDebounceDeviceState(this, interval) \ No newline at end of file diff --git a/demo/device-collective/src/jvmMain/kotlin/DeviceCollectiveModel.kt b/demo/device-collective/src/jvmMain/kotlin/DeviceCollectiveModel.kt index 510ab3e..320f477 100644 --- a/demo/device-collective/src/jvmMain/kotlin/DeviceCollectiveModel.kt +++ b/demo/device-collective/src/jvmMain/kotlin/DeviceCollectiveModel.kt @@ -4,16 +4,20 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.launch +import kotlinx.serialization.json.Json +import space.kscience.controls.api.DeviceMessage import space.kscience.controls.client.launchMagixService import space.kscience.controls.constructor.ModelConstructor import space.kscience.controls.constructor.MutableDeviceState import space.kscience.controls.constructor.onTimer import space.kscience.controls.manager.DeviceManager import space.kscience.controls.manager.install +import space.kscience.controls.manager.respondMessage import space.kscience.controls.peer.PeerConnection import space.kscience.dataforge.context.Context import space.kscience.dataforge.context.request import space.kscience.dataforge.io.Envelope +import space.kscience.dataforge.io.toByteArray import space.kscience.dataforge.meta.Meta import space.kscience.dataforge.names.parseAsName import space.kscience.magix.api.MagixEndpoint @@ -40,13 +44,17 @@ internal fun VirtualDeviceState( MutableDeviceState(GmcVelocity.zero) ) +private val json = Json { + ignoreUnknownKeys = true + prettyPrint = true +} internal class DeviceCollectiveModel( context: Context, val deviceStates: Collection, val visibilityRange: Distance = 0.5.kilometers, val radioRange: Distance = 5.kilometers, -) : ModelConstructor(context), PeerConnection { +) : ModelConstructor(context) { /** * Propagate movement @@ -70,32 +78,39 @@ internal class DeviceCollectiveModel( return allCurves.filterValues { it.distance in 0.kilometers..visibilityRange } } + inner class RadioPeerConnection(private val peerState: CollectiveDeviceState) : PeerConnection { + override suspend fun receive(address: String, contentId: String, requestMeta: Meta): Envelope? = null + + override suspend fun send(address: String, envelope: Envelope, requestMeta: Meta) { + devices.filter { it.value.configuration.radioFrequency == address }.filter { + GeoEllipsoid.WGS84.curveBetween(peerState.position.value, it.value.position.value).distance < radioRange + }.forEach { (id, target) -> + check(envelope.data != null) { "Envelope data is empty" } + val message = json.decodeFromString( + DeviceMessage.serializer(), + envelope.data?.toByteArray()?.decodeToString() ?: "" + ) + target.respondMessage(id.parseAsName(), message) + } + } + } + val devices = deviceStates.associate { val device = CollectiveDeviceConstructor( context = context, configuration = it.configuration, position = it.position, velocity = it.velocity, - peerConnection = this, + peerConnection = RadioPeerConnection(it), ) { locateVisible(it.id) } - //start movement program - device.moveInCircles() it.id to device } val roster = deviceStates.associate { it.id to it.configuration } - override suspend fun receive(address: String, contentId: String, requestMeta: Meta): Envelope { - TODO("Not yet implemented") - } - override suspend fun send(address: String, envelope: Envelope, requestMeta: Meta) { -// devices.values.filter { it.configuration.radioFrequency == address }.forEach { device -> -// ``` -// } - } } internal fun CoroutineScope.launchCollectiveMagixServer( diff --git a/demo/device-collective/src/jvmMain/kotlin/debugModel.kt b/demo/device-collective/src/jvmMain/kotlin/debugModel.kt index cdca5cd..c9e59b6 100644 --- a/demo/device-collective/src/jvmMain/kotlin/debugModel.kt +++ b/demo/device-collective/src/jvmMain/kotlin/debugModel.kt @@ -1,10 +1,8 @@ package space.kscience.controls.demo.collective -import kotlinx.coroutines.Job -import kotlinx.coroutines.delay -import kotlinx.coroutines.isActive -import kotlinx.coroutines.launch -import space.kscience.controls.spec.write +import kotlinx.coroutines.* +import space.kscience.controls.client.DeviceClient +import space.kscience.controls.client.write import space.kscience.dataforge.context.Context import space.kscience.kmath.geometry.degrees import space.kscience.kmath.geometry.radians @@ -49,7 +47,7 @@ internal fun generateModel( return model } -fun CollectiveDevice.moveInCircles(): Job = launch { +fun DeviceClient.moveInCircles(scope: CoroutineScope = this): Job = scope.launch { var bearing = Random.nextDouble(-PI, PI).radians write(CollectiveDevice.velocity, GmcVelocity(bearing, deviceVelocity)) while (isActive) { diff --git a/demo/device-collective/src/jvmMain/kotlin/main.kt b/demo/device-collective/src/jvmMain/kotlin/main.kt index b05f038..0d08cc8 100644 --- a/demo/device-collective/src/jvmMain/kotlin/main.kt +++ b/demo/device-collective/src/jvmMain/kotlin/main.kt @@ -7,6 +7,7 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.material.Button import androidx.compose.material.Card import androidx.compose.material.Checkbox import androidx.compose.material.Text @@ -18,19 +19,16 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp 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.CompletableDeferred -import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.* import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext +import kotlinx.coroutines.flow.sample import org.jetbrains.compose.splitpane.ExperimentalSplitPaneApi import org.jetbrains.compose.splitpane.HorizontalSplitPane import org.jetbrains.compose.splitpane.rememberSplitPaneState @@ -51,6 +49,7 @@ import space.kscience.maps.coordinates.Gmc import space.kscience.maps.coordinates.meters import space.kscience.maps.features.* import java.nio.file.Path +import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.seconds @@ -92,7 +91,8 @@ fun App() { collectiveModel.roster.forEach { (id, config) -> scope.launch { - devices[id] = magixClient.remoteDevice(parentContext, "listener", id, id.parseAsName()) + val deviceClient = magixClient.remoteDevice(parentContext, "listener", id, id.parseAsName()) + devices[id] = deviceClient } } } @@ -111,6 +111,8 @@ fun App() { var showOnlyVisible by remember { mutableStateOf(false) } + var movementProgram: Job? by remember { mutableStateOf(null) } + HorizontalSplitPane( splitPaneState = rememberSplitPaneState(0.9f) ) { @@ -126,9 +128,28 @@ fun App() { ) { 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", size = 3.dp) - .color(Color.Red) + device.position.valueFlow.sample(50.milliseconds).onEach { + val activeDevice = selectedDeviceId?.let { devices[it] } + val color = if (selectedDeviceId == device.id) { + Color.Magenta + } else if ( + showOnlyVisible && + activeDevice != null && + device.id in activeDevice.request(CollectiveDevice.visibleNeighbors) + ) { + Color.Cyan + } else { + Color.Red + } + + circle( + device.position.value, + id = device.id + ".position", + size = if (selectedDeviceId == device.id) 6.dp else 3.dp + ) + .color(color) + .modifyAttribute(ZAttribute, 10f) + .modifyAttribute(AlphaAttribute, if (selectedDeviceId == device.id) 1f else 0.5f) .modifyAttribute(AlphaAttribute, 0.5f) // does not work right now }.launchIn(scope) } @@ -139,24 +160,11 @@ fun App() { if (deviceMessage is PropertyChangedMessage && deviceMessage.property == "position") { val id = magixMessage.sourceEndpoint val position = gmcMetaConverter.read(deviceMessage.value) - val activeDevice = selectedDeviceId?.let { devices[it] } - if ( - activeDevice == null || - id == selectedDeviceId || - !showOnlyVisible || - id in activeDevice.request(CollectiveDevice.visibleNeighbors) - ) { - rectangle( - position, - id = id, - size = if (selectedDeviceId == id) DpSize(10.dp, 10.dp) else DpSize(5.dp, 5.dp) - ).color(if (selectedDeviceId == id) Color.Magenta else Color.Blue) - .modifyAttribute(AlphaAttribute, if (selectedDeviceId == id) 1f else 0.5f) - .onClick { selectedDeviceId = id } - } else { - removeFeature(id) - } + rectangle( + position, + id = id, + ).color(Color.Blue).onClick { selectedDeviceId = id } } }.launchIn(scope) @@ -168,6 +176,34 @@ fun App() { Column( modifier = Modifier.verticalScroll(rememberScrollState()) ) { + Button( + onClick = { + if (movementProgram == null) { + //start movement program + movementProgram = parentContext.launch { + devices.values.forEach { device -> + device.moveInCircles(this) + } + } + } else { + movementProgram?.cancel() + parentContext.launch { + devices.values.forEach { device -> + device.write(CollectiveDevice.velocity, GmcVelocity.zero) + } + } + movementProgram = null + } + }, + modifier = Modifier.fillMaxWidth() + ) { + if (movementProgram == null) { + Text("Move") + } else { + Text("Stop") + } + } + collectiveModel.roster.forEach { (id, _) -> Card( elevation = 16.dp, From eb126a60905ced9e30745f4a4e2b3d4f177ee548 Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Wed, 12 Jun 2024 16:31:14 +0300 Subject: [PATCH 08/10] Finalize collective demo --- .../kscience/controls/api/DeviceMessage.kt | 2 +- .../controls/manager/respondMessage.kt | 2 +- .../src/jvmMain/kotlin/CollectiveDevice.kt | 8 +- .../jvmMain/kotlin/DeviceCollectiveModel.kt | 169 +++++++++++++++--- .../src/jvmMain/kotlin/debugModel.kt | 58 ------ .../src/jvmMain/kotlin/main.kt | 44 ++++- 6 files changed, 188 insertions(+), 95 deletions(-) delete mode 100644 demo/device-collective/src/jvmMain/kotlin/debugModel.kt diff --git a/controls-core/src/commonMain/kotlin/space/kscience/controls/api/DeviceMessage.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/api/DeviceMessage.kt index f91beb5..1aeabf6 100644 --- a/controls-core/src/commonMain/kotlin/space/kscience/controls/api/DeviceMessage.kt +++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/api/DeviceMessage.kt @@ -73,7 +73,7 @@ public data class PropertySetMessage( public val property: String, public val value: Meta, override val sourceDevice: Name? = null, - override val targetDevice: Name, + override val targetDevice: Name?, override val comment: String? = null, @EncodeDefault override val time: Instant = Clock.System.now(), ) : DeviceMessage() { diff --git a/controls-core/src/commonMain/kotlin/space/kscience/controls/manager/respondMessage.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/manager/respondMessage.kt index a15bcef..3988c3c 100644 --- a/controls-core/src/commonMain/kotlin/space/kscience/controls/manager/respondMessage.kt +++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/manager/respondMessage.kt @@ -68,7 +68,7 @@ public suspend fun Device.respondMessage(deviceTarget: Name, request: DeviceMess /** * Process incoming [DeviceMessage], using hub naming to find target. - * If the `targetDevice` is `null`, then message is sent to each device in this hub + * If the `targetDevice` is `null`, then the message is sent to each device in this hub */ public suspend fun DeviceHub.respondHubMessage(request: DeviceMessage): List { return try { diff --git a/demo/device-collective/src/jvmMain/kotlin/CollectiveDevice.kt b/demo/device-collective/src/jvmMain/kotlin/CollectiveDevice.kt index 82170b9..c550830 100644 --- a/demo/device-collective/src/jvmMain/kotlin/CollectiveDevice.kt +++ b/demo/device-collective/src/jvmMain/kotlin/CollectiveDevice.kt @@ -23,7 +23,11 @@ class CollectiveDeviceConfiguration(deviceId: CollectiveDeviceId) : Scheme() { var deviceId by string(deviceId) var description by string() var reportInterval by int(500) - var radioFrequency by string(default = "169 MHz") + var radioFrequency by string(default = DEFAULT_FREQUENCY) + + companion object { + const val DEFAULT_FREQUENCY = "169 MHz" + } } typealias CollectiveDeviceRoster = Map @@ -111,4 +115,4 @@ class CollectiveDeviceConstructor( } override suspend fun listVisible(): Collection = observation.invoke().keys -} \ No newline at end of file +} diff --git a/demo/device-collective/src/jvmMain/kotlin/DeviceCollectiveModel.kt b/demo/device-collective/src/jvmMain/kotlin/DeviceCollectiveModel.kt index 320f477..b5e67e4 100644 --- a/demo/device-collective/src/jvmMain/kotlin/DeviceCollectiveModel.kt +++ b/demo/device-collective/src/jvmMain/kotlin/DeviceCollectiveModel.kt @@ -1,12 +1,14 @@ package space.kscience.controls.demo.collective -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import kotlinx.coroutines.launch +import kotlinx.coroutines.* +import kotlinx.io.writeString import kotlinx.serialization.json.Json import space.kscience.controls.api.DeviceMessage +import space.kscience.controls.api.PropertySetMessage +import space.kscience.controls.client.DeviceClient import space.kscience.controls.client.launchMagixService +import space.kscience.controls.client.write +import space.kscience.controls.constructor.DeviceState import space.kscience.controls.constructor.ModelConstructor import space.kscience.controls.constructor.MutableDeviceState import space.kscience.controls.constructor.onTimer @@ -14,18 +16,38 @@ import space.kscience.controls.manager.DeviceManager import space.kscience.controls.manager.install import space.kscience.controls.manager.respondMessage import space.kscience.controls.peer.PeerConnection +import space.kscience.controls.spec.name +import space.kscience.controls.spec.write import space.kscience.dataforge.context.Context import space.kscience.dataforge.context.request import space.kscience.dataforge.io.Envelope import space.kscience.dataforge.io.toByteArray import space.kscience.dataforge.meta.Meta import space.kscience.dataforge.names.parseAsName +import space.kscience.kmath.geometry.degrees +import space.kscience.kmath.geometry.radians import space.kscience.magix.api.MagixEndpoint import space.kscience.magix.rsocket.rSocketWithWebSockets import space.kscience.magix.server.startMagixServer import space.kscience.maps.coordinates.* +import kotlin.math.PI +import kotlin.random.Random +import kotlin.time.Duration +import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.Duration.Companion.seconds + +private val deviceVelocity = 0.1.kilometers + +private val center = Gmc.ofDegrees(55.925, 37.514) +private val radius = 0.01.degrees + +private val json = Json { + ignoreUnknownKeys = true + prettyPrint = true +} + internal data class CollectiveDeviceState( val id: CollectiveDeviceId, val configuration: CollectiveDeviceConfiguration, @@ -33,7 +55,7 @@ internal data class CollectiveDeviceState( val velocity: MutableDeviceState, ) -internal fun VirtualDeviceState( +internal fun CollectiveDeviceState( id: CollectiveDeviceId, position: Gmc, configuration: CollectiveDeviceConfiguration.() -> Unit = {}, @@ -44,16 +66,11 @@ internal fun VirtualDeviceState( MutableDeviceState(GmcVelocity.zero) ) -private val json = Json { - ignoreUnknownKeys = true - prettyPrint = true -} - internal class DeviceCollectiveModel( context: Context, val deviceStates: Collection, val visibilityRange: Distance = 0.5.kilometers, - val radioRange: Distance = 5.kilometers, + val radioRange: Distance = 1.kilometers, ) : ModelConstructor(context) { /** @@ -78,41 +95,89 @@ internal class DeviceCollectiveModel( return allCurves.filterValues { it.distance in 0.kilometers..visibilityRange } } - inner class RadioPeerConnection(private val peerState: CollectiveDeviceState) : PeerConnection { + inner class RadioPeerConnectionModel(private val position: DeviceState) : PeerConnection { override suspend fun receive(address: String, contentId: String, requestMeta: Meta): Envelope? = null override suspend fun send(address: String, envelope: Envelope, requestMeta: Meta) { - devices.filter { it.value.configuration.radioFrequency == address }.filter { - GeoEllipsoid.WGS84.curveBetween(peerState.position.value, it.value.position.value).distance < radioRange - }.forEach { (id, target) -> + devices.values.filter { it.configuration.radioFrequency == address }.filter { + GeoEllipsoid.WGS84.curveBetween(position.value, it.position.value).distance < radioRange + }.forEach { target -> check(envelope.data != null) { "Envelope data is empty" } val message = json.decodeFromString( DeviceMessage.serializer(), envelope.data?.toByteArray()?.decodeToString() ?: "" ) - target.respondMessage(id.parseAsName(), message) + target.respondMessage(target.configuration.deviceId.parseAsName(), message) } } } - val devices = deviceStates.associate { + val devices = deviceStates.associate { state -> val device = CollectiveDeviceConstructor( context = context, - configuration = it.configuration, - position = it.position, - velocity = it.velocity, - peerConnection = RadioPeerConnection(it), + configuration = state.configuration, + position = state.position, + velocity = state.velocity, + peerConnection = RadioPeerConnectionModel(state.position), ) { - locateVisible(it.id) + locateVisible(state.id) } - it.id to device + state.id to device + } + + internal fun createTrawler(position: Gmc, id: CollectiveDeviceId = "trawler"): CollectiveDeviceConstructor { + val state = CollectiveDeviceState( + id = id, + configuration = CollectiveDeviceConfiguration(id), + position = MutableDeviceState(position), + velocity = MutableDeviceState(GmcVelocity.zero) + ) + + val result = CollectiveDeviceConstructor( + context = context, + configuration = state.configuration, + position = state.position, + velocity = state.velocity, + peerConnection = RadioPeerConnectionModel(state.position), + ) { + locateVisible(state.id) + } + + // TODO move to CollectiveDeviceState + onTimer { prev, next -> + val delta = (next - prev) + state.position.value = state.position.value.moveWith(state.velocity.value, delta) + } + + result.onTimer(1.seconds) { _, _ -> + val envelope = Envelope { + data { + writeString( + json.encodeToString( + DeviceMessage.serializer(), + PropertySetMessage( + property = CollectiveDevice.velocity.name, + value = gmcVelocityMetaConverter.convert(state.velocity.value), + targetDevice = null + ) + ) + ) + } + } + + result.peerConnection.send( + CollectiveDeviceConfiguration.DEFAULT_FREQUENCY, + envelope + ) + } + + return result } val roster = deviceStates.associate { it.id to it.configuration } - - } + internal fun CoroutineScope.launchCollectiveMagixServer( collectiveModel: DeviceCollectiveModel, ): Job = launch(Dispatchers.IO) { @@ -133,4 +198,58 @@ internal fun CoroutineScope.launchCollectiveMagixServer( deviceContext.request(DeviceManager).launchMagixService(deviceEndpoint, id) } +} + + +internal fun generateModel( + context: Context, + size: Int = 50, + reportInterval: Duration = 500.milliseconds, + additionalConfiguration: CollectiveDeviceConfiguration.() -> Unit = {}, +): DeviceCollectiveModel { + val devices: List = List(size) { index -> + val id = "device[$index]" + + CollectiveDeviceState( + id = id, + Gmc( + center.latitude + radius * Random.nextDouble(), + center.longitude + radius * Random.nextDouble() + ) + ) { + deviceId = id + description = "Virtual remote device $id" + this.reportInterval = reportInterval.inWholeMilliseconds.toInt() + additionalConfiguration() + } + } + + val model = DeviceCollectiveModel(context, devices) + + return model +} + +fun DeviceClient.moveInCircles(scope: CoroutineScope = this): Job = scope.launch { + var bearing = Random.nextDouble(-PI, PI).radians + write(CollectiveDevice.velocity, GmcVelocity(bearing, deviceVelocity)) + while (isActive) { + delay(500) + bearing += 5.degrees + write(CollectiveDevice.velocity, GmcVelocity(bearing, deviceVelocity)) + } +} + + +internal fun CollectiveDeviceConstructor.moveTo( + targetPosition: Gmc, + speedLimit: Distance = deviceVelocity, + scope: CoroutineScope = this, +): Job = scope.launch { + do { + val curve = GeoEllipsoid.WGS84.curveBetween(position.value, targetPosition) + write(CollectiveDevice.velocity, GmcVelocity(curve.forward.bearing, speedLimit)) + delay(1.seconds) + } while (curve.distance > 0.1.kilometers) + write(CollectiveDevice.velocity, GmcVelocity.zero) + } \ No newline at end of file diff --git a/demo/device-collective/src/jvmMain/kotlin/debugModel.kt b/demo/device-collective/src/jvmMain/kotlin/debugModel.kt deleted file mode 100644 index c9e59b6..0000000 --- a/demo/device-collective/src/jvmMain/kotlin/debugModel.kt +++ /dev/null @@ -1,58 +0,0 @@ -package space.kscience.controls.demo.collective - -import kotlinx.coroutines.* -import space.kscience.controls.client.DeviceClient -import space.kscience.controls.client.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 -import kotlin.time.Duration -import kotlin.time.Duration.Companion.milliseconds - -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, - size: Int = 50, - reportInterval: Duration = 500.milliseconds, - additionalConfiguration: CollectiveDeviceConfiguration.() -> Unit = {}, -): DeviceCollectiveModel { - val devices: List = List(size) { index -> - val id = "device[$index]" - - VirtualDeviceState( - id = id, - Gmc( - center.latitude + radius * Random.nextDouble(), - center.longitude + radius * Random.nextDouble() - ) - ) { - deviceId = id - description = "Virtual remote device $id" - this.reportInterval = reportInterval.inWholeMilliseconds.toInt() - additionalConfiguration() - } - } - - val model = DeviceCollectiveModel(context, devices) - - return model -} - -fun DeviceClient.moveInCircles(scope: CoroutineScope = this): Job = scope.launch { - var bearing = Random.nextDouble(-PI, PI).radians - write(CollectiveDevice.velocity, GmcVelocity(bearing, deviceVelocity)) - while (isActive) { - delay(500) - bearing += 5.degrees - write(CollectiveDevice.velocity, GmcVelocity(bearing, deviceVelocity)) - } -} \ No newline at end of file diff --git a/demo/device-collective/src/jvmMain/kotlin/main.kt b/demo/device-collective/src/jvmMain/kotlin/main.kt index 0d08cc8..f92b0eb 100644 --- a/demo/device-collective/src/jvmMain/kotlin/main.kt +++ b/demo/device-collective/src/jvmMain/kotlin/main.kt @@ -7,16 +7,14 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.material.Button -import androidx.compose.material.Card -import androidx.compose.material.Checkbox -import androidx.compose.material.Text +import androidx.compose.material.* import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.pointer.isSecondaryPressed import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp @@ -58,7 +56,8 @@ fun rememberContext(name: String, contextBuilder: ContextBuilder.() -> Unit = {} Context(name, contextBuilder) } -private val gmcMetaConverter = MetaConverter.serializable() +internal val gmcMetaConverter = MetaConverter.serializable() +internal val gmcVelocityMetaConverter = MetaConverter.serializable() @Composable fun App() { @@ -80,7 +79,6 @@ fun App() { val devices = remember { mutableStateMapOf() } - LaunchedEffect(collectiveModel) { launchCollectiveMagixServer(collectiveModel) @@ -113,10 +111,27 @@ fun App() { var movementProgram: Job? by remember { mutableStateOf(null) } + val trawler: CollectiveDeviceConstructor = remember { + collectiveModel.createTrawler(Gmc.ofDegrees(55.925, 37.50)) + } + HorizontalSplitPane( splitPaneState = rememberSplitPaneState(0.9f) ) { first(400.dp) { + var clickPoint by remember { mutableStateOf(null) } + + CursorDropdownMenu(clickPoint != null, { clickPoint = null }) { + clickPoint?.let { point -> + TextButton({ + trawler.moveTo(point) + clickPoint = null + }) { + Text("Move trawler here") + } + } + } + MapView( mapTileProvider = remember { OpenStreetMapTileProvider( @@ -124,8 +139,15 @@ fun App() { cacheDirectory = Path.of("mapCache") ) }, - config = ViewConfig() + config = ViewConfig( + onClick = { event, point -> + if (event.buttons.isSecondaryPressed) { + clickPoint = point.focus + } + } + ) ) { + //draw real positions collectiveModel.deviceStates.forEach { device -> circle(device.position.value, id = device.id + ".position").color(Color.Red) device.position.valueFlow.sample(50.milliseconds).onEach { @@ -154,8 +176,8 @@ fun App() { }.launchIn(scope) } + //draw received data scope.launch { - client.await().subscribe(DeviceManager.magixFormat).onEach { (magixMessage, deviceMessage) -> if (deviceMessage is PropertyChangedMessage && deviceMessage.property == "position") { val id = magixMessage.sourceEndpoint @@ -169,6 +191,12 @@ fun App() { }.launchIn(scope) } + + // draw trawler + + trawler.position.valueFlow.onEach { + circle(it, id = "trawler").color(Color.Black) + }.launchIn(scope) } } second(200.dp) { From 92c4355f483769af77ef0c6e938366428807e1df Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Mon, 17 Jun 2024 17:49:12 +0300 Subject: [PATCH 09/10] update device-collective readme --- README.md | 9 +++++++++ controls-constructor/README.md | 4 ++-- controls-core/README.md | 4 ++-- controls-jupyter/README.md | 4 ++-- controls-magix/README.md | 4 ++-- controls-modbus/README.md | 4 ++-- controls-opcua/README.md | 4 ++-- controls-pi/README.md | 4 ++-- controls-ports-ktor/README.md | 4 ++-- controls-serial/README.md | 4 ++-- controls-server/README.md | 4 ++-- controls-storage/README.md | 4 ++-- controls-storage/controls-xodus/README.md | 4 ++-- controls-vision/README.md | 6 ++---- demo/device-collective/README.md | 13 +++++++++++++ magix/magix-api/README.md | 4 ++-- magix/magix-java-endpoint/README.md | 4 ++-- magix/magix-mqtt/README.md | 4 ++-- magix/magix-rabbit/README.md | 4 ++-- magix/magix-rsocket/README.md | 4 ++-- magix/magix-server/README.md | 4 ++-- magix/magix-storage/README.md | 4 ++-- magix/magix-storage/magix-storage-xodus/README.md | 4 ++-- magix/magix-zmq/README.md | 4 ++-- 24 files changed, 66 insertions(+), 46 deletions(-) create mode 100644 demo/device-collective/README.md diff --git a/README.md b/README.md index d5baaf9..6b9c9cd 100644 --- a/README.md +++ b/README.md @@ -134,6 +134,11 @@ Automatically checks consistency. > > **Maturity**: PROTOTYPE +### [controls-visualisation-compose](controls-visualisation-compose) +> Visualisation extension using compose-multiplatform +> +> **Maturity**: PROTOTYPE + ### [demo](demo) > > **Maturity**: EXPERIMENTAL @@ -159,6 +164,10 @@ Automatically checks consistency. > > **Maturity**: EXPERIMENTAL +### [demo/device-collective](demo/device-collective) +> +> **Maturity**: EXPERIMENTAL + ### [demo/echo](demo/echo) > > **Maturity**: EXPERIMENTAL diff --git a/controls-constructor/README.md b/controls-constructor/README.md index d388b01..7da4d0b 100644 --- a/controls-constructor/README.md +++ b/controls-constructor/README.md @@ -6,7 +6,7 @@ A low-code constructor for composite devices simulation ## Artifact: -The Maven coordinates of this project are `space.kscience:controls-constructor:0.4.0-dev-1`. +The Maven coordinates of this project are `space.kscience:controls-constructor:0.4.0-dev-4`. **Gradle Kotlin DSL:** ```kotlin @@ -16,6 +16,6 @@ repositories { } dependencies { - implementation("space.kscience:controls-constructor:0.4.0-dev-1") + implementation("space.kscience:controls-constructor:0.4.0-dev-4") } ``` diff --git a/controls-core/README.md b/controls-core/README.md index 71caf53..26ed618 100644 --- a/controls-core/README.md +++ b/controls-core/README.md @@ -16,7 +16,7 @@ Core interfaces for building a device server ## Artifact: -The Maven coordinates of this project are `space.kscience:controls-core:0.4.0-dev-1`. +The Maven coordinates of this project are `space.kscience:controls-core:0.4.0-dev-4`. **Gradle Kotlin DSL:** ```kotlin @@ -26,6 +26,6 @@ repositories { } dependencies { - implementation("space.kscience:controls-core:0.4.0-dev-1") + implementation("space.kscience:controls-core:0.4.0-dev-4") } ``` diff --git a/controls-jupyter/README.md b/controls-jupyter/README.md index 7d0fc4f..6100ddd 100644 --- a/controls-jupyter/README.md +++ b/controls-jupyter/README.md @@ -6,7 +6,7 @@ ## Artifact: -The Maven coordinates of this project are `space.kscience:controls-jupyter:0.4.0-dev-1`. +The Maven coordinates of this project are `space.kscience:controls-jupyter:0.4.0-dev-4`. **Gradle Kotlin DSL:** ```kotlin @@ -16,6 +16,6 @@ repositories { } dependencies { - implementation("space.kscience:controls-jupyter:0.4.0-dev-1") + implementation("space.kscience:controls-jupyter:0.4.0-dev-4") } ``` diff --git a/controls-magix/README.md b/controls-magix/README.md index 8c16ffd..c474221 100644 --- a/controls-magix/README.md +++ b/controls-magix/README.md @@ -12,7 +12,7 @@ Magix service for binding controls devices (both as RPC client and server) ## Artifact: -The Maven coordinates of this project are `space.kscience:controls-magix:0.4.0-dev-1`. +The Maven coordinates of this project are `space.kscience:controls-magix:0.4.0-dev-4`. **Gradle Kotlin DSL:** ```kotlin @@ -22,6 +22,6 @@ repositories { } dependencies { - implementation("space.kscience:controls-magix:0.4.0-dev-1") + implementation("space.kscience:controls-magix:0.4.0-dev-4") } ``` diff --git a/controls-modbus/README.md b/controls-modbus/README.md index 61e4b60..9705e81 100644 --- a/controls-modbus/README.md +++ b/controls-modbus/README.md @@ -14,7 +14,7 @@ Automatically checks consistency. ## Artifact: -The Maven coordinates of this project are `space.kscience:controls-modbus:0.4.0-dev-1`. +The Maven coordinates of this project are `space.kscience:controls-modbus:0.4.0-dev-4`. **Gradle Kotlin DSL:** ```kotlin @@ -24,6 +24,6 @@ repositories { } dependencies { - implementation("space.kscience:controls-modbus:0.4.0-dev-1") + implementation("space.kscience:controls-modbus:0.4.0-dev-4") } ``` diff --git a/controls-opcua/README.md b/controls-opcua/README.md index 03dc32b..6867492 100644 --- a/controls-opcua/README.md +++ b/controls-opcua/README.md @@ -12,7 +12,7 @@ A client and server connectors for OPC-UA via Eclipse Milo ## Artifact: -The Maven coordinates of this project are `space.kscience:controls-opcua:0.4.0-dev-1`. +The Maven coordinates of this project are `space.kscience:controls-opcua:0.4.0-dev-4`. **Gradle Kotlin DSL:** ```kotlin @@ -22,6 +22,6 @@ repositories { } dependencies { - implementation("space.kscience:controls-opcua:0.4.0-dev-1") + implementation("space.kscience:controls-opcua:0.4.0-dev-4") } ``` diff --git a/controls-pi/README.md b/controls-pi/README.md index 2873ac2..0afb324 100644 --- a/controls-pi/README.md +++ b/controls-pi/README.md @@ -6,7 +6,7 @@ Utils to work with controls-kt on Raspberry pi ## Artifact: -The Maven coordinates of this project are `space.kscience:controls-pi:0.4.0-dev-1`. +The Maven coordinates of this project are `space.kscience:controls-pi:0.4.0-dev-4`. **Gradle Kotlin DSL:** ```kotlin @@ -16,6 +16,6 @@ repositories { } dependencies { - implementation("space.kscience:controls-pi:0.4.0-dev-1") + implementation("space.kscience:controls-pi:0.4.0-dev-4") } ``` diff --git a/controls-ports-ktor/README.md b/controls-ports-ktor/README.md index 6b23d80..1cb277a 100644 --- a/controls-ports-ktor/README.md +++ b/controls-ports-ktor/README.md @@ -6,7 +6,7 @@ Implementation of byte ports on top os ktor-io asynchronous API ## Artifact: -The Maven coordinates of this project are `space.kscience:controls-ports-ktor:0.4.0-dev-1`. +The Maven coordinates of this project are `space.kscience:controls-ports-ktor:0.4.0-dev-4`. **Gradle Kotlin DSL:** ```kotlin @@ -16,6 +16,6 @@ repositories { } dependencies { - implementation("space.kscience:controls-ports-ktor:0.4.0-dev-1") + implementation("space.kscience:controls-ports-ktor:0.4.0-dev-4") } ``` diff --git a/controls-serial/README.md b/controls-serial/README.md index a961f55..ce95ee5 100644 --- a/controls-serial/README.md +++ b/controls-serial/README.md @@ -6,7 +6,7 @@ Implementation of direct serial port communication with JSerialComm ## Artifact: -The Maven coordinates of this project are `space.kscience:controls-serial:0.4.0-dev-1`. +The Maven coordinates of this project are `space.kscience:controls-serial:0.4.0-dev-4`. **Gradle Kotlin DSL:** ```kotlin @@ -16,6 +16,6 @@ repositories { } dependencies { - implementation("space.kscience:controls-serial:0.4.0-dev-1") + implementation("space.kscience:controls-serial:0.4.0-dev-4") } ``` diff --git a/controls-server/README.md b/controls-server/README.md index 114f618..896020d 100644 --- a/controls-server/README.md +++ b/controls-server/README.md @@ -6,7 +6,7 @@ A combined Magix event loop server with web server for visualization. ## Artifact: -The Maven coordinates of this project are `space.kscience:controls-server:0.4.0-dev-1`. +The Maven coordinates of this project are `space.kscience:controls-server:0.4.0-dev-4`. **Gradle Kotlin DSL:** ```kotlin @@ -16,6 +16,6 @@ repositories { } dependencies { - implementation("space.kscience:controls-server:0.4.0-dev-1") + implementation("space.kscience:controls-server:0.4.0-dev-4") } ``` diff --git a/controls-storage/README.md b/controls-storage/README.md index 64751e8..dd4ab3d 100644 --- a/controls-storage/README.md +++ b/controls-storage/README.md @@ -6,7 +6,7 @@ An API for stand-alone Controls-kt device or a hub. ## Artifact: -The Maven coordinates of this project are `space.kscience:controls-storage:0.4.0-dev-1`. +The Maven coordinates of this project are `space.kscience:controls-storage:0.4.0-dev-4`. **Gradle Kotlin DSL:** ```kotlin @@ -16,6 +16,6 @@ repositories { } dependencies { - implementation("space.kscience:controls-storage:0.4.0-dev-1") + implementation("space.kscience:controls-storage:0.4.0-dev-4") } ``` diff --git a/controls-storage/controls-xodus/README.md b/controls-storage/controls-xodus/README.md index e423c3f..44cdf26 100644 --- a/controls-storage/controls-xodus/README.md +++ b/controls-storage/controls-xodus/README.md @@ -6,7 +6,7 @@ An implementation of controls-storage on top of JetBrains Xodus. ## Artifact: -The Maven coordinates of this project are `space.kscience:controls-xodus:0.4.0-dev-1`. +The Maven coordinates of this project are `space.kscience:controls-xodus:0.4.0-dev-4`. **Gradle Kotlin DSL:** ```kotlin @@ -16,6 +16,6 @@ repositories { } dependencies { - implementation("space.kscience:controls-xodus:0.4.0-dev-1") + implementation("space.kscience:controls-xodus:0.4.0-dev-4") } ``` diff --git a/controls-vision/README.md b/controls-vision/README.md index cbfc917..ff2b648 100644 --- a/controls-vision/README.md +++ b/controls-vision/README.md @@ -2,13 +2,11 @@ Dashboard and visualization extensions for devices -Hello world! - ## Usage ## Artifact: -The Maven coordinates of this project are `space.kscience:controls-vision:0.4.0-dev-1`. +The Maven coordinates of this project are `space.kscience:controls-vision:0.4.0-dev-4`. **Gradle Kotlin DSL:** ```kotlin @@ -18,6 +16,6 @@ repositories { } dependencies { - implementation("space.kscience:controls-vision:0.4.0-dev-1") + implementation("space.kscience:controls-vision:0.4.0-dev-4") } ``` diff --git a/demo/device-collective/README.md b/demo/device-collective/README.md new file mode 100644 index 0000000..5f639a7 --- /dev/null +++ b/demo/device-collective/README.md @@ -0,0 +1,13 @@ +# Module device-collective + +# Running demo from gradle + +1. Install JDK 17-21 in the system (for example, from https://sdkman.io/jdks or https://github.com/ScoopInstaller/Java). +2. Clone the repository with Git. +3. Run `./gradlew :demo:device-collective:run` from the project root directory. + +# Install distribution + +1 and 2 from above. +3. Run `./gradlew :demo:device-collective:packageUberJarForCurrentOS` from the project root directory. +4. Go to `build/compose/jars/device-collective--.jar`. You can copy it and run with `java -jar .jar` diff --git a/magix/magix-api/README.md b/magix/magix-api/README.md index 6228a7e..6f16ac0 100644 --- a/magix/magix-api/README.md +++ b/magix/magix-api/README.md @@ -6,7 +6,7 @@ A kotlin API for magix standard and some zero-dependency magix services ## Artifact: -The Maven coordinates of this project are `space.kscience:magix-api:0.4.0-dev-1`. +The Maven coordinates of this project are `space.kscience:magix-api:0.4.0-dev-4`. **Gradle Kotlin DSL:** ```kotlin @@ -16,6 +16,6 @@ repositories { } dependencies { - implementation("space.kscience:magix-api:0.4.0-dev-1") + implementation("space.kscience:magix-api:0.4.0-dev-4") } ``` diff --git a/magix/magix-java-endpoint/README.md b/magix/magix-java-endpoint/README.md index 2db1d42..87359f1 100644 --- a/magix/magix-java-endpoint/README.md +++ b/magix/magix-java-endpoint/README.md @@ -6,7 +6,7 @@ Java API to work with magix endpoints without Kotlin ## Artifact: -The Maven coordinates of this project are `space.kscience:magix-java-endpoint:0.4.0-dev-1`. +The Maven coordinates of this project are `space.kscience:magix-java-endpoint:0.4.0-dev-4`. **Gradle Kotlin DSL:** ```kotlin @@ -16,6 +16,6 @@ repositories { } dependencies { - implementation("space.kscience:magix-java-endpoint:0.4.0-dev-1") + implementation("space.kscience:magix-java-endpoint:0.4.0-dev-4") } ``` diff --git a/magix/magix-mqtt/README.md b/magix/magix-mqtt/README.md index 7896472..0ee054a 100644 --- a/magix/magix-mqtt/README.md +++ b/magix/magix-mqtt/README.md @@ -6,7 +6,7 @@ MQTT client magix endpoint ## Artifact: -The Maven coordinates of this project are `space.kscience:magix-mqtt:0.4.0-dev-1`. +The Maven coordinates of this project are `space.kscience:magix-mqtt:0.4.0-dev-4`. **Gradle Kotlin DSL:** ```kotlin @@ -16,6 +16,6 @@ repositories { } dependencies { - implementation("space.kscience:magix-mqtt:0.4.0-dev-1") + implementation("space.kscience:magix-mqtt:0.4.0-dev-4") } ``` diff --git a/magix/magix-rabbit/README.md b/magix/magix-rabbit/README.md index eee4a21..35b43c7 100644 --- a/magix/magix-rabbit/README.md +++ b/magix/magix-rabbit/README.md @@ -6,7 +6,7 @@ RabbitMQ client magix endpoint ## Artifact: -The Maven coordinates of this project are `space.kscience:magix-rabbit:0.4.0-dev-1`. +The Maven coordinates of this project are `space.kscience:magix-rabbit:0.4.0-dev-4`. **Gradle Kotlin DSL:** ```kotlin @@ -16,6 +16,6 @@ repositories { } dependencies { - implementation("space.kscience:magix-rabbit:0.4.0-dev-1") + implementation("space.kscience:magix-rabbit:0.4.0-dev-4") } ``` diff --git a/magix/magix-rsocket/README.md b/magix/magix-rsocket/README.md index ef16b66..9f3a42c 100644 --- a/magix/magix-rsocket/README.md +++ b/magix/magix-rsocket/README.md @@ -6,7 +6,7 @@ Magix endpoint (client) based on RSocket ## Artifact: -The Maven coordinates of this project are `space.kscience:magix-rsocket:0.4.0-dev-1`. +The Maven coordinates of this project are `space.kscience:magix-rsocket:0.4.0-dev-4`. **Gradle Kotlin DSL:** ```kotlin @@ -16,6 +16,6 @@ repositories { } dependencies { - implementation("space.kscience:magix-rsocket:0.4.0-dev-1") + implementation("space.kscience:magix-rsocket:0.4.0-dev-4") } ``` diff --git a/magix/magix-server/README.md b/magix/magix-server/README.md index 17bdbdf..8259e0f 100644 --- a/magix/magix-server/README.md +++ b/magix/magix-server/README.md @@ -6,7 +6,7 @@ A magix event loop implementation in Kotlin. Includes HTTP/SSE and RSocket route ## Artifact: -The Maven coordinates of this project are `space.kscience:magix-server:0.4.0-dev-1`. +The Maven coordinates of this project are `space.kscience:magix-server:0.4.0-dev-4`. **Gradle Kotlin DSL:** ```kotlin @@ -16,6 +16,6 @@ repositories { } dependencies { - implementation("space.kscience:magix-server:0.4.0-dev-1") + implementation("space.kscience:magix-server:0.4.0-dev-4") } ``` diff --git a/magix/magix-storage/README.md b/magix/magix-storage/README.md index dbb2729..672458e 100644 --- a/magix/magix-storage/README.md +++ b/magix/magix-storage/README.md @@ -6,7 +6,7 @@ Magix history database API ## Artifact: -The Maven coordinates of this project are `space.kscience:magix-storage:0.4.0-dev-1`. +The Maven coordinates of this project are `space.kscience:magix-storage:0.4.0-dev-4`. **Gradle Kotlin DSL:** ```kotlin @@ -16,6 +16,6 @@ repositories { } dependencies { - implementation("space.kscience:magix-storage:0.4.0-dev-1") + implementation("space.kscience:magix-storage:0.4.0-dev-4") } ``` diff --git a/magix/magix-storage/magix-storage-xodus/README.md b/magix/magix-storage/magix-storage-xodus/README.md index 90a0050..2fb495a 100644 --- a/magix/magix-storage/magix-storage-xodus/README.md +++ b/magix/magix-storage/magix-storage-xodus/README.md @@ -6,7 +6,7 @@ ## Artifact: -The Maven coordinates of this project are `space.kscience:magix-storage-xodus:0.4.0-dev-1`. +The Maven coordinates of this project are `space.kscience:magix-storage-xodus:0.4.0-dev-4`. **Gradle Kotlin DSL:** ```kotlin @@ -16,6 +16,6 @@ repositories { } dependencies { - implementation("space.kscience:magix-storage-xodus:0.4.0-dev-1") + implementation("space.kscience:magix-storage-xodus:0.4.0-dev-4") } ``` diff --git a/magix/magix-zmq/README.md b/magix/magix-zmq/README.md index 6e924db..952e02e 100644 --- a/magix/magix-zmq/README.md +++ b/magix/magix-zmq/README.md @@ -6,7 +6,7 @@ ZMQ client endpoint for Magix ## Artifact: -The Maven coordinates of this project are `space.kscience:magix-zmq:0.4.0-dev-1`. +The Maven coordinates of this project are `space.kscience:magix-zmq:0.4.0-dev-4`. **Gradle Kotlin DSL:** ```kotlin @@ -16,6 +16,6 @@ repositories { } dependencies { - implementation("space.kscience:magix-zmq:0.4.0-dev-1") + implementation("space.kscience:magix-zmq:0.4.0-dev-4") } ``` From f13b7268d6f33a93842f64815f4444959ced2bad Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Mon, 17 Jun 2024 17:52:02 +0300 Subject: [PATCH 10/10] update device-collective readme --- demo/device-collective/README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/demo/device-collective/README.md b/demo/device-collective/README.md index 5f639a7..369ec04 100644 --- a/demo/device-collective/README.md +++ b/demo/device-collective/README.md @@ -8,6 +8,7 @@ # Install distribution -1 and 2 from above. +1. Install JDK 17-21 in the system (for example, from https://sdkman.io/jdks or https://github.com/ScoopInstaller/Java). +2. Clone the repository with Git. 3. Run `./gradlew :demo:device-collective:packageUberJarForCurrentOS` from the project root directory. 4. Go to `build/compose/jars/device-collective--.jar`. You can copy it and run with `java -jar .jar`