RSocket and demo device name refactor
This commit is contained in:
parent
3cbabd5d4b
commit
28e6e24cf7
@ -18,6 +18,91 @@ import space.kscience.dataforge.misc.DFExperimental
|
|||||||
@DFExperimental
|
@DFExperimental
|
||||||
public data class LogEntry(val content: String, val priority: Int = 0)
|
public data class LogEntry(val content: String, val priority: Int = 0)
|
||||||
|
|
||||||
|
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
private open class BasicReadOnlyDeviceProperty(
|
||||||
|
val device: DeviceBase,
|
||||||
|
override val name: String,
|
||||||
|
default: MetaItem?,
|
||||||
|
override val descriptor: PropertyDescriptor,
|
||||||
|
private val getter: suspend (before: MetaItem?) -> MetaItem,
|
||||||
|
) : ReadOnlyDeviceProperty {
|
||||||
|
|
||||||
|
override val scope: CoroutineScope get() = device.scope
|
||||||
|
|
||||||
|
private val state: MutableStateFlow<MetaItem?> = MutableStateFlow(default)
|
||||||
|
override val value: MetaItem? get() = state.value
|
||||||
|
|
||||||
|
override suspend fun invalidate() {
|
||||||
|
state.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun updateLogical(item: MetaItem) {
|
||||||
|
state.value = item
|
||||||
|
scope.launch {
|
||||||
|
device.sharedPropertyFlow.emit(Pair(name, item))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun read(force: Boolean): MetaItem {
|
||||||
|
//backup current value
|
||||||
|
val currentValue = value
|
||||||
|
return if (force || currentValue == null) {
|
||||||
|
//all device operations should be run on device context
|
||||||
|
//propagate error, but do not fail scope
|
||||||
|
val res = withContext(scope.coroutineContext + SupervisorJob(scope.coroutineContext[Job])) {
|
||||||
|
getter(currentValue)
|
||||||
|
}
|
||||||
|
updateLogical(res)
|
||||||
|
res
|
||||||
|
} else {
|
||||||
|
currentValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun flow(): StateFlow<MetaItem?> = state
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
private class BasicDeviceProperty(
|
||||||
|
device: DeviceBase,
|
||||||
|
name: String,
|
||||||
|
default: MetaItem?,
|
||||||
|
descriptor: PropertyDescriptor,
|
||||||
|
getter: suspend (MetaItem?) -> MetaItem,
|
||||||
|
private val setter: suspend (oldValue: MetaItem?, newValue: MetaItem) -> MetaItem?,
|
||||||
|
) : BasicReadOnlyDeviceProperty(device, name, default, descriptor, getter), DeviceProperty {
|
||||||
|
|
||||||
|
override var value: MetaItem?
|
||||||
|
get() = super.value
|
||||||
|
set(value) {
|
||||||
|
scope.launch {
|
||||||
|
if (value == null) {
|
||||||
|
invalidate()
|
||||||
|
} else {
|
||||||
|
write(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val writeLock = Mutex()
|
||||||
|
|
||||||
|
override suspend fun write(item: MetaItem) {
|
||||||
|
writeLock.withLock {
|
||||||
|
//fast return if value is not changed
|
||||||
|
if (item == value) return@withLock
|
||||||
|
val oldValue = value
|
||||||
|
//all device operations should be run on device context
|
||||||
|
withContext(scope.coroutineContext + SupervisorJob(scope.coroutineContext[Job])) {
|
||||||
|
setter(oldValue, item)?.let {
|
||||||
|
updateLogical(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Baseline implementation of [Device] interface
|
* Baseline implementation of [Device] interface
|
||||||
*/
|
*/
|
||||||
@ -33,7 +118,7 @@ public abstract class DeviceBase(override val context: Context) : Device {
|
|||||||
private val _actions = HashMap<String, DeviceAction>()
|
private val _actions = HashMap<String, DeviceAction>()
|
||||||
public val actions: Map<String, DeviceAction> get() = _actions
|
public val actions: Map<String, DeviceAction> get() = _actions
|
||||||
|
|
||||||
private val sharedPropertyFlow = MutableSharedFlow<Pair<String, MetaItem>>()
|
internal val sharedPropertyFlow = MutableSharedFlow<Pair<String, MetaItem>>()
|
||||||
|
|
||||||
override val propertyFlow: SharedFlow<Pair<String, MetaItem>> get() = sharedPropertyFlow
|
override val propertyFlow: SharedFlow<Pair<String, MetaItem>> get() = sharedPropertyFlow
|
||||||
|
|
||||||
@ -82,49 +167,6 @@ public abstract class DeviceBase(override val context: Context) : Device {
|
|||||||
override suspend fun execute(action: String, argument: MetaItem?): MetaItem? =
|
override suspend fun execute(action: String, argument: MetaItem?): MetaItem? =
|
||||||
(_actions[action] ?: error("Request with name $action not defined")).invoke(argument)
|
(_actions[action] ?: error("Request with name $action not defined")).invoke(argument)
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
|
||||||
private open inner class BasicReadOnlyDeviceProperty(
|
|
||||||
override val name: String,
|
|
||||||
default: MetaItem?,
|
|
||||||
override val descriptor: PropertyDescriptor,
|
|
||||||
private val getter: suspend (before: MetaItem?) -> MetaItem,
|
|
||||||
) : ReadOnlyDeviceProperty {
|
|
||||||
|
|
||||||
override val scope: CoroutineScope get() = this@DeviceBase.scope
|
|
||||||
|
|
||||||
private val state: MutableStateFlow<MetaItem?> = MutableStateFlow(default)
|
|
||||||
override val value: MetaItem? get() = state.value
|
|
||||||
|
|
||||||
override suspend fun invalidate() {
|
|
||||||
state.value = null
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun updateLogical(item: MetaItem) {
|
|
||||||
state.value = item
|
|
||||||
scope.launch {
|
|
||||||
sharedPropertyFlow.emit(Pair(name, item))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun read(force: Boolean): MetaItem {
|
|
||||||
//backup current value
|
|
||||||
val currentValue = value
|
|
||||||
return if (force || currentValue == null) {
|
|
||||||
//all device operations should be run on device context
|
|
||||||
//propagate error, but do not fail scope
|
|
||||||
val res = withContext(scope.coroutineContext + SupervisorJob(scope.coroutineContext[Job])) {
|
|
||||||
getter(currentValue)
|
|
||||||
}
|
|
||||||
updateLogical(res)
|
|
||||||
res
|
|
||||||
} else {
|
|
||||||
currentValue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun flow(): StateFlow<MetaItem?> = state
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a bound read-only property with given [getter]
|
* Create a bound read-only property with given [getter]
|
||||||
*/
|
*/
|
||||||
@ -135,6 +177,7 @@ public abstract class DeviceBase(override val context: Context) : Device {
|
|||||||
getter: suspend (MetaItem?) -> MetaItem,
|
getter: suspend (MetaItem?) -> MetaItem,
|
||||||
): ReadOnlyDeviceProperty {
|
): ReadOnlyDeviceProperty {
|
||||||
val property = BasicReadOnlyDeviceProperty(
|
val property = BasicReadOnlyDeviceProperty(
|
||||||
|
this,
|
||||||
name,
|
name,
|
||||||
default,
|
default,
|
||||||
PropertyDescriptor(name).apply(descriptorBuilder),
|
PropertyDescriptor(name).apply(descriptorBuilder),
|
||||||
@ -144,43 +187,6 @@ public abstract class DeviceBase(override val context: Context) : Device {
|
|||||||
return property
|
return property
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
|
||||||
private inner class BasicDeviceProperty(
|
|
||||||
name: String,
|
|
||||||
default: MetaItem?,
|
|
||||||
descriptor: PropertyDescriptor,
|
|
||||||
getter: suspend (MetaItem?) -> MetaItem,
|
|
||||||
private val setter: suspend (oldValue: MetaItem?, newValue: MetaItem) -> MetaItem?,
|
|
||||||
) : BasicReadOnlyDeviceProperty(name, default, descriptor, getter), DeviceProperty {
|
|
||||||
|
|
||||||
override var value: MetaItem?
|
|
||||||
get() = super.value
|
|
||||||
set(value) {
|
|
||||||
scope.launch {
|
|
||||||
if (value == null) {
|
|
||||||
invalidate()
|
|
||||||
} else {
|
|
||||||
write(value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val writeLock = Mutex()
|
|
||||||
|
|
||||||
override suspend fun write(item: MetaItem) {
|
|
||||||
writeLock.withLock {
|
|
||||||
//fast return if value is not changed
|
|
||||||
if (item == value) return@withLock
|
|
||||||
val oldValue = value
|
|
||||||
//all device operations should be run on device context
|
|
||||||
withContext(scope.coroutineContext + SupervisorJob(scope.coroutineContext[Job])) {
|
|
||||||
setter(oldValue, item)?.let {
|
|
||||||
updateLogical(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a bound mutable property with given [getter] and [setter]
|
* Create a bound mutable property with given [getter] and [setter]
|
||||||
@ -193,6 +199,7 @@ public abstract class DeviceBase(override val context: Context) : Device {
|
|||||||
setter: suspend (oldValue: MetaItem?, newValue: MetaItem) -> MetaItem?,
|
setter: suspend (oldValue: MetaItem?, newValue: MetaItem) -> MetaItem?,
|
||||||
): DeviceProperty {
|
): DeviceProperty {
|
||||||
val property = BasicDeviceProperty(
|
val property = BasicDeviceProperty(
|
||||||
|
this,
|
||||||
name,
|
name,
|
||||||
default,
|
default,
|
||||||
PropertyDescriptor(name).apply(descriptorBuilder),
|
PropertyDescriptor(name).apply(descriptorBuilder),
|
||||||
|
@ -40,18 +40,16 @@ public class DeviceManager(override val deviceName: String = "") : AbstractPlugi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface DeviceFactory<D : Device> : Factory<D>
|
public interface DeviceSpec<D : Device> : Factory<D>
|
||||||
|
|
||||||
public val Context.devices: DeviceManager get() = plugins.fetch(DeviceManager)
|
public fun <D : Device> DeviceManager.install(name: String, factory: DeviceSpec<D>, meta: Meta = Meta.EMPTY): D {
|
||||||
|
|
||||||
public fun <D : Device> DeviceManager.install(name: String, factory: DeviceFactory<D>, meta: Meta = Meta.EMPTY): D {
|
|
||||||
val device = factory(meta, context)
|
val device = factory(meta, context)
|
||||||
registerDevice(NameToken(name), device)
|
registerDevice(NameToken(name), device)
|
||||||
return device
|
return device
|
||||||
}
|
}
|
||||||
|
|
||||||
public fun <D : Device> DeviceManager.installing(
|
public fun <D : Device> DeviceManager.installing(
|
||||||
factory: DeviceFactory<D>,
|
factory: DeviceSpec<D>,
|
||||||
metaBuilder: MetaBuilder.() -> Unit = {},
|
metaBuilder: MetaBuilder.() -> Unit = {},
|
||||||
): ReadOnlyProperty<Any?, D> = ReadOnlyProperty { _, property ->
|
): ReadOnlyProperty<Any?, D> = ReadOnlyProperty { _, property ->
|
||||||
val name = property.name
|
val name = property.name
|
||||||
|
@ -3,12 +3,18 @@ plugins {
|
|||||||
`maven-publish`
|
`maven-publish`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
description = """
|
||||||
|
A stand-alone device tree web server which also works as magix event dispatcher.
|
||||||
|
The server is used to work with stand-alone devices without intermediate control system.
|
||||||
|
""".trimIndent()
|
||||||
|
|
||||||
val dataforgeVersion: String by rootProject.extra
|
val dataforgeVersion: String by rootProject.extra
|
||||||
val ktorVersion: String = "1.5.3"
|
val ktorVersion: String = "1.5.3"
|
||||||
|
|
||||||
dependencies{
|
dependencies {
|
||||||
implementation(project(":controls-core"))
|
implementation(project(":controls-core"))
|
||||||
implementation(project(":controls-tcp"))
|
implementation(project(":controls-tcp"))
|
||||||
|
implementation(projects.magix.magixServer)
|
||||||
implementation("io.ktor:ktor-server-cio:$ktorVersion")
|
implementation("io.ktor:ktor-server-cio:$ktorVersion")
|
||||||
implementation("io.ktor:ktor-websockets:$ktorVersion")
|
implementation("io.ktor:ktor-websockets:$ktorVersion")
|
||||||
implementation("io.ktor:ktor-serialization:$ktorVersion")
|
implementation("io.ktor:ktor-serialization:$ktorVersion")
|
||||||
|
@ -4,13 +4,11 @@ import io.ktor.application.ApplicationCall
|
|||||||
import io.ktor.http.ContentType
|
import io.ktor.http.ContentType
|
||||||
import io.ktor.http.cio.websocket.Frame
|
import io.ktor.http.cio.websocket.Frame
|
||||||
import io.ktor.response.respondText
|
import io.ktor.response.respondText
|
||||||
import kotlinx.serialization.json.Json
|
|
||||||
import kotlinx.serialization.json.JsonObjectBuilder
|
import kotlinx.serialization.json.JsonObjectBuilder
|
||||||
import kotlinx.serialization.json.buildJsonObject
|
import kotlinx.serialization.json.buildJsonObject
|
||||||
import ru.mipt.npm.controls.api.DeviceMessage
|
import ru.mipt.npm.controls.api.DeviceMessage
|
||||||
import ru.mipt.npm.controls.api.toMeta
|
import ru.mipt.npm.magix.api.MagixEndpoint
|
||||||
import space.kscience.dataforge.io.*
|
import space.kscience.dataforge.io.*
|
||||||
import space.kscience.dataforge.meta.MetaSerializer
|
|
||||||
|
|
||||||
|
|
||||||
internal fun Frame.toEnvelope(): Envelope {
|
internal fun Frame.toEnvelope(): Envelope {
|
||||||
@ -29,6 +27,7 @@ internal suspend fun ApplicationCall.respondJson(builder: JsonObjectBuilder.() -
|
|||||||
respondText(json.toString(), contentType = ContentType.Application.Json)
|
respondText(json.toString(), contentType = ContentType.Application.Json)
|
||||||
}
|
}
|
||||||
|
|
||||||
public suspend fun ApplicationCall.respondMessage(message: DeviceMessage) {
|
public suspend fun ApplicationCall.respondMessage(message: DeviceMessage): Unit = respondText(
|
||||||
respondText(Json.encodeToString(MetaSerializer, message.toMeta()), contentType = ContentType.Application.Json)
|
MagixEndpoint.magixJson.encodeToString(DeviceMessage.serializer(), message),
|
||||||
}
|
contentType = ContentType.Application.Json
|
||||||
|
)
|
@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
package ru.mipt.npm.controls.server
|
package ru.mipt.npm.controls.server
|
||||||
|
|
||||||
|
|
||||||
@ -20,9 +19,9 @@ import io.ktor.server.engine.embeddedServer
|
|||||||
import io.ktor.util.getValue
|
import io.ktor.util.getValue
|
||||||
import io.ktor.websocket.WebSockets
|
import io.ktor.websocket.WebSockets
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
import kotlinx.html.*
|
import kotlinx.html.*
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import kotlinx.serialization.json.JsonObject
|
|
||||||
import kotlinx.serialization.json.buildJsonArray
|
import kotlinx.serialization.json.buildJsonArray
|
||||||
import kotlinx.serialization.json.put
|
import kotlinx.serialization.json.put
|
||||||
import ru.mipt.npm.controls.api.DeviceMessage
|
import ru.mipt.npm.controls.api.DeviceMessage
|
||||||
@ -31,8 +30,11 @@ import ru.mipt.npm.controls.api.PropertySetMessage
|
|||||||
import ru.mipt.npm.controls.api.getOrNull
|
import ru.mipt.npm.controls.api.getOrNull
|
||||||
import ru.mipt.npm.controls.controllers.DeviceManager
|
import ru.mipt.npm.controls.controllers.DeviceManager
|
||||||
import ru.mipt.npm.controls.controllers.respondMessage
|
import ru.mipt.npm.controls.controllers.respondMessage
|
||||||
|
import ru.mipt.npm.magix.api.MagixEndpoint
|
||||||
|
import ru.mipt.npm.magix.server.GenericMagixMessage
|
||||||
|
import ru.mipt.npm.magix.server.magixModule
|
||||||
|
import ru.mipt.npm.magix.server.rawMagixServerSocket
|
||||||
import space.kscience.dataforge.meta.toJson
|
import space.kscience.dataforge.meta.toJson
|
||||||
import space.kscience.dataforge.meta.toMeta
|
|
||||||
import space.kscience.dataforge.meta.toMetaItem
|
import space.kscience.dataforge.meta.toMetaItem
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -40,7 +42,7 @@ import space.kscience.dataforge.meta.toMetaItem
|
|||||||
*/
|
*/
|
||||||
public fun CoroutineScope.startDeviceServer(
|
public fun CoroutineScope.startDeviceServer(
|
||||||
manager: DeviceManager,
|
manager: DeviceManager,
|
||||||
port: Int = 8111,
|
port: Int = MagixEndpoint.DEFAULT_MAGIX_HTTP_PORT,
|
||||||
host: String = "localhost",
|
host: String = "localhost",
|
||||||
): ApplicationEngine {
|
): ApplicationEngine {
|
||||||
|
|
||||||
@ -54,7 +56,7 @@ public fun CoroutineScope.startDeviceServer(
|
|||||||
call.respond(HttpStatusCode.BadRequest, cause.message ?: "")
|
call.respond(HttpStatusCode.BadRequest, cause.message ?: "")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
deviceModule(manager)
|
deviceManagerModule(manager)
|
||||||
routing {
|
routing {
|
||||||
get("/") {
|
get("/") {
|
||||||
call.respondRedirect("/dashboard")
|
call.respondRedirect("/dashboard")
|
||||||
@ -70,22 +72,13 @@ public fun ApplicationEngine.whenStarted(callback: Application.() -> Unit) {
|
|||||||
|
|
||||||
public const val WEB_SERVER_TARGET: String = "@webServer"
|
public const val WEB_SERVER_TARGET: String = "@webServer"
|
||||||
|
|
||||||
public fun Application.deviceModule(
|
public fun Application.deviceManagerModule(
|
||||||
manager: DeviceManager,
|
manager: DeviceManager,
|
||||||
deviceNames: Collection<String> = manager.devices.keys.map { it.toString() },
|
deviceNames: Collection<String> = manager.devices.keys.map { it.toString() },
|
||||||
route: String = "/",
|
route: String = "/",
|
||||||
|
rawSocketPort: Int = MagixEndpoint.DEFAULT_MAGIX_RAW_PORT,
|
||||||
|
buffer: Int = 100,
|
||||||
) {
|
) {
|
||||||
// val controllers = deviceNames.associateWith { name ->
|
|
||||||
// val device = manager.devices[name.toName()]
|
|
||||||
// DeviceController(device, name, manager.context)
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// fun generateFlow(target: String?) = if (target == null) {
|
|
||||||
// controllers.values.asFlow().flatMapMerge { it.output() }
|
|
||||||
// } else {
|
|
||||||
// controllers[target]?.output() ?: error("The device with target $target not found")
|
|
||||||
// }
|
|
||||||
|
|
||||||
if (featureOrNull(WebSockets) == null) {
|
if (featureOrNull(WebSockets) == null) {
|
||||||
install(WebSockets)
|
install(WebSockets)
|
||||||
}
|
}
|
||||||
@ -159,32 +152,11 @@ public fun Application.deviceModule(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// //Check if application supports websockets and if it does add a push channel
|
|
||||||
// if (this.application.featureOrNull(WebSockets) != null) {
|
|
||||||
// webSocket("ws") {
|
|
||||||
// //subscribe on device
|
|
||||||
// val target: String? by call.request.queryParameters
|
|
||||||
//
|
|
||||||
// try {
|
|
||||||
// application.log.debug("Opened server socket for ${call.request.queryParameters}")
|
|
||||||
//
|
|
||||||
// manager.controller.envelopeOutput().collect {
|
|
||||||
// outgoing.send(it.toFrame())
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// } catch (ex: Exception) {
|
|
||||||
// application.log.debug("Closed server socket for ${call.request.queryParameters}")
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
post("message") {
|
post("message") {
|
||||||
val body = call.receiveText()
|
val body = call.receiveText()
|
||||||
val json = Json.parseToJsonElement(body) as? JsonObject
|
|
||||||
?: throw IllegalArgumentException("The body is not a json object")
|
|
||||||
val meta = json.toMeta()
|
|
||||||
|
|
||||||
val request = DeviceMessage.fromMeta(meta)
|
val request: DeviceMessage = MagixEndpoint.magixJson.decodeFromString(DeviceMessage.serializer(), body)
|
||||||
|
|
||||||
val response = manager.respondMessage(request)
|
val response = manager.respondMessage(request)
|
||||||
call.respondMessage(response)
|
call.respondMessage(response)
|
||||||
@ -226,4 +198,12 @@ public fun Application.deviceModule(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val magixFlow = MutableSharedFlow<GenericMagixMessage>(
|
||||||
|
buffer,
|
||||||
|
extraBufferCapacity = buffer
|
||||||
|
)
|
||||||
|
|
||||||
|
rawMagixServerSocket(magixFlow, rawSocketPort)
|
||||||
|
magixModule(magixFlow)
|
||||||
}
|
}
|
@ -13,9 +13,11 @@ repositories{
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies{
|
dependencies{
|
||||||
implementation(project(":controls-core"))
|
implementation(projects.controlsCore)
|
||||||
implementation(project(":controls-server"))
|
//implementation(projects.controlsServer)
|
||||||
implementation(project(":controls-magix-client"))
|
implementation(projects.magix.magixServer)
|
||||||
|
implementation(projects.controlsMagixClient)
|
||||||
|
implementation(projects.magix.magixRsocket)
|
||||||
implementation("no.tornado:tornadofx:1.7.20")
|
implementation("no.tornado:tornadofx:1.7.20")
|
||||||
implementation("space.kscience:plotlykt-server:0.4.2")
|
implementation("space.kscience:plotlykt-server:0.4.2")
|
||||||
implementation("com.github.Ricky12Awesome:json-schema-serialization:0.6.6")
|
implementation("com.github.Ricky12Awesome:json-schema-serialization:0.6.6")
|
||||||
|
@ -6,10 +6,14 @@ import javafx.scene.control.Slider
|
|||||||
import javafx.scene.layout.Priority
|
import javafx.scene.layout.Priority
|
||||||
import javafx.stage.Stage
|
import javafx.stage.Stage
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import space.kscience.dataforge.context.ContextAware
|
import ru.mipt.npm.controls.api.DeviceMessage
|
||||||
import space.kscience.dataforge.context.Global
|
import ru.mipt.npm.controls.client.launchDfMagix
|
||||||
import space.kscience.dataforge.context.info
|
import ru.mipt.npm.controls.controllers.DeviceManager
|
||||||
import space.kscience.dataforge.context.logger
|
import ru.mipt.npm.controls.controllers.install
|
||||||
|
import ru.mipt.npm.magix.api.MagixEndpoint
|
||||||
|
import ru.mipt.npm.magix.rsocket.rSocketWithTcp
|
||||||
|
import ru.mipt.npm.magix.server.startMagixServer
|
||||||
|
import space.kscience.dataforge.context.*
|
||||||
import tornadofx.*
|
import tornadofx.*
|
||||||
import java.awt.Desktop
|
import java.awt.Desktop
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
@ -17,21 +21,32 @@ import java.net.URI
|
|||||||
class DemoController : Controller(), ContextAware {
|
class DemoController : Controller(), ContextAware {
|
||||||
|
|
||||||
var device: DemoDevice? = null
|
var device: DemoDevice? = null
|
||||||
var server: ApplicationEngine? = null
|
var magixServer: ApplicationEngine? = null
|
||||||
override val context = Global.buildContext("demoDevice")
|
var visualizer: ApplicationEngine? = null
|
||||||
|
|
||||||
|
override val context = Context("demoDevice") {
|
||||||
|
plugin(DeviceManager)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val deviceManager = context.fetch(DeviceManager)
|
||||||
|
|
||||||
fun init() {
|
fun init() {
|
||||||
context.launch {
|
context.launch {
|
||||||
val demo = DemoDevice(context)
|
device = deviceManager.install("demo", DemoDevice)
|
||||||
device = demo
|
//starting magix event loop
|
||||||
server = startDemoDeviceServer(context, demo)
|
magixServer = startMagixServer()
|
||||||
|
//Launch device client and connect it to the server
|
||||||
|
deviceManager.launchDfMagix(MagixEndpoint.rSocketWithTcp("localhost", DeviceMessage.serializer()))
|
||||||
|
visualizer = startDemoDeviceServer()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun shutdown() {
|
fun shutdown() {
|
||||||
logger.info { "Shutting down..." }
|
logger.info { "Shutting down..." }
|
||||||
server?.stop(1000, 5000)
|
visualizer?.stop(1000,5000)
|
||||||
logger.info { "Visualization server stopped" }
|
logger.info { "Visualization server stopped" }
|
||||||
|
magixServer?.stop(1000, 5000)
|
||||||
|
logger.info { "Magix server stopped" }
|
||||||
device?.close()
|
device?.close()
|
||||||
logger.info { "Device server stopped" }
|
logger.info { "Device server stopped" }
|
||||||
context.close()
|
context.close()
|
||||||
@ -89,7 +104,7 @@ class DemoControllerView : View(title = " Demo controller remote") {
|
|||||||
button("Show plots") {
|
button("Show plots") {
|
||||||
useMaxWidth = true
|
useMaxWidth = true
|
||||||
action {
|
action {
|
||||||
controller.server?.run {
|
controller.magixServer?.run {
|
||||||
val host = "localhost"//environment.connectors.first().host
|
val host = "localhost"//environment.connectors.first().host
|
||||||
val port = environment.connectors.first().port
|
val port = environment.connectors.first().port
|
||||||
val uri = URI("http", null, host, port, "/plots", null, null)
|
val uri = URI("http", null, host, port, "/plots", null, null)
|
||||||
|
@ -4,9 +4,9 @@ import kotlinx.coroutines.CoroutineScope
|
|||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.asCoroutineDispatcher
|
import kotlinx.coroutines.asCoroutineDispatcher
|
||||||
import ru.mipt.npm.controls.base.*
|
import ru.mipt.npm.controls.base.*
|
||||||
|
import ru.mipt.npm.controls.controllers.DeviceSpec
|
||||||
import ru.mipt.npm.controls.controllers.double
|
import ru.mipt.npm.controls.controllers.double
|
||||||
import space.kscience.dataforge.context.Context
|
import space.kscience.dataforge.context.Context
|
||||||
import space.kscience.dataforge.context.Factory
|
|
||||||
import space.kscience.dataforge.meta.Meta
|
import space.kscience.dataforge.meta.Meta
|
||||||
import space.kscience.dataforge.values.asValue
|
import space.kscience.dataforge.values.asValue
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
@ -30,7 +30,7 @@ class DemoDevice(context: Context) : DeviceBase(context) {
|
|||||||
|
|
||||||
val sinScale by writingVirtual(1.0.asValue())
|
val sinScale by writingVirtual(1.0.asValue())
|
||||||
var sinScaleValue by sinScale.double()
|
var sinScaleValue by sinScale.double()
|
||||||
val sin by readingNumber {
|
val sin: TypedReadOnlyDeviceProperty<Number> by readingNumber {
|
||||||
val time = Instant.now()
|
val time = Instant.now()
|
||||||
sin(time.toEpochMilli().toDouble() / timeScaleValue) * sinScaleValue
|
sin(time.toEpochMilli().toDouble() / timeScaleValue) * sinScaleValue
|
||||||
}
|
}
|
||||||
@ -67,7 +67,7 @@ class DemoDevice(context: Context) : DeviceBase(context) {
|
|||||||
executor.shutdown()
|
executor.shutdown()
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object : Factory<DemoDevice> {
|
companion object : DeviceSpec<DemoDevice> {
|
||||||
override fun invoke(meta: Meta, context: Context): DemoDevice = DemoDevice(context)
|
override fun invoke(meta: Meta, context: Context): DemoDevice = DemoDevice(context)
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,16 +1,18 @@
|
|||||||
package ru.mipt.npm.controls.demo
|
package ru.mipt.npm.controls.demo
|
||||||
|
|
||||||
|
import io.ktor.server.cio.CIO
|
||||||
import io.ktor.server.engine.ApplicationEngine
|
import io.ktor.server.engine.ApplicationEngine
|
||||||
|
import io.ktor.server.engine.embeddedServer
|
||||||
import kotlinx.coroutines.flow.*
|
import kotlinx.coroutines.flow.*
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.html.div
|
import kotlinx.html.div
|
||||||
import kotlinx.html.link
|
import kotlinx.html.link
|
||||||
import ru.mipt.npm.controls.controllers.devices
|
import ru.mipt.npm.controls.api.DeviceMessage
|
||||||
import ru.mipt.npm.controls.server.startDeviceServer
|
import ru.mipt.npm.controls.api.PropertyChangedMessage
|
||||||
import ru.mipt.npm.controls.server.whenStarted
|
import ru.mipt.npm.magix.api.MagixEndpoint
|
||||||
import space.kscience.dataforge.context.Context
|
import ru.mipt.npm.magix.rsocket.rSocketWithWebSockets
|
||||||
|
import space.kscience.dataforge.meta.MetaItem
|
||||||
import space.kscience.dataforge.meta.double
|
import space.kscience.dataforge.meta.double
|
||||||
import space.kscience.dataforge.names.NameToken
|
|
||||||
import space.kscience.plotly.layout
|
import space.kscience.plotly.layout
|
||||||
import space.kscience.plotly.models.Trace
|
import space.kscience.plotly.models.Trace
|
||||||
import space.kscience.plotly.plot
|
import space.kscience.plotly.plot
|
||||||
@ -49,16 +51,26 @@ suspend fun Trace.updateXYFrom(flow: Flow<Iterable<Pair<Double, Double>>>) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fun startDemoDeviceServer(context: Context, device: DemoDevice): ApplicationEngine {
|
suspend fun startDemoDeviceServer(magixHost: String = "localhost"): ApplicationEngine = embeddedServer(CIO, 8080) {
|
||||||
context.devices.registerDevice(NameToken("demo"), device)
|
val sinFlow = MutableSharedFlow<MetaItem?>()// = device.sin.flow()
|
||||||
val server = context.startDeviceServer(context.devices)
|
val cosFlow = MutableSharedFlow<MetaItem?>()// = device.cos.flow()
|
||||||
server.whenStarted {
|
|
||||||
|
launch {
|
||||||
|
val endpoint = MagixEndpoint.rSocketWithWebSockets(magixHost, DeviceMessage.serializer())
|
||||||
|
endpoint.subscribe().collect { magix ->
|
||||||
|
(magix.payload as? PropertyChangedMessage)?.let { message ->
|
||||||
|
when (message.property) {
|
||||||
|
"sin" -> sinFlow.emit(message.value)
|
||||||
|
"cos" -> cosFlow.emit(message.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
plotlyModule("plots").apply {
|
plotlyModule("plots").apply {
|
||||||
updateMode = PlotlyUpdateMode.PUSH
|
updateMode = PlotlyUpdateMode.PUSH
|
||||||
updateInterval = 50
|
updateInterval = 50
|
||||||
}.page { container ->
|
}.page { container ->
|
||||||
val sinFlow = device.sin.flow()
|
|
||||||
val cosFlow = device.cos.flow()
|
|
||||||
val sinCosFlow = sinFlow.zip(cosFlow) { sin, cos ->
|
val sinCosFlow = sinFlow.zip(cosFlow) { sin, cos ->
|
||||||
sin.double!! to cos.double!!
|
sin.double!! to cos.double!!
|
||||||
}
|
}
|
||||||
@ -77,7 +89,7 @@ fun startDemoDeviceServer(context: Context, device: DemoDevice): ApplicationEngi
|
|||||||
yaxis.title = "sin"
|
yaxis.title = "sin"
|
||||||
}
|
}
|
||||||
trace {
|
trace {
|
||||||
context.launch {
|
launch {
|
||||||
val flow: Flow<Iterable<Double>> = sinFlow.mapNotNull { it.double }.windowed(100)
|
val flow: Flow<Iterable<Double>> = sinFlow.mapNotNull { it.double }.windowed(100)
|
||||||
updateFrom(Trace.Y_AXIS, flow)
|
updateFrom(Trace.Y_AXIS, flow)
|
||||||
}
|
}
|
||||||
@ -92,7 +104,7 @@ fun startDemoDeviceServer(context: Context, device: DemoDevice): ApplicationEngi
|
|||||||
yaxis.title = "cos"
|
yaxis.title = "cos"
|
||||||
}
|
}
|
||||||
trace {
|
trace {
|
||||||
context.launch {
|
launch {
|
||||||
val flow: Flow<Iterable<Double>> = cosFlow.mapNotNull { it.double }.windowed(100)
|
val flow: Flow<Iterable<Double>> = cosFlow.mapNotNull { it.double }.windowed(100)
|
||||||
updateFrom(Trace.Y_AXIS, flow)
|
updateFrom(Trace.Y_AXIS, flow)
|
||||||
}
|
}
|
||||||
@ -110,7 +122,7 @@ fun startDemoDeviceServer(context: Context, device: DemoDevice): ApplicationEngi
|
|||||||
}
|
}
|
||||||
trace {
|
trace {
|
||||||
name = "non-synchronized"
|
name = "non-synchronized"
|
||||||
context.launch {
|
launch {
|
||||||
val flow: Flow<Iterable<Pair<Double, Double>>> = sinCosFlow.windowed(30)
|
val flow: Flow<Iterable<Pair<Double, Double>>> = sinCosFlow.windowed(30)
|
||||||
updateXYFrom(flow)
|
updateXYFrom(flow)
|
||||||
}
|
}
|
||||||
@ -119,7 +131,5 @@ fun startDemoDeviceServer(context: Context, device: DemoDevice): ApplicationEngi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return server
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -27,8 +27,16 @@ public interface MagixEndpoint<T> {
|
|||||||
)
|
)
|
||||||
|
|
||||||
public companion object {
|
public companion object {
|
||||||
public const val DEFAULT_MAGIX_WS_PORT: Int = 7777
|
/**
|
||||||
|
* A default port for HTTP/WS connections
|
||||||
|
*/
|
||||||
|
public const val DEFAULT_MAGIX_HTTP_PORT: Int = 7777
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A default port for raw TCP connections
|
||||||
|
*/
|
||||||
public const val DEFAULT_MAGIX_RAW_PORT: Int = 7778
|
public const val DEFAULT_MAGIX_RAW_PORT: Int = 7778
|
||||||
|
|
||||||
public val magixJson: Json = Json {
|
public val magixJson: Json = Json {
|
||||||
ignoreUnknownKeys = true
|
ignoreUnknownKeys = true
|
||||||
encodeDefaults = false
|
encodeDefaults = false
|
||||||
|
@ -16,10 +16,23 @@ public interface MagixClient<T> {
|
|||||||
|
|
||||||
Flow.Publisher<MagixMessage<T>> subscribe();
|
Flow.Publisher<MagixMessage<T>> subscribe();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a magix endpoint client using RSocket with raw tcp connection
|
||||||
|
* @param host host name of magix server event loop
|
||||||
|
* @param port port of magix server event loop
|
||||||
|
* @return the client
|
||||||
|
*/
|
||||||
static MagixClient<JsonElement> rSocketTcp(String host, int port) {
|
static MagixClient<JsonElement> rSocketTcp(String host, int port) {
|
||||||
return ControlsMagixClient.Companion.rSocketTcp(host, port, JsonElement.Companion.serializer());
|
return ControlsMagixClient.Companion.rSocketTcp(host, port, JsonElement.Companion.serializer());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param host host name of magix server event loop
|
||||||
|
* @param port port of magix server event loop
|
||||||
|
* @param path
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
static MagixClient<JsonElement> rSocketWs(String host, int port, String path) {
|
static MagixClient<JsonElement> rSocketWs(String host, int port, String path) {
|
||||||
return ControlsMagixClient.Companion.rSocketWs(host, port, JsonElement.Companion.serializer(), path);
|
return ControlsMagixClient.Companion.rSocketWs(host, port, JsonElement.Companion.serializer(), path);
|
||||||
}
|
}
|
||||||
|
@ -6,11 +6,11 @@ import kotlinx.serialization.KSerializer
|
|||||||
import ru.mipt.npm.magix.api.MagixEndpoint
|
import ru.mipt.npm.magix.api.MagixEndpoint
|
||||||
import ru.mipt.npm.magix.api.MagixMessage
|
import ru.mipt.npm.magix.api.MagixMessage
|
||||||
import ru.mipt.npm.magix.api.MagixMessageFilter
|
import ru.mipt.npm.magix.api.MagixMessageFilter
|
||||||
import ru.mipt.npm.magix.rsocket.RSocketMagixEndpoint
|
import ru.mipt.npm.magix.rsocket.rSocketWithTcp
|
||||||
import ru.mipt.npm.magix.rsocket.withTcp
|
import ru.mipt.npm.magix.rsocket.rSocketWithWebSockets
|
||||||
import java.util.concurrent.Flow
|
import java.util.concurrent.Flow
|
||||||
|
|
||||||
public class ControlsMagixClient<T>(
|
internal class ControlsMagixClient<T>(
|
||||||
private val endpoint: MagixEndpoint<T>,
|
private val endpoint: MagixEndpoint<T>,
|
||||||
private val filter: MagixMessageFilter,
|
private val filter: MagixMessageFilter,
|
||||||
) : MagixClient<T> {
|
) : MagixClient<T> {
|
||||||
@ -21,27 +21,27 @@ public class ControlsMagixClient<T>(
|
|||||||
|
|
||||||
override fun subscribe(): Flow.Publisher<MagixMessage<T>> = endpoint.subscribe(filter).asPublisher()
|
override fun subscribe(): Flow.Publisher<MagixMessage<T>> = endpoint.subscribe(filter).asPublisher()
|
||||||
|
|
||||||
public companion object {
|
companion object {
|
||||||
|
|
||||||
public fun <T> rSocketTcp(
|
fun <T> rSocketTcp(
|
||||||
host: String,
|
host: String,
|
||||||
port: Int,
|
port: Int,
|
||||||
payloadSerializer: KSerializer<T>
|
payloadSerializer: KSerializer<T>
|
||||||
): ControlsMagixClient<T> {
|
): ControlsMagixClient<T> {
|
||||||
val endpoint = runBlocking {
|
val endpoint = runBlocking {
|
||||||
RSocketMagixEndpoint.withTcp(host, port, payloadSerializer)
|
MagixEndpoint.rSocketWithTcp(host, payloadSerializer, port)
|
||||||
}
|
}
|
||||||
return ControlsMagixClient(endpoint, MagixMessageFilter())
|
return ControlsMagixClient(endpoint, MagixMessageFilter())
|
||||||
}
|
}
|
||||||
|
|
||||||
public fun <T> rSocketWs(
|
fun <T> rSocketWs(
|
||||||
host: String,
|
host: String,
|
||||||
port: Int,
|
port: Int,
|
||||||
payloadSerializer: KSerializer<T>,
|
payloadSerializer: KSerializer<T>,
|
||||||
path: String = "/rsocket"
|
path: String = "/rsocket"
|
||||||
): ControlsMagixClient<T> {
|
): ControlsMagixClient<T> {
|
||||||
val endpoint = runBlocking {
|
val endpoint = runBlocking {
|
||||||
RSocketMagixEndpoint.withWebSockets(host, port, payloadSerializer, path)
|
MagixEndpoint.rSocketWithWebSockets(host, payloadSerializer, port, path)
|
||||||
}
|
}
|
||||||
return ControlsMagixClient(endpoint, MagixMessageFilter())
|
return ControlsMagixClient(endpoint, MagixMessageFilter())
|
||||||
}
|
}
|
||||||
|
@ -43,24 +43,26 @@ public class RSocketMagixEndpoint<T>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public companion object {
|
public companion object
|
||||||
|
}
|
||||||
|
|
||||||
internal fun buildConnector(rSocketConfig: RSocketConnectorBuilder.ConnectionConfigBuilder.() -> Unit) =
|
|
||||||
|
internal fun buildConnector(rSocketConfig: RSocketConnectorBuilder.ConnectionConfigBuilder.() -> Unit) =
|
||||||
RSocketConnector {
|
RSocketConnector {
|
||||||
reconnectable(10)
|
reconnectable(10)
|
||||||
connectionConfig(rSocketConfig)
|
connectionConfig(rSocketConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build a websocket based endpoint connected to [host], [port] and given routing [path]
|
* Build a websocket based endpoint connected to [host], [port] and given routing [path]
|
||||||
*/
|
*/
|
||||||
public suspend fun <T> withWebSockets(
|
public suspend fun <T> MagixEndpoint.Companion.rSocketWithWebSockets(
|
||||||
host: String,
|
host: String,
|
||||||
port: Int,
|
|
||||||
payloadSerializer: KSerializer<T>,
|
payloadSerializer: KSerializer<T>,
|
||||||
|
port: Int = DEFAULT_MAGIX_HTTP_PORT,
|
||||||
path: String = "/rsocket",
|
path: String = "/rsocket",
|
||||||
rSocketConfig: RSocketConnectorBuilder.ConnectionConfigBuilder.() -> Unit = {},
|
rSocketConfig: RSocketConnectorBuilder.ConnectionConfigBuilder.() -> Unit = {},
|
||||||
): RSocketMagixEndpoint<T> {
|
): RSocketMagixEndpoint<T> {
|
||||||
val client = HttpClient {
|
val client = HttpClient {
|
||||||
install(WebSockets)
|
install(WebSockets)
|
||||||
install(RSocketSupport) {
|
install(RSocketSupport) {
|
||||||
@ -76,6 +78,4 @@ public class RSocketMagixEndpoint<T>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
return RSocketMagixEndpoint(coroutineContext, payloadSerializer, rSocket)
|
return RSocketMagixEndpoint(coroutineContext, payloadSerializer, rSocket)
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
@ -7,16 +7,17 @@ import io.rsocket.kotlin.core.RSocketConnectorBuilder
|
|||||||
import io.rsocket.kotlin.transport.ktor.clientTransport
|
import io.rsocket.kotlin.transport.ktor.clientTransport
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.serialization.KSerializer
|
import kotlinx.serialization.KSerializer
|
||||||
|
import ru.mipt.npm.magix.api.MagixEndpoint
|
||||||
import kotlin.coroutines.coroutineContext
|
import kotlin.coroutines.coroutineContext
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a plain TCP based [RSocketMagixEndpoint] connected to [host] and [port]
|
* Create a plain TCP based [RSocketMagixEndpoint] connected to [host] and [port]
|
||||||
*/
|
*/
|
||||||
public suspend fun <T> RSocketMagixEndpoint.Companion.withTcp(
|
public suspend fun <T> MagixEndpoint.Companion.rSocketWithTcp(
|
||||||
host: String,
|
host: String,
|
||||||
port: Int,
|
|
||||||
payloadSerializer: KSerializer<T>,
|
payloadSerializer: KSerializer<T>,
|
||||||
|
port: Int = DEFAULT_MAGIX_RAW_PORT,
|
||||||
tcpConfig: SocketOptions.TCPClientSocketOptions.() -> Unit = {},
|
tcpConfig: SocketOptions.TCPClientSocketOptions.() -> Unit = {},
|
||||||
rSocketConfig: RSocketConnectorBuilder.ConnectionConfigBuilder.() -> Unit = {},
|
rSocketConfig: RSocketConnectorBuilder.ConnectionConfigBuilder.() -> Unit = {},
|
||||||
): RSocketMagixEndpoint<T> {
|
): RSocketMagixEndpoint<T> {
|
||||||
|
@ -4,6 +4,10 @@ plugins {
|
|||||||
application
|
application
|
||||||
}
|
}
|
||||||
|
|
||||||
|
description = """
|
||||||
|
A magix event loop implementation in Kotlin. Includes HTTP/SSE and RSocket routes.
|
||||||
|
""".trimIndent()
|
||||||
|
|
||||||
kscience {
|
kscience {
|
||||||
useSerialization{
|
useSerialization{
|
||||||
json()
|
json()
|
||||||
|
@ -9,12 +9,28 @@ import io.rsocket.kotlin.core.RSocketServer
|
|||||||
import io.rsocket.kotlin.transport.ktor.serverTransport
|
import io.rsocket.kotlin.transport.ktor.serverTransport
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
|
import ru.mipt.npm.magix.api.MagixEndpoint.Companion.DEFAULT_MAGIX_HTTP_PORT
|
||||||
import ru.mipt.npm.magix.api.MagixEndpoint.Companion.DEFAULT_MAGIX_RAW_PORT
|
import ru.mipt.npm.magix.api.MagixEndpoint.Companion.DEFAULT_MAGIX_RAW_PORT
|
||||||
import ru.mipt.npm.magix.api.MagixEndpoint.Companion.DEFAULT_MAGIX_WS_PORT
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public fun CoroutineScope.rawMagixServerSocket(
|
||||||
|
magixFlow: MutableSharedFlow<GenericMagixMessage>,
|
||||||
|
rawSocketPort: Int = DEFAULT_MAGIX_RAW_PORT
|
||||||
|
): Job {
|
||||||
|
val tcpTransport = aSocket(ActorSelectorManager(Dispatchers.IO)).tcp().serverTransport(port = rawSocketPort)
|
||||||
|
val rSocketJob = RSocketServer().bind(tcpTransport, magixAcceptor(magixFlow))
|
||||||
|
coroutineContext[Job]?.invokeOnCompletion{
|
||||||
|
rSocketJob.cancel()
|
||||||
|
}
|
||||||
|
return rSocketJob;
|
||||||
|
}
|
||||||
|
|
||||||
public fun CoroutineScope.startMagixServer(
|
public fun CoroutineScope.startMagixServer(
|
||||||
port: Int = DEFAULT_MAGIX_WS_PORT,
|
port: Int = DEFAULT_MAGIX_HTTP_PORT,
|
||||||
rawSocketPort: Int = DEFAULT_MAGIX_RAW_PORT,
|
rawSocketPort: Int = DEFAULT_MAGIX_RAW_PORT,
|
||||||
buffer: Int = 100,
|
buffer: Int = 100,
|
||||||
): ApplicationEngine {
|
): ApplicationEngine {
|
||||||
@ -24,8 +40,7 @@ public fun CoroutineScope.startMagixServer(
|
|||||||
extraBufferCapacity = buffer
|
extraBufferCapacity = buffer
|
||||||
)
|
)
|
||||||
|
|
||||||
val tcpTransport = aSocket(ActorSelectorManager(Dispatchers.IO)).tcp().serverTransport(port = rawSocketPort)
|
rawMagixServerSocket(magixFlow, rawSocketPort)
|
||||||
RSocketServer().bind(tcpTransport, magixAcceptor(magixFlow))
|
|
||||||
|
|
||||||
return embeddedServer(CIO, port = port) {
|
return embeddedServer(CIO, port = port) {
|
||||||
magixModule(magixFlow)
|
magixModule(magixFlow)
|
||||||
|
@ -3,9 +3,9 @@ package ru.mipt.npm.magix.zmq
|
|||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.channelFlow
|
import kotlinx.coroutines.flow.channelFlow
|
||||||
|
import kotlinx.coroutines.flow.flowOn
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import kotlinx.serialization.KSerializer
|
import kotlinx.serialization.KSerializer
|
||||||
import kotlinx.serialization.encodeToString
|
|
||||||
import org.zeromq.SocketType
|
import org.zeromq.SocketType
|
||||||
import org.zeromq.ZContext
|
import org.zeromq.ZContext
|
||||||
import org.zeromq.ZMQ
|
import org.zeromq.ZMQ
|
||||||
@ -13,6 +13,7 @@ import org.zeromq.ZMQException
|
|||||||
import ru.mipt.npm.magix.api.MagixEndpoint
|
import ru.mipt.npm.magix.api.MagixEndpoint
|
||||||
import ru.mipt.npm.magix.api.MagixMessage
|
import ru.mipt.npm.magix.api.MagixMessage
|
||||||
import ru.mipt.npm.magix.api.MagixMessageFilter
|
import ru.mipt.npm.magix.api.MagixMessageFilter
|
||||||
|
import ru.mipt.npm.magix.api.filter
|
||||||
import kotlin.coroutines.CoroutineContext
|
import kotlin.coroutines.CoroutineContext
|
||||||
|
|
||||||
public class ZmqMagixEndpoint<T>(
|
public class ZmqMagixEndpoint<T>(
|
||||||
@ -28,7 +29,7 @@ public class ZmqMagixEndpoint<T>(
|
|||||||
val socket = zmqContext.createSocket(SocketType.XSUB)
|
val socket = zmqContext.createSocket(SocketType.XSUB)
|
||||||
socket.bind(address)
|
socket.bind(address)
|
||||||
|
|
||||||
val topic = MagixEndpoint.magixJson.encodeToString(filter)
|
val topic = "magix"//MagixEndpoint.magixJson.encodeToString(filter)
|
||||||
socket.subscribe(topic)
|
socket.subscribe(topic)
|
||||||
|
|
||||||
return channelFlow {
|
return channelFlow {
|
||||||
@ -39,6 +40,7 @@ public class ZmqMagixEndpoint<T>(
|
|||||||
}
|
}
|
||||||
while (activeFlag) {
|
while (activeFlag) {
|
||||||
try {
|
try {
|
||||||
|
//This is a blocking call.
|
||||||
val string = socket.recvStr()
|
val string = socket.recvStr()
|
||||||
val message = MagixEndpoint.magixJson.decodeFromString(serializer, string)
|
val message = MagixEndpoint.magixJson.decodeFromString(serializer, string)
|
||||||
send(message)
|
send(message)
|
||||||
@ -51,7 +53,7 @@ public class ZmqMagixEndpoint<T>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}.filter(filter).flowOn(Dispatchers.IO) //should be flown on IO because of blocking calls
|
||||||
}
|
}
|
||||||
|
|
||||||
private val publishSocket = zmqContext.createSocket(SocketType.XPUB).apply {
|
private val publishSocket = zmqContext.createSocket(SocketType.XPUB).apply {
|
||||||
|
@ -4,14 +4,13 @@ package ru.mipt.npm.devices.pimotionmaster
|
|||||||
|
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.flow.toList
|
|
||||||
import kotlinx.coroutines.flow.transformWhile
|
import kotlinx.coroutines.flow.transformWhile
|
||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.sync.Mutex
|
||||||
import kotlinx.coroutines.sync.withLock
|
import kotlinx.coroutines.sync.withLock
|
||||||
import ru.mipt.npm.controls.api.DeviceHub
|
import ru.mipt.npm.controls.api.DeviceHub
|
||||||
import ru.mipt.npm.controls.api.PropertyDescriptor
|
import ru.mipt.npm.controls.api.PropertyDescriptor
|
||||||
import ru.mipt.npm.controls.base.*
|
import ru.mipt.npm.controls.base.*
|
||||||
import ru.mipt.npm.controls.controllers.DeviceFactory
|
import ru.mipt.npm.controls.controllers.DeviceSpec
|
||||||
import ru.mipt.npm.controls.controllers.duration
|
import ru.mipt.npm.controls.controllers.duration
|
||||||
import ru.mipt.npm.controls.ports.*
|
import ru.mipt.npm.controls.ports.*
|
||||||
import space.kscience.dataforge.context.*
|
import space.kscience.dataforge.context.*
|
||||||
@ -343,7 +342,7 @@ class PiMotionMasterDevice(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object : DeviceFactory<PiMotionMasterDevice> {
|
companion object : DeviceSpec<PiMotionMasterDevice> {
|
||||||
override fun invoke(meta: Meta, context: Context): PiMotionMasterDevice = PiMotionMasterDevice(context)
|
override fun invoke(meta: Meta, context: Context): PiMotionMasterDevice = PiMotionMasterDevice(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user