diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..bbe1eed --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "magix/rfc"] + path = magix/rfc + url = https://github.com/waltz-controls/rfc diff --git a/.space/CODEOWNERS b/.space/CODEOWNERS new file mode 100644 index 0000000..9f836ea --- /dev/null +++ b/.space/CODEOWNERS @@ -0,0 +1 @@ +./space/* "Project Admin" diff --git a/build.gradle.kts b/build.gradle.kts index 6d7df71..c9162f9 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,19 +1,38 @@ +import space.kscience.gradle.isInDevelopment +import space.kscience.gradle.useApache2Licence +import space.kscience.gradle.useSPCTeam + plugins { - id("ru.mipt.npm.gradle.project") + id("space.kscience.gradle.project") } -val dataforgeVersion: String by extra("0.5.1") -val ktorVersion: String by extra(ru.mipt.npm.gradle.KScienceVersions.ktorVersion) -val rsocketVersion by extra("0.13.1") +val dataforgeVersion: String by extra("0.6.1-dev-4") +val ktorVersion: String by extra(space.kscience.gradle.KScienceVersions.ktorVersion) +val rsocketVersion by extra("0.15.4") +val xodusVersion by extra("2.0.1") allprojects { - group = "ru.mipt.npm" - version = "0.1.1" + group = "space.kscience" + version = "0.1.1-SNAPSHOT" + repositories{ + maven("https://maven.pkg.jetbrains.space/spc/p/sci/dev") + } } ksciencePublish { - github("controls.kt") - space() + pom("https://github.com/SciProgCentre/controls.kt") { + useApache2Licence() + useSPCTeam() + } + github("controls.kt", "SciProgCentre") + space( + if (isInDevelopment) { + "https://maven.pkg.jetbrains.space/spc/p/sci/dev" + } else { + "https://maven.pkg.jetbrains.space/spc/p/sci/release" + } + ) + space("https://maven.pkg.jetbrains.space/spc/p/controls/maven") } apiValidation { diff --git a/controls-core/build.gradle.kts b/controls-core/build.gradle.kts index 53769c9..8585f17 100644 --- a/controls-core/build.gradle.kts +++ b/controls-core/build.gradle.kts @@ -1,24 +1,20 @@ plugins { - id("ru.mipt.npm.gradle.mpp") + id("space.kscience.gradle.mpp") `maven-publish` } val dataforgeVersion: String by rootProject.extra kscience { - useCoroutines("1.4.1") + jvm() + js() + native() + useCoroutines() useSerialization{ json() } -} - -kotlin { - sourceSets { - commonMain{ - dependencies { - api("space.kscience:dataforge-io:$dataforgeVersion") - api(npm.kotlinx.datetime) - } - } + dependencies { + api("space.kscience:dataforge-io:$dataforgeVersion") + api(npmlibs.kotlinx.datetime) } -} \ No newline at end of file +} diff --git a/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/base/DeviceAction.kt b/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/base/DeviceAction.kt deleted file mode 100644 index b75b79f..0000000 --- a/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/base/DeviceAction.kt +++ /dev/null @@ -1,10 +0,0 @@ -package ru.mipt.npm.controls.base - -import ru.mipt.npm.controls.api.ActionDescriptor -import space.kscience.dataforge.meta.Meta - -public interface DeviceAction { - public val name: String - public val descriptor: ActionDescriptor - public suspend operator fun invoke(arg: Meta? = null): Meta? -} diff --git a/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/base/DeviceBase.kt b/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/base/DeviceBase.kt deleted file mode 100644 index 52d37bf..0000000 --- a/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/base/DeviceBase.kt +++ /dev/null @@ -1,252 +0,0 @@ -package ru.mipt.npm.controls.base - -import kotlinx.coroutines.* -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharedFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock -import ru.mipt.npm.controls.api.* -import space.kscience.dataforge.context.Context -import space.kscience.dataforge.meta.Meta -import space.kscience.dataforge.misc.DFExperimental -import kotlin.collections.set -import kotlin.coroutines.CoroutineContext - -//TODO move to DataForge-core -@DFExperimental -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: Meta?, - override val descriptor: PropertyDescriptor, - private val getter: suspend (before: Meta?) -> Meta, -) : ReadOnlyDeviceProperty { - - override val scope: CoroutineScope get() = device - - private val state: MutableStateFlow = MutableStateFlow(default) - override val value: Meta? get() = state.value - - override suspend fun invalidate() { - state.value = null - } - - override fun updateLogical(item: Meta) { - state.value = item - scope.launch { - device.sharedMessageFlow.emit( - PropertyChangedMessage( - property = name, - value = item, - ) - ) - } - } - - override suspend fun read(force: Boolean): Meta { - //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 = state -} - - -@OptIn(ExperimentalCoroutinesApi::class) -private class BasicDeviceProperty( - device: DeviceBase, - name: String, - default: Meta?, - descriptor: PropertyDescriptor, - getter: suspend (Meta?) -> Meta, - private val setter: suspend (oldValue: Meta?, newValue: Meta) -> Meta?, -) : BasicReadOnlyDeviceProperty(device, name, default, descriptor, getter), DeviceProperty { - - override var value: Meta? - get() = super.value - set(value) { - scope.launch { - if (value == null) { - invalidate() - } else { - write(value) - } - } - } - - private val writeLock = Mutex() - - override suspend fun write(item: Meta) { - 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 - */ -@Suppress("EXPERIMENTAL_API_USAGE") -public abstract class DeviceBase(final override val context: Context) : Device { - - override val coroutineContext: CoroutineContext = - context.coroutineContext + SupervisorJob(context.coroutineContext[Job]) - - private val _properties = HashMap() - public val properties: Map get() = _properties - private val _actions = HashMap() - public val actions: Map get() = _actions - - internal val sharedMessageFlow = MutableSharedFlow() - - override val messageFlow: SharedFlow get() = sharedMessageFlow - private val sharedLogFlow = MutableSharedFlow() - - /** - * The [SharedFlow] of log messages - */ - @DFExperimental - public val logFlow: SharedFlow - get() = sharedLogFlow - - protected suspend fun log(message: String, priority: Int = 0) { - sharedLogFlow.emit(LogEntry(message, priority)) - } - - override val propertyDescriptors: Collection - get() = _properties.values.map { it.descriptor } - - override val actionDescriptors: Collection - get() = _actions.values.map { it.descriptor } - - private fun

registerProperty(name: String, property: P) { - if (_properties.contains(name)) error("Property with name $name already registered") - _properties[name] = property - } - - internal fun registerAction(name: String, action: DeviceAction) { - if (_actions.contains(name)) error("Action with name $name already registered") - _actions[name] = action - } - - override suspend fun readProperty(propertyName: String): Meta = - (_properties[propertyName] ?: error("Property with name $propertyName not defined")).read() - - override fun getProperty(propertyName: String): Meta? = - (_properties[propertyName] ?: error("Property with name $propertyName not defined")).value - - override suspend fun invalidate(propertyName: String) { - (_properties[propertyName] ?: error("Property with name $propertyName not defined")).invalidate() - } - - override suspend fun writeProperty(propertyName: String, value: Meta) { - (_properties[propertyName] as? DeviceProperty ?: error("Property with name $propertyName not defined")).write( - value - ) - } - - override suspend fun execute(action: String, argument: Meta?): Meta? = - (_actions[action] ?: error("Request with name $action not defined")).invoke(argument) - - /** - * Create a bound read-only property with given [getter] - */ - public fun createReadOnlyProperty( - name: String, - default: Meta?, - descriptorBuilder: PropertyDescriptor.() -> Unit = {}, - getter: suspend (Meta?) -> Meta, - ): ReadOnlyDeviceProperty { - val property = BasicReadOnlyDeviceProperty( - this, - name, - default, - PropertyDescriptor(name).apply(descriptorBuilder), - getter - ) - registerProperty(name, property) - return property - } - - - /** - * Create a bound mutable property with given [getter] and [setter] - */ - internal fun createMutableProperty( - name: String, - default: Meta?, - descriptorBuilder: PropertyDescriptor.() -> Unit = {}, - getter: suspend (Meta?) -> Meta, - setter: suspend (oldValue: Meta?, newValue: Meta) -> Meta?, - ): DeviceProperty { - val property = BasicDeviceProperty( - this, - name, - default, - PropertyDescriptor(name).apply(descriptorBuilder), - getter, - setter - ) - registerProperty(name, property) - return property - } - - /** - * A stand-alone action - */ - private inner class BasicDeviceAction( - override val name: String, - override val descriptor: ActionDescriptor, - private val block: suspend (Meta?) -> Meta?, - ) : DeviceAction { - override suspend fun invoke(arg: Meta?): Meta? = - withContext(coroutineContext) { - block(arg) - } - } - - /** - * Create a new bound action - */ - internal fun createAction( - name: String, - descriptorBuilder: ActionDescriptor.() -> Unit = {}, - block: suspend (Meta?) -> Meta?, - ): DeviceAction { - val action = BasicDeviceAction(name, ActionDescriptor(name).apply(descriptorBuilder), block) - registerAction(name, action) - return action - } - - public companion object { - - } -} - - - diff --git a/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/base/DeviceProperty.kt b/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/base/DeviceProperty.kt deleted file mode 100644 index 5f67acf..0000000 --- a/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/base/DeviceProperty.kt +++ /dev/null @@ -1,74 +0,0 @@ -package ru.mipt.npm.controls.base - -import kotlinx.coroutines.* -import kotlinx.coroutines.flow.Flow -import ru.mipt.npm.controls.api.PropertyDescriptor -import space.kscience.dataforge.meta.Meta -import kotlin.time.Duration - -/** - * Read-only device property - */ -public interface ReadOnlyDeviceProperty { - /** - * Property name, should be unique in device - */ - public val name: String - - /** - * Property descriptor - */ - public val descriptor: PropertyDescriptor - - public val scope: CoroutineScope - - /** - * Erase logical value and force re-read from device on next [read] - */ - public suspend fun invalidate() - - /** - * Directly update property logical value and notify listener without writing it to device - */ - public fun updateLogical(item: Meta) - - /** - * Get cached value and return null if value is invalid or not initialized - */ - public val value: Meta? - - /** - * Read value either from cache if cache is valid or directly from physical device. - * If [force], reread from physical state even if the logical state is set. - */ - public suspend fun read(force: Boolean = false): Meta - - /** - * The [Flow] representing future logical states of the property. - * Produces null when the state is invalidated - */ - public fun flow(): Flow -} - - -/** - * Launch recurring force re-read job on a property scope with given [duration] between reads. - */ -public fun ReadOnlyDeviceProperty.readEvery(duration: Duration): Job = scope.launch { - while (isActive) { - read(true) - delay(duration) - } -} - -/** - * A writeable device property with non-suspended write - */ -public interface DeviceProperty : ReadOnlyDeviceProperty { - override var value: Meta? - - /** - * Write value to physical device. Invalidates logical value, but does not update it automatically - */ - public suspend fun write(item: Meta) -} \ No newline at end of file diff --git a/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/base/TypedDeviceProperty.kt b/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/base/TypedDeviceProperty.kt deleted file mode 100644 index b783fe2..0000000 --- a/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/base/TypedDeviceProperty.kt +++ /dev/null @@ -1,58 +0,0 @@ -package ru.mipt.npm.controls.base - -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.map -import space.kscience.dataforge.meta.Meta -import space.kscience.dataforge.meta.transformations.MetaConverter - -/** - * A type-safe wrapper on top of read-only property - */ -public open class TypedReadOnlyDeviceProperty( - private val property: ReadOnlyDeviceProperty, - protected val converter: MetaConverter, -) : ReadOnlyDeviceProperty by property { - - public fun updateLogical(obj: T) { - property.updateLogical(converter.objectToMeta(obj)) - } - - public open val typedValue: T? get() = value?.let { converter.metaToObject(it) } - - public suspend fun readTyped(force: Boolean = false): T { - val meta = read(force) - return converter.metaToObject(meta) - ?: error("Meta $meta could not be converted by $converter") - } - - public fun flowTyped(): Flow = flow().map { it?.let { converter.metaToObject(it) } } -} - -/** - * A type-safe wrapper for a read-write device property - */ -public class TypedDeviceProperty( - private val property: DeviceProperty, - converter: MetaConverter, -) : TypedReadOnlyDeviceProperty(property, converter), DeviceProperty { - - override var value: Meta? - get() = property.value - set(arg) { - property.value = arg - } - - public override var typedValue: T? - get() = value?.let { converter.metaToObject(it) } - set(arg) { - property.value = arg?.let { converter.objectToMeta(arg) } - } - - override suspend fun write(item: Meta) { - property.write(item) - } - - public suspend fun write(obj: T) { - property.write(converter.objectToMeta(obj)) - } -} \ No newline at end of file diff --git a/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/base/actionDelegates.kt b/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/base/actionDelegates.kt deleted file mode 100644 index 452e5a1..0000000 --- a/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/base/actionDelegates.kt +++ /dev/null @@ -1,58 +0,0 @@ -package ru.mipt.npm.controls.base - -import ru.mipt.npm.controls.api.ActionDescriptor -import space.kscience.dataforge.meta.Meta -import space.kscience.dataforge.meta.MutableMeta -import space.kscience.dataforge.values.Value -import kotlin.properties.PropertyDelegateProvider -import kotlin.properties.ReadOnlyProperty -import kotlin.reflect.KProperty - - -private fun D.provideAction(): ReadOnlyProperty = - ReadOnlyProperty { _: D, property: KProperty<*> -> - val name = property.name - return@ReadOnlyProperty actions[name]!! - } - -public typealias ActionDelegate = ReadOnlyProperty - -private class ActionProvider( - val owner: D, - val descriptorBuilder: ActionDescriptor.() -> Unit = {}, - val block: suspend (Meta?) -> Meta?, -) : PropertyDelegateProvider { - override operator fun provideDelegate(thisRef: D, property: KProperty<*>): ActionDelegate { - val name = property.name - owner.createAction(name, descriptorBuilder, block) - return owner.provideAction() - } -} - -public fun DeviceBase.requesting( - descriptorBuilder: ActionDescriptor.() -> Unit = {}, - action: suspend (Meta?) -> Meta?, -): PropertyDelegateProvider = ActionProvider(this, descriptorBuilder, action) - -public fun D.requestingValue( - descriptorBuilder: ActionDescriptor.() -> Unit = {}, - action: suspend (Meta?) -> Any?, -): PropertyDelegateProvider = ActionProvider(this, descriptorBuilder) { - val res = action(it) - Meta(Value.of(res)) -} - -public fun D.requestingMeta( - descriptorBuilder: ActionDescriptor.() -> Unit = {}, - action: suspend MutableMeta.(Meta?) -> Unit, -): PropertyDelegateProvider = ActionProvider(this, descriptorBuilder) { - Meta { action(it) } -} - -public fun DeviceBase.acting( - descriptorBuilder: ActionDescriptor.() -> Unit = {}, - action: suspend (Meta?) -> Unit, -): PropertyDelegateProvider = ActionProvider(this, descriptorBuilder) { - action(it) - null -} \ No newline at end of file diff --git a/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/base/devicePropertyDelegates.kt b/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/base/devicePropertyDelegates.kt deleted file mode 100644 index 0f47204..0000000 --- a/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/base/devicePropertyDelegates.kt +++ /dev/null @@ -1,283 +0,0 @@ -package ru.mipt.npm.controls.base - -import ru.mipt.npm.controls.api.PropertyDescriptor -import space.kscience.dataforge.meta.Meta -import space.kscience.dataforge.meta.MutableMeta -import space.kscience.dataforge.meta.boolean -import space.kscience.dataforge.meta.double -import space.kscience.dataforge.meta.transformations.MetaConverter -import space.kscience.dataforge.values.Null -import space.kscience.dataforge.values.Value -import space.kscience.dataforge.values.asValue -import kotlin.properties.PropertyDelegateProvider -import kotlin.properties.ReadOnlyProperty -import kotlin.reflect.KProperty - -private fun D.provideProperty(name: String): ReadOnlyProperty = - ReadOnlyProperty { _: D, _: KProperty<*> -> - return@ReadOnlyProperty properties.getValue(name) - } - -private fun D.provideProperty( - name: String, - converter: MetaConverter, -): ReadOnlyProperty> = - ReadOnlyProperty { _: D, _: KProperty<*> -> - return@ReadOnlyProperty TypedReadOnlyDeviceProperty(properties.getValue(name), converter) - } - - -public typealias ReadOnlyPropertyDelegate = ReadOnlyProperty -public typealias TypedReadOnlyPropertyDelegate = ReadOnlyProperty> - -private class ReadOnlyDevicePropertyProvider( - val owner: D, - val default: Meta?, - val descriptorBuilder: PropertyDescriptor.() -> Unit = {}, - private val getter: suspend (Meta?) -> Meta, -) : PropertyDelegateProvider { - - override operator fun provideDelegate(thisRef: D, property: KProperty<*>): ReadOnlyPropertyDelegate { - val name = property.name - owner.createReadOnlyProperty(name, default, descriptorBuilder, getter) - return owner.provideProperty(name) - } -} - -private class TypedReadOnlyDevicePropertyProvider( - val owner: D, - val default: Meta?, - val converter: MetaConverter, - val descriptorBuilder: PropertyDescriptor.() -> Unit = {}, - private val getter: suspend (Meta?) -> Meta, -) : PropertyDelegateProvider> { - - override operator fun provideDelegate(thisRef: D, property: KProperty<*>): TypedReadOnlyPropertyDelegate { - val name = property.name - owner.createReadOnlyProperty(name, default, descriptorBuilder, getter) - return owner.provideProperty(name, converter) - } -} - -public fun DeviceBase.reading( - default: Meta? = null, - descriptorBuilder: PropertyDescriptor.() -> Unit = {}, - getter: suspend (Meta?) -> Meta, -): PropertyDelegateProvider = ReadOnlyDevicePropertyProvider( - this, - default, - descriptorBuilder, - getter -) - -public fun DeviceBase.readingValue( - default: Value? = null, - descriptorBuilder: PropertyDescriptor.() -> Unit = {}, - getter: suspend () -> Any?, -): PropertyDelegateProvider = ReadOnlyDevicePropertyProvider( - this, - default?.let { Meta(it) }, - descriptorBuilder, - getter = { Meta(Value.of(getter())) } -) - -public fun DeviceBase.readingNumber( - default: Number? = null, - descriptorBuilder: PropertyDescriptor.() -> Unit = {}, - getter: suspend () -> Number, -): PropertyDelegateProvider> = TypedReadOnlyDevicePropertyProvider( - this, - default?.let { Meta(it.asValue()) }, - MetaConverter.number, - descriptorBuilder, - getter = { - val number = getter() - Meta(number.asValue()) - } -) - -public fun DeviceBase.readingDouble( - default: Number? = null, - descriptorBuilder: PropertyDescriptor.() -> Unit = {}, - getter: suspend () -> Double, -): PropertyDelegateProvider> = TypedReadOnlyDevicePropertyProvider( - this, - default?.let { Meta(it.asValue()) }, - MetaConverter.double, - descriptorBuilder, - getter = { - val number = getter() - Meta(number.asValue()) - } -) - -public fun DeviceBase.readingString( - default: String? = null, - descriptorBuilder: PropertyDescriptor.() -> Unit = {}, - getter: suspend () -> String, -): PropertyDelegateProvider> = TypedReadOnlyDevicePropertyProvider( - this, - default?.let { Meta(it.asValue()) }, - MetaConverter.string, - descriptorBuilder, - getter = { - val number = getter() - Meta(number.asValue()) - } -) - -public fun DeviceBase.readingBoolean( - default: Boolean? = null, - descriptorBuilder: PropertyDescriptor.() -> Unit = {}, - getter: suspend () -> Boolean, -): PropertyDelegateProvider> = TypedReadOnlyDevicePropertyProvider( - this, - default?.let { Meta(it.asValue()) }, - MetaConverter.boolean, - descriptorBuilder, - getter = { - val boolean = getter() - Meta(boolean.asValue()) - } -) - -public fun DeviceBase.readingMeta( - default: Meta? = null, - descriptorBuilder: PropertyDescriptor.() -> Unit = {}, - getter: suspend MutableMeta.() -> Unit, -): PropertyDelegateProvider> = TypedReadOnlyDevicePropertyProvider( - this, - default, - MetaConverter.meta, - descriptorBuilder, - getter = { - Meta { getter() } - } -) - -private fun DeviceBase.provideMutableProperty(name: String): ReadOnlyProperty = - ReadOnlyProperty { _: DeviceBase, _: KProperty<*> -> - return@ReadOnlyProperty properties[name] as DeviceProperty - } - -private fun DeviceBase.provideMutableProperty( - name: String, - converter: MetaConverter, -): ReadOnlyProperty> = - ReadOnlyProperty { _: DeviceBase, _: KProperty<*> -> - return@ReadOnlyProperty TypedDeviceProperty(properties[name] as DeviceProperty, converter) - } - -public typealias PropertyDelegate = ReadOnlyProperty -public typealias TypedPropertyDelegate = ReadOnlyProperty> - -private class DevicePropertyProvider( - val owner: D, - val default: Meta?, - val descriptorBuilder: PropertyDescriptor.() -> Unit = {}, - private val getter: suspend (Meta?) -> Meta, - private val setter: suspend (oldValue: Meta?, newValue: Meta) -> Meta?, -) : PropertyDelegateProvider { - - override operator fun provideDelegate(thisRef: D, property: KProperty<*>): PropertyDelegate { - val name = property.name - owner.createMutableProperty(name, default, descriptorBuilder, getter, setter) - return owner.provideMutableProperty(name) - } -} - -private class TypedDevicePropertyProvider( - val owner: D, - val default: Meta?, - val converter: MetaConverter, - val descriptorBuilder: PropertyDescriptor.() -> Unit = {}, - private val getter: suspend (Meta?) -> Meta, - private val setter: suspend (oldValue: Meta?, newValue: Meta) -> Meta?, -) : PropertyDelegateProvider> { - - override operator fun provideDelegate(thisRef: D, property: KProperty<*>): TypedPropertyDelegate { - val name = property.name - owner.createMutableProperty(name, default, descriptorBuilder, getter, setter) - return owner.provideMutableProperty(name, converter) - } -} - -public fun DeviceBase.writing( - default: Meta? = null, - descriptorBuilder: PropertyDescriptor.() -> Unit = {}, - getter: suspend (Meta?) -> Meta, - setter: suspend (oldValue: Meta?, newValue: Meta) -> Meta?, -): PropertyDelegateProvider = DevicePropertyProvider( - this, - default, - descriptorBuilder, - getter, - setter -) - -public fun DeviceBase.writingVirtual( - default: Meta, - descriptorBuilder: PropertyDescriptor.() -> Unit = {}, -): PropertyDelegateProvider = writing( - default, - descriptorBuilder, - getter = { it ?: default }, - setter = { _, newItem -> newItem } -) - -public fun DeviceBase.writingVirtual( - default: Value, - descriptorBuilder: PropertyDescriptor.() -> Unit = {}, -): PropertyDelegateProvider = writing( - Meta(default), - descriptorBuilder, - getter = { it ?: Meta(default) }, - setter = { _, newItem -> newItem } -) - -public fun D.writingDouble( - descriptorBuilder: PropertyDescriptor.() -> Unit = {}, - getter: suspend (Double) -> Double, - setter: suspend (oldValue: Double?, newValue: Double) -> Double?, -): PropertyDelegateProvider> { - val innerGetter: suspend (Meta?) -> Meta = { - Meta(getter(it.double ?: Double.NaN).asValue()) - } - - val innerSetter: suspend (oldValue: Meta?, newValue: Meta) -> Meta? = { oldValue, newValue -> - setter(oldValue.double, newValue.double ?: Double.NaN)?.asMeta() - } - - return TypedDevicePropertyProvider( - this, - Meta(Double.NaN.asValue()), - MetaConverter.double, - descriptorBuilder, - innerGetter, - innerSetter - ) -} - -public fun D.writingBoolean( - descriptorBuilder: PropertyDescriptor.() -> Unit = {}, - getter: suspend (Boolean?) -> Boolean, - setter: suspend (oldValue: Boolean?, newValue: Boolean) -> Boolean?, -): PropertyDelegateProvider> { - val innerGetter: suspend (Meta?) -> Meta = { - Meta(getter(it.boolean).asValue()) - } - - val innerSetter: suspend (oldValue: Meta?, newValue: Meta) -> Meta? = { oldValue, newValue -> - setter(oldValue.boolean, newValue.boolean ?: error("Can't convert $newValue to boolean"))?.asValue() - ?.let { Meta(it) } - } - - return TypedDevicePropertyProvider( - this, - Meta(Null), - MetaConverter.boolean, - descriptorBuilder, - innerGetter, - innerSetter - ) -} \ No newline at end of file diff --git a/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/ports/SynchronousPortHandler.kt b/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/ports/SynchronousPortHandler.kt deleted file mode 100644 index 508ce6d..0000000 --- a/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/ports/SynchronousPortHandler.kt +++ /dev/null @@ -1,34 +0,0 @@ -package ru.mipt.npm.controls.ports - -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock - -/** - * A port handler for synchronous (request-response) communication with a port. Only one request could be active at a time (others are suspended. - * The handler does not guarantee exclusive access to the port so the user mush ensure that no other controller handles port at the moment. - * - */ -public class SynchronousPortHandler(public val port: Port) { - private val mutex = Mutex() - - /** - * Send a single message and wait for the flow of respond messages. - */ - public suspend fun respond(data: ByteArray, transform: suspend Flow.() -> R): R { - return mutex.withLock { - port.send(data) - transform(port.receiving()) - } - } -} - -/** - * Send request and read incoming data blocks until the delimiter is encountered - */ -public suspend fun SynchronousPortHandler.respondWithDelimiter(data: ByteArray, delimiter: ByteArray): ByteArray { - return respond(data) { - withDelimiter(delimiter).first() - } -} \ No newline at end of file diff --git a/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/properties/DevicePropertySpec.kt b/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/properties/DevicePropertySpec.kt deleted file mode 100644 index 23ceffb..0000000 --- a/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/properties/DevicePropertySpec.kt +++ /dev/null @@ -1,85 +0,0 @@ -package ru.mipt.npm.controls.properties - -import ru.mipt.npm.controls.api.ActionDescriptor -import ru.mipt.npm.controls.api.Device -import ru.mipt.npm.controls.api.PropertyDescriptor -import space.kscience.dataforge.meta.Meta -import space.kscience.dataforge.meta.transformations.MetaConverter - - -/** - * This API is internal and should not be used in user code - */ -@RequiresOptIn -public annotation class InternalDeviceAPI - -public interface DevicePropertySpec { - /** - * Property name, should be unique in device - */ - public val name: String - - /** - * Property descriptor - */ - public val descriptor: PropertyDescriptor - - /** - * Meta item converter for resulting type - */ - public val converter: MetaConverter - - /** - * Read physical value from the given [device] - */ - @InternalDeviceAPI - public suspend fun read(device: D): T -} - -@OptIn(InternalDeviceAPI::class) -public suspend fun DevicePropertySpec.readMeta(device: D): Meta = - converter.objectToMeta(read(device)) - - -public interface WritableDevicePropertySpec : DevicePropertySpec { - /** - * Write physical value to a device - */ - @InternalDeviceAPI - public suspend fun write(device: D, value: T) -} - -@OptIn(InternalDeviceAPI::class) -public suspend fun WritableDevicePropertySpec.writeMeta(device: D, item: Meta) { - write(device, converter.metaToObject(item) ?: error("Meta $item could not be read with $converter")) -} - -public interface DeviceActionSpec { - /** - * Action name, should be unique in device - */ - public val name: String - - /** - * Action descriptor - */ - public val descriptor: ActionDescriptor - - public val inputConverter: MetaConverter - - public val outputConverter: MetaConverter - - /** - * Execute action on a device - */ - public suspend fun execute(device: D, input: I?): O? -} - -public suspend fun DeviceActionSpec.executeMeta( - device: D, - item: Meta? -): Meta? { - val arg = item?.let { inputConverter.metaToObject(item) } - val res = execute(device, arg) - return res?.let { outputConverter.objectToMeta(res) } -} \ No newline at end of file diff --git a/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/api/Device.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/api/Device.kt similarity index 82% rename from controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/api/Device.kt rename to controls-core/src/commonMain/kotlin/space/kscience/controls/api/Device.kt index 6298657..c122260 100644 --- a/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/api/Device.kt +++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/api/Device.kt @@ -1,4 +1,4 @@ -package ru.mipt.npm.controls.api +package space.kscience.controls.api import io.ktor.utils.io.core.Closeable import kotlinx.coroutines.CoroutineScope @@ -8,7 +8,7 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach -import ru.mipt.npm.controls.api.Device.Companion.DEVICE_TARGET +import space.kscience.controls.api.Device.Companion.DEVICE_TARGET import space.kscience.dataforge.context.ContextAware import space.kscience.dataforge.meta.Meta import space.kscience.dataforge.misc.Type @@ -21,6 +21,12 @@ import space.kscience.dataforge.names.Name */ @Type(DEVICE_TARGET) public interface Device : Closeable, ContextAware, CoroutineScope { + + /** + * Initial configuration meta for the device + */ + public val meta: Meta get() = Meta.EMPTY + /** * List of supported property descriptors */ @@ -51,13 +57,13 @@ public interface Device : Closeable, ContextAware, CoroutineScope { /** * Set property [value] for a property with name [propertyName]. - * In rare cases could suspend if the [Device] supports command queue and it is full at the moment. + * In rare cases could suspend if the [Device] supports command queue, and it is full at the moment. */ public suspend fun writeProperty(propertyName: String, value: Meta) /** * A subscription-based [Flow] of [DeviceMessage] provided by device. The flow is guaranteed to be readable - * multiple times + * multiple times. */ public val messageFlow: Flow @@ -67,6 +73,14 @@ public interface Device : Closeable, ContextAware, CoroutineScope { */ public suspend fun execute(action: String, argument: Meta? = null): Meta? + /** + * Initialize the device. This function suspends until the device is finished initialization + */ + public suspend fun open(): Unit = Unit + + /** + * Close and terminate the device. This function does not wait for device to be closed. + */ override fun close() { cancel("The device is closed") } @@ -85,9 +99,9 @@ public suspend fun Device.getOrReadProperty(propertyName: String): Meta = /** * Get a snapshot of logical state of the device * - * TODO currently this + * TODO currently this */ -public fun Device.getProperties(): Meta = Meta { +public fun Device.getAllProperties(): Meta = Meta { for (descriptor in propertyDescriptors) { setMeta(Name.parse(descriptor.name), getProperty(descriptor.name)) } @@ -97,4 +111,4 @@ public fun Device.getProperties(): Meta = Meta { * Subscribe on property changes for the whole device */ public fun Device.onPropertyChange(callback: suspend PropertyChangedMessage.() -> Unit): Job = - messageFlow.filterIsInstance().onEach(callback).launchIn(this) \ No newline at end of file + messageFlow.filterIsInstance().onEach(callback).launchIn(this) diff --git a/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/api/DeviceHub.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/api/DeviceHub.kt similarity index 98% rename from controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/api/DeviceHub.kt rename to controls-core/src/commonMain/kotlin/space/kscience/controls/api/DeviceHub.kt index aba8517..9565950 100644 --- a/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/api/DeviceHub.kt +++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/api/DeviceHub.kt @@ -1,4 +1,4 @@ -package ru.mipt.npm.controls.api +package space.kscience.controls.api import space.kscience.dataforge.meta.Meta import space.kscience.dataforge.names.* diff --git a/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/api/DeviceMessage.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/api/DeviceMessage.kt similarity index 96% rename from controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/api/DeviceMessage.kt rename to controls-core/src/commonMain/kotlin/space/kscience/controls/api/DeviceMessage.kt index f919f1e..80a7f9d 100644 --- a/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/api/DeviceMessage.kt +++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/api/DeviceMessage.kt @@ -1,4 +1,4 @@ -package ru.mipt.npm.controls.api +package space.kscience.controls.api import kotlinx.datetime.Clock import kotlinx.datetime.Instant @@ -7,7 +7,7 @@ import kotlinx.serialization.Serializable import kotlinx.serialization.json.Json import kotlinx.serialization.json.decodeFromJsonElement import kotlinx.serialization.json.encodeToJsonElement -import space.kscience.dataforge.io.SimpleEnvelope +import space.kscience.dataforge.io.Envelope import space.kscience.dataforge.meta.Meta import space.kscience.dataforge.meta.toJson import space.kscience.dataforge.meta.toMeta @@ -113,6 +113,8 @@ public data class GetDescriptionMessage( @SerialName("description") public data class DescriptionMessage( val description: Meta, + val properties: Collection, + val actions: Collection, override val sourceDevice: Name, override val targetDevice: Name? = null, override val comment: String? = null, @@ -219,4 +221,4 @@ public data class DeviceErrorMessage( public fun DeviceMessage.toMeta(): Meta = Json.encodeToJsonElement(this).toMeta() -public fun DeviceMessage.toEnvelope(): SimpleEnvelope = SimpleEnvelope(toMeta(), null) +public fun DeviceMessage.toEnvelope(): Envelope = Envelope(toMeta(), null) diff --git a/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/api/Socket.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/api/Socket.kt similarity index 85% rename from controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/api/Socket.kt rename to controls-core/src/commonMain/kotlin/space/kscience/controls/api/Socket.kt index eda8942..02598ba 100644 --- a/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/api/Socket.kt +++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/api/Socket.kt @@ -1,14 +1,13 @@ -package ru.mipt.npm.controls.api +package space.kscience.controls.api import io.ktor.utils.io.core.Closeable import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.collect import kotlinx.coroutines.launch /** - * A generic bi-directional sender/receiver object + * A generic bidirectional sender/receiver object */ public interface Socket : Closeable { /** diff --git a/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/api/descriptors.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/api/descriptors.kt similarity index 95% rename from controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/api/descriptors.kt rename to controls-core/src/commonMain/kotlin/space/kscience/controls/api/descriptors.kt index 1e70962..8e1705b 100644 --- a/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/api/descriptors.kt +++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/api/descriptors.kt @@ -1,4 +1,4 @@ -package ru.mipt.npm.controls.api +package space.kscience.controls.api import kotlinx.serialization.Serializable import space.kscience.dataforge.meta.descriptors.MetaDescriptor diff --git a/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/controllers/DeviceManager.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/manager/DeviceManager.kt similarity index 84% rename from controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/controllers/DeviceManager.kt rename to controls-core/src/commonMain/kotlin/space/kscience/controls/manager/DeviceManager.kt index 03880ca..a3a5bfd 100644 --- a/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/controllers/DeviceManager.kt +++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/manager/DeviceManager.kt @@ -1,7 +1,8 @@ -package ru.mipt.npm.controls.controllers +package space.kscience.controls.manager -import ru.mipt.npm.controls.api.Device -import ru.mipt.npm.controls.api.DeviceHub +import kotlinx.coroutines.launch +import space.kscience.controls.api.Device +import space.kscience.controls.api.DeviceHub import space.kscience.dataforge.context.* import space.kscience.dataforge.meta.Meta import space.kscience.dataforge.meta.MutableMeta @@ -30,7 +31,7 @@ public class DeviceManager : AbstractPlugin(), DeviceHub { override val tag: PluginTag = PluginTag("devices", group = PluginTag.DATAFORGE_GROUP) override val type: KClass = DeviceManager::class - override fun invoke(meta: Meta, context: Context): DeviceManager = DeviceManager() + override fun build(context: Context, meta: Meta): DeviceManager = DeviceManager() } } @@ -38,6 +39,9 @@ public class DeviceManager : AbstractPlugin(), DeviceHub { public fun DeviceManager.install(name: String, factory: Factory, meta: Meta = Meta.EMPTY): D { val device = factory(meta, context) registerDevice(NameToken(name), device) + device.launch { + device.open() + } return device } diff --git a/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/controllers/deviceMessages.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/manager/deviceMessages.kt similarity index 80% rename from controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/controllers/deviceMessages.kt rename to controls-core/src/commonMain/kotlin/space/kscience/controls/manager/deviceMessages.kt index 5158961..ea5d34c 100644 --- a/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/controllers/deviceMessages.kt +++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/manager/deviceMessages.kt @@ -1,15 +1,11 @@ -package ru.mipt.npm.controls.controllers +package space.kscience.controls.manager import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach -import kotlinx.serialization.json.Json -import kotlinx.serialization.json.encodeToJsonElement -import ru.mipt.npm.controls.api.* -import space.kscience.dataforge.meta.Meta -import space.kscience.dataforge.meta.toMeta +import space.kscience.controls.api.* import space.kscience.dataforge.names.Name import space.kscience.dataforge.names.plus @@ -48,21 +44,10 @@ public suspend fun Device.respondMessage(deviceTarget: Name, request: DeviceMess } is GetDescriptionMessage -> { - val descriptionMeta = Meta { - "properties" put { - propertyDescriptors.map { descriptor -> - descriptor.name put Json.encodeToJsonElement(descriptor).toMeta() - } - } - "actions" put { - actionDescriptors.map { descriptor -> - descriptor.name put Json.encodeToJsonElement(descriptor).toMeta() - } - } - } - DescriptionMessage( - description = descriptionMeta, + description = meta, + properties = propertyDescriptors, + actions = actionDescriptors, sourceDevice = deviceTarget, targetDevice = request.sourceDevice ) @@ -95,6 +80,8 @@ public suspend fun DeviceHub.respondHubMessage(request: DeviceMessage): DeviceMe * Collect all messages from given [DeviceHub], applying proper relative names */ public fun DeviceHub.hubMessageFlow(scope: CoroutineScope): Flow { + + //TODO could we avoid using downstream scope? val outbox = MutableSharedFlow() if (this is Device) { messageFlow.onEach { diff --git a/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/ports/Port.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/ports/Port.kt similarity index 82% rename from controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/ports/Port.kt rename to controls-core/src/commonMain/kotlin/space/kscience/controls/ports/Port.kt index 4cf672d..374c404 100644 --- a/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/ports/Port.kt +++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/ports/Port.kt @@ -1,16 +1,24 @@ -package ru.mipt.npm.controls.ports +package space.kscience.controls.ports import kotlinx.coroutines.* import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.receiveAsFlow -import ru.mipt.npm.controls.api.Socket +import space.kscience.controls.api.Socket import space.kscience.dataforge.context.* +import space.kscience.dataforge.misc.Type import kotlin.coroutines.CoroutineContext public interface Port : ContextAware, Socket -public typealias PortFactory = Factory +@Type(PortFactory.TYPE) +public interface PortFactory: Factory{ + public val type: String + + public companion object{ + public const val TYPE: String = "controls.port" + } +} public abstract class AbstractPort( override val context: Context, @@ -64,12 +72,10 @@ public abstract class AbstractPort( /** * Raw flow of incoming data chunks. The chunks are not guaranteed to be complete phrases. - * In order to form phrases some condition should used on top of it. + * In order to form phrases some condition should be used on top of it. * For example [delimitedIncoming] generates phrases with fixed delimiter. */ - override fun receiving(): Flow { - return incoming.receiveAsFlow() - } + override fun receiving(): Flow = incoming.receiveAsFlow() override fun close() { outgoing.close() diff --git a/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/ports/PortProxy.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/ports/PortProxy.kt similarity index 95% rename from controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/ports/PortProxy.kt rename to controls-core/src/commonMain/kotlin/space/kscience/controls/ports/PortProxy.kt index 686992d..4e51f6f 100644 --- a/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/ports/PortProxy.kt +++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/ports/PortProxy.kt @@ -1,8 +1,7 @@ -package ru.mipt.npm.controls.ports +package space.kscience.controls.ports import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.flow import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex diff --git a/controls-core/src/commonMain/kotlin/space/kscience/controls/ports/Ports.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/ports/Ports.kt new file mode 100644 index 0000000..8c652cc --- /dev/null +++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/ports/Ports.kt @@ -0,0 +1,34 @@ +package space.kscience.controls.ports + +import space.kscience.dataforge.context.* +import space.kscience.dataforge.meta.Meta +import space.kscience.dataforge.meta.string +import kotlin.reflect.KClass + +public class Ports : AbstractPlugin() { + + override val tag: PluginTag get() = Companion.tag + + private val portFactories by lazy { + context.gather(PortFactory.TYPE) + } + + private val portCache = mutableMapOf() + + public fun buildPort(meta: Meta): Port = portCache.getOrPut(meta) { + val type by meta.string { error("Port type is not defined") } + val factory = portFactories.values.firstOrNull { it.type == type } + ?: error("Port factory for type $type not found") + factory.build(context, meta) + } + + public companion object : PluginFactory { + + override val tag: PluginTag = PluginTag("controls.ports", group = PluginTag.DATAFORGE_GROUP) + + override val type: KClass = Ports::class + + override fun build(context: Context, meta: Meta): Ports = Ports() + + } +} \ No newline at end of file diff --git a/controls-core/src/commonMain/kotlin/space/kscience/controls/ports/SynchronousPort.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/ports/SynchronousPort.kt new file mode 100644 index 0000000..0ed4764 --- /dev/null +++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/ports/SynchronousPort.kt @@ -0,0 +1,42 @@ +package space.kscience.controls.ports + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock + +/** + * A port handler for synchronous (request-response) communication with a port. Only one request could be active at a time (others are suspended. + * The handler does not guarantee exclusive access to the port so the user mush ensure that no other controller handles port at the moment. + */ +public class SynchronousPort(public val port: Port, private val mutex: Mutex) : Port by port { + /** + * Send a single message and wait for the flow of respond messages. + */ + public suspend fun respond(data: ByteArray, transform: suspend Flow.() -> R): R = mutex.withLock { + port.send(data) + transform(port.receiving()) + } +} + +/** + * Provide a synchronous wrapper for a port + */ +public fun Port.synchronous(mutex: Mutex = Mutex()): SynchronousPort = SynchronousPort(this, mutex) + +/** + * Send request and read incoming data blocks until the delimiter is encountered + */ +public suspend fun SynchronousPort.respondWithDelimiter( + data: ByteArray, + delimiter: ByteArray, +): ByteArray = respond(data) { + withDelimiter(delimiter).first() +} + +public suspend fun SynchronousPort.respondStringWithDelimiter( + data: String, + delimiter: String, +): String = respond(data.encodeToByteArray()) { + withStringDelimiter(delimiter).first() +} \ No newline at end of file diff --git a/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/ports/phrases.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/ports/phrases.kt similarity index 68% rename from controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/ports/phrases.kt rename to controls-core/src/commonMain/kotlin/space/kscience/controls/ports/phrases.kt index 62d075a..21afa8d 100644 --- a/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/ports/phrases.kt +++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/ports/phrases.kt @@ -1,4 +1,4 @@ -package ru.mipt.npm.controls.ports +package space.kscience.controls.ports import io.ktor.utils.io.core.BytePacketBuilder import io.ktor.utils.io.core.readBytes @@ -10,10 +10,10 @@ import kotlinx.coroutines.flow.transform /** * Transform byte fragments into complete phrases using given delimiter. Not thread safe. */ -public fun Flow.withDelimiter(delimiter: ByteArray, expectedMessageSize: Int = 32): Flow { +public fun Flow.withDelimiter(delimiter: ByteArray): Flow { require(delimiter.isNotEmpty()) { "Delimiter must not be empty" } - val output = BytePacketBuilder(expectedMessageSize) + val output = BytePacketBuilder() var matcherPosition = 0 return transform { chunk -> @@ -40,12 +40,11 @@ public fun Flow.withDelimiter(delimiter: ByteArray, expectedMessageSi /** * Transform byte fragments into utf-8 phrases using utf-8 delimiter */ -public fun Flow.withDelimiter(delimiter: String, expectedMessageSize: Int = 32): Flow { - return withDelimiter(delimiter.encodeToByteArray(), expectedMessageSize).map { it.decodeToString() } +public fun Flow.withStringDelimiter(delimiter: String): Flow { + return withDelimiter(delimiter.encodeToByteArray()).map { it.decodeToString() } } /** * A flow of delimited phrases */ -public suspend fun Port.delimitedIncoming(delimiter: ByteArray, expectedMessageSize: Int = 32): Flow = - receiving().withDelimiter(delimiter, expectedMessageSize) +public fun Port.delimitedIncoming(delimiter: ByteArray): Flow = receiving().withDelimiter(delimiter) diff --git a/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/properties/DeviceBySpec.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/DeviceBase.kt similarity index 65% rename from controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/properties/DeviceBySpec.kt rename to controls-core/src/commonMain/kotlin/space/kscience/controls/spec/DeviceBase.kt index c569cc3..d1fd9b7 100644 --- a/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/properties/DeviceBySpec.kt +++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/DeviceBase.kt @@ -1,4 +1,4 @@ -package ru.mipt.npm.controls.properties +package space.kscience.controls.spec import kotlinx.coroutines.Job import kotlinx.coroutines.SupervisorJob @@ -7,30 +7,28 @@ import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock -import ru.mipt.npm.controls.api.* +import space.kscience.controls.api.* import space.kscience.dataforge.context.Context import space.kscience.dataforge.context.Global import space.kscience.dataforge.meta.Meta import kotlin.coroutines.CoroutineContext -/** - * A device generated from specification - * @param D recursive self-type for properties and actions - */ + @OptIn(InternalDeviceAPI::class) -public open class DeviceBySpec>( - public val spec: DeviceSpec, - context: Context = Global, - meta: Meta = Meta.EMPTY +public abstract class DeviceBase>( + override val context: Context = Global, + override val meta: Meta = Meta.EMPTY, ) : Device { - override var context: Context = context - internal set - public var meta: Meta = meta - internal set + /** + * Collection of property specifications + */ + public abstract val properties: Map> - public val properties: Map> get() = spec.properties - public val actions: Map> get() = spec.actions + /** + * Collection of action specifications + */ + public abstract val actions: Map> override val propertyDescriptors: Collection get() = properties.values.map { it.descriptor } @@ -42,6 +40,9 @@ public open class DeviceBySpec>( context.coroutineContext + SupervisorJob(context.coroutineContext[Job]) } + /** + * Logical state store + */ private val logicalState: HashMap = HashMap() private val sharedMessageFlow: MutableSharedFlow = MutableSharedFlow() @@ -68,6 +69,13 @@ public open class DeviceBySpec>( } } + /** + * Update logical state using given [spec] and its convertor + */ + protected suspend fun updateLogical(spec: DevicePropertySpec, value: T) { + updateLogical(spec.name, spec.converter.objectToMeta(value)) + } + /** * Force read physical value and push an update if it is changed. It does not matter if logical state is present. * The logical state is updated after read @@ -98,17 +106,21 @@ public open class DeviceBySpec>( } override suspend fun execute(action: String, argument: Meta?): Meta? = - actions[action]?.executeMeta(self, argument) + actions[action]?.executeWithMeta(self, argument) /** - * Read typed value and update/push event if needed + * Read typed value and update/push event if needed. + * Return null if property read is not successful or property is undefined. */ - public suspend fun DevicePropertySpec.read(): T { - val res = read(self) + public suspend fun DevicePropertySpec.readOrNull(): T? { + val res = read(self) ?: return null updateLogical(name, converter.objectToMeta(res)) return res } + public suspend fun DevicePropertySpec.read(): T = + readOrNull() ?: error("Failed to read property $name state") + public fun DevicePropertySpec.get(): T? = getProperty(name)?.let(converter::metaToObject) /** @@ -123,19 +135,37 @@ public open class DeviceBySpec>( } } - override fun close() { - with(spec) { self.onShutdown() } + /** + * Reset logical state of a property + */ + public suspend fun DevicePropertySpec.invalidate() { + invalidate(name) + } + + public suspend operator fun DeviceActionSpec.invoke(input: I? = null): O? = execute(self, input) + +} + +/** + * A device generated from specification + * @param D recursive self-type for properties and actions + */ +public open class DeviceBySpec>( + public val spec: DeviceSpec, + context: Context = Global, + meta: Meta = Meta.EMPTY, +) : DeviceBase(context, meta) { + override val properties: Map> get() = spec.properties + override val actions: Map> get() = spec.actions + + override suspend fun open(): Unit = with(spec) { + super.open() + self.onOpen() + } + + override fun close(): Unit = with(spec) { + self.onClose() super.close() } } -public suspend fun , T : Any> D.read( - propertySpec: DevicePropertySpec -): T = propertySpec.read() - -public fun , T> D.write( - propertySpec: WritableDevicePropertySpec, - value: T -): Job = launch { - propertySpec.write(value) -} diff --git a/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/DeviceMetaPropertySpec.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/DeviceMetaPropertySpec.kt new file mode 100644 index 0000000..809d940 --- /dev/null +++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/DeviceMetaPropertySpec.kt @@ -0,0 +1,15 @@ +package space.kscience.controls.spec + +import space.kscience.controls.api.Device +import space.kscience.controls.api.PropertyDescriptor +import space.kscience.dataforge.meta.Meta +import space.kscience.dataforge.meta.transformations.MetaConverter + +internal object DeviceMetaPropertySpec: DevicePropertySpec { + override val descriptor: PropertyDescriptor = PropertyDescriptor("@meta") + + override val converter: MetaConverter = MetaConverter.meta + + @InternalDeviceAPI + override suspend fun read(device: Device): Meta = device.meta +} \ No newline at end of file diff --git a/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/DevicePropertySpec.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/DevicePropertySpec.kt new file mode 100644 index 0000000..b545910 --- /dev/null +++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/DevicePropertySpec.kt @@ -0,0 +1,129 @@ +package space.kscience.controls.spec + +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.filterIsInstance +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch +import space.kscience.controls.api.ActionDescriptor +import space.kscience.controls.api.Device +import space.kscience.controls.api.PropertyChangedMessage +import space.kscience.controls.api.PropertyDescriptor +import space.kscience.dataforge.meta.Meta +import space.kscience.dataforge.meta.transformations.MetaConverter + + +/** + * This API is internal and should not be used in user code + */ +@RequiresOptIn("This API should not be called outside of Device internals") +public annotation class InternalDeviceAPI + +public interface DevicePropertySpec { + /** + * Property descriptor + */ + public val descriptor: PropertyDescriptor + + /** + * Meta item converter for resulting type + */ + public val converter: MetaConverter + + /** + * Read physical value from the given [device] + */ + @InternalDeviceAPI + public suspend fun read(device: D): T? +} + +/** + * Property name, should be unique in device + */ +public val DevicePropertySpec<*, *>.name: String get() = descriptor.name + +@OptIn(InternalDeviceAPI::class) +public suspend fun DevicePropertySpec.readMeta(device: D): Meta? = + read(device)?.let(converter::objectToMeta) + + +public interface WritableDevicePropertySpec : DevicePropertySpec { + /** + * Write physical value to a device + */ + @InternalDeviceAPI + public suspend fun write(device: D, value: T) +} + +@OptIn(InternalDeviceAPI::class) +public suspend fun WritableDevicePropertySpec.writeMeta(device: D, item: Meta) { + write(device, converter.metaToObject(item) ?: error("Meta $item could not be read with $converter")) +} + +public interface DeviceActionSpec { + /** + * Action descriptor + */ + public val descriptor: ActionDescriptor + + public val inputConverter: MetaConverter + + public val outputConverter: MetaConverter + + /** + * Execute action on a device + */ + public suspend fun execute(device: D, input: I?): O? +} + +/** + * Action name, should be unique in device + */ +public val DeviceActionSpec<*, *, *>.name: String get() = descriptor.name + +public suspend fun DeviceActionSpec.executeWithMeta( + device: D, + item: Meta?, +): Meta? { + val arg = item?.let { inputConverter.metaToObject(item) } + val res = execute(device, arg) + return res?.let { outputConverter.objectToMeta(res) } +} + + +public suspend fun , T : Any> D.read( + propertySpec: DevicePropertySpec, +): T = propertySpec.read() + +public suspend fun D.read( + propertySpec: DevicePropertySpec, +): T = propertySpec.converter.metaToObject(readProperty(propertySpec.name)) + ?: error("Property meta converter returned null") + +public fun D.write( + propertySpec: WritableDevicePropertySpec, + value: T, +): Job = launch { + writeProperty(propertySpec.name, propertySpec.converter.objectToMeta(value)) +} + +public fun , T> D.write( + propertySpec: WritableDevicePropertySpec, + value: T, +): Job = launch { + propertySpec.write(value) +} + +/** + * A type safe property change listener + */ +public fun Device.onPropertyChange( + spec: DevicePropertySpec, + callback: suspend PropertyChangedMessage.(T?) -> Unit, +): Job = messageFlow + .filterIsInstance() + .filter { it.property == spec.name } + .onEach { change -> + change.callback(spec.converter.metaToObject(change.value)) + }.launchIn(this) \ No newline at end of file diff --git a/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/properties/DeviceSpec.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/DeviceSpec.kt similarity index 52% rename from controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/properties/DeviceSpec.kt rename to controls-core/src/commonMain/kotlin/space/kscience/controls/spec/DeviceSpec.kt index 934220f..83364ee 100644 --- a/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/properties/DeviceSpec.kt +++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/DeviceSpec.kt @@ -1,10 +1,9 @@ -package ru.mipt.npm.controls.properties +package space.kscience.controls.spec import kotlinx.coroutines.withContext -import ru.mipt.npm.controls.api.ActionDescriptor -import ru.mipt.npm.controls.api.PropertyDescriptor -import space.kscience.dataforge.context.Context -import space.kscience.dataforge.context.Factory +import space.kscience.controls.api.ActionDescriptor +import space.kscience.controls.api.Device +import space.kscience.controls.api.PropertyDescriptor import space.kscience.dataforge.meta.Meta import space.kscience.dataforge.meta.transformations.MetaConverter import kotlin.properties.PropertyDelegateProvider @@ -14,28 +13,37 @@ import kotlin.reflect.KProperty import kotlin.reflect.KProperty1 @OptIn(InternalDeviceAPI::class) -public abstract class DeviceSpec>( - private val buildDevice: () -> D -) : Factory { - private val _properties = HashMap>() +public abstract class DeviceSpec { + //initializing meta property for everyone + private val _properties = hashMapOf>( + DeviceMetaPropertySpec.name to DeviceMetaPropertySpec + ) public val properties: Map> get() = _properties private val _actions = HashMap>() public val actions: Map> get() = _actions - public fun > registerProperty(deviceProperty: P): P { + + public open suspend fun D.onOpen() { + } + + public open fun D.onClose() { + } + + + public fun > registerProperty(deviceProperty: P): P { _properties[deviceProperty.name] = deviceProperty return deviceProperty } - public fun registerProperty( + public fun registerProperty( converter: MetaConverter, readOnlyProperty: KProperty1, - descriptorBuilder: PropertyDescriptor.() -> Unit = {} + descriptorBuilder: PropertyDescriptor.() -> Unit = {}, ): DevicePropertySpec { val deviceProperty = object : DevicePropertySpec { - override val name: String = readOnlyProperty.name - override val descriptor: PropertyDescriptor = PropertyDescriptor(this.name).apply(descriptorBuilder) + override val descriptor: PropertyDescriptor = + PropertyDescriptor(readOnlyProperty.name).apply(descriptorBuilder) override val converter: MetaConverter = converter override suspend fun read(device: D): T = withContext(device.coroutineContext) { readOnlyProperty.get(device) } @@ -43,16 +51,39 @@ public abstract class DeviceSpec>( return registerProperty(deviceProperty) } - public fun property( + public fun property( + converter: MetaConverter, + readOnlyProperty: KProperty1, + descriptorBuilder: PropertyDescriptor.() -> Unit = {}, + ): PropertyDelegateProvider, ReadOnlyProperty>> = + PropertyDelegateProvider { _, property -> + val deviceProperty = object : DevicePropertySpec { + override val descriptor: PropertyDescriptor = PropertyDescriptor(property.name).apply { + //TODO add type from converter + writable = true + }.apply(descriptorBuilder) + + override val converter: MetaConverter = converter + + override suspend fun read(device: D): T = withContext(device.coroutineContext) { + readOnlyProperty.get(device) + } + } + registerProperty(deviceProperty) + ReadOnlyProperty { _, _ -> + deviceProperty + } + } + + public fun mutableProperty( converter: MetaConverter, readWriteProperty: KMutableProperty1, - descriptorBuilder: PropertyDescriptor.() -> Unit = {} + descriptorBuilder: PropertyDescriptor.() -> Unit = {}, ): PropertyDelegateProvider, ReadOnlyProperty>> = PropertyDelegateProvider { _, property -> val deviceProperty = object : WritableDevicePropertySpec { - override val name: String = property.name - override val descriptor: PropertyDescriptor = PropertyDescriptor(name).apply { + override val descriptor: PropertyDescriptor = PropertyDescriptor(property.name).apply { //TODO add type from converter writable = true }.apply(descriptorBuilder) @@ -73,20 +104,19 @@ public abstract class DeviceSpec>( } } - public fun property( + public fun property( converter: MetaConverter, - name: String? = null, descriptorBuilder: PropertyDescriptor.() -> Unit = {}, - read: suspend D.() -> T + name: String? = null, + read: suspend D.() -> T?, ): PropertyDelegateProvider, ReadOnlyProperty, DevicePropertySpec>> = PropertyDelegateProvider { _: DeviceSpec, property -> val propertyName = name ?: property.name val deviceProperty = object : DevicePropertySpec { - override val name: String = propertyName - override val descriptor: PropertyDescriptor = PropertyDescriptor(this.name).apply(descriptorBuilder) + override val descriptor: PropertyDescriptor = PropertyDescriptor(propertyName).apply(descriptorBuilder) override val converter: MetaConverter = converter - override suspend fun read(device: D): T = withContext(device.coroutineContext) { device.read() } + override suspend fun read(device: D): T? = withContext(device.coroutineContext) { device.read() } } registerProperty(deviceProperty) ReadOnlyProperty, DevicePropertySpec> { _, _ -> @@ -94,21 +124,20 @@ public abstract class DeviceSpec>( } } - public fun property( + public fun mutableProperty( converter: MetaConverter, - name: String? = null, descriptorBuilder: PropertyDescriptor.() -> Unit = {}, - read: suspend D.() -> T, - write: suspend D.(T) -> Unit + name: String? = null, + read: suspend D.() -> T?, + write: suspend D.(T) -> Unit, ): PropertyDelegateProvider, ReadOnlyProperty, WritableDevicePropertySpec>> = PropertyDelegateProvider { _: DeviceSpec, property: KProperty<*> -> val propertyName = name ?: property.name val deviceProperty = object : WritableDevicePropertySpec { - override val name: String = propertyName - override val descriptor: PropertyDescriptor = PropertyDescriptor(this.name).apply(descriptorBuilder) + override val descriptor: PropertyDescriptor = PropertyDescriptor(propertyName).apply(descriptorBuilder) override val converter: MetaConverter = converter - override suspend fun read(device: D): T = withContext(device.coroutineContext) { device.read() } + override suspend fun read(device: D): T? = withContext(device.coroutineContext) { device.read() } override suspend fun write(device: D, value: T): Unit = withContext(device.coroutineContext) { device.write(value) @@ -121,22 +150,21 @@ public abstract class DeviceSpec>( } - public fun registerAction(deviceAction: DeviceActionSpec): DeviceActionSpec { + public fun registerAction(deviceAction: DeviceActionSpec): DeviceActionSpec { _actions[deviceAction.name] = deviceAction return deviceAction } - public fun action( + public fun action( inputConverter: MetaConverter, outputConverter: MetaConverter, - name: String? = null, descriptorBuilder: ActionDescriptor.() -> Unit = {}, - execute: suspend D.(I?) -> O? + name: String? = null, + execute: suspend D.(I?) -> O?, ): PropertyDelegateProvider, ReadOnlyProperty, DeviceActionSpec>> = PropertyDelegateProvider { _: DeviceSpec, property -> val actionName = name ?: property.name val deviceAction = object : DeviceActionSpec { - override val name: String = actionName override val descriptor: ActionDescriptor = ActionDescriptor(actionName).apply(descriptorBuilder) override val inputConverter: MetaConverter = inputConverter @@ -153,19 +181,68 @@ public abstract class DeviceSpec>( } /** - * The function is executed right after device initialization is finished + * An action that takes [Meta] and returns [Meta]. No conversions are done */ - public open fun D.onStartup() {} + public fun metaAction( + descriptorBuilder: ActionDescriptor.() -> Unit = {}, + name: String? = null, + execute: suspend D.(Meta?) -> Meta?, + ): PropertyDelegateProvider, ReadOnlyProperty, DeviceActionSpec>> = + action( + MetaConverter.Companion.meta, + MetaConverter.Companion.meta, + descriptorBuilder, + name + ) { + execute(it) + } /** - * The function is executed before device is shut down + * An action that takes no parameters and returns no values */ - public open fun D.onShutdown() {} - - - override fun invoke(meta: Meta, context: Context): D = buildDevice().apply { - this.context = context - this.meta = meta - onStartup() - } + public fun unitAction( + descriptorBuilder: ActionDescriptor.() -> Unit = {}, + name: String? = null, + execute: suspend D.() -> Unit, + ): PropertyDelegateProvider, ReadOnlyProperty, DeviceActionSpec>> = + action( + MetaConverter.Companion.meta, + MetaConverter.Companion.meta, + descriptorBuilder, + name + ) { + execute() + null + } } + + +/** + * Register a mutable logical property for a device + */ +@OptIn(InternalDeviceAPI::class) +public fun > DeviceSpec.logicalProperty( + converter: MetaConverter, + descriptorBuilder: PropertyDescriptor.() -> Unit = {}, + name: String? = null, +): PropertyDelegateProvider, ReadOnlyProperty>> = + PropertyDelegateProvider { _, property -> + val deviceProperty = object : WritableDevicePropertySpec { + val propertyName = name ?: property.name + override val descriptor: PropertyDescriptor = PropertyDescriptor(propertyName).apply { + //TODO add type from converter + writable = true + }.apply(descriptorBuilder) + + override val converter: MetaConverter = converter + + override suspend fun read(device: D): T? = device.getProperty(propertyName)?.let(converter::metaToObject) + + override suspend fun write(device: D, value: T): Unit = + device.writeProperty(propertyName, converter.objectToMeta(value)) + } + registerProperty(deviceProperty) + ReadOnlyProperty { _, _ -> + deviceProperty + } + } \ No newline at end of file diff --git a/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/properties/deviceExtensions.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/deviceExtensions.kt similarity index 69% rename from controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/properties/deviceExtensions.kt rename to controls-core/src/commonMain/kotlin/space/kscience/controls/spec/deviceExtensions.kt index 582c8a7..ed48d39 100644 --- a/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/properties/deviceExtensions.kt +++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/deviceExtensions.kt @@ -1,4 +1,4 @@ -package ru.mipt.npm.controls.properties +package space.kscience.controls.spec import kotlinx.coroutines.Job import kotlinx.coroutines.flow.Flow @@ -14,7 +14,7 @@ import kotlin.time.Duration * * The flow is canceled when the device scope is canceled */ -public fun , R> D.readRecurring(interval: Duration, reader: suspend D.() -> R): Flow = flow { +public fun , R> D.readRecurring(interval: Duration, reader: suspend D.() -> R): Flow = flow { while (isActive) { kotlinx.coroutines.delay(interval) emit(reader()) @@ -22,9 +22,9 @@ public fun , R> D.readRecurring(interval: Duration, reader: } /** - * Do a recurring task on a device. The task could + * Do a recurring (with a fixed delay) task on a device. */ -public fun > D.doRecurring(interval: Duration, task: suspend D.() -> Unit): Job = launch { +public fun > D.doRecurring(interval: Duration, task: suspend D.() -> Unit): Job = launch { while (isActive) { kotlinx.coroutines.delay(interval) task() diff --git a/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/base/misc.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/misc.kt similarity index 71% rename from controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/base/misc.kt rename to controls-core/src/commonMain/kotlin/space/kscience/controls/spec/misc.kt index 111789f..e264212 100644 --- a/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/base/misc.kt +++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/misc.kt @@ -1,12 +1,7 @@ -package ru.mipt.npm.controls.base +package space.kscience.controls.spec -import space.kscience.dataforge.meta.Meta -import space.kscience.dataforge.meta.double -import space.kscience.dataforge.meta.enum -import space.kscience.dataforge.meta.get +import space.kscience.dataforge.meta.* import space.kscience.dataforge.meta.transformations.MetaConverter -import space.kscience.dataforge.values.asValue -import space.kscience.dataforge.values.double import kotlin.time.Duration import kotlin.time.DurationUnit import kotlin.time.toDuration diff --git a/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/properties/propertySpecDelegates.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/propertySpecDelegates.kt similarity index 72% rename from controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/properties/propertySpecDelegates.kt rename to controls-core/src/commonMain/kotlin/space/kscience/controls/spec/propertySpecDelegates.kt index d087505..e2afcab 100644 --- a/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/properties/propertySpecDelegates.kt +++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/propertySpecDelegates.kt @@ -1,28 +1,28 @@ -package ru.mipt.npm.controls.properties +package space.kscience.controls.spec -import ru.mipt.npm.controls.api.PropertyDescriptor -import ru.mipt.npm.controls.api.metaDescriptor +import space.kscience.controls.api.PropertyDescriptor +import space.kscience.controls.api.metaDescriptor import space.kscience.dataforge.meta.Meta +import space.kscience.dataforge.meta.ValueType import space.kscience.dataforge.meta.transformations.MetaConverter -import space.kscience.dataforge.values.ValueType import kotlin.properties.PropertyDelegateProvider import kotlin.properties.ReadOnlyProperty //read only delegates -public fun > DeviceSpec.booleanProperty( - name: String? = null, +public fun > DeviceSpec.booleanProperty( descriptorBuilder: PropertyDescriptor.() -> Unit = {}, - read: suspend D.() -> Boolean + name: String? = null, + read: suspend D.() -> Boolean? ): PropertyDelegateProvider, ReadOnlyProperty, DevicePropertySpec>> = property( MetaConverter.boolean, - name, { metaDescriptor { type(ValueType.BOOLEAN) } descriptorBuilder() }, + name, read ) @@ -35,110 +35,110 @@ private inline fun numberDescriptor( descriptorBuilder() } -public fun > DeviceSpec.numberProperty( +public fun > DeviceSpec.numberProperty( name: String? = null, descriptorBuilder: PropertyDescriptor.() -> Unit = {}, - read: suspend D.() -> Number + read: suspend D.() -> Number? ): PropertyDelegateProvider, ReadOnlyProperty, DevicePropertySpec>> = property( MetaConverter.number, - name, numberDescriptor(descriptorBuilder), + name, read ) -public fun > DeviceSpec.doubleProperty( - name: String? = null, +public fun > DeviceSpec.doubleProperty( descriptorBuilder: PropertyDescriptor.() -> Unit = {}, - read: suspend D.() -> Double + name: String? = null, + read: suspend D.() -> Double? ): PropertyDelegateProvider, ReadOnlyProperty, DevicePropertySpec>> = property( MetaConverter.double, - name, numberDescriptor(descriptorBuilder), + name, read ) -public fun > DeviceSpec.stringProperty( - name: String? = null, +public fun > DeviceSpec.stringProperty( descriptorBuilder: PropertyDescriptor.() -> Unit = {}, - read: suspend D.() -> String + name: String? = null, + read: suspend D.() -> String? ): PropertyDelegateProvider, ReadOnlyProperty, DevicePropertySpec>> = property( MetaConverter.string, - name, { metaDescriptor { type(ValueType.STRING) } descriptorBuilder() }, + name, read ) -public fun > DeviceSpec.metaProperty( - name: String? = null, +public fun > DeviceSpec.metaProperty( descriptorBuilder: PropertyDescriptor.() -> Unit = {}, - read: suspend D.() -> Meta + name: String? = null, + read: suspend D.() -> Meta? ): PropertyDelegateProvider, ReadOnlyProperty, DevicePropertySpec>> = property( MetaConverter.meta, - name, { metaDescriptor { type(ValueType.STRING) } descriptorBuilder() }, + name, read ) //read-write delegates -public fun > DeviceSpec.booleanProperty( - name: String? = null, +public fun > DeviceSpec.booleanProperty( descriptorBuilder: PropertyDescriptor.() -> Unit = {}, - read: suspend D.() -> Boolean, + name: String? = null, + read: suspend D.() -> Boolean?, write: suspend D.(Boolean) -> Unit ): PropertyDelegateProvider, ReadOnlyProperty, WritableDevicePropertySpec>> = - property( + mutableProperty( MetaConverter.boolean, - name, { metaDescriptor { type(ValueType.BOOLEAN) } descriptorBuilder() }, + name, read, write ) -public fun > DeviceSpec.numberProperty( - name: String? = null, +public fun > DeviceSpec.numberProperty( descriptorBuilder: PropertyDescriptor.() -> Unit = {}, + name: String? = null, read: suspend D.() -> Number, write: suspend D.(Number) -> Unit ): PropertyDelegateProvider, ReadOnlyProperty, WritableDevicePropertySpec>> = - property(MetaConverter.number, name, numberDescriptor(descriptorBuilder), read, write) + mutableProperty(MetaConverter.number, numberDescriptor(descriptorBuilder), name, read, write) -public fun > DeviceSpec.doubleProperty( - name: String? = null, +public fun > DeviceSpec.doubleProperty( descriptorBuilder: PropertyDescriptor.() -> Unit = {}, + name: String? = null, read: suspend D.() -> Double, write: suspend D.(Double) -> Unit ): PropertyDelegateProvider, ReadOnlyProperty, WritableDevicePropertySpec>> = - property(MetaConverter.double, name, numberDescriptor(descriptorBuilder), read, write) + mutableProperty(MetaConverter.double, numberDescriptor(descriptorBuilder), name, read, write) -public fun > DeviceSpec.stringProperty( - name: String? = null, +public fun > DeviceSpec.stringProperty( descriptorBuilder: PropertyDescriptor.() -> Unit = {}, + name: String? = null, read: suspend D.() -> String, write: suspend D.(String) -> Unit ): PropertyDelegateProvider, ReadOnlyProperty, WritableDevicePropertySpec>> = - property(MetaConverter.string, name, descriptorBuilder, read, write) + mutableProperty(MetaConverter.string, descriptorBuilder, name, read, write) -public fun > DeviceSpec.metaProperty( - name: String? = null, +public fun > DeviceSpec.metaProperty( descriptorBuilder: PropertyDescriptor.() -> Unit = {}, + name: String? = null, read: suspend D.() -> Meta, write: suspend D.(Meta) -> Unit ): PropertyDelegateProvider, ReadOnlyProperty, WritableDevicePropertySpec>> = - property(MetaConverter.meta, name, descriptorBuilder, read, write) \ No newline at end of file + mutableProperty(MetaConverter.meta, descriptorBuilder, name, read, write) \ No newline at end of file diff --git a/controls-core/src/jvmMain/kotlin/ru/mipt/npm/controls/properties/delegates.kt b/controls-core/src/jvmMain/kotlin/ru/mipt/npm/controls/properties/delegates.kt deleted file mode 100644 index 7def81d..0000000 --- a/controls-core/src/jvmMain/kotlin/ru/mipt/npm/controls/properties/delegates.kt +++ /dev/null @@ -1,89 +0,0 @@ -package ru.mipt.npm.controls.controllers - -import kotlinx.coroutines.runBlocking -import ru.mipt.npm.controls.base.* -import space.kscience.dataforge.meta.Meta -import space.kscience.dataforge.meta.transformations.MetaConverter -import kotlin.properties.ReadOnlyProperty -import kotlin.properties.ReadWriteProperty -import kotlin.reflect.KProperty -import kotlin.time.Duration - -/** - * Blocking read of the value - */ -public operator fun ReadOnlyDeviceProperty.getValue(thisRef: Any?, property: KProperty<*>): Meta = - runBlocking(scope.coroutineContext) { - read() - } - -public operator fun TypedReadOnlyDeviceProperty.getValue(thisRef: Any?, property: KProperty<*>): T = - runBlocking(scope.coroutineContext) { - readTyped() - } - -public operator fun DeviceProperty.setValue(thisRef: Any?, property: KProperty<*>, value: Meta) { - this.value = value -} - -public operator fun TypedDeviceProperty.setValue(thisRef: Any?, property: KProperty<*>, value: T) { - this.typedValue = value -} - -public fun ReadOnlyDeviceProperty.convert( - metaConverter: MetaConverter, - forceRead: Boolean, -): ReadOnlyProperty { - return ReadOnlyProperty { _, _ -> - runBlocking(scope.coroutineContext) { - val meta = read(forceRead) - metaConverter.metaToObject(meta)?: error("Meta $meta could not be converted by $metaConverter") - } - } -} - -public fun DeviceProperty.convert( - metaConverter: MetaConverter, - forceRead: Boolean, -): ReadWriteProperty { - return object : ReadWriteProperty { - override fun getValue(thisRef: Any?, property: KProperty<*>): T = runBlocking(scope.coroutineContext) { - val meta = read(forceRead) - metaConverter.metaToObject(meta)?: error("Meta $meta could not be converted by $metaConverter") - } - - override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) { - this@convert.setValue(thisRef, property, value.let { metaConverter.objectToMeta(it) }) - } - } -} - -public fun ReadOnlyDeviceProperty.double(forceRead: Boolean = false): ReadOnlyProperty = - convert(MetaConverter.double, forceRead) - -public fun DeviceProperty.double(forceRead: Boolean = false): ReadWriteProperty = - convert(MetaConverter.double, forceRead) - -public fun ReadOnlyDeviceProperty.int(forceRead: Boolean = false): ReadOnlyProperty = - convert(MetaConverter.int, forceRead) - -public fun DeviceProperty.int(forceRead: Boolean = false): ReadWriteProperty = - convert(MetaConverter.int, forceRead) - -public fun ReadOnlyDeviceProperty.string(forceRead: Boolean = false): ReadOnlyProperty = - convert(MetaConverter.string, forceRead) - -public fun DeviceProperty.string(forceRead: Boolean = false): ReadWriteProperty = - convert(MetaConverter.string, forceRead) - -public fun ReadOnlyDeviceProperty.boolean(forceRead: Boolean = false): ReadOnlyProperty = - convert(MetaConverter.boolean, forceRead) - -public fun DeviceProperty.boolean(forceRead: Boolean = false): ReadWriteProperty = - convert(MetaConverter.boolean, forceRead) - -public fun ReadOnlyDeviceProperty.duration(forceRead: Boolean = false): ReadOnlyProperty = - convert(DurationConverter, forceRead) - -public fun DeviceProperty.duration(forceRead: Boolean = false): ReadWriteProperty = - convert(DurationConverter, forceRead) \ No newline at end of file diff --git a/controls-core/src/jvmMain/kotlin/ru/mipt/npm/controls/properties/getDeviceProperty.kt b/controls-core/src/jvmMain/kotlin/ru/mipt/npm/controls/properties/getDeviceProperty.kt deleted file mode 100644 index 3be61d6..0000000 --- a/controls-core/src/jvmMain/kotlin/ru/mipt/npm/controls/properties/getDeviceProperty.kt +++ /dev/null @@ -1,10 +0,0 @@ -package ru.mipt.npm.controls.properties - -import kotlinx.coroutines.runBlocking - -/** - * Blocking property get call - */ -public operator fun , T : Any> D.get( - propertySpec: DevicePropertySpec -): T = runBlocking { read(propertySpec) } \ No newline at end of file diff --git a/controls-core/src/jvmMain/kotlin/ru/mipt/npm/controls/misc/javaTimeMeta.kt b/controls-core/src/jvmMain/kotlin/space/kscience/controls/misc/javaTimeMeta.kt similarity index 84% rename from controls-core/src/jvmMain/kotlin/ru/mipt/npm/controls/misc/javaTimeMeta.kt rename to controls-core/src/jvmMain/kotlin/space/kscience/controls/misc/javaTimeMeta.kt index eec5774..891c30b 100644 --- a/controls-core/src/jvmMain/kotlin/ru/mipt/npm/controls/misc/javaTimeMeta.kt +++ b/controls-core/src/jvmMain/kotlin/space/kscience/controls/misc/javaTimeMeta.kt @@ -1,9 +1,8 @@ -package ru.mipt.npm.controls.misc +package space.kscience.controls.misc import space.kscience.dataforge.meta.Meta import space.kscience.dataforge.meta.get import space.kscience.dataforge.meta.long -import space.kscience.dataforge.values.long import java.time.Instant // TODO move to core diff --git a/controls-core/src/jvmMain/kotlin/ru/mipt/npm/controls/ports/TcpPort.kt b/controls-core/src/jvmMain/kotlin/space/kscience/controls/ports/TcpPort.kt similarity index 87% rename from controls-core/src/jvmMain/kotlin/ru/mipt/npm/controls/ports/TcpPort.kt rename to controls-core/src/jvmMain/kotlin/space/kscience/controls/ports/TcpPort.kt index cfd810b..77fec44 100644 --- a/controls-core/src/jvmMain/kotlin/ru/mipt/npm/controls/ports/TcpPort.kt +++ b/controls-core/src/jvmMain/kotlin/space/kscience/controls/ports/TcpPort.kt @@ -1,4 +1,4 @@ -package ru.mipt.npm.controls.ports +package space.kscience.controls.ports import kotlinx.coroutines.* import space.kscience.dataforge.context.Context @@ -52,19 +52,20 @@ public class TcpPort private constructor( } if (num < 0) cancel("The input channel is exhausted") } catch (ex: Exception) { - logger.error(ex){"Channel read error"} + logger.error(ex) { "Channel read error" } delay(1000) } } } - override suspend fun write(data: ByteArray) { + override suspend fun write(data: ByteArray): Unit = withContext(Dispatchers.IO){ futureChannel.await().write(ByteBuffer.wrap(data)) } + @OptIn(ExperimentalCoroutinesApi::class) override fun close() { listenerJob.cancel() - if(futureChannel.isCompleted){ + if (futureChannel.isCompleted) { futureChannel.getCompleted().close() } else { futureChannel.cancel() @@ -73,6 +74,9 @@ public class TcpPort private constructor( } public companion object : PortFactory { + + override val type: String = "tcp" + public fun open( context: Context, host: String, @@ -82,7 +86,7 @@ public class TcpPort private constructor( return TcpPort(context, host, port, coroutineContext) } - override fun invoke(meta: Meta, context: Context): Port { + override fun build(context: Context, meta: Meta): Port { val host = meta["host"].string ?: "localhost" val port = meta["port"].int ?: error("Port value for TCP port is not defined in $meta") return open(context, host, port) diff --git a/controls-core/src/jvmMain/kotlin/space/kscience/controls/ports/TcpPortPlugin.kt b/controls-core/src/jvmMain/kotlin/space/kscience/controls/ports/TcpPortPlugin.kt new file mode 100644 index 0000000..23174af --- /dev/null +++ b/controls-core/src/jvmMain/kotlin/space/kscience/controls/ports/TcpPortPlugin.kt @@ -0,0 +1,30 @@ +package space.kscience.controls.ports + +import space.kscience.dataforge.context.AbstractPlugin +import space.kscience.dataforge.context.Context +import space.kscience.dataforge.context.PluginFactory +import space.kscience.dataforge.context.PluginTag +import space.kscience.dataforge.meta.Meta +import space.kscience.dataforge.names.Name +import kotlin.reflect.KClass + +public class TcpPortPlugin : AbstractPlugin() { + + override val tag: PluginTag get() = Companion.tag + + override fun content(target: String): Map = when(target){ + PortFactory.TYPE -> mapOf(Name.EMPTY to TcpPort) + else -> emptyMap() + } + + public companion object : PluginFactory { + + override val tag: PluginTag = PluginTag("controls.ports.tcp", group = PluginTag.DATAFORGE_GROUP) + + override val type: KClass = TcpPortPlugin::class + + override fun build(context: Context, meta: Meta): TcpPortPlugin = TcpPortPlugin() + + } + +} \ No newline at end of file diff --git a/controls-core/src/jvmMain/kotlin/space/kscience/controls/spec/getDeviceProperty.kt b/controls-core/src/jvmMain/kotlin/space/kscience/controls/spec/getDeviceProperty.kt new file mode 100644 index 0000000..a7063f5 --- /dev/null +++ b/controls-core/src/jvmMain/kotlin/space/kscience/controls/spec/getDeviceProperty.kt @@ -0,0 +1,10 @@ +package space.kscience.controls.spec + +import kotlinx.coroutines.runBlocking + +/** + * Blocking property get call + */ +public operator fun , T : Any> D.get( + propertySpec: DevicePropertySpec +): T? = runBlocking { read(propertySpec) } \ No newline at end of file diff --git a/controls-core/src/jvmTest/kotlin/ru/mipt/npm/controls/ports/PortIOTest.kt b/controls-core/src/jvmTest/kotlin/space/kscience/controls/ports/PortIOTest.kt similarity index 95% rename from controls-core/src/jvmTest/kotlin/ru/mipt/npm/controls/ports/PortIOTest.kt rename to controls-core/src/jvmTest/kotlin/space/kscience/controls/ports/PortIOTest.kt index cdb3107..bdf6891 100644 --- a/controls-core/src/jvmTest/kotlin/ru/mipt/npm/controls/ports/PortIOTest.kt +++ b/controls-core/src/jvmTest/kotlin/space/kscience/controls/ports/PortIOTest.kt @@ -1,4 +1,4 @@ -package ru.mipt.npm.controls.ports +package space.kscience.controls.ports import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map diff --git a/controls-ktor-tcp/build.gradle.kts b/controls-ktor-tcp/build.gradle.kts new file mode 100644 index 0000000..2089b19 --- /dev/null +++ b/controls-ktor-tcp/build.gradle.kts @@ -0,0 +1,10 @@ +plugins { + id("space.kscience.gradle.jvm") +} + +val ktorVersion: String by rootProject.extra + +dependencies { + api(projects.controlsCore) + api("io.ktor:ktor-network:$ktorVersion") +} diff --git a/controls-tcp/src/jvmMain/kotlin/ru/mipt/npm/controls/ports/KtorTcpPort.kt b/controls-ktor-tcp/src/main/kotlin/space/kscience/controls/ports/KtorTcpPort.kt similarity index 89% rename from controls-tcp/src/jvmMain/kotlin/ru/mipt/npm/controls/ports/KtorTcpPort.kt rename to controls-ktor-tcp/src/main/kotlin/space/kscience/controls/ports/KtorTcpPort.kt index d3f48d3..7f906d3 100644 --- a/controls-tcp/src/jvmMain/kotlin/ru/mipt/npm/controls/ports/KtorTcpPort.kt +++ b/controls-ktor-tcp/src/main/kotlin/space/kscience/controls/ports/KtorTcpPort.kt @@ -1,4 +1,4 @@ -package ru.mipt.npm.controls.ports +package space.kscience.controls.ports import io.ktor.network.selector.ActorSelectorManager import io.ktor.network.sockets.aSocket @@ -16,7 +16,6 @@ import space.kscience.dataforge.meta.Meta import space.kscience.dataforge.meta.get import space.kscience.dataforge.meta.int import space.kscience.dataforge.meta.string -import java.net.InetSocketAddress import kotlin.coroutines.CoroutineContext public class KtorTcpPort internal constructor( @@ -29,7 +28,7 @@ public class KtorTcpPort internal constructor( override fun toString(): String = "port[tcp:$host:$port]" private val futureSocket = scope.async { - aSocket(ActorSelectorManager(Dispatchers.IO)).tcp().connect(InetSocketAddress(host, port)) + aSocket(ActorSelectorManager(Dispatchers.IO)).tcp().connect(host, port) } private val writeChannel = scope.async { @@ -38,7 +37,7 @@ public class KtorTcpPort internal constructor( private val listenerJob = scope.launch { val input = futureSocket.await().openReadChannel() - input.consumeEachBufferRange { buffer, last -> + input.consumeEachBufferRange { buffer, _ -> val array = ByteArray(buffer.remaining()) buffer.get(array) receive(array) @@ -56,7 +55,10 @@ public class KtorTcpPort internal constructor( super.close() } - public companion object: PortFactory { + public companion object : PortFactory { + + override val type: String = "tcp" + public fun open( context: Context, host: String, @@ -66,7 +68,7 @@ public class KtorTcpPort internal constructor( return KtorTcpPort(context, host, port, coroutineContext) } - override fun invoke(meta: Meta, context: Context): Port { + override fun build(context: Context, meta: Meta): Port { val host = meta["host"].string ?: "localhost" val port = meta["port"].int ?: error("Port value for TCP port is not defined in $meta") return open(context, host, port) diff --git a/controls-ktor-tcp/src/main/kotlin/space/kscience/controls/ports/KtorTcpPortPlugin.kt b/controls-ktor-tcp/src/main/kotlin/space/kscience/controls/ports/KtorTcpPortPlugin.kt new file mode 100644 index 0000000..1256455 --- /dev/null +++ b/controls-ktor-tcp/src/main/kotlin/space/kscience/controls/ports/KtorTcpPortPlugin.kt @@ -0,0 +1,30 @@ +package space.kscience.controls.ports + +import space.kscience.dataforge.context.AbstractPlugin +import space.kscience.dataforge.context.Context +import space.kscience.dataforge.context.PluginFactory +import space.kscience.dataforge.context.PluginTag +import space.kscience.dataforge.meta.Meta +import space.kscience.dataforge.names.Name +import kotlin.reflect.KClass + +public class KtorTcpPortPlugin : AbstractPlugin() { + + override val tag: PluginTag get() = Companion.tag + + override fun content(target: String): Map = when(target){ + PortFactory.TYPE -> mapOf(Name.EMPTY to KtorTcpPort) + else -> emptyMap() + } + + public companion object : PluginFactory { + + override val tag: PluginTag = PluginTag("controls.ports.serial", group = PluginTag.DATAFORGE_GROUP) + + override val type: KClass = KtorTcpPortPlugin::class + + override fun build(context: Context, meta: Meta): KtorTcpPortPlugin = KtorTcpPortPlugin() + + } + +} \ No newline at end of file diff --git a/controls-magix-client/build.gradle.kts b/controls-magix-client/build.gradle.kts index 3d50b56..a94e770 100644 --- a/controls-magix-client/build.gradle.kts +++ b/controls-magix-client/build.gradle.kts @@ -1,21 +1,16 @@ plugins { - id("ru.mipt.npm.gradle.mpp") + id("space.kscience.gradle.mpp") `maven-publish` } kscience{ + jvm() + js() useSerialization { json() } -} - -kotlin { - sourceSets { - commonMain { - dependencies { - implementation(project(":magix:magix-rsocket")) - implementation(project(":controls-core")) - } - } + dependencies { + implementation(project(":magix:magix-rsocket")) + implementation(project(":controls-core")) } } \ No newline at end of file diff --git a/controls-magix-client/src/commonMain/kotlin/ru/mipt/npm/controls/client/dfMagix.kt b/controls-magix-client/src/commonMain/kotlin/ru/mipt/npm/controls/client/dfMagix.kt deleted file mode 100644 index 2e92205..0000000 --- a/controls-magix-client/src/commonMain/kotlin/ru/mipt/npm/controls/client/dfMagix.kt +++ /dev/null @@ -1,64 +0,0 @@ -package ru.mipt.npm.controls.client - -import kotlinx.coroutines.Job -import kotlinx.coroutines.flow.catch -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.launch -import ru.mipt.npm.controls.api.DeviceMessage -import ru.mipt.npm.controls.controllers.DeviceManager -import ru.mipt.npm.controls.controllers.hubMessageFlow -import ru.mipt.npm.controls.controllers.respondHubMessage -import ru.mipt.npm.magix.api.MagixEndpoint -import ru.mipt.npm.magix.api.MagixMessage -import space.kscience.dataforge.context.error -import space.kscience.dataforge.context.logger - - -public const val DATAFORGE_MAGIX_FORMAT: String = "dataforge" - -internal fun generateId(request: MagixMessage<*>): String = if (request.id != null) { - "${request.id}.response" -} else { - "df[${request.payload.hashCode()}" -} - -/** - * Communicate with server in [Magix format](https://github.com/waltz-controls/rfc/tree/master/1) - */ -public fun DeviceManager.connectToMagix( - endpoint: MagixEndpoint, - endpointID: String = DATAFORGE_MAGIX_FORMAT, -): Job = context.launch { - endpoint.subscribe().onEach { request -> - val responsePayload = respondHubMessage(request.payload) - if (responsePayload != null) { - val response = MagixMessage( - format = DATAFORGE_MAGIX_FORMAT, - id = generateId(request), - parentId = request.id, - origin = endpointID, - payload = responsePayload - ) - - endpoint.broadcast(response) - } - }.catch { error -> - logger.error(error) { "Error while responding to message" } - }.launchIn(this) - - hubMessageFlow(this).onEach { payload -> - endpoint.broadcast( - MagixMessage( - format = DATAFORGE_MAGIX_FORMAT, - id = "df[${payload.hashCode()}]", - origin = endpointID, - payload = payload - ) - ) - }.catch { error -> - logger.error(error) { "Error while sending a message" } - }.launchIn(this) -} - - diff --git a/controls-magix-client/src/commonMain/kotlin/space/kscience/controls/client/controlsMagix.kt b/controls-magix-client/src/commonMain/kotlin/space/kscience/controls/client/controlsMagix.kt new file mode 100644 index 0000000..be77a4a --- /dev/null +++ b/controls-magix-client/src/commonMain/kotlin/space/kscience/controls/client/controlsMagix.kt @@ -0,0 +1,62 @@ +package space.kscience.controls.client + +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch +import space.kscience.controls.api.DeviceMessage +import space.kscience.controls.manager.DeviceManager +import space.kscience.controls.manager.hubMessageFlow +import space.kscience.controls.manager.respondHubMessage +import space.kscience.dataforge.context.error +import space.kscience.dataforge.context.logger +import space.kscience.magix.api.* + + +public val controlsMagixFormat: MagixFormat = MagixFormat( + DeviceMessage.serializer(), + setOf("controls-kt", "dataforge") +) + +internal fun generateId(request: MagixMessage): String = if (request.id != null) { + "${request.id}.response" +} else { + "df[${request.payload.hashCode()}" +} + +/** + * Communicate with server in [Magix format](https://github.com/waltz-controls/rfc/tree/master/1) + */ +public fun DeviceManager.connectToMagix( + endpoint: MagixEndpoint, + endpointID: String = controlsMagixFormat.defaultFormat, +): Job = context.launch { + endpoint.subscribe(controlsMagixFormat).onEach { (request, payload) -> + val responsePayload = respondHubMessage(payload) + if (responsePayload != null) { + endpoint.broadcast( + format = controlsMagixFormat, + origin = endpointID, + payload = responsePayload, + id = generateId(request), + parentId = request.id + ) + } + }.catch { error -> + logger.error(error) { "Error while responding to message" } + }.launchIn(this) + + hubMessageFlow(this).onEach { payload -> + endpoint.broadcast( + format = controlsMagixFormat, + origin = endpointID, + payload = payload, + id = "df[${payload.hashCode()}]" + ) + }.catch { error -> + logger.error(error) { "Error while sending a message" } + }.launchIn(this) +} + + diff --git a/controls-magix-client/src/commonMain/kotlin/ru/mipt/npm/controls/client/doocsMagix.kt b/controls-magix-client/src/commonMain/kotlin/space/kscience/controls/client/doocsMagix.kt similarity index 99% rename from controls-magix-client/src/commonMain/kotlin/ru/mipt/npm/controls/client/doocsMagix.kt rename to controls-magix-client/src/commonMain/kotlin/space/kscience/controls/client/doocsMagix.kt index 8f42deb..5655189 100644 --- a/controls-magix-client/src/commonMain/kotlin/ru/mipt/npm/controls/client/doocsMagix.kt +++ b/controls-magix-client/src/commonMain/kotlin/space/kscience/controls/client/doocsMagix.kt @@ -1,4 +1,4 @@ -package ru.mipt.npm.controls.client +package space.kscience.controls.client import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable diff --git a/controls-magix-client/src/commonMain/kotlin/ru/mipt/npm/controls/client/tangoMagix.kt b/controls-magix-client/src/commonMain/kotlin/space/kscience/controls/client/tangoMagix.kt similarity index 65% rename from controls-magix-client/src/commonMain/kotlin/ru/mipt/npm/controls/client/tangoMagix.kt rename to controls-magix-client/src/commonMain/kotlin/space/kscience/controls/client/tangoMagix.kt index 922098a..ddec8d6 100644 --- a/controls-magix-client/src/commonMain/kotlin/ru/mipt/npm/controls/client/tangoMagix.kt +++ b/controls-magix-client/src/commonMain/kotlin/space/kscience/controls/client/tangoMagix.kt @@ -1,18 +1,17 @@ -package ru.mipt.npm.controls.client +package space.kscience.controls.client import kotlinx.coroutines.Job import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import kotlinx.serialization.Serializable -import ru.mipt.npm.controls.api.get -import ru.mipt.npm.controls.api.getOrReadProperty -import ru.mipt.npm.controls.controllers.DeviceManager -import ru.mipt.npm.magix.api.MagixEndpoint -import ru.mipt.npm.magix.api.MagixMessage +import space.kscience.controls.api.get +import space.kscience.controls.api.getOrReadProperty +import space.kscience.controls.manager.DeviceManager import space.kscience.dataforge.context.error import space.kscience.dataforge.context.logger import space.kscience.dataforge.meta.Meta +import space.kscience.magix.api.* public const val TANGO_MAGIX_FORMAT: String = "tango" @@ -59,33 +58,39 @@ public data class TangoPayload( val argin: Meta? = null, val argout: Meta? = null, val data: Meta? = null, - val errors: List? = null + val errors: List? = null, ) +internal val tangoMagixFormat = MagixFormat( + TangoPayload.serializer(), + setOf("tango") +) + + public fun DeviceManager.launchTangoMagix( - endpoint: MagixEndpoint, + endpoint: MagixEndpoint, endpointID: String = TANGO_MAGIX_FORMAT, ): Job { - suspend fun respond(request: MagixMessage, payloadBuilder: (TangoPayload) -> TangoPayload) { + + suspend fun respond(request: MagixMessage, payload: TangoPayload, payloadBuilder: (TangoPayload) -> TangoPayload) { endpoint.broadcast( - request.copy( - id = generateId(request), - parentId = request.id, - origin = endpointID, - payload = payloadBuilder(request.payload) - ) + tangoMagixFormat, + id = generateId(request), + parentId = request.id, + origin = endpointID, + payload = payloadBuilder(payload) ) } return context.launch { - endpoint.subscribe().onEach { request -> + endpoint.subscribe(tangoMagixFormat).onEach { (request, payload) -> try { - val device = get(request.payload.device) - when (request.payload.action) { + val device = get(payload.device) + when (payload.action) { TangoAction.read -> { - val value = device.getOrReadProperty(request.payload.name) - respond(request) { requestPayload -> + val value = device.getOrReadProperty(payload.name) + respond(request, payload) { requestPayload -> requestPayload.copy( value = value, quality = TangoQuality.VALID @@ -93,12 +98,12 @@ public fun DeviceManager.launchTangoMagix( } } TangoAction.write -> { - request.payload.value?.let { value -> - device.writeProperty(request.payload.name, value) + payload.value?.let { value -> + device.writeProperty(payload.name, value) } //wait for value to be written and return final state - val value = device.getOrReadProperty(request.payload.name) - respond(request) { requestPayload -> + val value = device.getOrReadProperty(payload.name) + respond(request, payload) { requestPayload -> requestPayload.copy( value = value, quality = TangoQuality.VALID @@ -106,8 +111,8 @@ public fun DeviceManager.launchTangoMagix( } } TangoAction.exec -> { - val result = device.execute(request.payload.name, request.payload.argin) - respond(request) { requestPayload -> + val result = device.execute(payload.name, payload.argin) + respond(request, payload) { requestPayload -> requestPayload.copy( argout = result, quality = TangoQuality.VALID @@ -119,12 +124,11 @@ public fun DeviceManager.launchTangoMagix( } catch (ex: Exception) { logger.error(ex) { "Error while responding to message" } endpoint.broadcast( - request.copy( - id = generateId(request), - parentId = request.id, - origin = endpointID, - payload = request.payload.copy(quality = TangoQuality.WARNING) - ) + tangoMagixFormat, + id = generateId(request), + parentId = request.id, + origin = endpointID, + payload = payload.copy(quality = TangoQuality.WARNING) ) } }.launchIn(this) diff --git a/controls-opcua/build.gradle.kts b/controls-opcua/build.gradle.kts index a2a225d..8fe7564 100644 --- a/controls-opcua/build.gradle.kts +++ b/controls-opcua/build.gradle.kts @@ -1,14 +1,14 @@ plugins { - id("ru.mipt.npm.gradle.jvm") + id("space.kscience.gradle.jvm") } val ktorVersion: String by rootProject.extra -val miloVersion: String = "0.6.3" +val miloVersion: String = "0.6.7" dependencies { api(project(":controls-core")) - api("org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:${ru.mipt.npm.gradle.KScienceVersions.coroutinesVersion}") + api("org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:${space.kscience.gradle.KScienceVersions.coroutinesVersion}") api("org.eclipse.milo:sdk-client:$miloVersion") api("org.eclipse.milo:bsd-parser:$miloVersion") diff --git a/controls-opcua/src/main/kotlin/ru/mipt/npm/controls/opcua/client/MetaBsdParser.kt b/controls-opcua/src/main/kotlin/space/kscience/controls/opcua/client/MetaBsdParser.kt similarity index 98% rename from controls-opcua/src/main/kotlin/ru/mipt/npm/controls/opcua/client/MetaBsdParser.kt rename to controls-opcua/src/main/kotlin/space/kscience/controls/opcua/client/MetaBsdParser.kt index 171b74e..74257af 100644 --- a/controls-opcua/src/main/kotlin/ru/mipt/npm/controls/opcua/client/MetaBsdParser.kt +++ b/controls-opcua/src/main/kotlin/space/kscience/controls/opcua/client/MetaBsdParser.kt @@ -1,4 +1,4 @@ -package ru.mipt.npm.controls.opcua.client +package space.kscience.controls.opcua.client import org.eclipse.milo.opcua.binaryschema.AbstractCodec import org.eclipse.milo.opcua.binaryschema.parser.BsdParser @@ -11,12 +11,11 @@ import org.eclipse.milo.opcua.stack.core.types.builtin.* import org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.* import org.opcfoundation.opcua.binaryschema.EnumeratedType import org.opcfoundation.opcua.binaryschema.StructuredType -import ru.mipt.npm.controls.misc.instant -import ru.mipt.npm.controls.misc.toMeta +import space.kscience.controls.misc.instant +import space.kscience.controls.misc.toMeta import space.kscience.dataforge.meta.* import space.kscience.dataforge.names.Name import space.kscience.dataforge.names.asName -import space.kscience.dataforge.values.* import java.util.* diff --git a/controls-opcua/src/main/kotlin/ru/mipt/npm/controls/opcua/client/MiloDevice.kt b/controls-opcua/src/main/kotlin/space/kscience/controls/opcua/client/MiloDevice.kt similarity index 91% rename from controls-opcua/src/main/kotlin/ru/mipt/npm/controls/opcua/client/MiloDevice.kt rename to controls-opcua/src/main/kotlin/space/kscience/controls/opcua/client/MiloDevice.kt index de56325..7172e22 100644 --- a/controls-opcua/src/main/kotlin/ru/mipt/npm/controls/opcua/client/MiloDevice.kt +++ b/controls-opcua/src/main/kotlin/space/kscience/controls/opcua/client/MiloDevice.kt @@ -1,11 +1,11 @@ -package ru.mipt.npm.controls.opcua.client +package space.kscience.controls.opcua.client import kotlinx.coroutines.future.await import kotlinx.serialization.json.Json import org.eclipse.milo.opcua.sdk.client.OpcUaClient import org.eclipse.milo.opcua.stack.core.types.builtin.* import org.eclipse.milo.opcua.stack.core.types.enumerated.TimestampsToReturn -import ru.mipt.npm.controls.api.Device +import space.kscience.controls.api.Device import space.kscience.dataforge.meta.Meta import space.kscience.dataforge.meta.MetaSerializer import space.kscience.dataforge.meta.transformations.MetaConverter @@ -39,8 +39,8 @@ public suspend inline fun MiloDevice.readOpcWithTime( val time = data.serverTime ?: error("No server time provided") val meta: Meta = when (val content = data.value.value) { is T -> return content to time - content is Meta -> content as Meta - content is ExtensionObject -> (content as ExtensionObject).decode(client.dynamicSerializationContext) as Meta + is Meta -> content + is ExtensionObject -> content.decode(client.dynamicSerializationContext) as Meta else -> error("Incompatible OPC property value $content") } diff --git a/controls-opcua/src/main/kotlin/ru/mipt/npm/controls/opcua/client/MiloDeviceBySpec.kt b/controls-opcua/src/main/kotlin/space/kscience/controls/opcua/client/MiloDeviceBySpec.kt similarity index 93% rename from controls-opcua/src/main/kotlin/ru/mipt/npm/controls/opcua/client/MiloDeviceBySpec.kt rename to controls-opcua/src/main/kotlin/space/kscience/controls/opcua/client/MiloDeviceBySpec.kt index 351115c..cae0f5e 100644 --- a/controls-opcua/src/main/kotlin/ru/mipt/npm/controls/opcua/client/MiloDeviceBySpec.kt +++ b/controls-opcua/src/main/kotlin/space/kscience/controls/opcua/client/MiloDeviceBySpec.kt @@ -1,11 +1,11 @@ -package ru.mipt.npm.controls.opcua.client +package space.kscience.controls.opcua.client import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import org.eclipse.milo.opcua.sdk.client.OpcUaClient import org.eclipse.milo.opcua.stack.core.types.builtin.NodeId -import ru.mipt.npm.controls.properties.DeviceBySpec -import ru.mipt.npm.controls.properties.DeviceSpec +import space.kscience.controls.spec.DeviceBySpec +import space.kscience.controls.spec.DeviceSpec import space.kscience.dataforge.context.Context import space.kscience.dataforge.context.Global import space.kscience.dataforge.meta.Meta diff --git a/controls-opcua/src/main/kotlin/ru/mipt/npm/controls/opcua/client/miloClient.kt b/controls-opcua/src/main/kotlin/space/kscience/controls/opcua/client/miloClient.kt similarity index 98% rename from controls-opcua/src/main/kotlin/ru/mipt/npm/controls/opcua/client/miloClient.kt rename to controls-opcua/src/main/kotlin/space/kscience/controls/opcua/client/miloClient.kt index 2d489d6..8415b3a 100644 --- a/controls-opcua/src/main/kotlin/ru/mipt/npm/controls/opcua/client/miloClient.kt +++ b/controls-opcua/src/main/kotlin/space/kscience/controls/opcua/client/miloClient.kt @@ -1,4 +1,4 @@ -package ru.mipt.npm.controls.opcua.client +package space.kscience.controls.opcua.client import org.eclipse.milo.opcua.sdk.client.OpcUaClient import org.eclipse.milo.opcua.sdk.client.api.config.OpcUaClientConfigBuilder diff --git a/controls-opcua/src/main/kotlin/ru/mipt/npm/controls/opcua/server/DeviceNameSpace.kt b/controls-opcua/src/main/kotlin/space/kscience/controls/opcua/server/DeviceNameSpace.kt similarity index 94% rename from controls-opcua/src/main/kotlin/ru/mipt/npm/controls/opcua/server/DeviceNameSpace.kt rename to controls-opcua/src/main/kotlin/space/kscience/controls/opcua/server/DeviceNameSpace.kt index e187f5e..6b8e44c 100644 --- a/controls-opcua/src/main/kotlin/ru/mipt/npm/controls/opcua/server/DeviceNameSpace.kt +++ b/controls-opcua/src/main/kotlin/space/kscience/controls/opcua/server/DeviceNameSpace.kt @@ -1,4 +1,4 @@ -package ru.mipt.npm.controls.opcua.server +package space.kscience.controls.opcua.server import kotlinx.coroutines.launch import kotlinx.datetime.toJavaInstant @@ -18,17 +18,17 @@ import org.eclipse.milo.opcua.stack.core.AttributeId import org.eclipse.milo.opcua.stack.core.Identifiers import org.eclipse.milo.opcua.stack.core.types.builtin.DateTime import org.eclipse.milo.opcua.stack.core.types.builtin.LocalizedText -import ru.mipt.npm.controls.api.Device -import ru.mipt.npm.controls.api.DeviceHub -import ru.mipt.npm.controls.api.PropertyDescriptor -import ru.mipt.npm.controls.api.onPropertyChange -import ru.mipt.npm.controls.controllers.DeviceManager +import space.kscience.controls.api.Device +import space.kscience.controls.api.DeviceHub +import space.kscience.controls.api.PropertyDescriptor +import space.kscience.controls.api.onPropertyChange +import space.kscience.controls.manager.DeviceManager import space.kscience.dataforge.meta.Meta import space.kscience.dataforge.meta.MetaSerializer +import space.kscience.dataforge.meta.ValueType import space.kscience.dataforge.names.Name import space.kscience.dataforge.names.asName import space.kscience.dataforge.names.plus -import space.kscience.dataforge.values.ValueType public operator fun Device.get(propertyDescriptor: PropertyDescriptor): Meta? = getProperty(propertyDescriptor.name) @@ -208,5 +208,8 @@ public class DeviceNameSpace( } } +/** + * Serve devices from [deviceManager] as OPC-UA + */ public fun OpcUaServer.serveDevices(deviceManager: DeviceManager): DeviceNameSpace = DeviceNameSpace(this, deviceManager).apply { startup() } \ No newline at end of file diff --git a/controls-opcua/src/main/kotlin/ru/mipt/npm/controls/opcua/server/metaToOpc.kt b/controls-opcua/src/main/kotlin/space/kscience/controls/opcua/server/metaToOpc.kt similarity index 84% rename from controls-opcua/src/main/kotlin/ru/mipt/npm/controls/opcua/server/metaToOpc.kt rename to controls-opcua/src/main/kotlin/space/kscience/controls/opcua/server/metaToOpc.kt index cbcd2ec..20e85e9 100644 --- a/controls-opcua/src/main/kotlin/ru/mipt/npm/controls/opcua/server/metaToOpc.kt +++ b/controls-opcua/src/main/kotlin/space/kscience/controls/opcua/server/metaToOpc.kt @@ -1,14 +1,11 @@ -package ru.mipt.npm.controls.opcua.server +package space.kscience.controls.opcua.server import kotlinx.serialization.json.Json import org.eclipse.milo.opcua.stack.core.types.builtin.DataValue import org.eclipse.milo.opcua.stack.core.types.builtin.DateTime import org.eclipse.milo.opcua.stack.core.types.builtin.StatusCode import org.eclipse.milo.opcua.stack.core.types.builtin.Variant -import space.kscience.dataforge.meta.Meta -import space.kscience.dataforge.meta.MetaSerializer -import space.kscience.dataforge.meta.isLeaf -import space.kscience.dataforge.values.* +import space.kscience.dataforge.meta.* import java.time.Instant /** diff --git a/controls-opcua/src/main/kotlin/ru/mipt/npm/controls/opcua/server/nodeUtils.kt b/controls-opcua/src/main/kotlin/space/kscience/controls/opcua/server/nodeUtils.kt similarity index 98% rename from controls-opcua/src/main/kotlin/ru/mipt/npm/controls/opcua/server/nodeUtils.kt rename to controls-opcua/src/main/kotlin/space/kscience/controls/opcua/server/nodeUtils.kt index 783c229..26e0dfe 100644 --- a/controls-opcua/src/main/kotlin/ru/mipt/npm/controls/opcua/server/nodeUtils.kt +++ b/controls-opcua/src/main/kotlin/space/kscience/controls/opcua/server/nodeUtils.kt @@ -1,4 +1,4 @@ -package ru.mipt.npm.controls.opcua.server +package space.kscience.controls.opcua.server import org.eclipse.milo.opcua.sdk.core.AccessLevel import org.eclipse.milo.opcua.sdk.core.Reference diff --git a/controls-opcua/src/main/kotlin/ru/mipt/npm/controls/opcua/server/serverUtils.kt b/controls-opcua/src/main/kotlin/space/kscience/controls/opcua/server/serverUtils.kt similarity index 96% rename from controls-opcua/src/main/kotlin/ru/mipt/npm/controls/opcua/server/serverUtils.kt rename to controls-opcua/src/main/kotlin/space/kscience/controls/opcua/server/serverUtils.kt index 6ba3d36..083d953 100644 --- a/controls-opcua/src/main/kotlin/ru/mipt/npm/controls/opcua/server/serverUtils.kt +++ b/controls-opcua/src/main/kotlin/space/kscience/controls/opcua/server/serverUtils.kt @@ -1,4 +1,4 @@ -package ru.mipt.npm.controls.opcua.server +package space.kscience.controls.opcua.server import org.eclipse.milo.opcua.sdk.server.OpcUaServer import org.eclipse.milo.opcua.sdk.server.api.config.OpcUaServerConfig diff --git a/controls-serial/build.gradle.kts b/controls-serial/build.gradle.kts index 733348c..b28a210 100644 --- a/controls-serial/build.gradle.kts +++ b/controls-serial/build.gradle.kts @@ -1,5 +1,5 @@ plugins { - id("ru.mipt.npm.gradle.jvm") + id("space.kscience.gradle.jvm") `maven-publish` } diff --git a/controls-serial/src/main/kotlin/ru/mipt/npm/controls/serial/SerialPort.kt b/controls-serial/src/main/kotlin/space/kscience/controls/serial/SerialPort.kt similarity index 89% rename from controls-serial/src/main/kotlin/ru/mipt/npm/controls/serial/SerialPort.kt rename to controls-serial/src/main/kotlin/space/kscience/controls/serial/SerialPort.kt index 1c9ad27..05d4b64 100644 --- a/controls-serial/src/main/kotlin/ru/mipt/npm/controls/serial/SerialPort.kt +++ b/controls-serial/src/main/kotlin/space/kscience/controls/serial/SerialPort.kt @@ -1,10 +1,10 @@ -package ru.mipt.npm.controls.serial +package space.kscience.controls.serial import jssc.SerialPort.* import jssc.SerialPortEventListener -import ru.mipt.npm.controls.ports.AbstractPort -import ru.mipt.npm.controls.ports.Port -import ru.mipt.npm.controls.ports.PortFactory +import space.kscience.controls.ports.AbstractPort +import space.kscience.controls.ports.Port +import space.kscience.controls.ports.PortFactory import space.kscience.dataforge.context.Context import space.kscience.dataforge.meta.Meta import space.kscience.dataforge.meta.int @@ -58,6 +58,9 @@ public class SerialPort private constructor( public companion object : PortFactory { + override val type: String = "com" + + /** * Construct ComPort with given parameters */ @@ -77,7 +80,7 @@ public class SerialPort private constructor( return SerialPort(context, jssc, coroutineContext) } - override fun invoke(meta: Meta, context: Context): Port { + override fun build(context: Context, meta: Meta): Port { val name by meta.string { error("Serial port name not defined") } val baudRate by meta.int(BAUDRATE_9600) val dataBits by meta.int(DATABITS_8) diff --git a/controls-serial/src/main/kotlin/space/kscience/controls/serial/SerialPortPlugin.kt b/controls-serial/src/main/kotlin/space/kscience/controls/serial/SerialPortPlugin.kt new file mode 100644 index 0000000..871d266 --- /dev/null +++ b/controls-serial/src/main/kotlin/space/kscience/controls/serial/SerialPortPlugin.kt @@ -0,0 +1,31 @@ +package space.kscience.controls.serial + +import space.kscience.controls.ports.PortFactory +import space.kscience.dataforge.context.AbstractPlugin +import space.kscience.dataforge.context.Context +import space.kscience.dataforge.context.PluginFactory +import space.kscience.dataforge.context.PluginTag +import space.kscience.dataforge.meta.Meta +import space.kscience.dataforge.names.Name +import kotlin.reflect.KClass + +public class SerialPortPlugin : AbstractPlugin() { + + override val tag: PluginTag get() = Companion.tag + + override fun content(target: String): Map = when(target){ + PortFactory.TYPE -> mapOf(Name.EMPTY to SerialPort) + else -> emptyMap() + } + + public companion object : PluginFactory { + + override val tag: PluginTag = PluginTag("controls.ports.serial", group = PluginTag.DATAFORGE_GROUP) + + override val type: KClass = SerialPortPlugin::class + + override fun build(context: Context, meta: Meta): SerialPortPlugin = SerialPortPlugin() + + } + +} \ No newline at end of file diff --git a/controls-server/build.gradle.kts b/controls-server/build.gradle.kts index 7eba824..0553b72 100644 --- a/controls-server/build.gradle.kts +++ b/controls-server/build.gradle.kts @@ -1,5 +1,5 @@ plugins { - id("ru.mipt.npm.gradle.jvm") + id("space.kscience.gradle.jvm") `maven-publish` } @@ -12,10 +12,12 @@ val ktorVersion: String by rootProject.extra dependencies { implementation(project(":controls-core")) - implementation(project(":controls-tcp")) + implementation(project(":controls-ktor-tcp")) implementation(projects.magix.magixServer) implementation("io.ktor:ktor-server-cio:$ktorVersion") - implementation("io.ktor:ktor-websockets:$ktorVersion") - implementation("io.ktor:ktor-serialization:$ktorVersion") - implementation("io.ktor:ktor-html-builder:$ktorVersion") + implementation("io.ktor:ktor-server-websockets:$ktorVersion") + implementation("io.ktor:ktor-server-content-negotiation:$ktorVersion") + implementation("io.ktor:ktor-serialization-kotlinx-json:$ktorVersion") + implementation("io.ktor:ktor-server-html-builder:$ktorVersion") + implementation("io.ktor:ktor-server-status-pages:$ktorVersion") } \ No newline at end of file diff --git a/controls-server/src/main/kotlin/ru/mipt/npm/controls/server/deviceWebServer.kt b/controls-server/src/main/kotlin/space/kscience/controls/server/deviceWebServer.kt similarity index 77% rename from controls-server/src/main/kotlin/ru/mipt/npm/controls/server/deviceWebServer.kt rename to controls-server/src/main/kotlin/space/kscience/controls/server/deviceWebServer.kt index f896a24..65ffb07 100644 --- a/controls-server/src/main/kotlin/ru/mipt/npm/controls/server/deviceWebServer.kt +++ b/controls-server/src/main/kotlin/space/kscience/controls/server/deviceWebServer.kt @@ -1,24 +1,23 @@ -package ru.mipt.npm.controls.server +package space.kscience.controls.server -import io.ktor.application.* -import io.ktor.features.CORS -import io.ktor.features.StatusPages -import io.ktor.html.respondHtml import io.ktor.http.HttpStatusCode -import io.ktor.request.receiveText -import io.ktor.response.respond -import io.ktor.response.respondRedirect -import io.ktor.response.respondText -import io.ktor.routing.get -import io.ktor.routing.post -import io.ktor.routing.route -import io.ktor.routing.routing +import io.ktor.server.application.* import io.ktor.server.cio.CIO import io.ktor.server.engine.ApplicationEngine import io.ktor.server.engine.embeddedServer -import io.ktor.util.getValue -import io.ktor.websocket.WebSockets +import io.ktor.server.html.respondHtml +import io.ktor.server.plugins.statuspages.StatusPages +import io.ktor.server.request.receiveText +import io.ktor.server.response.respond +import io.ktor.server.response.respondRedirect +import io.ktor.server.response.respondText +import io.ktor.server.routing.get +import io.ktor.server.routing.post +import io.ktor.server.routing.route +import io.ktor.server.routing.routing +import io.ktor.server.util.getValue +import io.ktor.server.websocket.WebSockets import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.html.* @@ -27,19 +26,39 @@ import kotlinx.serialization.json.Json import kotlinx.serialization.json.buildJsonArray import kotlinx.serialization.json.encodeToJsonElement import kotlinx.serialization.json.put -import ru.mipt.npm.controls.api.DeviceMessage -import ru.mipt.npm.controls.api.PropertyGetMessage -import ru.mipt.npm.controls.api.PropertySetMessage -import ru.mipt.npm.controls.api.getOrNull -import ru.mipt.npm.controls.controllers.DeviceManager -import ru.mipt.npm.controls.controllers.respondHubMessage -import ru.mipt.npm.magix.api.MagixEndpoint -import ru.mipt.npm.magix.server.GenericMagixMessage -import ru.mipt.npm.magix.server.launchMagixServerRawRSocket -import ru.mipt.npm.magix.server.magixModule +import space.kscience.controls.api.DeviceMessage +import space.kscience.controls.api.PropertyGetMessage +import space.kscience.controls.api.PropertySetMessage +import space.kscience.controls.api.getOrNull +import space.kscience.controls.manager.DeviceManager +import space.kscience.controls.manager.respondHubMessage import space.kscience.dataforge.meta.toMeta import space.kscience.dataforge.names.Name import space.kscience.dataforge.names.asName +import space.kscience.magix.api.MagixEndpoint +import space.kscience.magix.api.MagixFlowPlugin +import space.kscience.magix.api.MagixMessage +import space.kscience.magix.server.magixModule + + + +private fun Application.deviceServerModule(manager: DeviceManager) { + install(WebSockets) +// install(CORS) { +// anyHost() +// } + install(StatusPages) { + exception { call, cause -> + call.respond(HttpStatusCode.BadRequest, cause.message ?: "") + } + } + deviceManagerModule(manager) + routing { + get("/") { + call.respondRedirect("/dashboard") + } + } +} /** * Create and start a web server for several devices @@ -48,26 +67,7 @@ public fun CoroutineScope.startDeviceServer( manager: DeviceManager, port: Int = MagixEndpoint.DEFAULT_MAGIX_HTTP_PORT, host: String = "localhost", -): ApplicationEngine { - - return this.embeddedServer(CIO, port, host) { - install(WebSockets) - install(CORS) { - anyHost() - } - install(StatusPages) { - exception { cause -> - call.respond(HttpStatusCode.BadRequest, cause.message ?: "") - } - } - deviceManagerModule(manager) - routing { - get("/") { - call.respondRedirect("/dashboard") - } - } - }.start() -} +): ApplicationEngine = embeddedServer(CIO, port, host, module = { deviceServerModule(manager) }).start() public fun ApplicationEngine.whenStarted(callback: Application.() -> Unit) { environment.monitor.subscribe(ApplicationStarted, callback) @@ -78,20 +78,20 @@ public val WEB_SERVER_TARGET: Name = "@webServer".asName() public fun Application.deviceManagerModule( manager: DeviceManager, + vararg plugins: MagixFlowPlugin, deviceNames: Collection = manager.devices.keys.map { it.toString() }, route: String = "/", - rawSocketPort: Int = MagixEndpoint.DEFAULT_MAGIX_RAW_PORT, buffer: Int = 100, ) { - if (featureOrNull(WebSockets) == null) { + if (pluginOrNull(WebSockets) == null) { install(WebSockets) } - if (featureOrNull(CORS) == null) { - install(CORS) { - anyHost() - } - } +// if (pluginOrNull(CORS) == null) { +// install(CORS) { +// anyHost() +// } +// } routing { route(route) { @@ -213,11 +213,13 @@ public fun Application.deviceManagerModule( } } - val magixFlow = MutableSharedFlow( + val magixFlow = MutableSharedFlow( buffer, extraBufferCapacity = buffer ) - launchMagixServerRawRSocket(magixFlow, rawSocketPort) + plugins.forEach { + it.start(this, magixFlow) + } magixModule(magixFlow) } \ No newline at end of file diff --git a/controls-server/src/main/kotlin/ru/mipt/npm/controls/server/responses.kt b/controls-server/src/main/kotlin/space/kscience/controls/server/responses.kt similarity index 79% rename from controls-server/src/main/kotlin/ru/mipt/npm/controls/server/responses.kt rename to controls-server/src/main/kotlin/space/kscience/controls/server/responses.kt index 8041acb..93d11c4 100644 --- a/controls-server/src/main/kotlin/ru/mipt/npm/controls/server/responses.kt +++ b/controls-server/src/main/kotlin/space/kscience/controls/server/responses.kt @@ -1,12 +1,12 @@ -package ru.mipt.npm.controls.server +package space.kscience.controls.server -import io.ktor.application.ApplicationCall import io.ktor.http.ContentType -import io.ktor.response.respondText +import io.ktor.server.application.ApplicationCall +import io.ktor.server.response.respondText import kotlinx.serialization.json.JsonObjectBuilder import kotlinx.serialization.json.buildJsonObject -import ru.mipt.npm.controls.api.DeviceMessage -import ru.mipt.npm.magix.api.MagixEndpoint +import space.kscience.controls.api.DeviceMessage +import space.kscience.magix.api.MagixEndpoint //internal fun Frame.toEnvelope(): Envelope { diff --git a/controls-storage/README.md b/controls-storage/README.md new file mode 100644 index 0000000..14289a1 --- /dev/null +++ b/controls-storage/README.md @@ -0,0 +1,12 @@ +# Description + +This module provides API to store [DeviceMessages](/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/api/DeviceMessage.kt) +from certain [DeviceManager](/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/controllers/DeviceManager.kt) +or [MagixMessages](magix/magix-api/src/commonMain/kotlin/ru/mipt/npm/magix/api/MagixMessage.kt) +from certain [magix server](/magix/magix-server/src/main/kotlin/ru/mipt/npm/magix/server/server.kt). + +# Usage + +All usage examples can be found in [VirtualCarController](/demo/car/src/main/kotlin/ru/mipt/npm/controls/demo/car/VirtualCarController.kt). + +For more details, you can see comments in source code of this module. diff --git a/controls-storage/build.gradle.kts b/controls-storage/build.gradle.kts new file mode 100644 index 0000000..6bf1aa2 --- /dev/null +++ b/controls-storage/build.gradle.kts @@ -0,0 +1,23 @@ +plugins { + id("space.kscience.gradle.mpp") + `maven-publish` +} + +val dataforgeVersion: String by rootProject.extra + +kscience{ + jvm() + js() + dependencies { + api(projects.controlsCore) + } + dependencies(jvmMain){ + api(projects.magix.magixApi) + api(projects.controlsMagixClient) + api(projects.magix.magixServer) + } +} + +readme{ + maturity = space.kscience.gradle.Maturity.PROTOTYPE +} diff --git a/controls-storage/controls-xodus/build.gradle.kts b/controls-storage/controls-xodus/build.gradle.kts new file mode 100644 index 0000000..b745b0b --- /dev/null +++ b/controls-storage/controls-xodus/build.gradle.kts @@ -0,0 +1,19 @@ +plugins { + id("space.kscience.gradle.jvm") + `maven-publish` +} + +val xodusVersion: String by rootProject.extra + +dependencies { + api(projects.controlsStorage) + implementation("org.jetbrains.xodus:xodus-entity-store:$xodusVersion") +// implementation("org.jetbrains.xodus:xodus-environment:$xodusVersion") +// implementation("org.jetbrains.xodus:xodus-vfs:$xodusVersion") + + testImplementation(npmlibs.kotlinx.coroutines.test) +} + +readme{ + maturity = space.kscience.gradle.Maturity.PROTOTYPE +} diff --git a/controls-storage/controls-xodus/src/main/kotlin/space/kscience/controls/xodus/XodusDeviceMessageStorage.kt b/controls-storage/controls-xodus/src/main/kotlin/space/kscience/controls/xodus/XodusDeviceMessageStorage.kt new file mode 100644 index 0000000..72cdd1f --- /dev/null +++ b/controls-storage/controls-xodus/src/main/kotlin/space/kscience/controls/xodus/XodusDeviceMessageStorage.kt @@ -0,0 +1,139 @@ +package space.kscience.controls.xodus + +import jetbrains.exodus.entitystore.Entity +import jetbrains.exodus.entitystore.PersistentEntityStore +import jetbrains.exodus.entitystore.PersistentEntityStores +import jetbrains.exodus.entitystore.StoreTransaction +import kotlinx.datetime.Instant +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.descriptors.serialDescriptor +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive +import space.kscience.controls.api.DeviceMessage +import space.kscience.controls.storage.DeviceMessageStorage +import space.kscience.controls.storage.workDirectory +import space.kscience.dataforge.context.Context +import space.kscience.dataforge.context.Factory +import space.kscience.dataforge.context.fetch +import space.kscience.dataforge.io.IOPlugin +import space.kscience.dataforge.meta.Meta +import space.kscience.dataforge.meta.get +import space.kscience.dataforge.meta.string +import space.kscience.dataforge.misc.DFExperimental +import space.kscience.dataforge.names.Name +import space.kscience.dataforge.names.matches +import space.kscience.dataforge.names.parseAsName + + +internal fun StoreTransaction.writeMessage(message: DeviceMessage): Unit { + val entity: Entity = newEntity(XodusDeviceMessageStorage.DEVICE_MESSAGE_ENTITY_TYPE) + val json = Json.encodeToJsonElement(DeviceMessage.serializer(), message).jsonObject + val type = json["type"]?.jsonPrimitive?.content ?: error("Message json representation must have type.") + entity.setProperty("type", type) + + message.sourceDevice?.let { + entity.setProperty(DeviceMessage::sourceDevice.name, it.toString()) + } + message.targetDevice?.let { + entity.setProperty(DeviceMessage::targetDevice.name, it.toString()) + } + message.time?.let { + entity.setProperty(DeviceMessage::targetDevice.name, it.toString()) + } + entity.setBlobString("json", Json.encodeToString(json)) +} + + +@OptIn(DFExperimental::class) +private fun Entity.propertyMatchesName(propertyName: String, pattern: Name? = null) = + pattern == null || getProperty(propertyName).toString().parseAsName().matches(pattern) + +private fun Entity.timeInRange(range: ClosedRange?): Boolean { + if (range == null) return true + val time: Instant? = getProperty(DeviceMessage::time.name)?.let { entityString -> + Instant.parse(entityString.toString()) + } + return time != null && time in range +} + +public class XodusDeviceMessageStorage( + private val entityStore: PersistentEntityStore, +) : DeviceMessageStorage, AutoCloseable { + + override suspend fun write(event: DeviceMessage) { + entityStore.executeInTransaction { txn -> + txn.writeMessage(event) + } + } + + override suspend fun readAll(): List = entityStore.computeInReadonlyTransaction { transaction -> + transaction.sort( + DEVICE_MESSAGE_ENTITY_TYPE, + DeviceMessage::time.name, + true + ).map { + Json.decodeFromString( + DeviceMessage.serializer(), + it.getBlobString("json") ?: error("No json content found") + ) + } + } + + override suspend fun read( + eventType: String, + range: ClosedRange?, + sourceDevice: Name?, + targetDevice: Name?, + ): List = entityStore.computeInReadonlyTransaction { transaction -> + transaction.find( + DEVICE_MESSAGE_ENTITY_TYPE, + "type", + eventType + ).asSequence().filter { + it.timeInRange(range) && + it.propertyMatchesName(DeviceMessage::sourceDevice.name, sourceDevice) && + it.propertyMatchesName(DeviceMessage::targetDevice.name, targetDevice) + }.map { + Json.decodeFromString( + DeviceMessage.serializer(), + it.getBlobString("json") ?: error("No json content found") + ) + }.sortedBy { it.time }.toList() + } + + override fun close() { + entityStore.close() + } + + public companion object : Factory { + internal const val DEVICE_MESSAGE_ENTITY_TYPE = "controls-kt.message" + public val XODUS_STORE_PROPERTY: Name = Name.of("xodus", "storagePath") + + override fun build(context: Context, meta: Meta): XodusDeviceMessageStorage { + val io = context.fetch(IOPlugin) + val storePath = io.workDirectory.resolve( + meta[XODUS_STORE_PROPERTY]?.string + ?: context.properties[XODUS_STORE_PROPERTY]?.string ?: "storage" + ) + + val entityStore = PersistentEntityStores.newInstance(storePath.toFile()) + + return XodusDeviceMessageStorage(entityStore) + } + } +} + +/** + * Query all messages of given type + */ +@OptIn(ExperimentalSerializationApi::class) +public suspend inline fun XodusDeviceMessageStorage.query( + range: ClosedRange? = null, + sourceDevice: Name? = null, + targetDevice: Name? = null, +): List = read(serialDescriptor().serialName, range, sourceDevice, targetDevice).map { + //Check that all types are correct + it as T +} diff --git a/controls-storage/controls-xodus/src/test/kotlin/PropertyHistoryTest.kt b/controls-storage/controls-xodus/src/test/kotlin/PropertyHistoryTest.kt new file mode 100644 index 0000000..1724079 --- /dev/null +++ b/controls-storage/controls-xodus/src/test/kotlin/PropertyHistoryTest.kt @@ -0,0 +1,77 @@ +import jetbrains.exodus.entitystore.PersistentEntityStores +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import kotlinx.datetime.Instant +import org.junit.jupiter.api.AfterAll +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.Test +import space.kscience.controls.api.PropertyChangedMessage +import space.kscience.controls.xodus.XodusDeviceMessageStorage +import space.kscience.controls.xodus.query +import space.kscience.controls.xodus.writeMessage +import space.kscience.dataforge.meta.Meta +import space.kscience.dataforge.names.Name +import space.kscience.dataforge.names.asName +import java.nio.file.Files + +internal class PropertyHistoryTest { + companion object { + val storeFile = Files.createTempDirectory("controls-xodus").toFile() + + + private val propertyChangedMessages = listOf( + PropertyChangedMessage( + "speed", + Meta.EMPTY, + time = Instant.fromEpochMilliseconds(1000), + sourceDevice = Name.of("virtual-car") + ), + PropertyChangedMessage( + "acceleration", + Meta.EMPTY, + time = Instant.fromEpochMilliseconds(1500), + sourceDevice = Name.of("virtual-car") + ), + PropertyChangedMessage( + "speed", + Meta.EMPTY, + time = Instant.fromEpochMilliseconds(2000), + sourceDevice = Name.of("magix-virtual-car") + ) + ) + + @BeforeAll + @JvmStatic + fun createEntities() { + PersistentEntityStores.newInstance(storeFile).use { + it.executeInTransaction { transaction -> + propertyChangedMessages.forEach { message -> + transaction.writeMessage(message) + } + } + } + } + + @AfterAll + @JvmStatic + fun deleteDatabase() { + storeFile.deleteRecursively() + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun getPropertyHistoryTest() = runTest { + PersistentEntityStores.newInstance(storeFile).use { entityStore -> + XodusDeviceMessageStorage(entityStore).use { storage -> + assertEquals( + propertyChangedMessages[0], + storage.query( + sourceDevice = "virtual-car".asName() + ).first { it.property == "speed" } + ) + } + } + } +} \ No newline at end of file diff --git a/controls-storage/src/commonMain/kotlin/space/kscience/controls/storage/DeviceMessageStorage.kt b/controls-storage/src/commonMain/kotlin/space/kscience/controls/storage/DeviceMessageStorage.kt new file mode 100644 index 0000000..87f4b74 --- /dev/null +++ b/controls-storage/src/commonMain/kotlin/space/kscience/controls/storage/DeviceMessageStorage.kt @@ -0,0 +1,23 @@ +package space.kscience.controls.storage + +import kotlinx.datetime.Instant +import space.kscience.controls.api.DeviceMessage +import space.kscience.dataforge.names.Name + +/** + * A storage for Controls-kt [DeviceMessage] + */ +public interface DeviceMessageStorage { + public suspend fun write(event: DeviceMessage) + + public suspend fun readAll(): List + + public suspend fun read( + eventType: String, + range: ClosedRange? = null, + sourceDevice: Name? = null, + targetDevice: Name? = null, + ): List + + public fun close() +} \ No newline at end of file diff --git a/controls-storage/src/commonMain/kotlin/space/kscience/controls/storage/storageCommon.kt b/controls-storage/src/commonMain/kotlin/space/kscience/controls/storage/storageCommon.kt new file mode 100644 index 0000000..2a453cf --- /dev/null +++ b/controls-storage/src/commonMain/kotlin/space/kscience/controls/storage/storageCommon.kt @@ -0,0 +1,64 @@ +package space.kscience.controls.storage + +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onCompletion +import kotlinx.coroutines.flow.onEach +import space.kscience.controls.api.DeviceMessage +import space.kscience.controls.manager.DeviceManager +import space.kscience.controls.manager.hubMessageFlow +import space.kscience.dataforge.context.Factory +import space.kscience.dataforge.context.debug +import space.kscience.dataforge.context.logger + +//TODO replace by plugin? +public fun DeviceManager.storage( + factory: Factory, +): DeviceMessageStorage = factory.build(context, meta) + +/** + * Begin to store DeviceMessages from this DeviceManager + * @param factory factory that will be used for creating persistent entity store instance. DefaultPersistentStoreFactory by default. + * DeviceManager's meta and context will be used for in invoke method. + * @param filterCondition allow you to specify messages which we want to store. Always true by default. + * @return Job which responsible for our storage + */ +public fun DeviceManager.storeMessages( + factory: Factory, + filterCondition: suspend (DeviceMessage) -> Boolean = { true }, +): Job { + val storage = factory.build(context, meta) + logger.debug { "Message storage with meta = $meta created" } + + return hubMessageFlow(context).filter(filterCondition).onEach { message -> + storage.write(message) + }.onCompletion { + storage.close() + logger.debug { "Message storage closed" } + }.launchIn(context) +} + +///** +// * @return the list of deviceMessages that describes changes of specified property of specified device sorted by time +// * @param sourceDeviceName a name of device, history of which property we want to get +// * @param propertyName a name of property, history of which we want to get +// * @param factory a factory that produce mongo clients +// */ +//public suspend fun getPropertyHistory( +// sourceDeviceName: String, +// propertyName: String, +// factory: Factory, +// meta: Meta = Meta.EMPTY, +//): List { +// return factory(meta).use { +// it.getPropertyHistory(sourceDeviceName, propertyName) +// } +//} +// +// +//public enum class StorageKind { +// DEVICE_HUB, +// MAGIX_SERVER +//} + diff --git a/controls-storage/src/jvmMain/kotlin/space/kscience/controls/storage/storageJvm.kt b/controls-storage/src/jvmMain/kotlin/space/kscience/controls/storage/storageJvm.kt new file mode 100644 index 0000000..40811a4 --- /dev/null +++ b/controls-storage/src/jvmMain/kotlin/space/kscience/controls/storage/storageJvm.kt @@ -0,0 +1,46 @@ +//package space.kscience.controls.storage +// +//import io.ktor.server.application.Application +//import kotlinx.coroutines.InternalCoroutinesApi +//import kotlinx.coroutines.flow.Flow +//import kotlinx.coroutines.flow.MutableSharedFlow +//import kotlinx.coroutines.flow.filter +//import kotlinx.coroutines.flow.onEach +//import kotlinx.coroutines.job +//import ru.mipt.npm.magix.server.GenericMagixMessage +//import space.kscience.dataforge.context.Factory +//import space.kscience.dataforge.meta.Meta +// +///** +// * Asynchronous version of synchronous API, so for more details check relative docs +// */ +// +//internal fun Flow.store( +// client: EventStorage, +// flowFilter: suspend (GenericMagixMessage) -> Boolean = { true }, +//) { +// filter(flowFilter).onEach { message -> +// client.storeMagixMessage(message) +// } +//} +// +///** Begin to store MagixMessages from certain flow +// * @param flow flow of messages which we will store +// * @param meta Meta which may have some configuration parameters for our storage and will be used in invoke method of factory +// * @param factory factory that will be used for creating persistent entity store instance. DefaultPersistentStoreFactory by default. +// * @param flowFilter allow you to specify messages which we want to store. Always true by default. +// */ +//@OptIn(InternalCoroutinesApi::class) +//public fun Application.store( +// flow: MutableSharedFlow, +// factory: Factory, +// meta: Meta = Meta.EMPTY, +// flowFilter: suspend (GenericMagixMessage) -> Boolean = { true }, +//) { +// val client = factory(meta) +// +// flow.store(client, flowFilter) +// coroutineContext.job.invokeOnCompletion(onCancelling = true) { +// client.close() +// } +//} diff --git a/controls-storage/src/jvmMain/kotlin/space/kscience/controls/storage/workDirectory.kt b/controls-storage/src/jvmMain/kotlin/space/kscience/controls/storage/workDirectory.kt new file mode 100644 index 0000000..7e4086f --- /dev/null +++ b/controls-storage/src/jvmMain/kotlin/space/kscience/controls/storage/workDirectory.kt @@ -0,0 +1,32 @@ +package space.kscience.controls.storage + +import space.kscience.dataforge.context.ContextBuilder +import space.kscience.dataforge.io.IOPlugin +import space.kscience.dataforge.meta.get +import space.kscience.dataforge.meta.set +import space.kscience.dataforge.meta.string +import java.nio.file.Path +import kotlin.io.path.Path + +//TODO remove on DF 0.6 + +internal val IOPlugin.Companion.WORK_DIRECTORY_KEY: String get() = ".dataforge" + +public val IOPlugin.workDirectory: Path + get() { + val workDirectoryPath = meta[IOPlugin.WORK_DIRECTORY_KEY].string + ?: context.properties[IOPlugin.WORK_DIRECTORY_KEY].string + ?: ".dataforge" + + return Path(workDirectoryPath) + } + +public fun ContextBuilder.workDirectory(path: String) { + properties { + set(IOPlugin.WORK_DIRECTORY_KEY, path) + } +} + +public fun ContextBuilder.workDirectory(path: Path){ + workDirectory(path.toAbsolutePath().toString()) +} diff --git a/controls-tcp/build.gradle.kts b/controls-tcp/build.gradle.kts deleted file mode 100644 index 88d2928..0000000 --- a/controls-tcp/build.gradle.kts +++ /dev/null @@ -1,17 +0,0 @@ -plugins { - id("ru.mipt.npm.gradle.mpp") -} - -val ktorVersion: String by rootProject.extra - -kotlin { - sourceSets { - commonMain { - dependencies { - api(project(":controls-core")) - api("io.ktor:ktor-network:$ktorVersion") - } - } - - } -} \ No newline at end of file diff --git a/demo/build.gradle.kts b/demo/all-things/build.gradle.kts similarity index 60% rename from demo/build.gradle.kts rename to demo/all-things/build.gradle.kts index 0fb18d9..1f86f1c 100644 --- a/demo/build.gradle.kts +++ b/demo/all-things/build.gradle.kts @@ -1,21 +1,19 @@ plugins { kotlin("jvm") - id("org.openjfx.javafxplugin") version "0.0.9" + id("org.openjfx.javafxplugin") version "0.0.13" application } -repositories{ +repositories { mavenCentral() - jcenter() maven("https://repo.kotlin.link") - maven("https://kotlin.bintray.com/kotlinx") } val ktorVersion: String by rootProject.extra val rsocketVersion: String by rootProject.extra -dependencies{ +dependencies { implementation(projects.controlsCore) //implementation(projects.controlsServer) implementation(projects.magix.magixServer) @@ -26,23 +24,23 @@ dependencies{ implementation("io.ktor:ktor-client-cio:$ktorVersion") implementation("no.tornado:tornadofx:1.7.20") - implementation("space.kscience:plotlykt-server:0.5.0-dev-1") - implementation("com.github.Ricky12Awesome:json-schema-serialization:0.6.6") - implementation("ch.qos.logback:logback-classic:1.2.3") + implementation("space.kscience:plotlykt-server:0.5.3-dev-1") +// implementation("com.github.Ricky12Awesome:json-schema-serialization:0.6.6") + implementation("ch.qos.logback:logback-classic:1.2.11") } tasks.withType().configureEach { kotlinOptions { jvmTarget = "11" - freeCompilerArgs = freeCompilerArgs + "-Xjvm-default=all" + freeCompilerArgs = freeCompilerArgs + listOf("-Xjvm-default=all", "-Xopt-in=kotlin.RequiresOptIn") } } -javafx{ - version = "14" +javafx { + version = "17" modules("javafx.controls") } -application{ - mainClass.set("ru.mipt.npm.controls.demo.DemoControllerViewKt") +application { + mainClass.set("space.kscience.controls.demo.DemoControllerViewKt") } \ No newline at end of file diff --git a/demo/src/main/kotlin/ru/mipt/npm/controls/demo/DemoControllerView.kt b/demo/all-things/src/main/kotlin/space/kscience/controls/demo/DemoControllerView.kt similarity index 76% rename from demo/src/main/kotlin/ru/mipt/npm/controls/demo/DemoControllerView.kt rename to demo/all-things/src/main/kotlin/space/kscience/controls/demo/DemoControllerView.kt index 8a273ab..802fc4d 100644 --- a/demo/src/main/kotlin/ru/mipt/npm/controls/demo/DemoControllerView.kt +++ b/demo/all-things/src/main/kotlin/space/kscience/controls/demo/DemoControllerView.kt @@ -1,4 +1,4 @@ -package ru.mipt.npm.controls.demo +package space.kscience.controls.demo import io.ktor.server.engine.ApplicationEngine import javafx.scene.Parent @@ -8,21 +8,22 @@ import javafx.stage.Stage import kotlinx.coroutines.launch import org.eclipse.milo.opcua.sdk.server.OpcUaServer import org.eclipse.milo.opcua.stack.core.types.builtin.LocalizedText -import ru.mipt.npm.controls.api.DeviceMessage -import ru.mipt.npm.controls.client.connectToMagix -import ru.mipt.npm.controls.controllers.DeviceManager -import ru.mipt.npm.controls.controllers.install -import ru.mipt.npm.controls.demo.DemoDevice.Companion.cosScale -import ru.mipt.npm.controls.demo.DemoDevice.Companion.sinScale -import ru.mipt.npm.controls.demo.DemoDevice.Companion.timeScale -import ru.mipt.npm.controls.opcua.server.OpcUaServer -import ru.mipt.npm.controls.opcua.server.endpoint -import ru.mipt.npm.controls.opcua.server.serveDevices -import ru.mipt.npm.magix.api.MagixEndpoint -import ru.mipt.npm.magix.rsocket.rSocketWithTcp -import ru.mipt.npm.magix.rsocket.rSocketWithWebSockets -import ru.mipt.npm.magix.server.startMagixServer +import space.kscience.controls.client.connectToMagix +import space.kscience.controls.demo.DemoDevice.Companion.cosScale +import space.kscience.controls.demo.DemoDevice.Companion.sinScale +import space.kscience.controls.demo.DemoDevice.Companion.timeScale +import space.kscience.controls.manager.DeviceManager +import space.kscience.controls.manager.install +import space.kscience.controls.opcua.server.OpcUaServer +import space.kscience.controls.opcua.server.endpoint +import space.kscience.controls.opcua.server.serveDevices import space.kscience.dataforge.context.* +import space.kscience.magix.api.MagixEndpoint +import space.kscience.magix.rsocket.rSocketWithTcp +import space.kscience.magix.rsocket.rSocketWithWebSockets +import space.kscience.magix.server.RSocketMagixFlowPlugin +import space.kscience.magix.server.startMagixServer +import space.kscince.magix.zmq.ZmqMagixFlowPlugin import tornadofx.* import java.awt.Desktop import java.net.URI @@ -33,7 +34,7 @@ class DemoController : Controller(), ContextAware { var magixServer: ApplicationEngine? = null var visualizer: ApplicationEngine? = null var opcUaServer: OpcUaServer = OpcUaServer { - setApplicationName(LocalizedText.english("ru.mipt.npm.controls.opcua")) + setApplicationName(LocalizedText.english("space.kscience.controls.opcua")) endpoint { setBindPort(9999) //use default endpoint @@ -44,19 +45,24 @@ class DemoController : Controller(), ContextAware { plugin(DeviceManager) } - private val deviceManager = context.fetch(DeviceManager) + private val deviceManager = context.request(DeviceManager) fun init() { context.launch { device = deviceManager.install("demo", DemoDevice) //starting magix event loop - magixServer = startMagixServer(enableRawRSocket = true, enableZmq = true) + magixServer = startMagixServer( + RSocketMagixFlowPlugin(), //TCP rsocket support + ZmqMagixFlowPlugin() //ZMQ support + ) //Launch device client and connect it to the server - val deviceEndpoint = MagixEndpoint.rSocketWithTcp("localhost", DeviceMessage.serializer()) + val deviceEndpoint = MagixEndpoint.rSocketWithTcp("localhost") deviceManager.connectToMagix(deviceEndpoint) - val visualEndpoint = MagixEndpoint.rSocketWithWebSockets("localhost", DeviceMessage.serializer()) + //connect visualization to a magix endpoint + val visualEndpoint = MagixEndpoint.rSocketWithWebSockets("localhost") visualizer = visualEndpoint.startDemoDeviceServer() + //serve devices as OPC-UA namespace opcUaServer.startup() opcUaServer.serveDevices(deviceManager) } diff --git a/demo/all-things/src/main/kotlin/space/kscience/controls/demo/DemoDevice.kt b/demo/all-things/src/main/kotlin/space/kscience/controls/demo/DemoDevice.kt new file mode 100644 index 0000000..10713f3 --- /dev/null +++ b/demo/all-things/src/main/kotlin/space/kscience/controls/demo/DemoDevice.kt @@ -0,0 +1,84 @@ +package space.kscience.controls.demo + +import kotlinx.coroutines.launch +import space.kscience.controls.api.metaDescriptor +import space.kscience.controls.spec.* +import space.kscience.dataforge.context.Context +import space.kscience.dataforge.context.Factory +import space.kscience.dataforge.meta.Meta +import space.kscience.dataforge.meta.ValueType +import space.kscience.dataforge.meta.descriptors.value +import space.kscience.dataforge.meta.transformations.MetaConverter +import java.time.Instant +import kotlin.time.Duration.Companion.milliseconds + + +class DemoDevice(context: Context, meta: Meta) : DeviceBySpec(DemoDevice, context, meta) { + private var timeScaleState = 5000.0 + private var sinScaleState = 1.0 + private var cosScaleState = 1.0 + + + companion object : DeviceSpec(), Factory { + + override fun build(context: Context, meta: Meta): DemoDevice = DemoDevice(context, meta) + + // register virtual properties based on actual object state + val timeScale by mutableProperty(MetaConverter.double, DemoDevice::timeScaleState) { + metaDescriptor { + type(ValueType.NUMBER) + } + info = "Real to virtual time scale" + } + + val sinScale by mutableProperty(MetaConverter.double, DemoDevice::sinScaleState) + val cosScale by mutableProperty(MetaConverter.double, DemoDevice::cosScaleState) + + val sin by doubleProperty { + val time = Instant.now() + kotlin.math.sin(time.toEpochMilli().toDouble() / timeScaleState) * sinScaleState + } + + val cos by doubleProperty { + val time = Instant.now() + kotlin.math.cos(time.toEpochMilli().toDouble() / timeScaleState) * sinScaleState + } + + val coordinates by metaProperty( + descriptorBuilder = { + metaDescriptor { + value("time", ValueType.NUMBER) + } + } + ) { + Meta { + val time = Instant.now() + "time" put time.toEpochMilli() + "x" put read(sin) + "y" put read(cos) + } + } + + + override suspend fun DemoDevice.onOpen() { + launch { + sinScale.read() + cosScale.read() + timeScale.read() + } + doRecurring(50.milliseconds) { + sin.read() + cos.read() + coordinates.read() + } + } + + val resetScale by action(MetaConverter.meta, MetaConverter.meta) { + timeScale.write(5000.0) + sinScale.write(1.0) + cosScale.write(1.0) + null + } + + } +} \ No newline at end of file diff --git a/demo/src/main/kotlin/ru/mipt/npm/controls/demo/demoDeviceServer.kt b/demo/all-things/src/main/kotlin/space/kscience/controls/demo/demoDeviceServer.kt similarity index 75% rename from demo/src/main/kotlin/ru/mipt/npm/controls/demo/demoDeviceServer.kt rename to demo/all-things/src/main/kotlin/space/kscience/controls/demo/demoDeviceServer.kt index 8d6f95b..5b6c7cc 100644 --- a/demo/src/main/kotlin/ru/mipt/npm/controls/demo/demoDeviceServer.kt +++ b/demo/all-things/src/main/kotlin/space/kscience/controls/demo/demoDeviceServer.kt @@ -1,21 +1,22 @@ -package ru.mipt.npm.controls.demo +package space.kscience.controls.demo -import io.ktor.application.install -import io.ktor.features.CORS +import io.ktor.server.application.install import io.ktor.server.cio.CIO import io.ktor.server.engine.ApplicationEngine import io.ktor.server.engine.embeddedServer -import io.ktor.websocket.WebSockets -import io.rsocket.kotlin.transport.ktor.server.RSocketSupport +import io.ktor.server.plugins.cors.routing.CORS +import io.ktor.server.websocket.WebSockets +import io.rsocket.kotlin.ktor.server.RSocketSupport import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import kotlinx.html.div import kotlinx.html.link -import ru.mipt.npm.controls.api.DeviceMessage -import ru.mipt.npm.controls.api.PropertyChangedMessage -import ru.mipt.npm.magix.api.MagixEndpoint +import space.kscience.controls.api.PropertyChangedMessage +import space.kscience.controls.client.controlsMagixFormat import space.kscience.dataforge.meta.Meta import space.kscience.dataforge.meta.double +import space.kscience.magix.api.MagixEndpoint +import space.kscience.magix.api.subscribe import space.kscience.plotly.layout import space.kscience.plotly.models.Trace import space.kscience.plotly.plot @@ -54,33 +55,33 @@ suspend fun Trace.updateXYFrom(flow: Flow>>) { } -suspend fun MagixEndpoint.startDemoDeviceServer(): ApplicationEngine = - embeddedServer(CIO, 9090) { - install(WebSockets) - install(RSocketSupport) +@Suppress("ExtractKtorModule") +suspend fun MagixEndpoint.startDemoDeviceServer(): ApplicationEngine = embeddedServer(CIO, 9091) { + install(WebSockets) + install(RSocketSupport) - install(CORS) { - anyHost() - } + install(CORS) { + anyHost() + } - val sinFlow = MutableSharedFlow()// = device.sin.flow() - val cosFlow = MutableSharedFlow()// = device.cos.flow() + val sinFlow = MutableSharedFlow()// = device.sin.flow() + val cosFlow = MutableSharedFlow()// = device.cos.flow() - launch { - subscribe().collect { magix -> - (magix.payload as? PropertyChangedMessage)?.let { message -> - when (message.property) { - "sin" -> sinFlow.emit(message.value) - "cos" -> cosFlow.emit(message.value) - } + launch { + subscribe(controlsMagixFormat).collect { (_, payload) -> + (payload as? PropertyChangedMessage)?.let { message -> + when (message.property) { + "sin" -> sinFlow.emit(message.value) + "cos" -> cosFlow.emit(message.value) } } } + } - plotlyModule().apply { - updateMode = PlotlyUpdateMode.PUSH - updateInterval = 50 - }.page { container -> + plotlyModule{ + updateMode = PlotlyUpdateMode.PUSH + updateInterval = 50 + page { container -> val sinCosFlow = sinFlow.zip(cosFlow) { sin, cos -> sin.double!! to cos.double!! } @@ -140,6 +141,8 @@ suspend fun MagixEndpoint.startDemoDeviceServer(): ApplicationEng } } } - } - }.apply { start() } + + } + } +}.apply { start() } diff --git a/demo/all-things/src/main/kotlin/space/kscience/controls/demo/generateMessageSchema.kt b/demo/all-things/src/main/kotlin/space/kscience/controls/demo/generateMessageSchema.kt new file mode 100644 index 0000000..d50ec2c --- /dev/null +++ b/demo/all-things/src/main/kotlin/space/kscience/controls/demo/generateMessageSchema.kt @@ -0,0 +1,10 @@ +package space.kscience.controls.demo + +//import com.github.ricky12awesome.jss.encodeToSchema +//import com.github.ricky12awesome.jss.globalJson +//import space.kscience.controls.api.DeviceMessage + +//fun main() { +// val schema = globalJson.encodeToSchema(DeviceMessage.serializer(), generateDefinitions = false) +// println(schema) +//} \ No newline at end of file diff --git a/demo/car/build.gradle.kts b/demo/car/build.gradle.kts new file mode 100644 index 0000000..27e5776 --- /dev/null +++ b/demo/car/build.gradle.kts @@ -0,0 +1,52 @@ +plugins { + kotlin("jvm") + id("org.openjfx.javafxplugin") version "0.0.10" + application +} + + +repositories { + mavenCentral() + maven("https://repo.kotlin.link") +} + +val ktorVersion: String by rootProject.extra +val rsocketVersion: String by rootProject.extra + +dependencies { + implementation(projects.controlsCore) + implementation(projects.magix.magixApi) + implementation(projects.magix.magixServer) + implementation(projects.magix.magixRsocket) + implementation(projects.magix.magixZmq) + implementation(projects.controlsMagixClient) + implementation(projects.controlsStorage.controlsXodus) + implementation(projects.magix.magixStorage.magixStorageXodus) +// implementation(projects.controlsMongo) + + implementation("io.ktor:ktor-client-cio:$ktorVersion") + implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.3.1") + implementation("no.tornado:tornadofx:1.7.20") + implementation("space.kscience:plotlykt-server:0.5.0") + implementation("ch.qos.logback:logback-classic:1.2.11") + implementation("org.jetbrains.xodus:xodus-entity-store:1.3.232") + implementation("org.jetbrains.xodus:xodus-environment:1.3.232") + implementation("org.jetbrains.xodus:xodus-vfs:1.3.232") +// implementation("org.litote.kmongo:kmongo-coroutine-serialization:4.4.0") +} + +tasks.withType().configureEach { + kotlinOptions { + jvmTarget = "11" + freeCompilerArgs = freeCompilerArgs + listOf("-Xjvm-default=all", "-Xopt-in=kotlin.RequiresOptIn") + } +} + +javafx { + version = "14" + modules("javafx.controls") +} + +application { + mainClass.set("space.kscience.controls.demo.car.VirtualCarControllerKt") +} \ No newline at end of file diff --git a/demo/car/src/main/kotlin/space/kscience/controls/demo/car/IVirtualCar.kt b/demo/car/src/main/kotlin/space/kscience/controls/demo/car/IVirtualCar.kt new file mode 100644 index 0000000..3bb8c79 --- /dev/null +++ b/demo/car/src/main/kotlin/space/kscience/controls/demo/car/IVirtualCar.kt @@ -0,0 +1,27 @@ +package space.kscience.controls.demo.car + +import space.kscience.controls.api.Device +import space.kscience.controls.spec.DeviceSpec + +interface IVirtualCar : Device { + var speedState: Vector2D + var locationState: Vector2D + var accelerationState: Vector2D + + companion object : DeviceSpec() { + /** + * Read-only speed + */ + val speed by property(Vector2D, IVirtualCar::speedState) + + /** + * Read-only location + */ + val location by property(Vector2D, IVirtualCar::locationState) + + /** + * writable acceleration + */ + val acceleration by mutableProperty(Vector2D, IVirtualCar::accelerationState) + } +} \ No newline at end of file diff --git a/demo/car/src/main/kotlin/space/kscience/controls/demo/car/MagixVirtualCar.kt b/demo/car/src/main/kotlin/space/kscience/controls/demo/car/MagixVirtualCar.kt new file mode 100644 index 0000000..5cf3386 --- /dev/null +++ b/demo/car/src/main/kotlin/space/kscience/controls/demo/car/MagixVirtualCar.kt @@ -0,0 +1,48 @@ +package space.kscience.controls.demo.car + +import kotlinx.coroutines.launch +import space.kscience.controls.api.PropertyChangedMessage +import space.kscience.controls.client.controlsMagixFormat +import space.kscience.dataforge.context.Context +import space.kscience.dataforge.context.Factory +import space.kscience.dataforge.meta.Meta +import space.kscience.dataforge.meta.get +import space.kscience.dataforge.meta.string +import space.kscience.dataforge.names.Name +import space.kscience.magix.api.MagixEndpoint +import space.kscience.magix.api.subscribe +import space.kscience.magix.rsocket.rSocketWithWebSockets +import kotlin.time.ExperimentalTime + +class MagixVirtualCar(context: Context, meta: Meta) : VirtualCar(context, meta) { + + private fun MagixEndpoint.launchMagixVirtualCarUpdate() = launch { + subscribe(controlsMagixFormat).collect { (_, payload) -> + (payload as? PropertyChangedMessage)?.let { message -> + if (message.sourceDevice == Name.parse("virtual-car")) { + when (message.property) { + "acceleration" -> IVirtualCar.acceleration.write(Vector2D.metaToObject(message.value)) + } + } + } + } + } + + + @OptIn(ExperimentalTime::class) + override suspend fun open() { + super.open() + + val magixEndpoint = MagixEndpoint.rSocketWithWebSockets( + meta["magixServerHost"].string ?: "localhost", + ) + + launch { + magixEndpoint.launchMagixVirtualCarUpdate() + } + } + + companion object : Factory { + override fun build(context: Context, meta: Meta): MagixVirtualCar = MagixVirtualCar(context, meta) + } +} diff --git a/demo/car/src/main/kotlin/space/kscience/controls/demo/car/VirtualCar.kt b/demo/car/src/main/kotlin/space/kscience/controls/demo/car/VirtualCar.kt new file mode 100644 index 0000000..db0638c --- /dev/null +++ b/demo/car/src/main/kotlin/space/kscience/controls/demo/car/VirtualCar.kt @@ -0,0 +1,115 @@ +@file:OptIn(ExperimentalTime::class) + +package space.kscience.controls.demo.car + +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.datetime.Clock +import kotlinx.datetime.Instant +import space.kscience.controls.spec.DeviceBySpec +import space.kscience.controls.spec.doRecurring +import space.kscience.dataforge.context.Context +import space.kscience.dataforge.context.Factory +import space.kscience.dataforge.meta.Meta +import space.kscience.dataforge.meta.MetaRepr +import space.kscience.dataforge.meta.double +import space.kscience.dataforge.meta.get +import space.kscience.dataforge.meta.transformations.MetaConverter +import kotlin.math.pow +import kotlin.time.Duration +import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.ExperimentalTime + +data class Vector2D(var x: Double = 0.0, var y: Double = 0.0) : MetaRepr { + + override fun toMeta(): Meta = objectToMeta(this) + + operator fun div(arg: Double): Vector2D = Vector2D(x / arg, y / arg) + + companion object CoordinatesMetaConverter : MetaConverter { + override fun metaToObject(meta: Meta): Vector2D = Vector2D( + meta["x"].double ?: 0.0, + meta["y"].double ?: 0.0 + ) + + override fun objectToMeta(obj: Vector2D): Meta = Meta { + "x" put obj.x + "y" put obj.y + } + } +} + +open class VirtualCar(context: Context, meta: Meta) : DeviceBySpec(IVirtualCar, context, meta), IVirtualCar { + private val timeScale = 1e-3 + + private val mass by meta.double(1000.0) // mass in kilograms + + override var speedState: Vector2D = Vector2D() + + override var locationState: Vector2D = Vector2D() + + override var accelerationState: Vector2D = Vector2D() + set(value) { + update() + field = value + } + + private var timeState: Instant? = null + + private fun update(newTime: Instant = Clock.System.now()) { + //initialize time if it is not initialized + if (timeState == null) { + timeState = newTime + return + } + + val dt: Double = (newTime - (timeState ?: return)).inWholeMilliseconds.toDouble() * timeScale + + locationState.apply { + x += speedState.x * dt + accelerationState.x * dt.pow(2) / 2.0 + y += speedState.y * dt + accelerationState.y * dt.pow(2) / 2.0 + } + + speedState.apply { + x += dt * accelerationState.x + y += dt * accelerationState.y + } + + //TODO apply friction. One can introduce rotation of the cabin and different friction coefficients along the axis + launch { + //update logical states + IVirtualCar.location.read() + IVirtualCar.speed.read() + IVirtualCar.acceleration.read() + } + + } + + fun applyForce(force: Vector2D, duration: Duration) { + launch { + update() + accelerationState = force / mass + delay(duration) + accelerationState.apply { + x = 0.0 + y = 0.0 + } + update() + } + } + + @OptIn(ExperimentalTime::class) + override suspend fun open() { + super.open() + //initializing the clock + timeState = Clock.System.now() + //starting regular updates + doRecurring(100.milliseconds) { + update() + } + } + + companion object : Factory { + override fun build(context: Context, meta: Meta): VirtualCar = VirtualCar(context, meta) + } +} diff --git a/demo/car/src/main/kotlin/space/kscience/controls/demo/car/VirtualCarController.kt b/demo/car/src/main/kotlin/space/kscience/controls/demo/car/VirtualCarController.kt new file mode 100644 index 0000000..1f98ef7 --- /dev/null +++ b/demo/car/src/main/kotlin/space/kscience/controls/demo/car/VirtualCarController.kt @@ -0,0 +1,146 @@ +package space.kscience.controls.demo.car + +import io.ktor.server.engine.ApplicationEngine +import javafx.beans.property.DoubleProperty +import javafx.scene.Parent +import javafx.scene.control.TextField +import javafx.scene.layout.Priority +import javafx.stage.Stage +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch +import space.kscience.controls.client.connectToMagix +import space.kscience.controls.demo.car.IVirtualCar.Companion.acceleration +import space.kscience.controls.manager.DeviceManager +import space.kscience.controls.manager.install +import space.kscience.controls.storage.storeMessages +import space.kscience.controls.xodus.XodusDeviceMessageStorage +import space.kscience.dataforge.context.* +import space.kscience.dataforge.meta.Meta +import space.kscience.magix.api.MagixEndpoint +import space.kscience.magix.rsocket.rSocketWithTcp +import space.kscience.magix.server.RSocketMagixFlowPlugin +import space.kscience.magix.server.startMagixServer +import space.kscience.magix.storage.xodus.storeInXodus +import space.kscince.magix.zmq.ZmqMagixFlowPlugin +import tornadofx.* +import java.nio.file.Paths + +class VirtualCarController : Controller(), ContextAware { + + var virtualCar: VirtualCar? = null + var magixVirtualCar: MagixVirtualCar? = null + var magixServer: ApplicationEngine? = null + var xodusStorageJob: Job? = null + var storageEndpoint: MagixEndpoint? = null + //var mongoStorageJob: Job? = null + + override val context = Context("demoDevice") { + plugin(DeviceManager) + } + + private val deviceManager = context.fetch(DeviceManager, Meta { + "xodusConfig" put { + "entityStorePath" put deviceEntityStorePath.toString() + } + }) + + fun init() { + context.launch { + virtualCar = deviceManager.install("virtual-car", VirtualCar) + + //starting magix event loop and connect it to entity store + magixServer = startMagixServer(RSocketMagixFlowPlugin(), ZmqMagixFlowPlugin()) + + storageEndpoint = MagixEndpoint.rSocketWithTcp("localhost").apply { + storeInXodus(this@launch, magixEntityStorePath) + } + + magixVirtualCar = deviceManager.install("magix-virtual-car", MagixVirtualCar) + //connect to device entity store + xodusStorageJob = deviceManager.storeMessages(XodusDeviceMessageStorage) + //Create mongo client and connect to MongoDB + //mongoStorageJob = deviceManager.storeMessages(DefaultAsynchronousMongoClientFactory) + //Launch device client and connect it to the server + val deviceEndpoint = MagixEndpoint.rSocketWithTcp("localhost") + deviceManager.connectToMagix(deviceEndpoint) + } + } + + fun shutdown() { + logger.info { "Shutting down..." } + magixServer?.stop(1000, 5000) + logger.info { "Magix server stopped" } + magixVirtualCar?.close() + logger.info { "Magix virtual car server stopped" } + virtualCar?.close() + logger.info { "Virtual car server stopped" } + context.close() + } + + companion object { + val deviceEntityStorePath = Paths.get(".messages") + val magixEntityStorePath = Paths.get(".server_messages") + } +} + + +class VirtualCarControllerView : View(title = " Virtual car controller remote") { + private val controller: VirtualCarController by inject() + private var accelerationXProperty: DoubleProperty by singleAssign() + private var accelerationXTF: TextField by singleAssign() + private var accelerationYProperty: DoubleProperty by singleAssign() + private var accelerationYTF: TextField by singleAssign() + + override val root: Parent = vbox { + hbox { + label("AccelerationX") + pane { + hgrow = Priority.ALWAYS + } + accelerationXProperty = doubleProperty() + accelerationXTF = textfield(accelerationXProperty) + } + hbox { + label("AccelerationY") + pane { + hgrow = Priority.ALWAYS + } + accelerationYProperty = doubleProperty() + accelerationYTF = textfield(accelerationYProperty) + } + button("Submit") { + useMaxWidth = true + action { + controller.virtualCar?.run { + launch { + acceleration.write( + Vector2D( + accelerationXProperty.get(), + accelerationYProperty.get() + ) + ) + } + } + } + } + } +} + +class VirtualCarControllerApp : App(VirtualCarControllerView::class) { + private val controller: VirtualCarController by inject() + + override fun start(stage: Stage) { + super.start(stage) + controller.init() + } + + override fun stop() { + controller.shutdown() + super.stop() + } +} + + +fun main() { + launch() +} \ No newline at end of file diff --git a/demo/echo/build.gradle.kts b/demo/echo/build.gradle.kts new file mode 100644 index 0000000..47ac2af --- /dev/null +++ b/demo/echo/build.gradle.kts @@ -0,0 +1,32 @@ +plugins { + kotlin("jvm") + application +} + +repositories { + mavenCentral() + maven("https://repo.kotlin.link") +} + +val ktorVersion: String by rootProject.extra +val rsocketVersion: String by rootProject.extra + +dependencies { + implementation(projects.magix.magixServer) + implementation(projects.magix.magixRsocket) + implementation(projects.magix.magixZmq) + implementation("io.ktor:ktor-client-cio:$ktorVersion") + + implementation("ch.qos.logback:logback-classic:1.2.11") +} + +tasks.withType().configureEach { + kotlinOptions { + jvmTarget = "11" + freeCompilerArgs = freeCompilerArgs + listOf("-Xjvm-default=all", "-Xopt-in=kotlin.RequiresOptIn") + } +} + +application { + mainClass.set("space.kscience.controls.demo.echo.MainKt") +} \ No newline at end of file diff --git a/demo/echo/src/main/kotlin/space/kscience/controls/demo/echo/main.kt b/demo/echo/src/main/kotlin/space/kscience/controls/demo/echo/main.kt new file mode 100644 index 0000000..3dbb43d --- /dev/null +++ b/demo/echo/src/main/kotlin/space/kscience/controls/demo/echo/main.kt @@ -0,0 +1,87 @@ +package space.kscience.controls.demo.echo + +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.serialization.json.JsonObject +import org.slf4j.LoggerFactory +import space.kscience.magix.api.MagixEndpoint +import space.kscience.magix.api.MagixFlowPlugin +import space.kscience.magix.api.MagixMessage +import space.kscience.magix.api.MagixMessageFilter +import space.kscience.magix.rsocket.rSocketStreamWithWebSockets +import space.kscience.magix.server.startMagixServer +import kotlin.time.ExperimentalTime +import kotlin.time.measureTime + +private suspend fun MagixEndpoint.collectEcho(scope: CoroutineScope, n: Int) { + val complete = CompletableDeferred() + + val responseIds = HashSet() + + scope.launch { + subscribe( + MagixMessageFilter( + origin = listOf("loop") + ) + ).collect { message -> + if (message.id?.endsWith(".response") == true) { + responseIds.add(message.parentId!!) + } + val parentId = message.parentId + if (parentId != null && parentId.toInt() >= n - 1) { + println("Losses ${(1 - responseIds.size.toDouble() / n) * 100}%") + complete.complete(true) + cancel() + } + } + } + + scope.launch { + repeat(n) { + if (it % 20 == 0) delay(1) + broadcast( + MagixMessage( + format = "test", + payload = JsonObject(emptyMap()), + origin = "test", + target = "loop", + id = it.toString() + ) + ) + } + } + + complete.await() + println("completed") +} + + +@OptIn(ExperimentalTime::class) +suspend fun main(): Unit = coroutineScope { + launch(Dispatchers.Default) { + val server = startMagixServer(MagixFlowPlugin { _, flow -> + val logger = LoggerFactory.getLogger("echo") + //echo each message + flow.onEach { message -> + if (message.parentId == null) { + val m = message.copy(origin = "loop", parentId = message.id, id = message.id + ".response") + logger.info(m.toString()) + flow.emit(m) + } + }.launchIn(this) + }) + + + val responseTime = measureTime { + MagixEndpoint.rSocketStreamWithWebSockets("localhost").use { + it.collectEcho(this, 5000) + } + } + + println(responseTime) + + server.stop(500, 500) + cancel() + } +} \ No newline at end of file diff --git a/magix/magix-demo/build.gradle.kts b/demo/magix-demo/build.gradle.kts similarity index 89% rename from magix/magix-demo/build.gradle.kts rename to demo/magix-demo/build.gradle.kts index 0b6e72b..411b9c4 100644 --- a/magix/magix-demo/build.gradle.kts +++ b/demo/magix-demo/build.gradle.kts @@ -1,5 +1,5 @@ plugins { - id("ru.mipt.npm.gradle.jvm") + id("space.kscience.gradle.jvm") application } diff --git a/magix/magix-demo/src/main/kotlin/zmq.kt b/demo/magix-demo/src/main/kotlin/zmq.kt similarity index 70% rename from magix/magix-demo/src/main/kotlin/zmq.kt rename to demo/magix-demo/src/main/kotlin/zmq.kt index 2a73640..7ac9974 100644 --- a/magix/magix-demo/src/main/kotlin/zmq.kt +++ b/demo/magix-demo/src/main/kotlin/zmq.kt @@ -7,33 +7,32 @@ import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.isActive import kotlinx.serialization.json.* import org.slf4j.LoggerFactory -import ru.mipt.npm.magix.api.MagixEndpoint -import ru.mipt.npm.magix.api.MagixMessage -import ru.mipt.npm.magix.server.startMagixServer -import ru.mipt.npm.magix.zmq.ZmqMagixEndpoint +import space.kscience.magix.api.MagixEndpoint +import space.kscience.magix.api.MagixMessage +import space.kscience.magix.server.RSocketMagixFlowPlugin +import space.kscience.magix.server.startMagixServer +import space.kscince.magix.zmq.ZmqMagixEndpoint +import space.kscince.magix.zmq.ZmqMagixFlowPlugin import java.awt.Desktop import java.net.URI -suspend fun MagixEndpoint.sendJson( +suspend fun MagixEndpoint.sendJson( origin: String, format: String = "json", target: String? = null, id: String? = null, parentId: String? = null, user: JsonElement? = null, - builder: JsonObjectBuilder.() -> Unit -): Unit = broadcast(MagixMessage(format, origin, buildJsonObject(builder), target, id, parentId, user)) + builder: JsonObjectBuilder.() -> Unit, +): Unit = broadcast(MagixMessage(format, buildJsonObject(builder), origin, target, id, parentId, user)) internal const val numberOfMessages = 100 suspend fun main(): Unit = coroutineScope { val logger = LoggerFactory.getLogger("magix-demo") logger.info("Starting magix server") - val server = startMagixServer( - buffer = 10, - enableRawRSocket = false //Disable rsocket to avoid kotlin 1.5 compatibility issue - ) + val server = startMagixServer(RSocketMagixFlowPlugin(), ZmqMagixFlowPlugin(), buffer = 10) server.apply { val host = "localhost"//environment.connectors.first().host @@ -44,11 +43,11 @@ suspend fun main(): Unit = coroutineScope { logger.info("Starting client") //Create zmq magix endpoint and wait for to finish - ZmqMagixEndpoint("tcp://localhost", JsonObject.serializer()).use { client -> + ZmqMagixEndpoint("localhost", "tcp").use { client -> logger.info("Starting subscription") client.subscribe().onEach { println(it.payload) - if (it.payload["index"]?.jsonPrimitive?.int == numberOfMessages) { + if (it.payload.jsonObject["index"]?.jsonPrimitive?.int == numberOfMessages) { logger.info("Index $numberOfMessages reached. Terminating") cancel() } diff --git a/demo/mks-pdr900/build.gradle.kts b/demo/mks-pdr900/build.gradle.kts new file mode 100644 index 0000000..bae6263 --- /dev/null +++ b/demo/mks-pdr900/build.gradle.kts @@ -0,0 +1,21 @@ +plugins { + id("space.kscience.gradle.jvm") + application +} + +//TODO to be moved to a separate project +// +//application{ +// mainClass.set("ru.mipt.npm.devices.pimotionmaster.PiMotionMasterAppKt") +//} + +kotlin{ + explicitApi = null +} + +val ktorVersion: String by rootProject.extra +val dataforgeVersion: String by extra + +dependencies { + implementation(projects.controlsKtorTcp) +} diff --git a/demo/mks-pdr900/src/main/kotlin/center/sciprog/devices/mks/MksPdr900Device.kt b/demo/mks-pdr900/src/main/kotlin/center/sciprog/devices/mks/MksPdr900Device.kt new file mode 100644 index 0000000..304102e --- /dev/null +++ b/demo/mks-pdr900/src/main/kotlin/center/sciprog/devices/mks/MksPdr900Device.kt @@ -0,0 +1,109 @@ +package center.sciprog.devices.mks + +import kotlinx.coroutines.withTimeoutOrNull +import space.kscience.controls.ports.Ports +import space.kscience.controls.ports.SynchronousPort +import space.kscience.controls.ports.respondStringWithDelimiter +import space.kscience.controls.ports.synchronous +import space.kscience.controls.spec.* +import space.kscience.dataforge.context.Context +import space.kscience.dataforge.context.Factory +import space.kscience.dataforge.context.request +import space.kscience.dataforge.meta.Meta +import space.kscience.dataforge.meta.get +import space.kscience.dataforge.meta.int +import space.kscience.dataforge.meta.transformations.MetaConverter + + +//TODO this device is not tested +class MksPdr900Device(context: Context, meta: Meta) : DeviceBySpec(MksPdr900Device, context, meta) { + + private val address by meta.int(253) + + private val portDelegate = lazy { + val ports = context.request(Ports) + ports.buildPort(meta["port"] ?: error("Port is not defined in device configuration")).synchronous() + } + + private val port: SynchronousPort by portDelegate + + private val responsePattern: Regex by lazy { + ("@${address}ACK(.*);FF").toRegex() + } + + private suspend fun talk(requestContent: String): String? = withTimeoutOrNull(5000) { + val answer = port.respondStringWithDelimiter(String.format("@%s%s;FF", address, requestContent), ";FF") + responsePattern.matchEntire(answer)?.groups?.get(1)?.value + ?: error("Message $answer does not match $responsePattern") + } + + public suspend fun readPowerOn(): Boolean = when (val answer = talk("FP?")) { + "ON" -> true + "OFF" -> false + else -> error("Unknown answer for 'FP?': $answer") + } + + + public suspend fun writePowerOn(powerOnValue: Boolean) { + error.invalidate() + if (powerOnValue) { + val ans = talk("FP!ON") + if (ans == "ON") { + updateLogical(powerOn, true) + } else { + updateLogical(error, "Failed to set power state") + } + } else { + val ans = talk("FP!OFF") + if (ans == "OFF") { + updateLogical(powerOn, false) + } else { + updateLogical(error, "Failed to set power state") + } + } + } + + public suspend fun readChannelData(channel: Int): Double? { + val answer: String? = talk("PR$channel?") + error.invalidate() + return if (answer.isNullOrEmpty()) { + // updateState(PortSensor.CONNECTED_STATE, false) + updateLogical(error, "No connection") + null + } else { + val res = answer.toDouble() + if (res <= 0) { + updateLogical(powerOn, false) + updateLogical(error, "No power") + null + } else { + res + } + } + } + + + companion object : DeviceSpec(), Factory { + + const val DEFAULT_CHANNEL: Int = 5 + + override fun build(context: Context, meta: Meta): MksPdr900Device = MksPdr900Device(context, meta) + + val powerOn by booleanProperty(read = MksPdr900Device::readPowerOn, write = MksPdr900Device::writePowerOn) + + val channel by logicalProperty(MetaConverter.int) + + val value by doubleProperty(read = { + readChannelData(channel.get() ?: DEFAULT_CHANNEL) + }) + + val error by logicalProperty(MetaConverter.string) + + + override fun MksPdr900Device.onClose() { + if (portDelegate.isInitialized()) { + port.close() + } + } + } +} diff --git a/demo/mks-pdr900/src/main/kotlin/center/sciprog/devices/mks/NullableStringMetaConverter.kt b/demo/mks-pdr900/src/main/kotlin/center/sciprog/devices/mks/NullableStringMetaConverter.kt new file mode 100644 index 0000000..40c20ed --- /dev/null +++ b/demo/mks-pdr900/src/main/kotlin/center/sciprog/devices/mks/NullableStringMetaConverter.kt @@ -0,0 +1,10 @@ +package center.sciprog.devices.mks + +import space.kscience.dataforge.meta.Meta +import space.kscience.dataforge.meta.string +import space.kscience.dataforge.meta.transformations.MetaConverter + +object NullableStringMetaConverter : MetaConverter { + override fun metaToObject(meta: Meta): String? = meta.string + override fun objectToMeta(obj: String?): Meta = Meta {} +} \ No newline at end of file diff --git a/motors/build.gradle.kts b/demo/motors/build.gradle.kts similarity index 61% rename from motors/build.gradle.kts rename to demo/motors/build.gradle.kts index a2bd21d..b60ebb9 100644 --- a/motors/build.gradle.kts +++ b/demo/motors/build.gradle.kts @@ -1,11 +1,16 @@ plugins { - id("ru.mipt.npm.gradle.jvm") - `maven-publish` + id("space.kscience.gradle.jvm") application + id("org.openjfx.javafxplugin") } //TODO to be moved to a separate project +javafx { + version = "17" + modules = listOf("javafx.controls") +} + application{ mainClass.set("ru.mipt.npm.devices.pimotionmaster.PiMotionMasterAppKt") } @@ -14,14 +19,11 @@ kotlin{ explicitApi = null } -kscience{ - useFx(ru.mipt.npm.gradle.FXModule.CONTROLS, configuration = ru.mipt.npm.gradle.DependencyConfiguration.IMPLEMENTATION) -} - val ktorVersion: String by rootProject.extra +val dataforgeVersion: String by extra dependencies { - implementation(project(":controls-tcp")) + implementation(project(":controls-ktor-tcp")) implementation(project(":controls-magix-client")) implementation("no.tornado:tornadofx:1.7.20") } diff --git a/motors/docs/C885T0002-TN-C-885.PIMotionMaster-EN.pdf b/demo/motors/docs/C885T0002-TN-C-885.PIMotionMaster-EN.pdf similarity index 100% rename from motors/docs/C885T0002-TN-C-885.PIMotionMaster-EN.pdf rename to demo/motors/docs/C885T0002-TN-C-885.PIMotionMaster-EN.pdf diff --git a/motors/src/main/kotlin/ru/mipt/npm/devices/pimotionmaster/PiMotionMasterApp.kt b/demo/motors/src/main/kotlin/ru/mipt/npm/devices/pimotionmaster/PiMotionMasterApp.kt similarity index 75% rename from motors/src/main/kotlin/ru/mipt/npm/devices/pimotionmaster/PiMotionMasterApp.kt rename to demo/motors/src/main/kotlin/ru/mipt/npm/devices/pimotionmaster/PiMotionMasterApp.kt index 67d475f..45fe757 100644 --- a/motors/src/main/kotlin/ru/mipt/npm/devices/pimotionmaster/PiMotionMasterApp.kt +++ b/demo/motors/src/main/kotlin/ru/mipt/npm/devices/pimotionmaster/PiMotionMasterApp.kt @@ -11,9 +11,12 @@ import javafx.scene.layout.VBox import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.launch -import ru.mipt.npm.controls.controllers.DeviceManager -import ru.mipt.npm.controls.controllers.installing -import space.kscience.dataforge.context.Global +import ru.mipt.npm.devices.pimotionmaster.PiMotionMasterDevice.Axis.Companion.maxPosition +import ru.mipt.npm.devices.pimotionmaster.PiMotionMasterDevice.Axis.Companion.minPosition +import ru.mipt.npm.devices.pimotionmaster.PiMotionMasterDevice.Axis.Companion.position +import space.kscience.controls.manager.DeviceManager +import space.kscience.controls.manager.installing +import space.kscience.dataforge.context.Context import space.kscience.dataforge.context.fetch import tornadofx.* @@ -21,7 +24,7 @@ class PiMotionMasterApp : App(PiMotionMasterView::class) class PiMotionMasterController : Controller() { //initialize context - val context = Global.buildContext("piMotionMaster"){ + val context = Context("piMotionMaster"){ plugin(DeviceManager) } @@ -40,28 +43,30 @@ fun VBox.piMotionMasterAxis( alignment = Pos.CENTER label(axisName) coroutineScope.launch { - val min = axis.minPosition.readTyped(true) - val max = axis.maxPosition.readTyped(true) - val positionProperty = axis.position.fxProperty(axis) - val startPosition = axis.position.readTyped(true) - runLater { - vbox { - hgrow = Priority.ALWAYS - slider(min..max, startPosition) { - minWidth = 300.0 - isShowTickLabels = true - isShowTickMarks = true - minorTickCount = 10 - majorTickUnit = 1.0 - valueProperty().onChange { - coroutineScope.launch { - axis.move(value) + with(axis) { + val min: Double = minPosition.read() + val max: Double = maxPosition.read() + val positionProperty = fxProperty(position) + val startPosition = position.read() + runLater { + vbox { + hgrow = Priority.ALWAYS + slider(min..max, startPosition) { + minWidth = 300.0 + isShowTickLabels = true + isShowTickMarks = true + minorTickCount = 10 + majorTickUnit = 1.0 + valueProperty().onChange { + coroutineScope.launch { + axis.move(value) + } } } - } - slider(min..max) { - isDisable = true - valueProperty().bind(positionProperty) + slider(min..max) { + isDisable = true + valueProperty().bind(positionProperty) + } } } } @@ -82,7 +87,7 @@ class PiMotionMasterView : View() { private val controller: PiMotionMasterController by inject() val device = controller.motionMaster - private val connectedProperty: ReadOnlyProperty = device.connected.fxProperty(device) + private val connectedProperty: ReadOnlyProperty = device.fxProperty(PiMotionMasterDevice.connected) private val debugServerJobProperty = SimpleObjectProperty() private val debugServerStarted = debugServerJobProperty.booleanBinding { it != null } //private val axisList = FXCollections.observableArrayList>() diff --git a/demo/motors/src/main/kotlin/ru/mipt/npm/devices/pimotionmaster/PiMotionMasterDevice.kt b/demo/motors/src/main/kotlin/ru/mipt/npm/devices/pimotionmaster/PiMotionMasterDevice.kt new file mode 100644 index 0000000..d6c9482 --- /dev/null +++ b/demo/motors/src/main/kotlin/ru/mipt/npm/devices/pimotionmaster/PiMotionMasterDevice.kt @@ -0,0 +1,357 @@ +@file:Suppress("unused", "MemberVisibilityCanBePrivate") + +package ru.mipt.npm.devices.pimotionmaster + +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.toList +import kotlinx.coroutines.flow.transformWhile +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import kotlinx.coroutines.withTimeout +import space.kscience.controls.api.DeviceHub +import space.kscience.controls.api.PropertyDescriptor +import space.kscience.controls.ports.* +import space.kscience.controls.spec.* +import space.kscience.dataforge.context.* +import space.kscience.dataforge.meta.Meta +import space.kscience.dataforge.meta.asValue +import space.kscience.dataforge.meta.double +import space.kscience.dataforge.meta.get +import space.kscience.dataforge.meta.transformations.MetaConverter +import space.kscience.dataforge.names.NameToken +import kotlin.collections.component1 +import kotlin.collections.component2 +import kotlin.time.Duration +import kotlin.time.Duration.Companion.milliseconds + +class PiMotionMasterDevice( + context: Context, + private val portFactory: PortFactory = KtorTcpPort, +) : DeviceBySpec(PiMotionMasterDevice, context), DeviceHub { + + private var port: Port? = null + //TODO make proxy work + //PortProxy { portFactory(address ?: error("The device is not connected"), context) } + + + fun disconnect() { + runBlocking { + disconnect.invoke() + } + } + + var timeoutValue: Duration = 200.milliseconds + + /** + * Name-friendly accessor for axis + */ + var axes: Map = emptyMap() + private set + + override val devices: Map = axes.mapKeys { (key, _) -> NameToken(key) } + + private suspend fun failIfError(message: (Int) -> String = { "Failed with error code $it" }) { + val errorCode = getErrorCode() + if (errorCode != 0) error(message(errorCode)) + } + + fun connect(host: String, port: Int) { + runBlocking { + connect(Meta { + "host" put host + "port" put port + }) + } + } + + private val mutex = Mutex() + + private suspend fun dispatchError(errorCode: Int) { + logger.error { "Error code: $errorCode" } + //TODO add error handling + } + + private suspend fun sendCommandInternal(command: String, vararg arguments: String) { + val joinedArguments = if (arguments.isEmpty()) { + "" + } else { + arguments.joinToString(prefix = " ", separator = " ", postfix = "") + } + val stringToSend = "$command$joinedArguments\n" + port?.send(stringToSend) ?: error("Not connected to device") + } + + suspend fun getErrorCode(): Int = mutex.withLock { + withTimeout(timeoutValue) { + sendCommandInternal("ERR?") + val errorString = port?.receiving()?.withStringDelimiter("\n")?.first() ?: error("Not connected to device") + errorString.trim().toInt() + } + } + + /** + * Send a synchronous request and receive a list of lines as a response + */ + @OptIn(ExperimentalCoroutinesApi::class) + private suspend fun request(command: String, vararg arguments: String): List = mutex.withLock { + try { + withTimeout(timeoutValue) { + sendCommandInternal(command, *arguments) + val phrases = port?.receiving()?.withStringDelimiter("\n") ?: error("Not connected to device") + phrases.transformWhile { line -> + emit(line) + line.endsWith(" \n") + }.toList() + } + } catch (ex: Throwable) { + logger.warn { "Error during PIMotionMaster request. Requesting error code." } + val errorCode = getErrorCode() + dispatchError(errorCode) + logger.warn { "Error code $errorCode" } + error("Error code $errorCode") + } + } + + private suspend fun requestAndParse(command: String, vararg arguments: String): Map = buildMap { + request(command, *arguments).forEach { line -> + val (key, value) = line.split("=") + put(key, value.trim()) + } + } + + /** + * Send a synchronous command + */ + private suspend fun send(command: String, vararg arguments: String) { + mutex.withLock { + withTimeout(timeoutValue) { + sendCommandInternal(command, *arguments) + } + } + } + + companion object : DeviceSpec(), Factory { + + override fun build(context: Context, meta: Meta): PiMotionMasterDevice = PiMotionMasterDevice(context) + + val connected by booleanProperty(descriptorBuilder = { + info = "True if the connection address is defined and the device is initialized" + }) { + port != null + } + + + val initialize by unitAction { + send("INI") + } + + val identity by stringProperty { + request("*IDN?").first() + } + + val firmwareVersion by stringProperty { + request("VER?").first() + } + + val stop by unitAction({ + info = "Stop all axis" + }) { + send("STP") + } + + val connect by metaAction(descriptorBuilder = { + info = "Connect to specific port and initialize axis" + }) { portSpec -> + //Clear current actions if present + if (port != null) { + disconnect() + } + //Update port + //address = portSpec.node + port = portFactory(portSpec ?: Meta.EMPTY, context) + updateLogical(connected, true) +// connector.open() + //Initialize axes + if (portSpec != null) { + val idn = identity.read() + failIfError { "Can't connect to $portSpec. Error code: $it" } + logger.info { "Connected to $idn on $portSpec" } + val ids = request("SAI?").map { it.trim() } + if (ids != axes.keys.toList()) { + //re-define axes if needed + axes = ids.associateWith { Axis(this, it) } + } + Meta(ids.map { it.asValue() }.asValue()) + initialize() + failIfError() + } + null + } + + val disconnect by metaAction({ + info = "Disconnect the program from the device if it is connected" + }) { + port?.let{ + stop() + it.close() + } + port = null + updateLogical(connected, false) + null + } + + + val timeout by mutableProperty(MetaConverter.duration, PiMotionMasterDevice::timeoutValue) { + info = "Timeout" + } + } + + + class Axis( + val mm: PiMotionMasterDevice, + val axisId: String + ) : DeviceBySpec(Axis, mm.context) { + + /** + * TODO Move to head device and abstract + */ + private suspend fun readAxisBoolean(command: String): Boolean = + (mm.requestAndParse(command, axisId)[axisId]?.toIntOrNull() + ?: error("Malformed $command response. Should include integer value for $axisId")) != 0 + + /** + * TODO Move to head device and abstract + */ + private suspend fun writeAxisBoolean(command: String, value: Boolean): Boolean { + val boolean = if (value) { + "1" + } else { + "0" + } + mm.send(command, axisId, boolean) + mm.failIfError() + return value + } + + suspend fun move(target: Double) { + move(target.asMeta()) + } + + companion object : DeviceSpec() { + + private fun axisBooleanProperty( + command: String, + descriptorBuilder: PropertyDescriptor.() -> Unit = {} + ) = booleanProperty( + read = { + readAxisBoolean("$command?") + }, + write = { + writeAxisBoolean(command, it) + }, + descriptorBuilder = descriptorBuilder + ) + + private fun axisNumberProperty( + command: String, + descriptorBuilder: PropertyDescriptor.() -> Unit = {} + ) = doubleProperty( + read = { + mm.requestAndParse("$command?", axisId)[axisId]?.toDoubleOrNull() + ?: error("Malformed $command response. Should include float value for $axisId") + }, + write = { newValue -> + mm.send(command, axisId, newValue.toString()) + mm.failIfError() + }, + descriptorBuilder = descriptorBuilder + ) + + val enabled by axisBooleanProperty("EAX") { + info = "Motor enable state." + } + + val halt by unitAction { + mm.send("HLT", axisId) + } + + val targetPosition by axisNumberProperty("MOV") { + info = """ + Sets a new absolute target position for the specified axis. + Servo mode must be switched on for the commanded axis prior to using this command (closed-loop operation). + """.trimIndent() + } + + val onTarget by booleanProperty({ + info = "Queries the on-target state of the specified axis." + }) { + readAxisBoolean("ONT?") + } + + val reference by booleanProperty({ + info = "Get Referencing Result" + }) { + readAxisBoolean("FRF?") + } + + val moveToReference by unitAction { + mm.send("FRF", axisId) + } + + val minPosition by doubleProperty({ + info = "Minimal position value for the axis" + }) { + mm.requestAndParse("TMN?", axisId)[axisId]?.toDoubleOrNull() + ?: error("Malformed `TMN?` response. Should include float value for $axisId") + } + + val maxPosition by doubleProperty({ + info = "Maximal position value for the axis" + }) { + mm.requestAndParse("TMX?", axisId)[axisId]?.toDoubleOrNull() + ?: error("Malformed `TMX?` response. Should include float value for $axisId") + } + + val position by doubleProperty({ + info = "The current axis position." + }) { + mm.requestAndParse("POS?", axisId)[axisId]?.toDoubleOrNull() + ?: error("Malformed `POS?` response. Should include float value for $axisId") + } + + val openLoopTarget by axisNumberProperty("OMA") { + info = "Position for open-loop operation." + } + + val closedLoop by axisBooleanProperty("SVO") { + info = "Servo closed loop mode" + } + + val velocity by axisNumberProperty("VEL") { + info = "Velocity value for closed-loop operation" + } + + val move by metaAction { + val target = it.double ?: it?.get("target").double ?: error("Unacceptable target value $it") + closedLoop.write(true) + //optionally set velocity + it?.get("velocity").double?.let { v -> + velocity.write(v) + } + targetPosition.write(target) + //read `onTarget` and `position` properties in a cycle until movement is complete + while (!onTarget.read()) { + position.read() + delay(200) + } + null + } + + } + + } + +} \ No newline at end of file diff --git a/motors/src/main/kotlin/ru/mipt/npm/devices/pimotionmaster/PiMotionMasterVirtualDevice.kt b/demo/motors/src/main/kotlin/ru/mipt/npm/devices/pimotionmaster/PiMotionMasterVirtualDevice.kt similarity index 98% rename from motors/src/main/kotlin/ru/mipt/npm/devices/pimotionmaster/PiMotionMasterVirtualDevice.kt rename to demo/motors/src/main/kotlin/ru/mipt/npm/devices/pimotionmaster/PiMotionMasterVirtualDevice.kt index cb490e1..8efe4e9 100644 --- a/motors/src/main/kotlin/ru/mipt/npm/devices/pimotionmaster/PiMotionMasterVirtualDevice.kt +++ b/demo/motors/src/main/kotlin/ru/mipt/npm/devices/pimotionmaster/PiMotionMasterVirtualDevice.kt @@ -5,9 +5,9 @@ import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.* import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock -import ru.mipt.npm.controls.api.Socket -import ru.mipt.npm.controls.ports.AbstractPort -import ru.mipt.npm.controls.ports.withDelimiter +import space.kscience.controls.api.Socket +import space.kscience.controls.ports.AbstractPort +import space.kscience.controls.ports.withDelimiter import space.kscience.dataforge.context.* import kotlin.math.abs import kotlin.time.Duration diff --git a/demo/motors/src/main/kotlin/ru/mipt/npm/devices/pimotionmaster/fxDeviceProperties.kt b/demo/motors/src/main/kotlin/ru/mipt/npm/devices/pimotionmaster/fxDeviceProperties.kt new file mode 100644 index 0000000..8e399a4 --- /dev/null +++ b/demo/motors/src/main/kotlin/ru/mipt/npm/devices/pimotionmaster/fxDeviceProperties.kt @@ -0,0 +1,66 @@ +package ru.mipt.npm.devices.pimotionmaster + +import javafx.beans.property.ObjectPropertyBase +import javafx.beans.property.Property +import javafx.beans.property.ReadOnlyProperty +import space.kscience.controls.api.Device +import space.kscience.controls.spec.* +import space.kscience.dataforge.context.info +import space.kscience.dataforge.context.logger +import tornadofx.* + +/** + * Bind a FX property to a device property with a given [spec] + */ +fun Device.fxProperty( + spec: DevicePropertySpec +): ReadOnlyProperty = object : ObjectPropertyBase() { + override fun getBean(): Any = this + override fun getName(): String = spec.name + + init { + //Read incoming changes + onPropertyChange(spec) { + if (it != null) { + runLater { + try { + set(it) + } catch (ex: Throwable) { + logger.info { "Failed to set property $name to $it" } + } + } + } else { + invalidated() + } + } + } +} + +fun D.fxProperty(spec: WritableDevicePropertySpec): Property = + object : ObjectPropertyBase() { + override fun getBean(): Any = this + override fun getName(): String = spec.name + + init { + //Read incoming changes + onPropertyChange(spec) { + if (it != null) { + runLater { + try { + set(it) + } catch (ex: Throwable) { + logger.info { "Failed to set property $name to $it" } + } + } + } else { + invalidated() + } + } + + onChange { newValue -> + if (newValue != null) { + write(spec, newValue) + } + } + } + } diff --git a/motors/src/main/kotlin/ru/mipt/npm/devices/pimotionmaster/piDebugServer.kt b/demo/motors/src/main/kotlin/ru/mipt/npm/devices/pimotionmaster/piDebugServer.kt similarity index 94% rename from motors/src/main/kotlin/ru/mipt/npm/devices/pimotionmaster/piDebugServer.kt rename to demo/motors/src/main/kotlin/ru/mipt/npm/devices/pimotionmaster/piDebugServer.kt index f5a4c81..021dff5 100644 --- a/motors/src/main/kotlin/ru/mipt/npm/devices/pimotionmaster/piDebugServer.kt +++ b/demo/motors/src/main/kotlin/ru/mipt/npm/devices/pimotionmaster/piDebugServer.kt @@ -8,10 +8,8 @@ import io.ktor.util.InternalAPI import io.ktor.util.moveToByteArray import io.ktor.utils.io.writeAvailable import kotlinx.coroutines.* -import kotlinx.coroutines.flow.collect import space.kscience.dataforge.context.Context import space.kscience.dataforge.context.Global -import java.net.InetSocketAddress val exceptionHandler = CoroutineExceptionHandler { _, throwable -> throwable.printStackTrace() @@ -20,7 +18,7 @@ val exceptionHandler = CoroutineExceptionHandler { _, throwable -> @OptIn(InternalAPI::class) fun Context.launchPiDebugServer(port: Int, axes: List): Job = launch(exceptionHandler) { val virtualDevice = PiMotionMasterVirtualDevice(this@launchPiDebugServer, axes) - val server = aSocket(ActorSelectorManager(Dispatchers.IO)).tcp().bind(InetSocketAddress("localhost", port)) + val server = aSocket(ActorSelectorManager(Dispatchers.IO)).tcp().bind("localhost", port) println("Started virtual port server at ${server.localAddress}") while (isActive) { diff --git a/demo/src/main/kotlin/ru/mipt/npm/controls/demo/DemoDevice.kt b/demo/src/main/kotlin/ru/mipt/npm/controls/demo/DemoDevice.kt deleted file mode 100644 index 7882cbc..0000000 --- a/demo/src/main/kotlin/ru/mipt/npm/controls/demo/DemoDevice.kt +++ /dev/null @@ -1,60 +0,0 @@ -package ru.mipt.npm.controls.demo - -import kotlinx.coroutines.launch -import ru.mipt.npm.controls.properties.* -import space.kscience.dataforge.meta.Meta -import space.kscience.dataforge.meta.transformations.MetaConverter -import java.time.Instant -import kotlin.time.Duration -import kotlin.time.ExperimentalTime - - -class DemoDevice : DeviceBySpec(DemoDevice) { - private var timeScaleState = 5000.0 - private var sinScaleState = 1.0 - private var cosScaleState = 1.0 - - companion object : DeviceSpec(::DemoDevice) { - // register virtual properties based on actual object state - val timeScale by property(MetaConverter.double, DemoDevice::timeScaleState) - val sinScale by property(MetaConverter.double, DemoDevice::sinScaleState) - val cosScale by property(MetaConverter.double, DemoDevice::cosScaleState) - - val sin by doubleProperty { - val time = Instant.now() - kotlin.math.sin(time.toEpochMilli().toDouble() / timeScaleState) * sinScaleState - } - - val cos by doubleProperty { - val time = Instant.now() - kotlin.math.cos(time.toEpochMilli().toDouble() / timeScaleState) * sinScaleState - } - - val coordinates by metaProperty { - Meta { - val time = Instant.now() - "time" put time.toEpochMilli() - "x" put read(sin) - "y" put read(cos) - } - } - - val resetScale by action(MetaConverter.meta, MetaConverter.meta) { - timeScale.write(5000.0) - sinScale.write(1.0) - cosScale.write(1.0) - null - } - - @OptIn(ExperimentalTime::class) - override fun DemoDevice.onStartup() { - launch { - sinScale.read() - cosScale.read() - } - doRecurring(Duration.milliseconds(50)){ - coordinates.read() - } - } - } -} \ No newline at end of file diff --git a/demo/src/main/kotlin/ru/mipt/npm/controls/demo/generateMessageSchema.kt b/demo/src/main/kotlin/ru/mipt/npm/controls/demo/generateMessageSchema.kt deleted file mode 100644 index 420bca3..0000000 --- a/demo/src/main/kotlin/ru/mipt/npm/controls/demo/generateMessageSchema.kt +++ /dev/null @@ -1,10 +0,0 @@ -package ru.mipt.npm.controls.demo - -import com.github.ricky12awesome.jss.encodeToSchema -import com.github.ricky12awesome.jss.globalJson -import ru.mipt.npm.controls.api.DeviceMessage - -fun main() { - val schema = globalJson.encodeToSchema(DeviceMessage.serializer(), generateDefinitions = false) - println(schema) -} \ No newline at end of file diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..d422be3 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,10 @@ +kotlin.code.style=official +kotlin.mpp.stability.nowarn=true +kotlin.native.ignoreDisabledTargets=true + +org.gradle.parallel=true + +publishing.github=false +publishing.sonatype=false + +toolsVersion=0.14.3-kotlin-1.8.10 \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index ffed3a2..070cb70 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/magix/magix-api/build.gradle.kts b/magix/magix-api/build.gradle.kts index c6eb397..5f8bdf3 100644 --- a/magix/magix-api/build.gradle.kts +++ b/magix/magix-api/build.gradle.kts @@ -1,9 +1,12 @@ plugins { - id("ru.mipt.npm.gradle.mpp") + id("space.kscience.gradle.mpp") `maven-publish` } kscience { + jvm() + js() + native() useCoroutines() useSerialization{ json() diff --git a/magix/magix-api/src/commonMain/kotlin/ru/mipt/npm/magix/api/MagixEndpoint.kt b/magix/magix-api/src/commonMain/kotlin/ru/mipt/npm/magix/api/MagixEndpoint.kt deleted file mode 100644 index 78ab54d..0000000 --- a/magix/magix-api/src/commonMain/kotlin/ru/mipt/npm/magix/api/MagixEndpoint.kt +++ /dev/null @@ -1,83 +0,0 @@ -package ru.mipt.npm.magix.api - -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.map -import kotlinx.serialization.KSerializer -import kotlinx.serialization.json.Json -import kotlinx.serialization.json.JsonElement - -/** - * Inwards API of magix endpoint used to build services - */ -public interface MagixEndpoint { - - /** - * Subscribe to a [Flow] of messages - */ - public fun subscribe( - filter: MagixMessageFilter = MagixMessageFilter.ALL, - ): Flow> - - - /** - * Send an event - */ - public suspend fun broadcast( - message: MagixMessage, - ) - - public companion object { - /** - * 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 - - /** - * A default PUB port for ZMQ connections - */ - public const val DEFAULT_MAGIX_ZMQ_PUB_PORT: Int = 7781 - - /** - * A default PULL port for ZMQ connections - */ - public const val DEFAULT_MAGIX_ZMQ_PULL_PORT: Int = 7782 - - - public val magixJson: Json = Json { - ignoreUnknownKeys = true - encodeDefaults = false - } - } -} - -/** - * Specialize this raw json endpoint to use specific serializer - */ -public fun MagixEndpoint.specialize( - payloadSerializer: KSerializer -): MagixEndpoint = object : MagixEndpoint { - override fun subscribe( - filter: MagixMessageFilter - ): Flow> = this@specialize.subscribe(filter).map { message -> - message.replacePayload { payload -> - MagixEndpoint.magixJson.decodeFromJsonElement(payloadSerializer, payload) - } - } - - override suspend fun broadcast(message: MagixMessage) { - this@specialize.broadcast( - message.replacePayload { payload -> - MagixEndpoint.magixJson.encodeToJsonElement( - payloadSerializer, - payload - ) - } - ) - } - -} \ No newline at end of file diff --git a/magix/magix-api/src/commonMain/kotlin/space/kscience/magix/api/MagixEndpoint.kt b/magix/magix-api/src/commonMain/kotlin/space/kscience/magix/api/MagixEndpoint.kt new file mode 100644 index 0000000..9baafea --- /dev/null +++ b/magix/magix-api/src/commonMain/kotlin/space/kscience/magix/api/MagixEndpoint.kt @@ -0,0 +1,58 @@ +package space.kscience.magix.api + +import kotlinx.coroutines.flow.Flow +import kotlinx.serialization.json.Json + +/** + * Inwards API of magix endpoint used to build services + */ +public interface MagixEndpoint { + + /** + * Subscribe to a [Flow] of messages + */ + public fun subscribe( + filter: MagixMessageFilter = MagixMessageFilter.ALL, + ): Flow + + + /** + * Send an event + */ + public suspend fun broadcast( + message: MagixMessage, + ) + + /** + * Close the endpoint and the associated connection if it exists + */ + public fun close() + + public companion object { + /** + * 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 + + /** + * A default PUB port for ZMQ connections + */ + public const val DEFAULT_MAGIX_ZMQ_PUB_PORT: Int = 7781 + + /** + * A default PULL port for ZMQ connections + */ + public const val DEFAULT_MAGIX_ZMQ_PULL_PORT: Int = 7782 + + + public val magixJson: Json = Json { + ignoreUnknownKeys = true + encodeDefaults = false + } + } +} \ No newline at end of file diff --git a/magix/magix-api/src/commonMain/kotlin/space/kscience/magix/api/MagixFlowPlugin.kt b/magix/magix-api/src/commonMain/kotlin/space/kscience/magix/api/MagixFlowPlugin.kt new file mode 100644 index 0000000..8cf9ccd --- /dev/null +++ b/magix/magix-api/src/commonMain/kotlin/space/kscience/magix/api/MagixFlowPlugin.kt @@ -0,0 +1,9 @@ +package space.kscience.magix.api + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.MutableSharedFlow + +public fun interface MagixFlowPlugin { + public fun start(scope: CoroutineScope, magixFlow: MutableSharedFlow): Job +} \ No newline at end of file diff --git a/magix/magix-api/src/commonMain/kotlin/space/kscience/magix/api/MagixFormat.kt b/magix/magix-api/src/commonMain/kotlin/space/kscience/magix/api/MagixFormat.kt new file mode 100644 index 0000000..4921129 --- /dev/null +++ b/magix/magix-api/src/commonMain/kotlin/space/kscience/magix/api/MagixFormat.kt @@ -0,0 +1,47 @@ +package space.kscience.magix.api + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import kotlinx.serialization.KSerializer +import kotlinx.serialization.json.JsonElement +import space.kscience.magix.api.MagixEndpoint.Companion.magixJson + +public data class MagixFormat( + val serializer: KSerializer, + val formats: Set, +) { + val defaultFormat: String get() = formats.firstOrNull() ?: "magix" +} + +public fun MagixEndpoint.subscribe( + format: MagixFormat, + originFilter: Collection? = null, + targetFilter: Collection? = null, +): Flow> = subscribe( + MagixMessageFilter(format = format.formats, origin = originFilter, target = targetFilter) +).map { + val value: T = magixJson.decodeFromJsonElement(format.serializer, it.payload) + it to value +} + +public suspend fun MagixEndpoint.broadcast( + format: MagixFormat, + payload: T, + target: String? = null, + id: String? = null, + parentId: String? = null, + user: JsonElement? = null, + origin: String = format.defaultFormat, +) { + val message = MagixMessage( + format = format.defaultFormat, + payload = magixJson.encodeToJsonElement(format.serializer, payload), + origin = origin, + target = target, + id = id, + parentId = parentId, + user = user + ) + broadcast(message) +} + diff --git a/magix/magix-api/src/commonMain/kotlin/ru/mipt/npm/magix/api/MagixMessage.kt b/magix/magix-api/src/commonMain/kotlin/space/kscience/magix/api/MagixMessage.kt similarity index 68% rename from magix/magix-api/src/commonMain/kotlin/ru/mipt/npm/magix/api/MagixMessage.kt rename to magix/magix-api/src/commonMain/kotlin/space/kscience/magix/api/MagixMessage.kt index eaf2098..c25c08a 100644 --- a/magix/magix-api/src/commonMain/kotlin/ru/mipt/npm/magix/api/MagixMessage.kt +++ b/magix/magix-api/src/commonMain/kotlin/space/kscience/magix/api/MagixMessage.kt @@ -1,4 +1,4 @@ -package ru.mipt.npm.magix.api +package space.kscience.magix.api import kotlinx.serialization.Serializable import kotlinx.serialization.json.JsonElement @@ -24,19 +24,12 @@ import kotlinx.serialization.json.JsonElement * */ @Serializable -public data class MagixMessage( +public data class MagixMessage( val format: String, + val payload: JsonElement, val origin: String, - val payload: T, val target: String? = null, val id: String? = null, val parentId: String? = null, val user: JsonElement? = null, -) - -/** - * Create message with same field but replaced payload - */ -@Suppress("UNCHECKED_CAST") -public fun MagixMessage.replacePayload(payloadTransform: (T) -> R): MagixMessage = - MagixMessage(format, origin, payloadTransform(payload), target, id, parentId, user) \ No newline at end of file +) \ No newline at end of file diff --git a/magix/magix-api/src/commonMain/kotlin/ru/mipt/npm/magix/api/MagixMessageFilter.kt b/magix/magix-api/src/commonMain/kotlin/space/kscience/magix/api/MagixMessageFilter.kt similarity index 67% rename from magix/magix-api/src/commonMain/kotlin/ru/mipt/npm/magix/api/MagixMessageFilter.kt rename to magix/magix-api/src/commonMain/kotlin/space/kscience/magix/api/MagixMessageFilter.kt index 2f941c0..f497b97 100644 --- a/magix/magix-api/src/commonMain/kotlin/ru/mipt/npm/magix/api/MagixMessageFilter.kt +++ b/magix/magix-api/src/commonMain/kotlin/space/kscience/magix/api/MagixMessageFilter.kt @@ -1,4 +1,4 @@ -package ru.mipt.npm.magix.api +package space.kscience.magix.api import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.filter @@ -6,9 +6,9 @@ import kotlinx.serialization.Serializable @Serializable public data class MagixMessageFilter( - val format: List? = null, - val origin: List? = null, - val target: List? = null, + val format: Collection? = null, + val origin: Collection? = null, + val target: Collection? = null, ) { public companion object { public val ALL: MagixMessageFilter = MagixMessageFilter() @@ -18,13 +18,12 @@ public data class MagixMessageFilter( /** * Filter a [Flow] of messages based on given filter */ -public fun Flow>.filter(filter: MagixMessageFilter): Flow> { +public fun Flow.filter(filter: MagixMessageFilter): Flow { if (filter == MagixMessageFilter.ALL) { return this } return filter { message -> filter.format?.contains(message.format) ?: true - && filter.origin?.contains(message.origin) ?: true && filter.origin?.contains(message.origin) ?: true && filter.target?.contains(message.target) ?: true } diff --git a/magix/magix-api/src/commonMain/kotlin/space/kscience/magix/api/MagixRegistry.kt b/magix/magix-api/src/commonMain/kotlin/space/kscience/magix/api/MagixRegistry.kt new file mode 100644 index 0000000..7b3b111 --- /dev/null +++ b/magix/magix-api/src/commonMain/kotlin/space/kscience/magix/api/MagixRegistry.kt @@ -0,0 +1,17 @@ +package space.kscience.magix.api + +import kotlinx.serialization.json.JsonElement + +/** + * An interface to access distributed Magix property registry + */ +public interface MagixRegistry { + /** + * Request a property with name [propertyName] and user authentication data [user]. + * + * Return a property value in its generic form or null if it is not present. + * + * Throw an exception if property is present but access is denied. + */ + public suspend fun request(propertyName: String, user: JsonElement? = null): JsonElement? +} diff --git a/magix/magix-api/src/commonMain/kotlin/ru/mipt/npm/magix/api/converters.kt b/magix/magix-api/src/commonMain/kotlin/space/kscience/magix/api/converters.kt similarity index 65% rename from magix/magix-api/src/commonMain/kotlin/ru/mipt/npm/magix/api/converters.kt rename to magix/magix-api/src/commonMain/kotlin/space/kscience/magix/api/converters.kt index f1c854c..441e141 100644 --- a/magix/magix-api/src/commonMain/kotlin/ru/mipt/npm/magix/api/converters.kt +++ b/magix/magix-api/src/commonMain/kotlin/space/kscience/magix/api/converters.kt @@ -1,30 +1,30 @@ -package ru.mipt.npm.magix.api +package space.kscience.magix.api import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach +import kotlinx.serialization.json.JsonElement /** * Launch magix message converter service */ public fun CoroutineScope.launchMagixConverter( - inputEndpoint: MagixEndpoint, - outputEndpoint: MagixEndpoint, + endpoint: MagixEndpoint, filter: MagixMessageFilter, outputFormat: String, newOrigin: String? = null, - transformer: suspend (T) -> R, -): Job = inputEndpoint.subscribe(filter).onEach { message-> + transformer: suspend (JsonElement) -> JsonElement, +): Job = endpoint.subscribe(filter).onEach { message-> val newPayload = transformer(message.payload) - val transformed: MagixMessage = MagixMessage( + val transformed: MagixMessage = MagixMessage( outputFormat, - newOrigin ?: message.origin, newPayload, + newOrigin ?: message.origin, message.target, message.id, message.parentId, message.user ) - outputEndpoint.broadcast(transformed) + endpoint.broadcast(transformed) }.launchIn(this) diff --git a/magix/magix-java-client/build.gradle.kts b/magix/magix-java-client/build.gradle.kts index 5e34637..63041b3 100644 --- a/magix/magix-java-client/build.gradle.kts +++ b/magix/magix-java-client/build.gradle.kts @@ -1,10 +1,26 @@ +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile +import space.kscience.gradle.KScienceVersions + plugins { java - id("ru.mipt.npm.gradle.jvm") + id("space.kscience.gradle.jvm") `maven-publish` } dependencies { implementation(project(":magix:magix-rsocket")) - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-jdk9:${ru.mipt.npm.gradle.KScienceVersions.coroutinesVersion}") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-jdk9:${KScienceVersions.coroutinesVersion}") } + +//java { +// sourceCompatibility = KScienceVersions.JVM_TARGET +// targetCompatibility = KScienceVersions.JVM_TARGET +//} + + +//FIXME https://youtrack.jetbrains.com/issue/KT-52815/Compiler-option-Xjdk-release-fails-to-compile-mixed-projects +tasks.withType{ + kotlinOptions { + freeCompilerArgs -= "-Xjdk-release=11" + } +} \ No newline at end of file diff --git a/magix/magix-java-client/src/main/java/ru/mipt/npm/magix/client/MagixClient.java b/magix/magix-java-client/src/main/java/space/kscience/magix/client/MagixClient.java similarity index 74% rename from magix/magix-java-client/src/main/java/ru/mipt/npm/magix/client/MagixClient.java rename to magix/magix-java-client/src/main/java/space/kscience/magix/client/MagixClient.java index 50d6f09..3270544 100644 --- a/magix/magix-java-client/src/main/java/ru/mipt/npm/magix/client/MagixClient.java +++ b/magix/magix-java-client/src/main/java/space/kscience/magix/client/MagixClient.java @@ -1,7 +1,7 @@ -package ru.mipt.npm.magix.client; +package space.kscience.magix.client; import kotlinx.serialization.json.JsonElement; -import ru.mipt.npm.magix.api.MagixMessage; +import space.kscience.magix.api.MagixMessage; import java.io.IOException; import java.util.concurrent.Flow; @@ -12,9 +12,9 @@ import java.util.concurrent.Flow; * @param */ public interface MagixClient { - void broadcast(MagixMessage msg) throws IOException; + void broadcast(MagixMessage msg) throws IOException; - Flow.Publisher> subscribe(); + Flow.Publisher subscribe(); /** * Create a magix endpoint client using RSocket with raw tcp connection @@ -23,17 +23,16 @@ public interface MagixClient { * @return the client */ static MagixClient rSocketTcp(String host, int port) { - return ControlsMagixClient.Companion.rSocketTcp(host, port, JsonElement.Companion.serializer()); + return ControlsMagixClient.Companion.rSocketTcp(host, port); } /** * * @param host host name of magix server event loop * @param port port of magix server event loop - * @param path - * @return + * @param path context path for WS connection */ static MagixClient rSocketWs(String host, int port, String path) { - return ControlsMagixClient.Companion.rSocketWs(host, port, JsonElement.Companion.serializer(), path); + return ControlsMagixClient.Companion.rSocketWs(host, port, path); } } diff --git a/magix/magix-java-client/src/main/kotlin/ru/mipt/npm/magix/client/ControlsMagixClient.kt b/magix/magix-java-client/src/main/kotlin/space/kscience/magix/client/ControlsMagixClient.kt similarity index 51% rename from magix/magix-java-client/src/main/kotlin/ru/mipt/npm/magix/client/ControlsMagixClient.kt rename to magix/magix-java-client/src/main/kotlin/space/kscience/magix/client/ControlsMagixClient.kt index 476a73d..1efa746 100644 --- a/magix/magix-java-client/src/main/kotlin/ru/mipt/npm/magix/client/ControlsMagixClient.kt +++ b/magix/magix-java-client/src/main/kotlin/space/kscience/magix/client/ControlsMagixClient.kt @@ -1,35 +1,33 @@ -package ru.mipt.npm.magix.client +package space.kscience.magix.client import kotlinx.coroutines.jdk9.asPublisher import kotlinx.coroutines.runBlocking -import kotlinx.serialization.KSerializer -import ru.mipt.npm.magix.api.MagixEndpoint -import ru.mipt.npm.magix.api.MagixMessage -import ru.mipt.npm.magix.api.MagixMessageFilter -import ru.mipt.npm.magix.rsocket.rSocketWithTcp -import ru.mipt.npm.magix.rsocket.rSocketWithWebSockets +import space.kscience.magix.api.MagixEndpoint +import space.kscience.magix.api.MagixMessage +import space.kscience.magix.api.MagixMessageFilter +import space.kscience.magix.rsocket.rSocketWithTcp +import space.kscience.magix.rsocket.rSocketWithWebSockets import java.util.concurrent.Flow internal class ControlsMagixClient( - private val endpoint: MagixEndpoint, + private val endpoint: MagixEndpoint, private val filter: MagixMessageFilter, ) : MagixClient { - override fun broadcast(msg: MagixMessage): Unit = runBlocking { + override fun broadcast(msg: MagixMessage): Unit = runBlocking { endpoint.broadcast(msg) } - override fun subscribe(): Flow.Publisher> = endpoint.subscribe(filter).asPublisher() + override fun subscribe(): Flow.Publisher = endpoint.subscribe(filter).asPublisher() companion object { fun rSocketTcp( host: String, port: Int, - payloadSerializer: KSerializer ): ControlsMagixClient { val endpoint = runBlocking { - MagixEndpoint.rSocketWithTcp(host, payloadSerializer, port) + MagixEndpoint.rSocketWithTcp(host, port) } return ControlsMagixClient(endpoint, MagixMessageFilter()) } @@ -37,11 +35,10 @@ internal class ControlsMagixClient( fun rSocketWs( host: String, port: Int, - payloadSerializer: KSerializer, path: String = "/rsocket" ): ControlsMagixClient { val endpoint = runBlocking { - MagixEndpoint.rSocketWithWebSockets(host, payloadSerializer, port, path) + MagixEndpoint.rSocketWithWebSockets(host, port, path) } return ControlsMagixClient(endpoint, MagixMessageFilter()) } diff --git a/magix/magix-rabbit/build.gradle.kts b/magix/magix-rabbit/build.gradle.kts new file mode 100644 index 0000000..3d7bc4d --- /dev/null +++ b/magix/magix-rabbit/build.gradle.kts @@ -0,0 +1,17 @@ +plugins { + id("space.kscience.gradle.jvm") + `maven-publish` +} + +description = """ + RabbitMQ client magix endpoint +""".trimIndent() + +dependencies { + api(projects.magix.magixApi) + implementation("com.rabbitmq:amqp-client:5.14.2") +} + +readme{ + maturity = space.kscience.gradle.Maturity.PROTOTYPE +} diff --git a/magix/magix-rabbit/src/main/kotlin/space/kscience/magix/rabbit/RabbitMQMagixEndpoint.kt b/magix/magix-rabbit/src/main/kotlin/space/kscience/magix/rabbit/RabbitMQMagixEndpoint.kt new file mode 100644 index 0000000..33af457 --- /dev/null +++ b/magix/magix-rabbit/src/main/kotlin/space/kscience/magix/rabbit/RabbitMQMagixEndpoint.kt @@ -0,0 +1,80 @@ +package space.kscience.magix.rabbit + +import com.rabbitmq.client.* +import kotlinx.coroutines.cancel +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.launch +import space.kscience.magix.api.MagixEndpoint +import space.kscience.magix.api.MagixMessage +import space.kscience.magix.api.MagixMessageFilter +import space.kscience.magix.api.filter +import space.kscience.magix.rabbit.RabbitMQMagixEndpoint.Companion.DEFAULT_MAGIX_QUEUE_NAME + +/** + * A magix endpoint for RabbitMQ message broker + */ +public class RabbitMQMagixEndpoint( + private val connection: Connection, + private val queueName: String = DEFAULT_MAGIX_QUEUE_NAME, +) : MagixEndpoint, AutoCloseable { + + private val rabbitChannel by lazy { + connection.createChannel().apply { + queueDeclare(queueName, false, false, false, null) + } + } + + override fun subscribe(filter: MagixMessageFilter): Flow = callbackFlow { + val deliverCallback: DeliverCallback = DeliverCallback { _: String, message: Delivery -> + val magixMessage = MagixEndpoint.magixJson.decodeFromString( + MagixMessage.serializer(), message.body.decodeToString() + ) + launch { + send(magixMessage) + } + } + + val cancelCallback: CancelCallback = CancelCallback { + cancel("Rabbit consumer is closed") + } + + val consumerTag = rabbitChannel.basicConsume( + queueName, + true, + deliverCallback, + cancelCallback + ) + + awaitClose { + rabbitChannel.basicCancel(consumerTag) + } + }.filter(filter) + + override suspend fun broadcast(message: MagixMessage) { + rabbitChannel.basicPublish( + "", + queueName, + null, + MagixEndpoint.magixJson.encodeToString(MagixMessage.serializer(), message).encodeToByteArray() + ) + } + + override fun close() { + rabbitChannel.close() + connection.close() + } + + public companion object { + public const val DEFAULT_MAGIX_QUEUE_NAME: String = "magix" + } +} + +public fun MagixEndpoint.Companion.rabbit( + address: String, + queueName: String = DEFAULT_MAGIX_QUEUE_NAME, +): RabbitMQMagixEndpoint { + val connection = ConnectionFactory().newConnection(address) + return RabbitMQMagixEndpoint(connection, queueName) +} \ No newline at end of file diff --git a/magix/magix-rsocket/build.gradle.kts b/magix/magix-rsocket/build.gradle.kts index 1e0647c..39aa8aa 100644 --- a/magix/magix-rsocket/build.gradle.kts +++ b/magix/magix-rsocket/build.gradle.kts @@ -1,5 +1,5 @@ plugins { - id("ru.mipt.npm.gradle.mpp") + id("space.kscience.gradle.mpp") `maven-publish` } @@ -7,22 +7,31 @@ description = """ Magix endpoint (client) based on RSocket """.trimIndent() -kscience { - useSerialization { - json() - } -} - val ktorVersion: String by rootProject.extra val rsocketVersion: String by rootProject.extra +kscience { + jvm() + js() + native() + useSerialization { + json() + } + dependencies { + api(projects.magix.magixApi) + implementation("io.ktor:ktor-client-core:$ktorVersion") + implementation("io.rsocket.kotlin:rsocket-ktor-client:$rsocketVersion") + } + dependencies(jvmMain) { + implementation("io.rsocket.kotlin:rsocket-transport-ktor-tcp:$rsocketVersion") + } +} + kotlin { sourceSets { - commonMain { + getByName("linuxX64Main") { dependencies { - api(projects.magix.magixApi) - implementation("io.ktor:ktor-client-core:$ktorVersion") - implementation("io.rsocket.kotlin:rsocket-transport-ktor-client:$rsocketVersion") + implementation("io.rsocket.kotlin:rsocket-transport-ktor-tcp:$rsocketVersion") } } } diff --git a/magix/magix-rsocket/src/commonMain/kotlin/ru/mipt/npm/magix/rsocket/RSocketMagixEndpoint.kt b/magix/magix-rsocket/src/commonMain/kotlin/space/kscience/magix/rsocket/RSocketMagixEndpoint.kt similarity index 51% rename from magix/magix-rsocket/src/commonMain/kotlin/ru/mipt/npm/magix/rsocket/RSocketMagixEndpoint.kt rename to magix/magix-rsocket/src/commonMain/kotlin/space/kscience/magix/rsocket/RSocketMagixEndpoint.kt index 42639ab..43c505c 100644 --- a/magix/magix-rsocket/src/commonMain/kotlin/ru/mipt/npm/magix/rsocket/RSocketMagixEndpoint.kt +++ b/magix/magix-rsocket/src/commonMain/kotlin/space/kscience/magix/rsocket/RSocketMagixEndpoint.kt @@ -1,51 +1,50 @@ -package ru.mipt.npm.magix.rsocket +package space.kscience.magix.rsocket import io.ktor.client.HttpClient -import io.ktor.client.features.websocket.WebSockets +import io.ktor.client.plugins.websocket.WebSockets +import io.ktor.utils.io.core.Closeable import io.rsocket.kotlin.RSocket import io.rsocket.kotlin.core.RSocketConnector import io.rsocket.kotlin.core.RSocketConnectorBuilder +import io.rsocket.kotlin.ktor.client.RSocketSupport +import io.rsocket.kotlin.ktor.client.rSocket import io.rsocket.kotlin.payload.buildPayload import io.rsocket.kotlin.payload.data -import io.rsocket.kotlin.transport.ktor.client.RSocketSupport -import io.rsocket.kotlin.transport.ktor.client.rSocket -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.* import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map -import kotlinx.coroutines.withContext -import kotlinx.serialization.KSerializer -import kotlinx.serialization.encodeToString -import ru.mipt.npm.magix.api.MagixEndpoint -import ru.mipt.npm.magix.api.MagixMessage -import ru.mipt.npm.magix.api.MagixMessageFilter +import space.kscience.magix.api.MagixEndpoint +import space.kscience.magix.api.MagixMessage +import space.kscience.magix.api.MagixMessageFilter +import space.kscience.magix.api.filter import kotlin.coroutines.CoroutineContext import kotlin.coroutines.coroutineContext -public class RSocketMagixEndpoint( - payloadSerializer: KSerializer, +public class RSocketMagixEndpoint( private val rSocket: RSocket, private val coroutineContext: CoroutineContext, -) : MagixEndpoint { - - private val serializer = MagixMessage.serializer(payloadSerializer) +) : MagixEndpoint, Closeable { override fun subscribe( filter: MagixMessageFilter, - ): Flow> { - val payload = buildPayload { data(MagixEndpoint.magixJson.encodeToString(filter)) } + ): Flow { + val payload = buildPayload { data(MagixEndpoint.magixJson.encodeToString(MagixMessageFilter.serializer(), filter)) } val flow = rSocket.requestStream(payload) return flow.map { - MagixEndpoint.magixJson.decodeFromString(serializer, it.data.readText()) - }.flowOn(coroutineContext[CoroutineDispatcher]?:Dispatchers.Unconfined) + MagixEndpoint.magixJson.decodeFromString(MagixMessage.serializer(), it.data.readText()) + }.filter(filter).flowOn(coroutineContext[CoroutineDispatcher] ?: Dispatchers.Unconfined) } - override suspend fun broadcast(message: MagixMessage) { - withContext(coroutineContext) { - val payload = buildPayload { data(MagixEndpoint.magixJson.encodeToString(serializer, message)) } - rSocket.fireAndForget(payload) + override suspend fun broadcast(message: MagixMessage): Unit = withContext(coroutineContext) { + val payload = buildPayload { + data(MagixEndpoint.magixJson.encodeToString(MagixMessage.serializer(), message)) } + rSocket.fireAndForget(payload) + } + + override fun close() { + rSocket.cancel() } public companion object @@ -61,13 +60,12 @@ internal fun buildConnector(rSocketConfig: RSocketConnectorBuilder.ConnectionCon /** * Build a websocket based endpoint connected to [host], [port] and given routing [path] */ -public suspend fun MagixEndpoint.Companion.rSocketWithWebSockets( +public suspend fun MagixEndpoint.Companion.rSocketWithWebSockets( host: String, - payloadSerializer: KSerializer, port: Int = DEFAULT_MAGIX_HTTP_PORT, path: String = "/rsocket", rSocketConfig: RSocketConnectorBuilder.ConnectionConfigBuilder.() -> Unit = {}, -): RSocketMagixEndpoint { +): RSocketMagixEndpoint { val client = HttpClient { install(WebSockets) install(RSocketSupport) { @@ -78,9 +76,9 @@ public suspend fun MagixEndpoint.Companion.rSocketWithWebSockets( val rSocket = client.rSocket(host, port, path) //Ensure client is closed after rSocket if finished - rSocket.job.invokeOnCompletion { + rSocket.coroutineContext[Job]?.invokeOnCompletion { client.close() } - return RSocketMagixEndpoint(payloadSerializer, rSocket, coroutineContext) + return RSocketMagixEndpoint(rSocket, coroutineContext) } \ No newline at end of file diff --git a/magix/magix-rsocket/src/commonMain/kotlin/space/kscience/magix/rsocket/RSocketStreamMagixEndpoint.kt b/magix/magix-rsocket/src/commonMain/kotlin/space/kscience/magix/rsocket/RSocketStreamMagixEndpoint.kt new file mode 100644 index 0000000..1875602 --- /dev/null +++ b/magix/magix-rsocket/src/commonMain/kotlin/space/kscience/magix/rsocket/RSocketStreamMagixEndpoint.kt @@ -0,0 +1,98 @@ +package space.kscience.magix.rsocket + +import io.ktor.client.HttpClient +import io.ktor.client.plugins.websocket.WebSockets +import io.ktor.utils.io.core.Closeable +import io.rsocket.kotlin.RSocket +import io.rsocket.kotlin.core.RSocketConnectorBuilder +import io.rsocket.kotlin.ktor.client.RSocketSupport +import io.rsocket.kotlin.ktor.client.rSocket +import io.rsocket.kotlin.payload.Payload +import io.rsocket.kotlin.payload.buildPayload +import io.rsocket.kotlin.payload.data +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.cancel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import space.kscience.magix.api.MagixEndpoint +import space.kscience.magix.api.MagixMessage +import space.kscience.magix.api.MagixMessageFilter +import space.kscience.magix.api.filter +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.coroutineContext + +/** + * RSocket endpoint based on established channel. This way it works a bit faster than [RSocketMagixEndpoint] + * for sending and receiving, but less flexible in terms of filters. One general [streamFilter] could be set + * in constructor and applied on the loop side. Filters in [subscribe] are applied on the endpoint side on top + * of received data. + */ +public class RSocketStreamMagixEndpoint( + private val rSocket: RSocket, + private val coroutineContext: CoroutineContext, + public val streamFilter: MagixMessageFilter = MagixMessageFilter(), +) : MagixEndpoint, Closeable { + + private val output: MutableSharedFlow = MutableSharedFlow() + + private val input: Flow by lazy { + rSocket.requestChannel( + buildPayload { + data( + MagixEndpoint.magixJson.encodeToString( + MagixMessageFilter.serializer(), + streamFilter + ) + ) + }, + output.map { message -> + buildPayload { + data(MagixEndpoint.magixJson.encodeToString(MagixMessage.serializer(), message)) + } + }.flowOn(coroutineContext[CoroutineDispatcher] ?: Dispatchers.Unconfined) + ) + } + + override fun subscribe( + filter: MagixMessageFilter, + ): Flow { + return input.map { + MagixEndpoint.magixJson.decodeFromString(MagixMessage.serializer(), it.data.readText()) + }.filter(filter).flowOn(coroutineContext[CoroutineDispatcher] ?: Dispatchers.Unconfined) + } + + override suspend fun broadcast(message: MagixMessage): Unit { + output.emit(message) + } + + override fun close() { + rSocket.cancel() + } +} + +public suspend fun MagixEndpoint.Companion.rSocketStreamWithWebSockets( + host: String, + port: Int = DEFAULT_MAGIX_HTTP_PORT, + path: String = "/rsocket", + rSocketConfig: RSocketConnectorBuilder.ConnectionConfigBuilder.() -> Unit = {}, +): RSocketStreamMagixEndpoint { + 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.coroutineContext[Job]?.invokeOnCompletion { + client.close() + } + + return RSocketStreamMagixEndpoint(rSocket, coroutineContext) +} \ No newline at end of file diff --git a/magix/magix-rsocket/src/jvmMain/kotlin/space/kscience/magix/rsocket/withTcp.kt b/magix/magix-rsocket/src/jvmMain/kotlin/space/kscience/magix/rsocket/withTcp.kt new file mode 100644 index 0000000..23fe0be --- /dev/null +++ b/magix/magix-rsocket/src/jvmMain/kotlin/space/kscience/magix/rsocket/withTcp.kt @@ -0,0 +1,44 @@ +package space.kscience.magix.rsocket + +import io.ktor.network.sockets.SocketOptions +import io.rsocket.kotlin.core.RSocketConnectorBuilder +import io.rsocket.kotlin.transport.ktor.tcp.TcpClientTransport +import space.kscience.magix.api.MagixEndpoint +import kotlin.coroutines.coroutineContext + + +/** + * Create a plain TCP based [RSocketMagixEndpoint] connected to [host] and [port] + */ +public suspend fun MagixEndpoint.Companion.rSocketWithTcp( + host: String, + port: Int = DEFAULT_MAGIX_RAW_PORT, + tcpConfig: SocketOptions.TCPClientSocketOptions.() -> Unit = {}, + rSocketConfig: RSocketConnectorBuilder.ConnectionConfigBuilder.() -> Unit = {}, +): RSocketMagixEndpoint { + val transport = TcpClientTransport( + hostname = host, + port = port, + configure = tcpConfig + ) + val rSocket = buildConnector(rSocketConfig).connect(transport) + + return RSocketMagixEndpoint(rSocket, coroutineContext) +} + + +public suspend fun MagixEndpoint.Companion.rSocketStreamWithTcp( + host: String, + port: Int = DEFAULT_MAGIX_RAW_PORT, + tcpConfig: SocketOptions.TCPClientSocketOptions.() -> Unit = {}, + rSocketConfig: RSocketConnectorBuilder.ConnectionConfigBuilder.() -> Unit = {}, +): RSocketStreamMagixEndpoint { + val transport = TcpClientTransport( + hostname = host, + port = port, + configure = tcpConfig + ) + val rSocket = buildConnector(rSocketConfig).connect(transport) + + return RSocketStreamMagixEndpoint(rSocket, coroutineContext) +} \ No newline at end of file diff --git a/magix/magix-rsocket/src/jvmMain/kotlin/ru/mipt/npm/magix/rsocket/withTcp.kt b/magix/magix-rsocket/src/linuxX64Main/kotlin/rsocket/withTcp.kt similarity index 52% rename from magix/magix-rsocket/src/jvmMain/kotlin/ru/mipt/npm/magix/rsocket/withTcp.kt rename to magix/magix-rsocket/src/linuxX64Main/kotlin/rsocket/withTcp.kt index 90c2ffa..117bcdd 100644 --- a/magix/magix-rsocket/src/jvmMain/kotlin/ru/mipt/npm/magix/rsocket/withTcp.kt +++ b/magix/magix-rsocket/src/linuxX64Main/kotlin/rsocket/withTcp.kt @@ -1,34 +1,29 @@ -package ru.mipt.npm.magix.rsocket +package rsocket -import io.ktor.network.selector.ActorSelectorManager import io.ktor.network.sockets.SocketOptions -import io.ktor.util.InternalAPI import io.rsocket.kotlin.core.RSocketConnectorBuilder -import io.rsocket.kotlin.transport.ktor.TcpClientTransport -import kotlinx.coroutines.Dispatchers -import kotlinx.serialization.KSerializer -import ru.mipt.npm.magix.api.MagixEndpoint +import io.rsocket.kotlin.transport.ktor.tcp.TcpClientTransport +import space.kscience.magix.api.MagixEndpoint +import space.kscience.magix.rsocket.RSocketMagixEndpoint +import space.kscience.magix.rsocket.buildConnector import kotlin.coroutines.coroutineContext /** * Create a plain TCP based [RSocketMagixEndpoint] connected to [host] and [port] */ -@OptIn(InternalAPI::class) -public suspend fun MagixEndpoint.Companion.rSocketWithTcp( +public suspend fun MagixEndpoint.Companion.rSocketWithTcp( host: String, - payloadSerializer: KSerializer, port: Int = DEFAULT_MAGIX_RAW_PORT, tcpConfig: SocketOptions.TCPClientSocketOptions.() -> Unit = {}, rSocketConfig: RSocketConnectorBuilder.ConnectionConfigBuilder.() -> Unit = {}, -): RSocketMagixEndpoint { +): RSocketMagixEndpoint { val transport = TcpClientTransport( - ActorSelectorManager(Dispatchers.IO), hostname = host, port = port, configure = tcpConfig ) val rSocket = buildConnector(rSocketConfig).connect(transport) - return RSocketMagixEndpoint(payloadSerializer, rSocket, coroutineContext) + return RSocketMagixEndpoint(rSocket, coroutineContext) } diff --git a/magix/magix-server/build.gradle.kts b/magix/magix-server/build.gradle.kts index 7f01ea8..4a3b102 100644 --- a/magix/magix-server/build.gradle.kts +++ b/magix/magix-server/build.gradle.kts @@ -1,5 +1,5 @@ plugins { - id("ru.mipt.npm.gradle.jvm") + id("space.kscience.gradle.jvm") `maven-publish` application } @@ -16,17 +16,16 @@ kscience { val dataforgeVersion: String by rootProject.extra val rsocketVersion: String by rootProject.extra -val ktorVersion: String = ru.mipt.npm.gradle.KScienceVersions.ktorVersion +val ktorVersion: String = space.kscience.gradle.KScienceVersions.ktorVersion dependencies{ - api(project(":magix:magix-api")) + api(projects.magix.magixApi) api("io.ktor:ktor-server-cio:$ktorVersion") - api("io.ktor:ktor-websockets:$ktorVersion") - api("io.ktor:ktor-serialization:$ktorVersion") - api("io.ktor:ktor-html-builder:$ktorVersion") + api("io.ktor:ktor-server-websockets:$ktorVersion") + api("io.ktor:ktor-server-content-negotiation:$ktorVersion") + api("io.ktor:ktor-serialization-kotlinx-json:$ktorVersion") + api("io.ktor:ktor-server-html-builder:$ktorVersion") - api("io.rsocket.kotlin:rsocket-core:$rsocketVersion") - api("io.rsocket.kotlin:rsocket-transport-ktor-server:$rsocketVersion") - - api("org.zeromq:jeromq:0.5.2") + api("io.rsocket.kotlin:rsocket-ktor-server:$rsocketVersion") + api("io.rsocket.kotlin:rsocket-transport-ktor-tcp:$rsocketVersion") } \ No newline at end of file diff --git a/magix/magix-server/src/main/kotlin/ru/mipt/npm/magix/server/magixModule.kt b/magix/magix-server/src/main/kotlin/ru/mipt/npm/magix/server/magixModule.kt deleted file mode 100644 index 3211243..0000000 --- a/magix/magix-server/src/main/kotlin/ru/mipt/npm/magix/server/magixModule.kt +++ /dev/null @@ -1,164 +0,0 @@ -package ru.mipt.npm.magix.server - -import io.ktor.application.* -import io.ktor.features.CORS -import io.ktor.features.ContentNegotiation -import io.ktor.html.respondHtml -import io.ktor.request.receive -import io.ktor.routing.get -import io.ktor.routing.post -import io.ktor.routing.route -import io.ktor.routing.routing -import io.ktor.serialization.json -import io.ktor.util.getValue -import io.ktor.websocket.WebSockets -import io.rsocket.kotlin.ConnectionAcceptor -import io.rsocket.kotlin.RSocketRequestHandler -import io.rsocket.kotlin.payload.Payload -import io.rsocket.kotlin.payload.buildPayload -import io.rsocket.kotlin.payload.data -import io.rsocket.kotlin.transport.ktor.server.RSocketSupport -import io.rsocket.kotlin.transport.ktor.server.rSocket -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.* -import kotlinx.html.* -import kotlinx.serialization.KSerializer -import kotlinx.serialization.json.JsonElement -import ru.mipt.npm.magix.api.MagixEndpoint.Companion.magixJson -import ru.mipt.npm.magix.api.MagixMessage -import ru.mipt.npm.magix.api.MagixMessageFilter -import ru.mipt.npm.magix.api.filter -import java.util.* - -public typealias GenericMagixMessage = MagixMessage - -internal val genericMessageSerializer: KSerializer> = - MagixMessage.serializer(JsonElement.serializer()) - - -internal fun CoroutineScope.magixAcceptor(magixFlow: MutableSharedFlow) = ConnectionAcceptor { - RSocketRequestHandler { - //handler for request/stream - requestStream { request: Payload -> - val filter = magixJson.decodeFromString(MagixMessageFilter.serializer(), request.data.readText()) - magixFlow.filter(filter).map { message -> - val string = magixJson.encodeToString(genericMessageSerializer, message) - buildPayload { data(string) } - } - } - fireAndForget { request: Payload -> - val message = magixJson.decodeFromString(genericMessageSerializer, request.data.readText()) - magixFlow.emit(message) - } - // bi-directional connection - requestChannel { request: Payload, input: Flow -> - input.onEach { - magixFlow.emit(magixJson.decodeFromString(genericMessageSerializer, it.data.readText())) - }.launchIn(this@magixAcceptor) - - val filter = magixJson.decodeFromString(MagixMessageFilter.serializer(), request.data.readText()) - - magixFlow.filter(filter).map { message -> - val string = magixJson.encodeToString(genericMessageSerializer, message) - buildPayload { data(string) } - } - } - } -} - -/** - * Create a message filter from call parameters - */ -private fun ApplicationCall.buildFilter(): MagixMessageFilter { - val query = request.queryParameters - - if (query.isEmpty()) { - return MagixMessageFilter.ALL - } - - val format: List? by query - val origin: List? by query - return MagixMessageFilter( - format, - origin - ) -} - -/** - * Attache magix http/sse and websocket-based rsocket event loop + statistics page to existing [MutableSharedFlow] - */ -public fun Application.magixModule(magixFlow: MutableSharedFlow, route: String = "/") { - if (featureOrNull(WebSockets) == null) { - install(WebSockets) - } - - if (featureOrNull(CORS) == null) { - install(CORS) { - //TODO consider more safe policy - anyHost() - } - } - if (featureOrNull(ContentNegotiation) == null) { - install(ContentNegotiation) { - json() - } - } - - if (featureOrNull(RSocketSupport) == null) { - install(RSocketSupport) - } - - routing { - route(route) { - get("state") { - call.respondHtml { - head { - meta { - httpEquiv = "refresh" - content = "2" - } - } - body { - h1 { +"Magix loop statistics" } - h2 { +"Number of subscribers: ${magixFlow.subscriptionCount.value}" } - h3 { +"Replay cache size: ${magixFlow.replayCache.size}" } - h3 { +"Replay cache:" } - ol { - magixFlow.replayCache.forEach { message -> - li { - code { - +magixJson.encodeToString(genericMessageSerializer, message) - } - } - } - } - } - } - } - //SSE server. Filter from query - get("sse") { - val filter = call.buildFilter() - val sseFlow = magixFlow.filter(filter).map { - val data = magixJson.encodeToString(genericMessageSerializer, it) - val id = UUID.randomUUID() - SseEvent(data, id = id.toString(), event = "message") - } - call.respondSse(sseFlow) - } - post("broadcast") { - val message = call.receive() - magixFlow.emit(message) - } - //rSocket server. Filter from Payload - rSocket("rsocket", acceptor = magixAcceptor(magixFlow)) - } - } -} - -/** - * Create a new loop [MutableSharedFlow] with given [buffer] and setup magix module based on it - */ -public fun Application.magixModule(route: String = "/", buffer: Int = 100) { - val magixFlow = MutableSharedFlow(buffer) - magixModule(magixFlow, route) -} \ No newline at end of file diff --git a/magix/magix-server/src/main/kotlin/ru/mipt/npm/magix/server/server.kt b/magix/magix-server/src/main/kotlin/ru/mipt/npm/magix/server/server.kt deleted file mode 100644 index 8f425e3..0000000 --- a/magix/magix-server/src/main/kotlin/ru/mipt/npm/magix/server/server.kt +++ /dev/null @@ -1,77 +0,0 @@ -package ru.mipt.npm.magix.server - -import io.ktor.application.Application -import io.ktor.network.selector.ActorSelectorManager -import io.ktor.server.cio.CIO -import io.ktor.server.engine.ApplicationEngine -import io.ktor.server.engine.embeddedServer -import io.ktor.util.InternalAPI -import io.rsocket.kotlin.core.RSocketServer -import io.rsocket.kotlin.transport.ktor.TcpServerTransport -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import kotlinx.coroutines.flow.MutableSharedFlow -import org.slf4j.LoggerFactory -import ru.mipt.npm.magix.api.MagixEndpoint -import ru.mipt.npm.magix.api.MagixEndpoint.Companion.DEFAULT_MAGIX_HTTP_PORT -import ru.mipt.npm.magix.api.MagixEndpoint.Companion.DEFAULT_MAGIX_RAW_PORT - -/** - * Raw TCP magix server - */ -@OptIn(InternalAPI::class) -public fun CoroutineScope.launchMagixServerRawRSocket( - magixFlow: MutableSharedFlow, - rawSocketPort: Int = DEFAULT_MAGIX_RAW_PORT -): Job { - val tcpTransport = TcpServerTransport(ActorSelectorManager(Dispatchers.IO), port = rawSocketPort) - val rSocketJob = RSocketServer().bind(tcpTransport, magixAcceptor(magixFlow)) - coroutineContext[Job]?.invokeOnCompletion { - rSocketJob.cancel() - } - return rSocketJob; -} - -/** - * A combined RSocket/TCP server - * @param applicationConfiguration optional additional configuration for magix loop server - */ -public fun CoroutineScope.startMagixServer( - port: Int = DEFAULT_MAGIX_HTTP_PORT, - buffer: Int = 100, - enableRawRSocket: Boolean = true, - enableZmq: Boolean = true, - applicationConfiguration: Application.(MutableSharedFlow) -> Unit = {} -): ApplicationEngine { - val logger = LoggerFactory.getLogger("magix-server") - val magixFlow = MutableSharedFlow( - buffer, - extraBufferCapacity = buffer - ) - - if (enableRawRSocket) { - //Start tcpRSocket server - val rawRSocketPort = DEFAULT_MAGIX_RAW_PORT - logger.info("Starting magix raw rsocket server on port $rawRSocketPort") - launchMagixServerRawRSocket(magixFlow, rawRSocketPort) - } - if (enableZmq) { - //Start ZMQ server socket pair - val zmqPubSocketPort: Int = MagixEndpoint.DEFAULT_MAGIX_ZMQ_PUB_PORT - val zmqPullSocketPort: Int = MagixEndpoint.DEFAULT_MAGIX_ZMQ_PULL_PORT - logger.info("Starting magix zmq server on pub port $zmqPubSocketPort and pull port $zmqPullSocketPort") - launchMagixServerZmqSocket( - magixFlow, - zmqPubSocketPort = zmqPubSocketPort, - zmqPullSocketPort = zmqPullSocketPort - ) - } - - return embeddedServer(CIO, host = "localhost", port = port) { - magixModule(magixFlow) - applicationConfiguration(magixFlow) - }.apply { - start() - } -} \ No newline at end of file diff --git a/magix/magix-server/src/main/kotlin/ru/mipt/npm/magix/server/zmqMagixServerSocket.kt b/magix/magix-server/src/main/kotlin/ru/mipt/npm/magix/server/zmqMagixServerSocket.kt deleted file mode 100644 index e62acd7..0000000 --- a/magix/magix-server/src/main/kotlin/ru/mipt/npm/magix/server/zmqMagixServerSocket.kt +++ /dev/null @@ -1,45 +0,0 @@ -package ru.mipt.npm.magix.server - -import kotlinx.coroutines.* -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach -import org.slf4j.LoggerFactory -import org.zeromq.SocketType -import org.zeromq.ZContext -import ru.mipt.npm.magix.api.MagixEndpoint - -public fun CoroutineScope.launchMagixServerZmqSocket( - magixFlow: MutableSharedFlow, - localHost: String = "tcp://*", - zmqPubSocketPort: Int = MagixEndpoint.DEFAULT_MAGIX_ZMQ_PUB_PORT, - zmqPullSocketPort: Int = MagixEndpoint.DEFAULT_MAGIX_ZMQ_PULL_PORT, -): Job = launch(Dispatchers.IO) { - val logger = LoggerFactory.getLogger("magix-server-zmq") - - ZContext().use { context -> - //launch publishing job - val pubSocket = context.createSocket(SocketType.PUB) - pubSocket.bind("$localHost:$zmqPubSocketPort") - magixFlow.onEach { message -> - val string = MagixEndpoint.magixJson.encodeToString(genericMessageSerializer, message) - pubSocket.send(string) - logger.debug("Published: $string") - }.launchIn(this) - - //launch pulling job - val pullSocket = context.createSocket(SocketType.PULL) - pullSocket.bind("$localHost:$zmqPullSocketPort") - pullSocket.receiveTimeOut = 500 - //suspending loop while pulling is active - while (isActive) { - val string: String? = pullSocket.recvStr() - if (string != null) { - logger.debug("Received: $string") - val message = MagixEndpoint.magixJson.decodeFromString(genericMessageSerializer, string) - magixFlow.emit(message) - } - } - } -} - diff --git a/magix/magix-server/src/main/kotlin/space/kscience/magix/server/RSocketMagixFlowPlugin.kt b/magix/magix-server/src/main/kotlin/space/kscience/magix/server/RSocketMagixFlowPlugin.kt new file mode 100644 index 0000000..a2f875d --- /dev/null +++ b/magix/magix-server/src/main/kotlin/space/kscience/magix/server/RSocketMagixFlowPlugin.kt @@ -0,0 +1,74 @@ +package space.kscience.magix.server + +import io.rsocket.kotlin.ConnectionAcceptor +import io.rsocket.kotlin.RSocketRequestHandler +import io.rsocket.kotlin.core.RSocketServer +import io.rsocket.kotlin.payload.Payload +import io.rsocket.kotlin.payload.buildPayload +import io.rsocket.kotlin.payload.data +import io.rsocket.kotlin.transport.ktor.tcp.TcpServer +import io.rsocket.kotlin.transport.ktor.tcp.TcpServerTransport +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.* +import kotlinx.serialization.encodeToString +import space.kscience.magix.api.* +import space.kscience.magix.api.MagixEndpoint.Companion.DEFAULT_MAGIX_RAW_PORT + +/** + * Raw TCP magix server + */ +public class RSocketMagixFlowPlugin(public val port: Int = DEFAULT_MAGIX_RAW_PORT): MagixFlowPlugin { + override fun start(scope: CoroutineScope, magixFlow: MutableSharedFlow): Job { + val tcpTransport = TcpServerTransport(port = port) + val rSocketJob: TcpServer = RSocketServer().bindIn(scope, tcpTransport, acceptor(scope, magixFlow)) + + scope.coroutineContext[Job]?.invokeOnCompletion { + rSocketJob.handlerJob.cancel() + } + + return rSocketJob.handlerJob + } + + public companion object{ + public fun acceptor( + coroutineScope: CoroutineScope, + magixFlow: MutableSharedFlow, + ): ConnectionAcceptor = ConnectionAcceptor { + RSocketRequestHandler(coroutineScope.coroutineContext) { + //handler for request/stream + requestStream { request: Payload -> + val filter = MagixEndpoint.magixJson.decodeFromString(MagixMessageFilter.serializer(), request.data.readText()) + magixFlow.filter(filter).map { message -> + val string = MagixEndpoint.magixJson.encodeToString(MagixMessage.serializer(), message) + buildPayload { data(string) } + } + } + //single send + fireAndForget { request: Payload -> + val message = MagixEndpoint.magixJson.decodeFromString(MagixMessage.serializer(), request.data.readText()) + magixFlow.emit(message) + } + // bi-directional connection + requestChannel { request: Payload, input: Flow -> + input.onEach { + magixFlow.emit(MagixEndpoint.magixJson.decodeFromString(MagixMessage.serializer(), it.data.readText())) + }.launchIn(this) + + val filterText = request.data.readText() + + val filter = if(filterText.isNotBlank()){ + MagixEndpoint.magixJson.decodeFromString(MagixMessageFilter.serializer(), filterText) + } else { + MagixMessageFilter() + } + + magixFlow.filter(filter).map { message -> + val string = MagixEndpoint.magixJson.encodeToString(message) + buildPayload { data(string) } + } + } + } + } + } +} \ No newline at end of file diff --git a/magix/magix-server/src/main/kotlin/space/kscience/magix/server/magixModule.kt b/magix/magix-server/src/main/kotlin/space/kscience/magix/server/magixModule.kt new file mode 100644 index 0000000..e5cb6cb --- /dev/null +++ b/magix/magix-server/src/main/kotlin/space/kscience/magix/server/magixModule.kt @@ -0,0 +1,119 @@ +package space.kscience.magix.server + +import io.ktor.serialization.kotlinx.json.json +import io.ktor.server.application.* +import io.ktor.server.html.respondHtml +import io.ktor.server.plugins.contentnegotiation.ContentNegotiation +import io.ktor.server.request.receive +import io.ktor.server.routing.* +import io.ktor.server.util.getValue +import io.ktor.server.websocket.WebSockets +import io.rsocket.kotlin.ktor.server.RSocketSupport +import io.rsocket.kotlin.ktor.server.rSocket +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.map +import kotlinx.html.* +import kotlinx.serialization.encodeToString +import space.kscience.magix.api.MagixEndpoint.Companion.magixJson +import space.kscience.magix.api.MagixMessage +import space.kscience.magix.api.MagixMessageFilter +import space.kscience.magix.api.filter +import java.util.* + + +/** + * Create a message filter from call parameters + */ +private fun ApplicationCall.buildFilter(): MagixMessageFilter { + val query = request.queryParameters + + if (query.isEmpty()) { + return MagixMessageFilter.ALL + } + + val format: List? by query + val origin: List? by query + return MagixMessageFilter( + format, + origin + ) +} + +/** + * Attach magix http/sse and websocket-based rsocket event loop + statistics page to existing [MutableSharedFlow] + */ +public fun Application.magixModule(magixFlow: MutableSharedFlow, route: String = "/") { + if (pluginOrNull(WebSockets) == null) { + install(WebSockets) + } + +// if (pluginOrNull(CORS) == null) { +// install(CORS) { +// //TODO consider more safe policy +// anyHost() +// } +// } + if (pluginOrNull(ContentNegotiation) == null) { + install(ContentNegotiation) { + json() + } + } + + if (pluginOrNull(RSocketSupport) == null) { + install(RSocketSupport) + } + + routing { + route(route) { + get("state") { + call.respondHtml { + head { + meta { + httpEquiv = "refresh" + content = "2" + } + } + body { + h1 { +"Magix loop statistics" } + h2 { +"Number of subscribers: ${magixFlow.subscriptionCount.value}" } + h3 { +"Replay cache size: ${magixFlow.replayCache.size}" } + h3 { +"Replay cache:" } + ol { + magixFlow.replayCache.forEach { message -> + li { + code { + +magixJson.encodeToString(message) + } + } + } + } + } + } + } + //SSE server. Filter from query + get("sse") { + val filter = call.buildFilter() + val sseFlow = magixFlow.filter(filter).map { + val data = magixJson.encodeToString(it) + val id = UUID.randomUUID() + SseEvent(data, id = id.toString(), event = "message") + } + call.respondSse(sseFlow) + } + post("broadcast") { + val message = call.receive() + magixFlow.emit(message) + } + //rSocket server. Filter from Payload + rSocket("rsocket", acceptor = RSocketMagixFlowPlugin.acceptor( application, magixFlow)) + } + } +} + +/** + * Create a new loop [MutableSharedFlow] with given [buffer] and setup magix module based on it + */ +public fun Application.magixModule(route: String = "/", buffer: Int = 100) { + val magixFlow = MutableSharedFlow(buffer) + magixModule(magixFlow, route) +} \ No newline at end of file diff --git a/magix/magix-server/src/main/kotlin/space/kscience/magix/server/server.kt b/magix/magix-server/src/main/kotlin/space/kscience/magix/server/server.kt new file mode 100644 index 0000000..2396e25 --- /dev/null +++ b/magix/magix-server/src/main/kotlin/space/kscience/magix/server/server.kt @@ -0,0 +1,36 @@ +package space.kscience.magix.server + +import io.ktor.server.cio.CIO +import io.ktor.server.engine.ApplicationEngine +import io.ktor.server.engine.embeddedServer +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.flow.MutableSharedFlow +import space.kscience.magix.api.MagixEndpoint.Companion.DEFAULT_MAGIX_HTTP_PORT +import space.kscience.magix.api.MagixFlowPlugin +import space.kscience.magix.api.MagixMessage + + +/** + * A combined RSocket/TCP/ZMQ server + */ +public fun CoroutineScope.startMagixServer( + vararg plugins: MagixFlowPlugin, + port: Int = DEFAULT_MAGIX_HTTP_PORT, + buffer: Int = 1000, +): ApplicationEngine { + + val magixFlow = MutableSharedFlow( + replay = buffer, + extraBufferCapacity = buffer, + onBufferOverflow = BufferOverflow.DROP_OLDEST + ) + + plugins.forEach { + it.start(this, magixFlow) + } + + return embeddedServer(CIO, host = "localhost", port = port, module = { magixModule(magixFlow) }).apply { + start() + } +} \ No newline at end of file diff --git a/magix/magix-server/src/main/kotlin/ru/mipt/npm/magix/server/sse.kt b/magix/magix-server/src/main/kotlin/space/kscience/magix/server/sse.kt similarity index 83% rename from magix/magix-server/src/main/kotlin/ru/mipt/npm/magix/server/sse.kt rename to magix/magix-server/src/main/kotlin/space/kscience/magix/server/sse.kt index a1c057f..21db0b9 100644 --- a/magix/magix-server/src/main/kotlin/ru/mipt/npm/magix/server/sse.kt +++ b/magix/magix-server/src/main/kotlin/space/kscience/magix/server/sse.kt @@ -1,14 +1,13 @@ -package ru.mipt.npm.magix.server +package space.kscience.magix.server -import io.ktor.application.ApplicationCall import io.ktor.http.CacheControl import io.ktor.http.ContentType -import io.ktor.response.cacheControl -import io.ktor.response.respondBytesWriter +import io.ktor.server.application.ApplicationCall +import io.ktor.server.response.cacheControl +import io.ktor.server.response.respondBytesWriter import io.ktor.utils.io.ByteWriteChannel import io.ktor.utils.io.writeStringUtf8 import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.collect /** * The data class representing a SSE Event that will be sent to the client. diff --git a/magix/magix-storage/magix-storage-mongo/build.gradle.kts b/magix/magix-storage/magix-storage-mongo/build.gradle.kts new file mode 100644 index 0000000..51ed9f4 --- /dev/null +++ b/magix/magix-storage/magix-storage-mongo/build.gradle.kts @@ -0,0 +1,15 @@ +plugins { + id("space.kscience.gradle.jvm") + `maven-publish` +} + +val kmongoVersion = "4.5.1" + +dependencies { + implementation(projects.controlsStorage) + implementation("org.litote.kmongo:kmongo-coroutine-serialization:$kmongoVersion") +} + +readme{ + maturity = space.kscience.gradle.Maturity.PROTOTYPE +} diff --git a/magix/magix-storage/magix-storage-mongo/src/main/kotlin/ru/mipt/npm/controls/mongo/MongoEventStorage.kt b/magix/magix-storage/magix-storage-mongo/src/main/kotlin/ru/mipt/npm/controls/mongo/MongoEventStorage.kt new file mode 100644 index 0000000..5d02e92 --- /dev/null +++ b/magix/magix-storage/magix-storage-mongo/src/main/kotlin/ru/mipt/npm/controls/mongo/MongoEventStorage.kt @@ -0,0 +1,71 @@ +package space.kscience.controls.mongo + +import kotlinx.serialization.KSerializer +import kotlinx.serialization.json.Json +import org.litote.kmongo.coroutine.CoroutineClient +import org.litote.kmongo.coroutine.coroutine +import org.litote.kmongo.coroutine.insertOne +import org.litote.kmongo.reactivestreams.KMongo +import space.kscience.controls.api.DeviceMessage +import space.kscience.controls.api.PropertyChangedMessage +import space.kscience.controls.storage.EventStorage +import ru.mipt.npm.magix.server.GenericMagixMessage +import space.kscience.dataforge.context.Context +import space.kscience.dataforge.context.Factory +import space.kscience.dataforge.meta.Meta +import space.kscience.dataforge.meta.get +import space.kscience.dataforge.meta.string +import space.kscience.dataforge.names.Name + +private const val DEFAULT_DEVICE_MESSAGE_DATABASE_NAME: String = "deviceMessage" +private const val DEFAULT_MAGIX_MESSAGE_DATABASE_NAME = "magixMessage" +private const val DEFAULT_MONGO_DATABASE_URL = "mongodb://mongoadmin:secret@localhost:27888" +private val MONGO_DEVICE_MESSAGE_DATABASE_NAME_PROPERTY: Name = Name.of("mongo", "deviceMessageDatabaseName") +public val MONGO_MAGIX_MESSAGE_DATABASE_NAME_PROPERTY: Name = Name.of("mongo", "magixMessageDatabaseName") +public val MONGO_DATABASE_URL_PROPERTY: Name = Name.of("mongo", "databaseUrl") + +internal class MongoEventStorage( + private val client: CoroutineClient, + private val meta: Meta = Meta.EMPTY, +) : EventStorage { + override suspend fun storeDeviceMessage(value: T, serializer: KSerializer) { + val collection = client + .getDatabase( + meta[MONGO_DEVICE_MESSAGE_DATABASE_NAME_PROPERTY]?.string + ?: DEFAULT_DEVICE_MESSAGE_DATABASE_NAME + ) + .getCollection() + + collection.insertOne(Json.encodeToString(serializer, value)) + } + + override suspend fun storeMagixMessage(value: T, serializer: KSerializer) { + val collection = client + .getDatabase(meta[MONGO_MAGIX_MESSAGE_DATABASE_NAME_PROPERTY]?.string + ?: DEFAULT_MAGIX_MESSAGE_DATABASE_NAME) + .getCollection() + + collection.insertOne(Json.encodeToString(serializer, value)) + } + + override suspend fun getPropertyHistory( + sourceDeviceName: String, + propertyName: String, + ): List { + TODO("Not yet implemented: problems with deserialization") + } + + override fun close() { + client.close() + } +} + +public object DefaultAsynchronousMongoClientFactory : Factory { + override fun invoke(meta: Meta, context: Context): EventStorage { + val client = meta[MONGO_DATABASE_URL_PROPERTY]?.string?.let { + KMongo.createClient(it).coroutine + } ?: KMongo.createClient(DEFAULT_MONGO_DATABASE_URL).coroutine + + return MongoEventStorage(client, meta) + } +} diff --git a/magix/magix-storage/magix-storage-xodus/build.gradle.kts b/magix/magix-storage/magix-storage-xodus/build.gradle.kts new file mode 100644 index 0000000..63a1b44 --- /dev/null +++ b/magix/magix-storage/magix-storage-xodus/build.gradle.kts @@ -0,0 +1,21 @@ +plugins { + id("space.kscience.gradle.jvm") + `maven-publish` +} + +val xodusVersion: String by rootProject.extra + +kscience{ + useCoroutines() +} + +dependencies { + api(projects.magix.magixApi) + implementation("org.jetbrains.xodus:xodus-entity-store:$xodusVersion") + + testImplementation(npmlibs.kotlinx.coroutines.test) +} + +readme{ + maturity = space.kscience.gradle.Maturity.PROTOTYPE +} diff --git a/magix/magix-storage/magix-storage-xodus/src/main/kotlin/space/kscience/magix/storage/xodus/XodusMagixStorage.kt b/magix/magix-storage/magix-storage-xodus/src/main/kotlin/space/kscience/magix/storage/xodus/XodusMagixStorage.kt new file mode 100644 index 0000000..39eb4ea --- /dev/null +++ b/magix/magix-storage/magix-storage-xodus/src/main/kotlin/space/kscience/magix/storage/xodus/XodusMagixStorage.kt @@ -0,0 +1,111 @@ +package space.kscience.magix.storage.xodus + +import jetbrains.exodus.entitystore.Entity +import jetbrains.exodus.entitystore.PersistentEntityStore +import jetbrains.exodus.entitystore.PersistentEntityStores +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.JsonObject +import space.kscience.magix.api.MagixEndpoint +import space.kscience.magix.api.MagixEndpoint.Companion.magixJson +import space.kscience.magix.api.MagixMessage +import space.kscience.magix.api.MagixMessageFilter +import java.nio.file.Path + +public class XodusMagixStorage( + scope: CoroutineScope, + private val store: PersistentEntityStore, + endpoint: MagixEndpoint, + filter: MagixMessageFilter = MagixMessageFilter(), +) : AutoCloseable { + + //TODO consider message buffering + internal val subscriptionJob = endpoint.subscribe(filter).onEach { message -> + store.executeInTransaction { transaction -> + transaction.newEntity(MAGIC_MESSAGE_ENTITY_TYPE).apply { + setProperty(MagixMessage::origin.name, message.origin) + setProperty(MagixMessage::format.name, message.format) + + setBlobString(MagixMessage::payload.name, MagixEndpoint.magixJson.encodeToString(message.payload)) + + message.target?.let { + setProperty(MagixMessage::target.name, it) + } + message.id?.let { + setProperty(MagixMessage::id.name, it) + } + message.parentId?.let { + setProperty(MagixMessage::parentId.name, it) + } + message.user?.let { + setBlobString(MagixMessage::user.name, MagixEndpoint.magixJson.encodeToString(it)) + } + } + } + }.launchIn(scope) + + private fun Entity.parseMagixMessage(): MagixMessage = MagixMessage( + format = getProperty(MagixMessage::format.name).toString(), + payload = getBlobString(MagixMessage::payload.name)?.let { + magixJson.parseToJsonElement(it) + } ?: JsonObject(emptyMap()), + origin = getProperty(MagixMessage::origin.name).toString(), + target = getProperty(MagixMessage::target.name)?.toString(), + id = getProperty(MagixMessage::id.name)?.toString(), + parentId = getProperty(MagixMessage::parentId.name)?.toString(), + user = getBlobString(MagixMessage::user.name)?.let { + magixJson.parseToJsonElement(it) + }, + ) + + public fun readByFormat( + format: String, + block: (Sequence) -> Unit, + ): Unit = store.executeInReadonlyTransaction { transaction -> + val sequence = transaction.find( + MAGIC_MESSAGE_ENTITY_TYPE, + MagixMessage::format.name, + format + ).asSequence().map { entity -> + entity.parseMagixMessage() + } + block(sequence) + } + + public fun readAll( + block: (Sequence) -> Unit, + ): Unit = store.executeInReadonlyTransaction { transaction -> + val sequence = transaction.getAll(MAGIC_MESSAGE_ENTITY_TYPE).asSequence().map { entity -> + entity.parseMagixMessage() + } + block(sequence) + } + + override fun close() { + subscriptionJob.cancel() + } + + public companion object { + public const val MAGIC_MESSAGE_ENTITY_TYPE: String = "magix.message" + } +} + +/** + * Start writing all incoming messages with given [filter] to [xodusStore] + */ +public fun MagixEndpoint.storeInXodus( + scope: CoroutineScope, + xodusStore: PersistentEntityStore, + filter: MagixMessageFilter = MagixMessageFilter(), +): XodusMagixStorage = XodusMagixStorage(scope, xodusStore, this, filter) + +public fun MagixEndpoint.storeInXodus( + scope: CoroutineScope, + path: Path, + filter: MagixMessageFilter = MagixMessageFilter(), +): XodusMagixStorage { + val store = PersistentEntityStores.newInstance(path.toFile()) + return XodusMagixStorage(scope, store, this, filter) +} \ No newline at end of file diff --git a/magix/magix-zmq/build.gradle.kts b/magix/magix-zmq/build.gradle.kts index cf9e4be..e7c9f77 100644 --- a/magix/magix-zmq/build.gradle.kts +++ b/magix/magix-zmq/build.gradle.kts @@ -1,5 +1,5 @@ plugins { - id("ru.mipt.npm.gradle.jvm") + id("space.kscience.gradle.jvm") `maven-publish` } @@ -9,5 +9,6 @@ description = """ dependencies { api(projects.magix.magixApi) + api("org.slf4j:slf4j-api:2.0.6") implementation("org.zeromq:jeromq:0.5.2") } diff --git a/magix/magix-zmq/src/main/kotlin/ru/mipt/npm/magix/zmq/ZmqMagixEndpoint.kt b/magix/magix-zmq/src/main/kotlin/space/kscince/magix/zmq/ZmqMagixEndpoint.kt similarity index 65% rename from magix/magix-zmq/src/main/kotlin/ru/mipt/npm/magix/zmq/ZmqMagixEndpoint.kt rename to magix/magix-zmq/src/main/kotlin/space/kscince/magix/zmq/ZmqMagixEndpoint.kt index d9d6be5..d8c8343 100644 --- a/magix/magix-zmq/src/main/kotlin/ru/mipt/npm/magix/zmq/ZmqMagixEndpoint.kt +++ b/magix/magix-zmq/src/main/kotlin/space/kscince/magix/zmq/ZmqMagixEndpoint.kt @@ -1,36 +1,33 @@ -package ru.mipt.npm.magix.zmq +package space.kscince.magix.zmq import kotlinx.coroutines.* import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.channelFlow import kotlinx.coroutines.flow.flowOn -import kotlinx.serialization.KSerializer import org.zeromq.SocketType import org.zeromq.ZContext import org.zeromq.ZMQ import org.zeromq.ZMQException -import ru.mipt.npm.magix.api.MagixEndpoint -import ru.mipt.npm.magix.api.MagixMessage -import ru.mipt.npm.magix.api.MagixMessageFilter -import ru.mipt.npm.magix.api.filter +import space.kscience.magix.api.MagixEndpoint +import space.kscience.magix.api.MagixMessage +import space.kscience.magix.api.MagixMessageFilter +import space.kscience.magix.api.filter import kotlin.coroutines.CoroutineContext import kotlin.coroutines.coroutineContext -public class ZmqMagixEndpoint( +public class ZmqMagixEndpoint( private val host: String, - payloadSerializer: KSerializer, + private val protocol: String, private val pubPort: Int = MagixEndpoint.DEFAULT_MAGIX_ZMQ_PUB_PORT, private val pullPort: Int = MagixEndpoint.DEFAULT_MAGIX_ZMQ_PULL_PORT, - private val coroutineContext: CoroutineContext = Dispatchers.IO -) : MagixEndpoint, AutoCloseable { + private val coroutineContext: CoroutineContext = Dispatchers.IO, +) : MagixEndpoint, AutoCloseable { private val zmqContext by lazy { ZContext() } - private val serializer = MagixMessage.serializer(payloadSerializer) - @OptIn(ExperimentalCoroutinesApi::class) - override fun subscribe(filter: MagixMessageFilter): Flow> { + override fun subscribe(filter: MagixMessageFilter): Flow { val socket = zmqContext.createSocket(SocketType.SUB) - socket.connect("$host:$pubPort") + socket.connect("$protocol://$host:$pubPort") socket.subscribe("") return channelFlow { @@ -42,7 +39,7 @@ public class ZmqMagixEndpoint( //This is a blocking call. val string: String? = socket.recvStr() if (string != null) { - val message = MagixEndpoint.magixJson.decodeFromString(serializer, string) + val message = MagixEndpoint.magixJson.decodeFromString(MagixMessage.serializer(), string) send(message) } } catch (t: Throwable) { @@ -54,19 +51,18 @@ public class ZmqMagixEndpoint( } } } - }.filter(filter).flowOn( - coroutineContext[CoroutineDispatcher] ?: Dispatchers.IO - ) //should be flown on IO because of blocking calls + }.filter(filter).flowOn(coroutineContext[CoroutineDispatcher] ?: Dispatchers.IO) + //should be flown on IO because of blocking calls } private val publishSocket by lazy { zmqContext.createSocket(SocketType.PUSH).apply { - connect("$host:$pullPort") + connect("$protocol://$host:$pullPort") } } - override suspend fun broadcast(message: MagixMessage): Unit = withContext(coroutineContext) { - val string = MagixEndpoint.magixJson.encodeToString(serializer, message) + override suspend fun broadcast(message: MagixMessage): Unit = withContext(coroutineContext) { + val string = MagixEndpoint.magixJson.encodeToString(MagixMessage.serializer(), message) publishSocket.send(string) } @@ -75,14 +71,14 @@ public class ZmqMagixEndpoint( } } -public suspend fun MagixEndpoint.Companion.zmq( +public suspend fun MagixEndpoint.Companion.zmq( host: String, - payloadSerializer: KSerializer, + protocol: String = "tcp", pubPort: Int = DEFAULT_MAGIX_ZMQ_PUB_PORT, pullPort: Int = DEFAULT_MAGIX_ZMQ_PULL_PORT, -): ZmqMagixEndpoint = ZmqMagixEndpoint( +): ZmqMagixEndpoint = ZmqMagixEndpoint( host, - payloadSerializer, + protocol, pubPort, pullPort, coroutineContext = coroutineContext diff --git a/magix/magix-zmq/src/main/kotlin/space/kscince/magix/zmq/ZmqMagixFlowPlugin.kt b/magix/magix-zmq/src/main/kotlin/space/kscince/magix/zmq/ZmqMagixFlowPlugin.kt new file mode 100644 index 0000000..1f075ac --- /dev/null +++ b/magix/magix-zmq/src/main/kotlin/space/kscince/magix/zmq/ZmqMagixFlowPlugin.kt @@ -0,0 +1,53 @@ +package space.kscince.magix.zmq + +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.encodeToString +import org.slf4j.LoggerFactory +import org.zeromq.SocketType +import org.zeromq.ZContext +import space.kscience.magix.api.MagixEndpoint +import space.kscience.magix.api.MagixFlowPlugin +import space.kscience.magix.api.MagixMessage + + +public class ZmqMagixFlowPlugin( + public val localHost: String = "tcp://*", + public val zmqPubSocketPort: Int = MagixEndpoint.DEFAULT_MAGIX_ZMQ_PUB_PORT, + public val zmqPullSocketPort: Int = MagixEndpoint.DEFAULT_MAGIX_ZMQ_PULL_PORT, +) : MagixFlowPlugin { + override fun start(scope: CoroutineScope, magixFlow: MutableSharedFlow): Job = + scope.launch(Dispatchers.IO) { + val logger = LoggerFactory.getLogger("magix-server-zmq") + + ZContext().use { context -> + //launch publishing job + val pubSocket = context.createSocket(SocketType.PUB) + pubSocket.bind("$localHost:$zmqPubSocketPort") + magixFlow.onEach { message -> + val string = MagixEndpoint.magixJson.encodeToString(message) + pubSocket.send(string) + logger.debug("Published: $string") + }.launchIn(this) + + //launch pulling job + val pullSocket = context.createSocket(SocketType.PULL) + pullSocket.bind("$localHost:$zmqPullSocketPort") + pullSocket.receiveTimeOut = 500 + //suspending loop while pulling is active + while (isActive) { + val string: String? = pullSocket.recvStr() + if (string != null) { + logger.debug("Received: $string") + val message = MagixEndpoint.magixJson.decodeFromString(string) + magixFlow.emit(message) + } + } + } + } + + +} \ No newline at end of file diff --git a/magix/rfc b/magix/rfc new file mode 160000 index 0000000..5ae42aa --- /dev/null +++ b/magix/rfc @@ -0,0 +1 @@ +Subproject commit 5ae42aa297fbd2ab2239601f064e1d1239487590 diff --git a/motors/src/main/kotlin/ru/mipt/npm/devices/pimotionmaster/PiMotionMasterDevice.kt b/motors/src/main/kotlin/ru/mipt/npm/devices/pimotionmaster/PiMotionMasterDevice.kt deleted file mode 100644 index 20c5dd1..0000000 --- a/motors/src/main/kotlin/ru/mipt/npm/devices/pimotionmaster/PiMotionMasterDevice.kt +++ /dev/null @@ -1,347 +0,0 @@ -@file:Suppress("unused", "MemberVisibilityCanBePrivate") - -package ru.mipt.npm.devices.pimotionmaster - -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.toList -import kotlinx.coroutines.flow.transformWhile -import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock -import kotlinx.coroutines.withTimeout -import ru.mipt.npm.controls.api.DeviceHub -import ru.mipt.npm.controls.api.PropertyDescriptor -import ru.mipt.npm.controls.base.* -import ru.mipt.npm.controls.controllers.duration -import ru.mipt.npm.controls.ports.* -import space.kscience.dataforge.context.* -import space.kscience.dataforge.meta.Meta -import space.kscience.dataforge.meta.double -import space.kscience.dataforge.meta.get -import space.kscience.dataforge.names.NameToken -import space.kscience.dataforge.values.asValue -import kotlin.collections.component1 -import kotlin.collections.component2 -import kotlin.time.Duration - -class PiMotionMasterDevice( - context: Context, - private val portFactory: PortFactory = KtorTcpPort, -) : DeviceBase(context), DeviceHub { - - private var port: Port? = null - //TODO make proxy work - //PortProxy { portFactory(address ?: error("The device is not connected"), context) } - - - val connected by readingBoolean(false, descriptorBuilder = { - info = "True if the connection address is defined and the device is initialized" - }) { - port != null - } - - - val connect: DeviceAction by acting({ - info = "Connect to specific port and initialize axis" - }) { portSpec -> - //Clear current actions if present - if (port != null) { - disconnect() - } - //Update port - //address = portSpec.node - port = portFactory(portSpec ?: Meta.EMPTY, context) - connected.updateLogical(true) -// connector.open() - //Initialize axes - if (portSpec != null) { - val idn = identity.read() - failIfError { "Can't connect to $portSpec. Error code: $it" } - logger.info { "Connected to $idn on $portSpec" } - val ids = request("SAI?").map { it.trim() } - if (ids != axes.keys.toList()) { - //re-define axes if needed - axes = ids.associateWith { Axis(it) } - } - Meta(ids.map { it.asValue() }.asValue()) - initialize() - failIfError() - } - } - - val disconnect: DeviceAction by acting({ - info = "Disconnect the program from the device if it is connected" - }) { - if (port != null) { - stop() - port?.close() - } - port = null - connected.updateLogical(false) - } - - fun disconnect() { - runBlocking { - disconnect.invoke() - } - } - - val timeout: DeviceProperty by writingVirtual(200.asValue()) { - info = "Timeout" - } - - var timeoutValue: Duration by timeout.duration() - - /** - * Name-friendly accessor for axis - */ - var axes: Map = emptyMap() - private set - - override val devices: Map = axes.mapKeys { (key, _) -> NameToken(key) } - - private suspend fun failIfError(message: (Int) -> String = { "Failed with error code $it" }) { - val errorCode = getErrorCode() - if (errorCode != 0) error(message(errorCode)) - } - - fun connect(host: String, port: Int) { - runBlocking { - connect(Meta { - "host" put host - "port" put port - }) - } - } - - private val mutex = Mutex() - - private suspend fun dispatchError(errorCode: Int) { - logger.error { "Error code: $errorCode" } - //TODO add error handling - } - - private suspend fun sendCommandInternal(command: String, vararg arguments: String) { - val joinedArguments = if (arguments.isEmpty()) { - "" - } else { - arguments.joinToString(prefix = " ", separator = " ", postfix = "") - } - val stringToSend = "$command$joinedArguments\n" - port?.send(stringToSend) ?: error("Not connected to device") - } - - suspend fun getErrorCode(): Int = mutex.withLock { - withTimeout(timeoutValue) { - sendCommandInternal("ERR?") - val errorString = port?.receiving()?.withDelimiter("\n")?.first() ?: error("Not connected to device") - errorString.trim().toInt() - } - } - - /** - * Send a synchronous request and receive a list of lines as a response - */ - @OptIn(ExperimentalCoroutinesApi::class) - private suspend fun request(command: String, vararg arguments: String): List = mutex.withLock { - try { - withTimeout(timeoutValue) { - sendCommandInternal(command, *arguments) - val phrases = port?.receiving()?.withDelimiter("\n") ?: error("Not connected to device") - phrases.transformWhile { line -> - emit(line) - line.endsWith(" \n") - }.toList() - } - } catch (ex: Throwable) { - logger.warn { "Error during PIMotionMaster request. Requesting error code." } - val errorCode = getErrorCode() - dispatchError(errorCode) - logger.warn { "Error code $errorCode" } - error("Error code $errorCode") - } - } - - private suspend fun requestAndParse(command: String, vararg arguments: String): Map = buildMap { - request(command, *arguments).forEach { line -> - val (key, value) = line.split("=") - put(key, value.trim()) - } - } - - /** - * Send a synchronous command - */ - private suspend fun send(command: String, vararg arguments: String) { - mutex.withLock { - withTimeout(timeoutValue) { - sendCommandInternal(command, *arguments) - } - } - } - - val initialize: DeviceAction by acting { - send("INI") - } - - val identity: ReadOnlyDeviceProperty by readingString { - request("*IDN?").first() - } - - val firmwareVersion: ReadOnlyDeviceProperty by readingString { - request("VER?").first() - } - - val stop: DeviceAction by acting( - descriptorBuilder = { - info = "Stop all axis" - }, - action = { send("STP") } - ) - - inner class Axis(val axisId: String) : DeviceBase(context) { - - private suspend fun readAxisBoolean(command: String): Boolean = - requestAndParse(command, axisId)[axisId]?.toIntOrNull() - ?: error("Malformed $command response. Should include integer value for $axisId") != 0 - - private suspend fun writeAxisBoolean(command: String, value: Boolean): Boolean { - val boolean = if (value) { - "1" - } else { - "0" - } - send(command, axisId, boolean) - failIfError() - return value - } - - private fun axisBooleanProperty(command: String, descriptorBuilder: PropertyDescriptor.() -> Unit = {}) = - writingBoolean( - getter = { readAxisBoolean("$command?") }, - setter = { _, newValue -> - writeAxisBoolean(command, newValue) - }, - descriptorBuilder = descriptorBuilder - ) - - private fun axisNumberProperty(command: String, descriptorBuilder: PropertyDescriptor.() -> Unit = {}) = - writingDouble( - getter = { - requestAndParse("$command?", axisId)[axisId]?.toDoubleOrNull() - ?: error("Malformed $command response. Should include float value for $axisId") - }, - setter = { _, newValue -> - send(command, axisId, newValue.toString()) - failIfError() - newValue - }, - descriptorBuilder = descriptorBuilder - ) - - val enabled by axisBooleanProperty("EAX") { - info = "Motor enable state." - } - - val halt: DeviceAction by acting { - send("HLT", axisId) - } - - val targetPosition by axisNumberProperty("MOV") { - info = """ - Sets a new absolute target position for the specified axis. - Servo mode must be switched on for the commanded axis prior to using this command (closed-loop operation). - """.trimIndent() - } - - val onTarget: TypedReadOnlyDeviceProperty by readingBoolean( - descriptorBuilder = { - info = "Queries the on-target state of the specified axis." - }, - getter = { - readAxisBoolean("ONT?") - } - ) - - val reference: ReadOnlyDeviceProperty by readingBoolean( - descriptorBuilder = { - info = "Get Referencing Result" - }, - getter = { - readAxisBoolean("FRF?") - } - ) - - val moveToReference by acting { - send("FRF", axisId) - } - - val minPosition by readingDouble( - descriptorBuilder = { - info = "Minimal position value for the axis" - }, - getter = { - requestAndParse("TMN?", axisId)[axisId]?.toDoubleOrNull() - ?: error("Malformed `TMN?` response. Should include float value for $axisId") - } - ) - - val maxPosition by readingDouble( - descriptorBuilder = { - info = "Maximal position value for the axis" - }, - getter = { - requestAndParse("TMX?", axisId)[axisId]?.toDoubleOrNull() - ?: error("Malformed `TMX?` response. Should include float value for $axisId") - } - ) - - val position by readingDouble( - descriptorBuilder = { - info = "The current axis position." - }, - getter = { - requestAndParse("POS?", axisId)[axisId]?.toDoubleOrNull() - ?: error("Malformed `POS?` response. Should include float value for $axisId") - } - ) - - val openLoopTarget: DeviceProperty by axisNumberProperty("OMA") { - info = "Position for open-loop operation." - } - - val closedLoop: TypedDeviceProperty by axisBooleanProperty("SVO") { - info = "Servo closed loop mode" - } - - val velocity: TypedDeviceProperty by axisNumberProperty("VEL") { - info = "Velocity value for closed-loop operation" - } - - val move by acting { - val target = it.double ?: it?.get("target").double ?: error("Unacceptable target value $it") - closedLoop.write(true) - //optionally set velocity - it?.get("velocity").double?.let { v -> - velocity.write(v) - } - targetPosition.write(target) - //read `onTarget` and `position` properties in a cycle until movement is complete - while (!onTarget.readTyped(true)) { - position.read(true) - delay(200) - } - } - - suspend fun move(target: Double) { - move(target.asMeta()) - } - } - - companion object : Factory { - override fun invoke(meta: Meta, context: Context): PiMotionMasterDevice = PiMotionMasterDevice(context) - } - -} \ No newline at end of file diff --git a/motors/src/main/kotlin/ru/mipt/npm/devices/pimotionmaster/fxDeviceProperties.kt b/motors/src/main/kotlin/ru/mipt/npm/devices/pimotionmaster/fxDeviceProperties.kt deleted file mode 100644 index 48ef6d2..0000000 --- a/motors/src/main/kotlin/ru/mipt/npm/devices/pimotionmaster/fxDeviceProperties.kt +++ /dev/null @@ -1,60 +0,0 @@ -package ru.mipt.npm.devices.pimotionmaster - -import javafx.beans.property.ObjectPropertyBase -import javafx.beans.property.Property -import javafx.beans.property.ReadOnlyProperty -import kotlinx.coroutines.flow.catch -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach -import ru.mipt.npm.controls.api.Device -import ru.mipt.npm.controls.base.TypedDeviceProperty -import ru.mipt.npm.controls.base.TypedReadOnlyDeviceProperty -import space.kscience.dataforge.context.info -import space.kscience.dataforge.context.logger -import tornadofx.* - -fun TypedReadOnlyDeviceProperty.fxProperty(ownerDevice: Device?): ReadOnlyProperty = - object : ObjectPropertyBase() { - override fun getBean(): Any? = ownerDevice - override fun getName(): String = this@fxProperty.name - - init { - //Read incoming changes - flowTyped().onEach { - if (it != null) { - runLater { - set(it) - } - } else { - invalidated() - } - }.catch { - ownerDevice?.logger?.info { "Failed to set property $name to $it" } - }.launchIn(scope) - } - } - -fun TypedDeviceProperty.fxProperty(ownerDevice: Device?): Property = - object : ObjectPropertyBase() { - override fun getBean(): Any? = ownerDevice - override fun getName(): String = this@fxProperty.name - - init { - //Read incoming changes - flowTyped().onEach { - if (it != null) { - runLater { - set(it) - } - } else { - invalidated() - } - }.catch { - ownerDevice?.logger?.info { "Failed to set property $name to $it" } - }.launchIn(scope) - - onChange { - typedValue = it - } - } - } diff --git a/settings.gradle.kts b/settings.gradle.kts index cf5e79e..a8f250b 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -4,49 +4,66 @@ enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") enableFeaturePreview("VERSION_CATALOGS") pluginManagement { - val toolsVersion = "0.10.4" + + val toolsVersion: String by extra repositories { - maven("https://repo.kotlin.link") - mavenCentral() + mavenLocal() gradlePluginPortal() + mavenCentral() + maven("https://repo.kotlin.link") } plugins { - id("ru.mipt.npm.gradle.project") version toolsVersion - id("ru.mipt.npm.gradle.mpp") version toolsVersion - id("ru.mipt.npm.gradle.jvm") version toolsVersion - id("ru.mipt.npm.gradle.js") version toolsVersion + id("space.kscience.gradle.project") version toolsVersion + id("space.kscience.gradle.mpp") version toolsVersion + id("space.kscience.gradle.jvm") version toolsVersion + id("space.kscience.gradle.js") version toolsVersion + id("org.openjfx.javafxplugin") version "0.0.13" } } dependencyResolutionManagement { + + val toolsVersion: String by extra + repositories { - maven("https://repo.kotlin.link") + mavenLocal() mavenCentral() + maven("https://repo.kotlin.link") } versionCatalogs { - create("npm") { - from("ru.mipt.npm:version-catalog:0.10.4") + create("npmlibs") { + from("space.kscience:version-catalog:$toolsVersion") } } } include( ":controls-core", - ":controls-tcp", + ":controls-ktor-tcp", ":controls-serial", ":controls-server", ":controls-opcua", - ":demo", +// ":controls-mongo", + ":controls-storage", + ":controls-storage:controls-xodus", ":magix", ":magix:magix-api", ":magix:magix-server", ":magix:magix-rsocket", ":magix:magix-java-client", ":magix:magix-zmq", - ":magix:magix-demo", + ":magix:magix-rabbit", + +// ":magix:magix-storage", + ":magix:magix-storage:magix-storage-xodus", ":controls-magix-client", - ":motors" -) \ No newline at end of file + ":demo:all-things", + ":demo:magix-demo", + ":demo:car", + ":demo:motors", + ":demo:echo", + ":demo:mks-pdr900" +)