Finalize collective demo
This commit is contained in:
parent
a5bb42706b
commit
eb126a6090
@ -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() {
|
||||
|
@ -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 {
|
||||
|
@ -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>
|
||||
|
@ -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) {
|
||||
@ -134,3 +199,57 @@ 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)
|
||||
|
||||
}
|
@ -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))
|
||||
}
|
||||
}
|
@ -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) {
|
||||
|
Loading…
Reference in New Issue
Block a user