Finalize collective demo

This commit is contained in:
Alexander Nozik 2024-06-12 16:31:14 +03:00
parent a5bb42706b
commit eb126a6090
6 changed files with 188 additions and 95 deletions

View File

@ -73,7 +73,7 @@ public data class PropertySetMessage(
public val property: String,
public val value: Meta,
override val sourceDevice: Name? = null,
override val targetDevice: Name,
override val targetDevice: Name?,
override val comment: String? = null,
@EncodeDefault override val time: Instant = Clock.System.now(),
) : DeviceMessage() {

View File

@ -68,7 +68,7 @@ public suspend fun Device.respondMessage(deviceTarget: Name, request: DeviceMess
/**
* 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> {
return try {

View File

@ -23,7 +23,11 @@ class CollectiveDeviceConfiguration(deviceId: CollectiveDeviceId) : Scheme() {
var deviceId by string(deviceId)
var description by string()
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>
@ -111,4 +115,4 @@ class CollectiveDeviceConstructor(
}
override suspend fun listVisible(): Collection<CollectiveDeviceId> = observation.invoke().keys
}
}

View File

@ -1,12 +1,14 @@
package space.kscience.controls.demo.collective
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlinx.coroutines.*
import kotlinx.io.writeString
import kotlinx.serialization.json.Json
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.write
import space.kscience.controls.constructor.DeviceState
import space.kscience.controls.constructor.ModelConstructor
import space.kscience.controls.constructor.MutableDeviceState
import space.kscience.controls.constructor.onTimer
@ -14,18 +16,38 @@ 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.controls.spec.name
import space.kscience.controls.spec.write
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.kmath.geometry.degrees
import space.kscience.kmath.geometry.radians
import space.kscience.magix.api.MagixEndpoint
import space.kscience.magix.rsocket.rSocketWithWebSockets
import space.kscience.magix.server.startMagixServer
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(
val id: CollectiveDeviceId,
val configuration: CollectiveDeviceConfiguration,
@ -33,7 +55,7 @@ internal data class CollectiveDeviceState(
val velocity: MutableDeviceState<GmcVelocity>,
)
internal fun VirtualDeviceState(
internal fun CollectiveDeviceState(
id: CollectiveDeviceId,
position: Gmc,
configuration: CollectiveDeviceConfiguration.() -> Unit = {},
@ -44,16 +66,11 @@ 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,
val radioRange: Distance = 1.kilometers,
) : ModelConstructor(context) {
/**
@ -78,41 +95,89 @@ internal class DeviceCollectiveModel(
return allCurves.filterValues { it.distance in 0.kilometers..visibilityRange }
}
inner class RadioPeerConnection(private val peerState: CollectiveDeviceState) : PeerConnection {
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.filter { it.value.configuration.radioFrequency == address }.filter {
GeoEllipsoid.WGS84.curveBetween(peerState.position.value, it.value.position.value).distance < radioRange
}.forEach { (id, target) ->
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(id.parseAsName(), message)
target.respondMessage(target.configuration.deviceId.parseAsName(), message)
}
}
}
val devices = deviceStates.associate {
val devices = deviceStates.associate { state ->
val device = CollectiveDeviceConstructor(
context = context,
configuration = it.configuration,
position = it.position,
velocity = it.velocity,
peerConnection = RadioPeerConnection(it),
configuration = state.configuration,
position = state.position,
velocity = state.velocity,
peerConnection = RadioPeerConnectionModel(state.position),
) {
locateVisible(it.id)
locateVisible(state.id)
}
it.id to device
state.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 }
}
internal fun CoroutineScope.launchCollectiveMagixServer(
collectiveModel: DeviceCollectiveModel,
): Job = launch(Dispatchers.IO) {
@ -133,4 +198,58 @@ internal fun CoroutineScope.launchCollectiveMagixServer(
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)
}

View File

@ -1,58 +0,0 @@
package space.kscience.controls.demo.collective
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
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 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))
}
}

View File

@ -7,16 +7,14 @@ 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
import androidx.compose.material.*
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.isSecondaryPressed
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
@ -58,7 +56,8 @@ fun rememberContext(name: String, contextBuilder: ContextBuilder.() -> Unit = {}
Context(name, contextBuilder)
}
private val gmcMetaConverter = MetaConverter.serializable<Gmc>()
internal val gmcMetaConverter = MetaConverter.serializable<Gmc>()
internal val gmcVelocityMetaConverter = MetaConverter.serializable<GmcVelocity>()
@Composable
fun App() {
@ -80,7 +79,6 @@ fun App() {
val devices = remember { mutableStateMapOf<CollectiveDeviceId, DeviceClient>() }
LaunchedEffect(collectiveModel) {
launchCollectiveMagixServer(collectiveModel)
@ -113,10 +111,27 @@ fun App() {
var movementProgram: Job? by remember { mutableStateOf(null) }
val trawler: CollectiveDeviceConstructor = remember {
collectiveModel.createTrawler(Gmc.ofDegrees(55.925, 37.50))
}
HorizontalSplitPane(
splitPaneState = rememberSplitPaneState(0.9f)
) {
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(
mapTileProvider = remember {
OpenStreetMapTileProvider(
@ -124,8 +139,15 @@ fun App() {
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 ->
circle(device.position.value, id = device.id + ".position").color(Color.Red)
device.position.valueFlow.sample(50.milliseconds).onEach {
@ -154,8 +176,8 @@ fun App() {
}.launchIn(scope)
}
//draw received data
scope.launch {
client.await().subscribe(DeviceManager.magixFormat).onEach { (magixMessage, deviceMessage) ->
if (deviceMessage is PropertyChangedMessage && deviceMessage.property == "position") {
val id = magixMessage.sourceEndpoint
@ -169,6 +191,12 @@ fun App() {
}.launchIn(scope)
}
// draw trawler
trawler.position.valueFlow.onEach {
circle(it, id = "trawler").color(Color.Black)
}.launchIn(scope)
}
}
second(200.dp) {