Compare commits
2 Commits
60a693b1b3
...
eb126a6090
Author | SHA1 | Date | |
---|---|---|---|
eb126a6090 | |||
a5bb42706b |
@ -73,7 +73,7 @@ public data class PropertySetMessage(
|
|||||||
public val property: String,
|
public val property: String,
|
||||||
public val value: Meta,
|
public val value: Meta,
|
||||||
override val sourceDevice: Name? = null,
|
override val sourceDevice: Name? = null,
|
||||||
override val targetDevice: Name,
|
override val targetDevice: Name?,
|
||||||
override val comment: String? = null,
|
override val comment: String? = null,
|
||||||
@EncodeDefault override val time: Instant = Clock.System.now(),
|
@EncodeDefault override val time: Instant = Clock.System.now(),
|
||||||
) : DeviceMessage() {
|
) : DeviceMessage() {
|
||||||
|
@ -68,7 +68,7 @@ public suspend fun Device.respondMessage(deviceTarget: Name, request: DeviceMess
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Process incoming [DeviceMessage], using hub naming to find target.
|
* Process incoming [DeviceMessage], using hub naming to find target.
|
||||||
* If the `targetDevice` is `null`, then message is sent to each device in this hub
|
* If the `targetDevice` is `null`, then the message is sent to each device in this hub
|
||||||
*/
|
*/
|
||||||
public suspend fun DeviceHub.respondHubMessage(request: DeviceMessage): List<DeviceMessage> {
|
public suspend fun DeviceHub.respondHubMessage(request: DeviceMessage): List<DeviceMessage> {
|
||||||
return try {
|
return try {
|
||||||
|
@ -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]
|
||||||
|
@ -23,7 +23,11 @@ class CollectiveDeviceConfiguration(deviceId: CollectiveDeviceId) : Scheme() {
|
|||||||
var deviceId by string(deviceId)
|
var deviceId by string(deviceId)
|
||||||
var description by string()
|
var description by string()
|
||||||
var reportInterval by int(500)
|
var reportInterval by int(500)
|
||||||
var radioFrequency by string(default = "169 MHz")
|
var radioFrequency by string(default = DEFAULT_FREQUENCY)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val DEFAULT_FREQUENCY = "169 MHz"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
typealias CollectiveDeviceRoster = Map<CollectiveDeviceId, CollectiveDeviceConfiguration>
|
typealias CollectiveDeviceRoster = Map<CollectiveDeviceId, CollectiveDeviceConfiguration>
|
||||||
@ -81,12 +85,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)
|
@ -1,27 +1,53 @@
|
|||||||
package space.kscience.controls.demo.collective
|
package space.kscience.controls.demo.collective
|
||||||
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.io.writeString
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.serialization.json.Json
|
||||||
import kotlinx.coroutines.launch
|
import space.kscience.controls.api.DeviceMessage
|
||||||
|
import space.kscience.controls.api.PropertySetMessage
|
||||||
|
import space.kscience.controls.client.DeviceClient
|
||||||
import space.kscience.controls.client.launchMagixService
|
import space.kscience.controls.client.launchMagixService
|
||||||
|
import space.kscience.controls.client.write
|
||||||
|
import space.kscience.controls.constructor.DeviceState
|
||||||
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.controls.spec.name
|
||||||
|
import space.kscience.controls.spec.write
|
||||||
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.kmath.geometry.degrees
|
||||||
|
import space.kscience.kmath.geometry.radians
|
||||||
import space.kscience.magix.api.MagixEndpoint
|
import space.kscience.magix.api.MagixEndpoint
|
||||||
import space.kscience.magix.rsocket.rSocketWithWebSockets
|
import space.kscience.magix.rsocket.rSocketWithWebSockets
|
||||||
import space.kscience.magix.server.startMagixServer
|
import space.kscience.magix.server.startMagixServer
|
||||||
import space.kscience.maps.coordinates.*
|
import space.kscience.maps.coordinates.*
|
||||||
|
import kotlin.math.PI
|
||||||
|
import kotlin.random.Random
|
||||||
|
import kotlin.time.Duration
|
||||||
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
private val deviceVelocity = 0.1.kilometers
|
||||||
|
|
||||||
|
private val center = Gmc.ofDegrees(55.925, 37.514)
|
||||||
|
private val radius = 0.01.degrees
|
||||||
|
|
||||||
|
private val json = Json {
|
||||||
|
ignoreUnknownKeys = true
|
||||||
|
prettyPrint = true
|
||||||
|
}
|
||||||
|
|
||||||
internal data class CollectiveDeviceState(
|
internal data class CollectiveDeviceState(
|
||||||
val id: CollectiveDeviceId,
|
val id: CollectiveDeviceId,
|
||||||
val configuration: CollectiveDeviceConfiguration,
|
val configuration: CollectiveDeviceConfiguration,
|
||||||
@ -29,7 +55,7 @@ internal data class CollectiveDeviceState(
|
|||||||
val velocity: MutableDeviceState<GmcVelocity>,
|
val velocity: MutableDeviceState<GmcVelocity>,
|
||||||
)
|
)
|
||||||
|
|
||||||
internal fun VirtualDeviceState(
|
internal fun CollectiveDeviceState(
|
||||||
id: CollectiveDeviceId,
|
id: CollectiveDeviceId,
|
||||||
position: Gmc,
|
position: Gmc,
|
||||||
configuration: CollectiveDeviceConfiguration.() -> Unit = {},
|
configuration: CollectiveDeviceConfiguration.() -> Unit = {},
|
||||||
@ -40,13 +66,12 @@ internal fun VirtualDeviceState(
|
|||||||
MutableDeviceState(GmcVelocity.zero)
|
MutableDeviceState(GmcVelocity.zero)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
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 = 1.kilometers,
|
||||||
) : ModelConstructor(context), PeerConnection {
|
) : ModelConstructor(context) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Propagate movement
|
* Propagate movement
|
||||||
@ -70,34 +95,89 @@ internal class DeviceCollectiveModel(
|
|||||||
return allCurves.filterValues { it.distance in 0.kilometers..visibilityRange }
|
return allCurves.filterValues { it.distance in 0.kilometers..visibilityRange }
|
||||||
}
|
}
|
||||||
|
|
||||||
val devices = deviceStates.associate {
|
inner class RadioPeerConnectionModel(private val position: DeviceState<Gmc>) : PeerConnection {
|
||||||
|
override suspend fun receive(address: String, contentId: String, requestMeta: Meta): Envelope? = null
|
||||||
|
|
||||||
|
override suspend fun send(address: String, envelope: Envelope, requestMeta: Meta) {
|
||||||
|
devices.values.filter { it.configuration.radioFrequency == address }.filter {
|
||||||
|
GeoEllipsoid.WGS84.curveBetween(position.value, it.position.value).distance < radioRange
|
||||||
|
}.forEach { target ->
|
||||||
|
check(envelope.data != null) { "Envelope data is empty" }
|
||||||
|
val message = json.decodeFromString(
|
||||||
|
DeviceMessage.serializer(),
|
||||||
|
envelope.data?.toByteArray()?.decodeToString() ?: ""
|
||||||
|
)
|
||||||
|
target.respondMessage(target.configuration.deviceId.parseAsName(), message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val devices = deviceStates.associate { state ->
|
||||||
val device = CollectiveDeviceConstructor(
|
val device = CollectiveDeviceConstructor(
|
||||||
context = context,
|
context = context,
|
||||||
configuration = it.configuration,
|
configuration = state.configuration,
|
||||||
position = it.position,
|
position = state.position,
|
||||||
velocity = it.velocity,
|
velocity = state.velocity,
|
||||||
peerConnection = this,
|
peerConnection = RadioPeerConnectionModel(state.position),
|
||||||
) {
|
) {
|
||||||
locateVisible(it.id)
|
locateVisible(state.id)
|
||||||
}
|
}
|
||||||
//start movement program
|
state.id to device
|
||||||
device.moveInCircles()
|
}
|
||||||
it.id to device
|
|
||||||
|
internal fun createTrawler(position: Gmc, id: CollectiveDeviceId = "trawler"): CollectiveDeviceConstructor {
|
||||||
|
val state = CollectiveDeviceState(
|
||||||
|
id = id,
|
||||||
|
configuration = CollectiveDeviceConfiguration(id),
|
||||||
|
position = MutableDeviceState(position),
|
||||||
|
velocity = MutableDeviceState(GmcVelocity.zero)
|
||||||
|
)
|
||||||
|
|
||||||
|
val result = CollectiveDeviceConstructor(
|
||||||
|
context = context,
|
||||||
|
configuration = state.configuration,
|
||||||
|
position = state.position,
|
||||||
|
velocity = state.velocity,
|
||||||
|
peerConnection = RadioPeerConnectionModel(state.position),
|
||||||
|
) {
|
||||||
|
locateVisible(state.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO move to CollectiveDeviceState
|
||||||
|
onTimer { prev, next ->
|
||||||
|
val delta = (next - prev)
|
||||||
|
state.position.value = state.position.value.moveWith(state.velocity.value, delta)
|
||||||
|
}
|
||||||
|
|
||||||
|
result.onTimer(1.seconds) { _, _ ->
|
||||||
|
val envelope = Envelope {
|
||||||
|
data {
|
||||||
|
writeString(
|
||||||
|
json.encodeToString(
|
||||||
|
DeviceMessage.serializer(),
|
||||||
|
PropertySetMessage(
|
||||||
|
property = CollectiveDevice.velocity.name,
|
||||||
|
value = gmcVelocityMetaConverter.convert(state.velocity.value),
|
||||||
|
targetDevice = null
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result.peerConnection.send(
|
||||||
|
CollectiveDeviceConfiguration.DEFAULT_FREQUENCY,
|
||||||
|
envelope
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
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(
|
||||||
collectiveModel: DeviceCollectiveModel,
|
collectiveModel: DeviceCollectiveModel,
|
||||||
): Job = launch(Dispatchers.IO) {
|
): Job = launch(Dispatchers.IO) {
|
||||||
@ -119,3 +199,57 @@ internal fun CoroutineScope.launchCollectiveMagixServer(
|
|||||||
deviceContext.request(DeviceManager).launchMagixService(deviceEndpoint, id)
|
deviceContext.request(DeviceManager).launchMagixService(deviceEndpoint, id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
internal fun generateModel(
|
||||||
|
context: Context,
|
||||||
|
size: Int = 50,
|
||||||
|
reportInterval: Duration = 500.milliseconds,
|
||||||
|
additionalConfiguration: CollectiveDeviceConfiguration.() -> Unit = {},
|
||||||
|
): DeviceCollectiveModel {
|
||||||
|
val devices: List<CollectiveDeviceState> = List(size) { index ->
|
||||||
|
val id = "device[$index]"
|
||||||
|
|
||||||
|
CollectiveDeviceState(
|
||||||
|
id = id,
|
||||||
|
Gmc(
|
||||||
|
center.latitude + radius * Random.nextDouble(),
|
||||||
|
center.longitude + radius * Random.nextDouble()
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
deviceId = id
|
||||||
|
description = "Virtual remote device $id"
|
||||||
|
this.reportInterval = reportInterval.inWholeMilliseconds.toInt()
|
||||||
|
additionalConfiguration()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val model = DeviceCollectiveModel(context, devices)
|
||||||
|
|
||||||
|
return model
|
||||||
|
}
|
||||||
|
|
||||||
|
fun DeviceClient.moveInCircles(scope: CoroutineScope = this): Job = scope.launch {
|
||||||
|
var bearing = Random.nextDouble(-PI, PI).radians
|
||||||
|
write(CollectiveDevice.velocity, GmcVelocity(bearing, deviceVelocity))
|
||||||
|
while (isActive) {
|
||||||
|
delay(500)
|
||||||
|
bearing += 5.degrees
|
||||||
|
write(CollectiveDevice.velocity, GmcVelocity(bearing, deviceVelocity))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
internal fun CollectiveDeviceConstructor.moveTo(
|
||||||
|
targetPosition: Gmc,
|
||||||
|
speedLimit: Distance = deviceVelocity,
|
||||||
|
scope: CoroutineScope = this,
|
||||||
|
): Job = scope.launch {
|
||||||
|
do {
|
||||||
|
val curve = GeoEllipsoid.WGS84.curveBetween(position.value, targetPosition)
|
||||||
|
write(CollectiveDevice.velocity, GmcVelocity(curve.forward.bearing, speedLimit))
|
||||||
|
delay(1.seconds)
|
||||||
|
} while (curve.distance > 0.1.kilometers)
|
||||||
|
write(CollectiveDevice.velocity, GmcVelocity.zero)
|
||||||
|
|
||||||
|
}
|
@ -1,60 +0,0 @@
|
|||||||
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 space.kscience.dataforge.context.Context
|
|
||||||
import space.kscience.kmath.geometry.degrees
|
|
||||||
import space.kscience.kmath.geometry.radians
|
|
||||||
import space.kscience.maps.coordinates.Gmc
|
|
||||||
import space.kscience.maps.coordinates.kilometers
|
|
||||||
import kotlin.math.PI
|
|
||||||
import kotlin.random.Random
|
|
||||||
import kotlin.time.Duration
|
|
||||||
import kotlin.time.Duration.Companion.milliseconds
|
|
||||||
|
|
||||||
private val deviceVelocity = 0.1.kilometers
|
|
||||||
|
|
||||||
private val center = Gmc.ofDegrees(55.925, 37.514)
|
|
||||||
private val radius = 0.01.degrees
|
|
||||||
|
|
||||||
|
|
||||||
internal fun generateModel(
|
|
||||||
context: Context,
|
|
||||||
size: Int = 50,
|
|
||||||
reportInterval: Duration = 500.milliseconds,
|
|
||||||
additionalConfiguration: CollectiveDeviceConfiguration.() -> Unit = {},
|
|
||||||
): DeviceCollectiveModel {
|
|
||||||
val devices: List<CollectiveDeviceState> = List(size) { index ->
|
|
||||||
val id = "device[$index]"
|
|
||||||
|
|
||||||
VirtualDeviceState(
|
|
||||||
id = id,
|
|
||||||
Gmc(
|
|
||||||
center.latitude + radius * Random.nextDouble(),
|
|
||||||
center.longitude + radius * Random.nextDouble()
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
deviceId = id
|
|
||||||
description = "Virtual remote device $id"
|
|
||||||
this.reportInterval = reportInterval.inWholeMilliseconds.toInt()
|
|
||||||
additionalConfiguration()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val model = DeviceCollectiveModel(context, devices)
|
|
||||||
|
|
||||||
return model
|
|
||||||
}
|
|
||||||
|
|
||||||
fun CollectiveDevice.moveInCircles(): Job = launch {
|
|
||||||
var bearing = Random.nextDouble(-PI, PI).radians
|
|
||||||
write(CollectiveDevice.velocity, GmcVelocity(bearing, deviceVelocity))
|
|
||||||
while (isActive) {
|
|
||||||
delay(500)
|
|
||||||
bearing += 5.degrees
|
|
||||||
write(CollectiveDevice.velocity, GmcVelocity(bearing, deviceVelocity))
|
|
||||||
}
|
|
||||||
}
|
|
@ -7,30 +7,26 @@ 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.Card
|
import androidx.compose.material.*
|
||||||
import androidx.compose.material.Checkbox
|
|
||||||
import androidx.compose.material.Text
|
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.input.pointer.isSecondaryPressed
|
||||||
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 +47,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
|
||||||
|
|
||||||
|
|
||||||
@ -59,7 +56,8 @@ fun rememberContext(name: String, contextBuilder: ContextBuilder.() -> Unit = {}
|
|||||||
Context(name, contextBuilder)
|
Context(name, contextBuilder)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val gmcMetaConverter = MetaConverter.serializable<Gmc>()
|
internal val gmcMetaConverter = MetaConverter.serializable<Gmc>()
|
||||||
|
internal val gmcVelocityMetaConverter = MetaConverter.serializable<GmcVelocity>()
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun App() {
|
fun App() {
|
||||||
@ -81,7 +79,6 @@ fun App() {
|
|||||||
|
|
||||||
val devices = remember { mutableStateMapOf<CollectiveDeviceId, DeviceClient>() }
|
val devices = remember { mutableStateMapOf<CollectiveDeviceId, DeviceClient>() }
|
||||||
|
|
||||||
|
|
||||||
LaunchedEffect(collectiveModel) {
|
LaunchedEffect(collectiveModel) {
|
||||||
launchCollectiveMagixServer(collectiveModel)
|
launchCollectiveMagixServer(collectiveModel)
|
||||||
|
|
||||||
@ -92,7 +89,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,10 +109,29 @@ fun App() {
|
|||||||
|
|
||||||
var showOnlyVisible by remember { mutableStateOf(false) }
|
var showOnlyVisible by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
var movementProgram: Job? by remember { mutableStateOf(null) }
|
||||||
|
|
||||||
|
val trawler: CollectiveDeviceConstructor = remember {
|
||||||
|
collectiveModel.createTrawler(Gmc.ofDegrees(55.925, 37.50))
|
||||||
|
}
|
||||||
|
|
||||||
HorizontalSplitPane(
|
HorizontalSplitPane(
|
||||||
splitPaneState = rememberSplitPaneState(0.9f)
|
splitPaneState = rememberSplitPaneState(0.9f)
|
||||||
) {
|
) {
|
||||||
first(400.dp) {
|
first(400.dp) {
|
||||||
|
var clickPoint by remember { mutableStateOf<Gmc?>(null) }
|
||||||
|
|
||||||
|
CursorDropdownMenu(clickPoint != null, { clickPoint = null }) {
|
||||||
|
clickPoint?.let { point ->
|
||||||
|
TextButton({
|
||||||
|
trawler.moveTo(point)
|
||||||
|
clickPoint = null
|
||||||
|
}) {
|
||||||
|
Text("Move trawler here")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
MapView(
|
MapView(
|
||||||
mapTileProvider = remember {
|
mapTileProvider = remember {
|
||||||
OpenStreetMapTileProvider(
|
OpenStreetMapTileProvider(
|
||||||
@ -122,45 +139,64 @@ fun App() {
|
|||||||
cacheDirectory = Path.of("mapCache")
|
cacheDirectory = Path.of("mapCache")
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
config = ViewConfig()
|
config = ViewConfig(
|
||||||
|
onClick = { event, point ->
|
||||||
|
if (event.buttons.isSecondaryPressed) {
|
||||||
|
clickPoint = point.focus
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
|
//draw real positions
|
||||||
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//draw received data
|
||||||
scope.launch {
|
scope.launch {
|
||||||
|
|
||||||
client.await().subscribe(DeviceManager.magixFormat).onEach { (magixMessage, deviceMessage) ->
|
client.await().subscribe(DeviceManager.magixFormat).onEach { (magixMessage, deviceMessage) ->
|
||||||
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 (
|
rectangle(
|
||||||
activeDevice == null ||
|
position,
|
||||||
id == selectedDeviceId ||
|
id = id,
|
||||||
!showOnlyVisible ||
|
).color(Color.Blue).onClick { selectedDeviceId = id }
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}.launchIn(scope)
|
}.launchIn(scope)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// draw trawler
|
||||||
|
|
||||||
|
trawler.position.valueFlow.onEach {
|
||||||
|
circle(it, id = "trawler").color(Color.Black)
|
||||||
|
}.launchIn(scope)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
second(200.dp) {
|
second(200.dp) {
|
||||||
@ -168,6 +204,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