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,