Change visualization for collective
This commit is contained in:
parent
60a693b1b3
commit
a5bb42706b
@ -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]
|
||||||
|
@ -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())
|
||||||
|
@ -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)
|
@ -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(
|
||||||
|
@ -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) {
|
||||||
|
@ -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,
|
||||||
|
Loading…
Reference in New Issue
Block a user