Dev #6

Merged
altavir merged 75 commits from dev into master 2021-10-23 11:02:48 +03:00
18 changed files with 326 additions and 267 deletions
Showing only changes of commit 28e6e24cf7 - Show all commits
controls-core/src/commonMain/kotlin/ru/mipt/npm/controls
controls-server
build.gradle.kts
src/main/kotlin/ru/mipt/npm/controls/server
demo
magix
magix-api/src/commonMain/kotlin/ru/mipt/npm/magix/api
magix-java-client/src/main
java/ru/mipt/npm/magix/client
kotlin/ru/mipt/npm/magix/client
magix-rsocket/src
commonMain/kotlin/ru/mipt/npm/magix/rsocket
jvmMain/kotlin/ru/mipt/npm/magix/rsocket
magix-server
build.gradle.kts
src/main/kotlin/ru/mipt/npm/magix/server
magix-zmq/src/main/kotlin/ru/mipt/npm/magix/zmq
motors/src/main/kotlin/ru/mipt/npm/devices/pimotionmaster

@ -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`
} }
val dataforgeVersion: String by rootProject.extra description = """
val ktorVersion: String = "1.5.3" 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()
dependencies{ val dataforgeVersion: String by rootProject.extra
val ktorVersion: String = "1.5.3"
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,77 +51,85 @@ 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 {
plotlyModule("plots").apply { launch {
updateMode = PlotlyUpdateMode.PUSH val endpoint = MagixEndpoint.rSocketWithWebSockets(magixHost, DeviceMessage.serializer())
updateInterval = 50 endpoint.subscribe().collect { magix ->
}.page { container -> (magix.payload as? PropertyChangedMessage)?.let { message ->
val sinFlow = device.sin.flow() when (message.property) {
val cosFlow = device.cos.flow() "sin" -> sinFlow.emit(message.value)
val sinCosFlow = sinFlow.zip(cosFlow) { sin, cos -> "cos" -> cosFlow.emit(message.value)
sin.double!! to cos.double!!
}
link {
rel = "stylesheet"
href = "https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css"
attributes["integrity"] = "sha384-9aIt2nRpC12Uk9gS9baDl411NQApFmC26EwAOH8WgZl5MYYxFfc+NcPb1dKGj7Sk"
attributes["crossorigin"] = "anonymous"
}
div("row") {
div("col-6") {
plot(renderer = container) {
layout {
title = "sin property"
xaxis.title = "point index"
yaxis.title = "sin"
}
trace {
context.launch {
val flow: Flow<Iterable<Double>> = sinFlow.mapNotNull { it.double }.windowed(100)
updateFrom(Trace.Y_AXIS, flow)
}
}
}
} }
div("col-6") { }
plot(renderer = container) { }
layout { }
title = "cos property"
xaxis.title = "point index" plotlyModule("plots").apply {
yaxis.title = "cos" updateMode = PlotlyUpdateMode.PUSH
} updateInterval = 50
trace { }.page { container ->
context.launch { val sinCosFlow = sinFlow.zip(cosFlow) { sin, cos ->
val flow: Flow<Iterable<Double>> = cosFlow.mapNotNull { it.double }.windowed(100) sin.double!! to cos.double!!
updateFrom(Trace.Y_AXIS, flow) }
} link {
rel = "stylesheet"
href = "https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css"
attributes["integrity"] = "sha384-9aIt2nRpC12Uk9gS9baDl411NQApFmC26EwAOH8WgZl5MYYxFfc+NcPb1dKGj7Sk"
attributes["crossorigin"] = "anonymous"
}
div("row") {
div("col-6") {
plot(renderer = container) {
layout {
title = "sin property"
xaxis.title = "point index"
yaxis.title = "sin"
}
trace {
launch {
val flow: Flow<Iterable<Double>> = sinFlow.mapNotNull { it.double }.windowed(100)
updateFrom(Trace.Y_AXIS, flow)
} }
} }
} }
} }
div("row") { div("col-6") {
div("col-12") { plot(renderer = container) {
plot(renderer = container) { layout {
layout { title = "cos property"
title = "cos vs sin" xaxis.title = "point index"
xaxis.title = "sin" yaxis.title = "cos"
yaxis.title = "cos" }
trace {
launch {
val flow: Flow<Iterable<Double>> = cosFlow.mapNotNull { it.double }.windowed(100)
updateFrom(Trace.Y_AXIS, flow)
} }
trace { }
name = "non-synchronized" }
context.launch { }
val flow: Flow<Iterable<Pair<Double, Double>>> = sinCosFlow.windowed(30) }
updateXYFrom(flow) div("row") {
} div("col-12") {
plot(renderer = container) {
layout {
title = "cos vs sin"
xaxis.title = "sin"
yaxis.title = "cos"
}
trace {
name = "non-synchronized"
launch {
val flow: Flow<Iterable<Pair<Double, Double>>> = sinCosFlow.windowed(30)
updateXYFrom(flow)
} }
} }
} }
} }
} }
} }
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,39 +43,39 @@ public class RSocketMagixEndpoint<T>(
} }
} }
public companion object { public companion object
}
internal fun buildConnector(rSocketConfig: RSocketConnectorBuilder.ConnectionConfigBuilder.() -> Unit) =
RSocketConnector {
reconnectable(10)
connectionConfig(rSocketConfig)
}
/** internal fun buildConnector(rSocketConfig: RSocketConnectorBuilder.ConnectionConfigBuilder.() -> Unit) =
* Build a websocket based endpoint connected to [host], [port] and given routing [path] RSocketConnector {
*/ reconnectable(10)
public suspend fun <T> withWebSockets( connectionConfig(rSocketConfig)
host: String, }
port: Int,
payloadSerializer: KSerializer<T>,
path: String = "/rsocket",
rSocketConfig: RSocketConnectorBuilder.ConnectionConfigBuilder.() -> Unit = {},
): RSocketMagixEndpoint<T> {
val client = HttpClient {
install(WebSockets)
install(RSocketSupport) {
connector = buildConnector(rSocketConfig)
}
}
val rSocket = client.rSocket(host, port, path) /**
* Build a websocket based endpoint connected to [host], [port] and given routing [path]
//Ensure client is closed after rSocket if finished */
rSocket.job.invokeOnCompletion { public suspend fun <T> MagixEndpoint.Companion.rSocketWithWebSockets(
client.close() host: String,
} payloadSerializer: KSerializer<T>,
port: Int = DEFAULT_MAGIX_HTTP_PORT,
return RSocketMagixEndpoint(coroutineContext, payloadSerializer, rSocket) path: String = "/rsocket",
rSocketConfig: RSocketConnectorBuilder.ConnectionConfigBuilder.() -> Unit = {},
): RSocketMagixEndpoint<T> {
val client = HttpClient {
install(WebSockets)
install(RSocketSupport) {
connector = buildConnector(rSocketConfig)
} }
} }
val rSocket = client.rSocket(host, port, path)
//Ensure client is closed after rSocket if finished
rSocket.job.invokeOnCompletion {
client.close()
}
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)
} }