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,
contentId: String,
requestMeta: Meta = Meta.EMPTY,
): Envelope
): Envelope?
/**
* Send an [envelope] to a device on a given [address]

View File

@ -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<Collection<CollectiveDeviceId>> = stateOf(emptyList())

View File

@ -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<T>(
class DebounceDeviceState<T>(
val origin: DeviceState<T>,
val interval: Duration,
) : DeviceState<T> {
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)"
}
fun <T> DeviceState<T>.sample(interval: Duration) = SampleDeviceState(this, interval)
fun <T> DeviceState<T>.debounce(interval: Duration) = DebounceDeviceState(this, interval)
@OptIn(FlowPreview::class)
class MutableSampleDeviceState<T>(
class MutableDebounceDeviceState<T>(
val origin: MutableDeviceState<T>,
val interval: Duration,
) : MutableDeviceState<T> {
@ -32,4 +33,4 @@ class MutableSampleDeviceState<T>(
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.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<CollectiveDeviceState>,
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(

View File

@ -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) {

View File

@ -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,