Change visualization for collective

This commit is contained in:
Alexander Nozik 2024-06-12 11:56:27 +03:00
parent 60a693b1b3
commit a5bb42706b
6 changed files with 102 additions and 52 deletions

View File

@ -21,7 +21,7 @@ public interface PeerConnection {
address: String, address: String,
contentId: String, contentId: String,
requestMeta: Meta = Meta.EMPTY, requestMeta: Meta = Meta.EMPTY,
): Envelope ): Envelope?
/** /**
* Send an [envelope] to a device on a given [address] * Send an [envelope] to a device on a given [address]

View File

@ -81,12 +81,12 @@ class CollectiveDeviceConstructor(
val position = registerAsProperty( val position = registerAsProperty(
CollectiveDevice.position, CollectiveDevice.position,
position.sample(configuration.reportInterval.milliseconds) position.debounce(configuration.reportInterval.milliseconds)
) )
val velocity = registerAsProperty( val velocity = registerAsProperty(
CollectiveDevice.velocity, CollectiveDevice.velocity,
velocity.sample(configuration.reportInterval.milliseconds) velocity.debounce(configuration.reportInterval.milliseconds)
) )
private val _visibleNeighbors: MutableDeviceState<Collection<CollectiveDeviceId>> = stateOf(emptyList()) private val _visibleNeighbors: MutableDeviceState<Collection<CollectiveDeviceId>> = stateOf(emptyList())

View File

@ -2,27 +2,28 @@ package space.kscience.controls.demo.collective
import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.sample import kotlinx.coroutines.flow.sample
import space.kscience.controls.constructor.DeviceState import space.kscience.controls.constructor.DeviceState
import space.kscience.controls.constructor.MutableDeviceState import space.kscience.controls.constructor.MutableDeviceState
import kotlin.time.Duration import kotlin.time.Duration
@OptIn(FlowPreview::class) @OptIn(FlowPreview::class)
class SampleDeviceState<T>( class DebounceDeviceState<T>(
val origin: DeviceState<T>, val origin: DeviceState<T>,
val interval: Duration, val interval: Duration,
) : DeviceState<T> { ) : DeviceState<T> {
override val value: T by origin::value override val value: T by origin::value
override val valueFlow: Flow<T> get() = origin.valueFlow.sample(interval) override val valueFlow: Flow<T> get() = origin.valueFlow.debounce(interval)
override fun toString(): String = "DebounceDeviceState($value, interval=$interval)" override fun toString(): String = "DebounceDeviceState($value, interval=$interval)"
} }
fun <T> DeviceState<T>.sample(interval: Duration) = SampleDeviceState(this, interval) fun <T> DeviceState<T>.debounce(interval: Duration) = DebounceDeviceState(this, interval)
@OptIn(FlowPreview::class) @OptIn(FlowPreview::class)
class MutableSampleDeviceState<T>( class MutableDebounceDeviceState<T>(
val origin: MutableDeviceState<T>, val origin: MutableDeviceState<T>,
val interval: Duration, val interval: Duration,
) : MutableDeviceState<T> { ) : MutableDeviceState<T> {
@ -32,4 +33,4 @@ class MutableSampleDeviceState<T>(
override fun toString(): String = "DebounceDeviceState($value, interval=$interval)" override fun toString(): String = "DebounceDeviceState($value, interval=$interval)"
} }
fun <T> MutableDeviceState<T>.sample(interval: Duration) = MutableSampleDeviceState(this, interval) fun <T> MutableDeviceState<T>.debounce(interval: Duration) = MutableDebounceDeviceState(this, interval)

View File

@ -4,16 +4,20 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.launch 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.client.launchMagixService
import space.kscience.controls.constructor.ModelConstructor import space.kscience.controls.constructor.ModelConstructor
import space.kscience.controls.constructor.MutableDeviceState import space.kscience.controls.constructor.MutableDeviceState
import space.kscience.controls.constructor.onTimer import space.kscience.controls.constructor.onTimer
import space.kscience.controls.manager.DeviceManager import space.kscience.controls.manager.DeviceManager
import space.kscience.controls.manager.install import space.kscience.controls.manager.install
import space.kscience.controls.manager.respondMessage
import space.kscience.controls.peer.PeerConnection import space.kscience.controls.peer.PeerConnection
import space.kscience.dataforge.context.Context import space.kscience.dataforge.context.Context
import space.kscience.dataforge.context.request import space.kscience.dataforge.context.request
import space.kscience.dataforge.io.Envelope import space.kscience.dataforge.io.Envelope
import space.kscience.dataforge.io.toByteArray
import space.kscience.dataforge.meta.Meta import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.names.parseAsName import space.kscience.dataforge.names.parseAsName
import space.kscience.magix.api.MagixEndpoint import space.kscience.magix.api.MagixEndpoint
@ -40,13 +44,17 @@ internal fun VirtualDeviceState(
MutableDeviceState(GmcVelocity.zero) MutableDeviceState(GmcVelocity.zero)
) )
private val json = Json {
ignoreUnknownKeys = true
prettyPrint = true
}
internal class DeviceCollectiveModel( internal class DeviceCollectiveModel(
context: Context, context: Context,
val deviceStates: Collection<CollectiveDeviceState>, val deviceStates: Collection<CollectiveDeviceState>,
val visibilityRange: Distance = 0.5.kilometers, val visibilityRange: Distance = 0.5.kilometers,
val radioRange: Distance = 5.kilometers, val radioRange: Distance = 5.kilometers,
) : ModelConstructor(context), PeerConnection { ) : ModelConstructor(context) {
/** /**
* Propagate movement * Propagate movement
@ -70,32 +78,39 @@ internal class DeviceCollectiveModel(
return allCurves.filterValues { it.distance in 0.kilometers..visibilityRange } 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 devices = deviceStates.associate {
val device = CollectiveDeviceConstructor( val device = CollectiveDeviceConstructor(
context = context, context = context,
configuration = it.configuration, configuration = it.configuration,
position = it.position, position = it.position,
velocity = it.velocity, velocity = it.velocity,
peerConnection = this, peerConnection = RadioPeerConnection(it),
) { ) {
locateVisible(it.id) locateVisible(it.id)
} }
//start movement program
device.moveInCircles()
it.id to device it.id to device
} }
val roster = deviceStates.associate { it.id to it.configuration } 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( internal fun CoroutineScope.launchCollectiveMagixServer(

View File

@ -1,10 +1,8 @@
package space.kscience.controls.demo.collective package space.kscience.controls.demo.collective
import kotlinx.coroutines.Job import kotlinx.coroutines.*
import kotlinx.coroutines.delay import space.kscience.controls.client.DeviceClient
import kotlinx.coroutines.isActive import space.kscience.controls.client.write
import kotlinx.coroutines.launch
import space.kscience.controls.spec.write
import space.kscience.dataforge.context.Context import space.kscience.dataforge.context.Context
import space.kscience.kmath.geometry.degrees import space.kscience.kmath.geometry.degrees
import space.kscience.kmath.geometry.radians import space.kscience.kmath.geometry.radians
@ -49,7 +47,7 @@ internal fun generateModel(
return model return model
} }
fun CollectiveDevice.moveInCircles(): Job = launch { fun DeviceClient.moveInCircles(scope: CoroutineScope = this): Job = scope.launch {
var bearing = Random.nextDouble(-PI, PI).radians var bearing = Random.nextDouble(-PI, PI).radians
write(CollectiveDevice.velocity, GmcVelocity(bearing, deviceVelocity)) write(CollectiveDevice.velocity, GmcVelocity(bearing, deviceVelocity))
while (isActive) { while (isActive) {

View File

@ -7,6 +7,7 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material.Button
import androidx.compose.material.Card import androidx.compose.material.Card
import androidx.compose.material.Checkbox import androidx.compose.material.Checkbox
import androidx.compose.material.Text import androidx.compose.material.Text
@ -18,19 +19,16 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Window import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application import androidx.compose.ui.window.application
import io.ktor.client.HttpClient import io.ktor.client.HttpClient
import io.ktor.client.engine.cio.CIO import io.ktor.client.engine.cio.CIO
import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch import kotlinx.coroutines.flow.sample
import kotlinx.coroutines.withContext
import org.jetbrains.compose.splitpane.ExperimentalSplitPaneApi import org.jetbrains.compose.splitpane.ExperimentalSplitPaneApi
import org.jetbrains.compose.splitpane.HorizontalSplitPane import org.jetbrains.compose.splitpane.HorizontalSplitPane
import org.jetbrains.compose.splitpane.rememberSplitPaneState 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.coordinates.meters
import space.kscience.maps.features.* import space.kscience.maps.features.*
import java.nio.file.Path import java.nio.file.Path
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds import kotlin.time.Duration.Companion.seconds
@ -92,7 +91,8 @@ fun App() {
collectiveModel.roster.forEach { (id, config) -> collectiveModel.roster.forEach { (id, config) ->
scope.launch { 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 showOnlyVisible by remember { mutableStateOf(false) }
var movementProgram: Job? by remember { mutableStateOf(null) }
HorizontalSplitPane( HorizontalSplitPane(
splitPaneState = rememberSplitPaneState(0.9f) splitPaneState = rememberSplitPaneState(0.9f)
) { ) {
@ -126,9 +128,28 @@ fun App() {
) { ) {
collectiveModel.deviceStates.forEach { device -> collectiveModel.deviceStates.forEach { device ->
circle(device.position.value, id = device.id + ".position").color(Color.Red) circle(device.position.value, id = device.id + ".position").color(Color.Red)
device.position.valueFlow.onEach { device.position.valueFlow.sample(50.milliseconds).onEach {
circle(device.position.value, id = device.id + ".position", size = 3.dp) val activeDevice = selectedDeviceId?.let { devices[it] }
.color(Color.Red) 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 .modifyAttribute(AlphaAttribute, 0.5f) // does not work right now
}.launchIn(scope) }.launchIn(scope)
} }
@ -139,24 +160,11 @@ fun App() {
if (deviceMessage is PropertyChangedMessage && deviceMessage.property == "position") { if (deviceMessage is PropertyChangedMessage && deviceMessage.property == "position") {
val id = magixMessage.sourceEndpoint val id = magixMessage.sourceEndpoint
val position = gmcMetaConverter.read(deviceMessage.value) 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( rectangle(
position, position,
id = id, id = id,
size = if (selectedDeviceId == id) DpSize(10.dp, 10.dp) else DpSize(5.dp, 5.dp) ).color(Color.Blue).onClick { selectedDeviceId = id }
).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) }.launchIn(scope)
@ -168,6 +176,34 @@ fun App() {
Column( Column(
modifier = Modifier.verticalScroll(rememberScrollState()) 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, _) -> collectiveModel.roster.forEach { (id, _) ->
Card( Card(
elevation = 16.dp, elevation = 16.dp,