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