From 4b5bc40a4f5cb2ff06b49092753a828aa9fb8e1b Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Tue, 8 Sep 2020 10:13:14 +0300 Subject: [PATCH] Unisolate properties --- build.gradle.kts | 6 + dataforge-device-client/build.gradle.kts | 12 +- dataforge-device-core/build.gradle.kts | 5 +- .../hep/dataforge/control/api/Device.kt | 5 +- .../dataforge/control/api/DeviceListener.kt | 6 +- .../hep/dataforge/control/api/descriptors.kt | 12 +- .../hep/dataforge/control/base/Action.kt | 72 +--- .../hep/dataforge/control/base/DeviceBase.kt | 196 +++++++++-- .../control/base/IsolatedDeviceProperty.kt | 329 ------------------ .../dataforge/control/base/actionDelegates.kt | 59 ++++ .../control/base/devicePropertyDelegates.kt | 196 +++++++++++ .../control/controllers/DeviceController.kt | 2 +- .../control/controllers/delegates.kt | 12 +- dataforge-device-serial/build.gradle.kts | 4 +- dataforge-device-server/build.gradle.kts | 6 +- demo/build.gradle.kts | 3 +- .../hep/dataforge/control/demo/DemoDevice.kt | 2 +- docs/schemes/direct-vs-loop.vsdx | Bin 0 -> 41268 bytes motors/build.gradle.kts | 4 +- .../pimotionmaster/PiMotionMasterDevice.kt | 64 ++-- settings.gradle.kts | 22 +- 21 files changed, 524 insertions(+), 493 deletions(-) delete mode 100644 dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/base/IsolatedDeviceProperty.kt create mode 100644 dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/base/actionDelegates.kt create mode 100644 dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/base/devicePropertyDelegates.kt create mode 100644 docs/schemes/direct-vs-loop.vsdx diff --git a/build.gradle.kts b/build.gradle.kts index ec6fd86..22435c0 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,3 +1,8 @@ +plugins{ + kotlin("jvm") version "1.4.0" apply false + kotlin("js") version "1.4.0" apply false +} + val dataforgeVersion by extra("0.1.9-dev-2") allprojects { @@ -6,6 +11,7 @@ allprojects { maven("https://dl.bintray.com/pdvrieze/maven") maven("http://maven.jzy3d.org/releases") maven("https://kotlin.bintray.com/js-externals") + maven("https://maven.pkg.github.com/altavir/kotlin-logging/") } group = "hep.dataforge" diff --git a/dataforge-device-client/build.gradle.kts b/dataforge-device-client/build.gradle.kts index 6367217..5404d11 100644 --- a/dataforge-device-client/build.gradle.kts +++ b/dataforge-device-client/build.gradle.kts @@ -1,19 +1,11 @@ plugins { - id("kscience.mpp") - id("kscience.publish") + id("ru.mipt.npm.mpp") + id("ru.mipt.npm.publish") } val ktorVersion: String by extra("1.4.0") kotlin { -// js { -// browser { -// dceTask { -// keep("ktor-ktor-io.\$\$importsForInline\$\$.ktor-ktor-io.io.ktor.utils.io") -// } -// } -// } - sourceSets { commonMain { dependencies { diff --git a/dataforge-device-core/build.gradle.kts b/dataforge-device-core/build.gradle.kts index 8c2e9de..1a47028 100644 --- a/dataforge-device-core/build.gradle.kts +++ b/dataforge-device-core/build.gradle.kts @@ -1,6 +1,6 @@ plugins { - id("kscience.mpp") - id("kscience.publish") + id("ru.mipt.npm.mpp") + id("ru.mipt.npm.publish") } val dataforgeVersion: String by rootProject.extra @@ -15,7 +15,6 @@ kotlin { commonMain{ dependencies { api("hep.dataforge:dataforge-io:$dataforgeVersion") - //implementation("org.jetbrains.kotlinx:atomicfu-common:0.14.3") } } jvmMain{ diff --git a/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/api/Device.kt b/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/api/Device.kt index ed3420e..990b42e 100644 --- a/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/api/Device.kt +++ b/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/api/Device.kt @@ -3,7 +3,6 @@ package hep.dataforge.control.api import hep.dataforge.control.api.Device.Companion.DEVICE_TARGET import hep.dataforge.io.Envelope import hep.dataforge.io.EnvelopeBuilder -import hep.dataforge.io.Responder import hep.dataforge.meta.Meta import hep.dataforge.meta.MetaItem import hep.dataforge.provider.Type @@ -15,7 +14,7 @@ import kotlinx.io.Closeable * General interface describing a managed Device */ @Type(DEVICE_TARGET) -public interface Device : Responder, Closeable { +public interface Device : Closeable { /** * List of supported property descriptors */ @@ -73,7 +72,7 @@ public interface Device : Responder, Closeable { * [setProperty], [getProperty] or [execute] and not defined for a generic device. * */ - override suspend fun respond(request: Envelope): EnvelopeBuilder = error("No binary response defined") + public suspend fun respondWithData(request: Envelope): EnvelopeBuilder = error("No binary response defined") override fun close() { scope.cancel("The device is closed") diff --git a/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/api/DeviceListener.kt b/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/api/DeviceListener.kt index 0aa2275..483d05c 100644 --- a/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/api/DeviceListener.kt +++ b/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/api/DeviceListener.kt @@ -6,9 +6,9 @@ import hep.dataforge.meta.MetaItem * PropertyChangeListener Interface * [value] is a new value that property has after a change; null is for invalid state. */ -interface DeviceListener { - fun propertyChanged(propertyName: String, value: MetaItem<*>?) - fun actionExecuted(action: String, argument: MetaItem<*>?, result: MetaItem<*>?) {} +public interface DeviceListener { + public fun propertyChanged(propertyName: String, value: MetaItem<*>?) + public fun actionExecuted(action: String, argument: MetaItem<*>?, result: MetaItem<*>?) {} //TODO add general message listener method } \ No newline at end of file diff --git a/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/api/descriptors.kt b/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/api/descriptors.kt index a138ea5..a317553 100644 --- a/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/api/descriptors.kt +++ b/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/api/descriptors.kt @@ -6,17 +6,17 @@ import hep.dataforge.meta.string /** * A descriptor for property */ -class PropertyDescriptor(name: String) : Scheme() { - val name by string(name) - var info by string() +public class PropertyDescriptor(name: String) : Scheme() { + public val name: String by string(name) + public var info: String? by string() } /** * A descriptor for property */ -class ActionDescriptor(name: String) : Scheme() { - val name by string(name) - var info by string() +public class ActionDescriptor(name: String) : Scheme() { + public val name: String by string(name) + public var info: String? by string() //var descriptor by spec(ItemDescriptor) } diff --git a/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/base/Action.kt b/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/base/Action.kt index 205083f..962c364 100644 --- a/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/base/Action.kt +++ b/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/base/Action.kt @@ -1,74 +1,10 @@ package hep.dataforge.control.base import hep.dataforge.control.api.ActionDescriptor -import hep.dataforge.meta.MetaBuilder import hep.dataforge.meta.MetaItem -import hep.dataforge.values.Value -import kotlin.properties.ReadOnlyProperty -import kotlin.reflect.KProperty -interface Action { - val name: String - val descriptor: ActionDescriptor - suspend operator fun invoke(arg: MetaItem<*>? = null): MetaItem<*>? -} - -private fun DeviceBase.actionExecuted(action: String, argument: MetaItem<*>?, result: MetaItem<*>?){ - notifyListeners { actionExecuted(action, argument, result) } -} - -/** - * A stand-alone action - */ -class IsolatedAction( - override val name: String, - override val descriptor: ActionDescriptor, - val callback: (action: String, argument: MetaItem<*>?, result: MetaItem<*>?) -> Unit, - val block: suspend (MetaItem<*>?) -> MetaItem<*>? -) : Action { - override suspend fun invoke(arg: MetaItem<*>?): MetaItem<*>? = block(arg).also { - callback(name, arg, it) - } -} - -class ActionDelegate( - val owner: D, - val descriptorBuilder: ActionDescriptor.() -> Unit = {}, - val block: suspend (MetaItem<*>?) -> MetaItem<*>? -) : ReadOnlyProperty { - override fun getValue(thisRef: D, property: KProperty<*>): Action { - val name = property.name - return owner.registerAction(name) { - IsolatedAction(name, ActionDescriptor(name).apply(descriptorBuilder), owner::actionExecuted, block) - } - } -} - -fun D.request( - descriptorBuilder: ActionDescriptor.() -> Unit = {}, - block: suspend (MetaItem<*>?) -> MetaItem<*>? -): ActionDelegate = ActionDelegate(this, descriptorBuilder, block) - -fun D.requestValue( - descriptorBuilder: ActionDescriptor.() -> Unit = {}, - block: suspend (MetaItem<*>?) -> Any? -): ActionDelegate = ActionDelegate(this, descriptorBuilder) { - val res = block(it) - MetaItem.ValueItem(Value.of(res)) -} - -fun D.requestMeta( - descriptorBuilder: ActionDescriptor.() -> Unit = {}, - block: suspend MetaBuilder.(MetaItem<*>?) -> Unit -): ActionDelegate = ActionDelegate(this, descriptorBuilder) { - val res = MetaBuilder().apply { block(it) } - MetaItem.NodeItem(res) -} - -fun D.action( - descriptorBuilder: ActionDescriptor.() -> Unit = {}, - block: suspend (MetaItem<*>?) -> Unit -): ActionDelegate = ActionDelegate(this, descriptorBuilder) { - block(it) - null +public interface Action { + public val name: String + public val descriptor: ActionDescriptor + public suspend operator fun invoke(arg: MetaItem<*>? = null): MetaItem<*>? } \ No newline at end of file diff --git a/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/base/DeviceBase.kt b/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/base/DeviceBase.kt index 1647bc9..879b8ba 100644 --- a/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/base/DeviceBase.kt +++ b/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/base/DeviceBase.kt @@ -5,14 +5,23 @@ import hep.dataforge.control.api.Device import hep.dataforge.control.api.DeviceListener import hep.dataforge.control.api.PropertyDescriptor import hep.dataforge.meta.MetaItem +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import kotlinx.coroutines.withContext /** * Baseline implementation of [Device] interface */ -abstract class DeviceBase : Device { - private val properties = HashMap() - private val actions = HashMap() +public abstract class DeviceBase : Device { + private val _properties = HashMap() + public val properties: Map get() = _properties + private val _actions = HashMap() + public val actions: Map get() = _actions private val listeners = ArrayList>(4) @@ -24,11 +33,11 @@ abstract class DeviceBase : Device { listeners.removeAll { it.first == owner } } - fun notifyListeners(block: DeviceListener.() -> Unit) { + internal fun notifyListeners(block: DeviceListener.() -> Unit) { listeners.forEach { it.second.block() } } - fun notifyPropertyChanged(propertyName: String) { + public fun notifyPropertyChanged(propertyName: String) { scope.launch { val value = getProperty(propertyName) notifyListeners { propertyChanged(propertyName, value) } @@ -36,41 +45,188 @@ abstract class DeviceBase : Device { } override val propertyDescriptors: Collection - get() = properties.values.map { it.descriptor } + get() = _properties.values.map { it.descriptor } override val actionDescriptors: Collection - get() = actions.values.map { it.descriptor } + get() = _actions.values.map { it.descriptor } - internal fun registerProperty(name: String, builder: () -> ReadOnlyDeviceProperty): ReadOnlyDeviceProperty { - return properties.getOrPut(name, builder) + internal fun

registerProperty(name: String, property: P) { + if (_properties.contains(name)) error("Property with name $name already registered") + _properties[name] = property } - internal fun registerMutableProperty(name: String, builder: () -> DeviceProperty): DeviceProperty { - return properties.getOrPut(name, builder) as DeviceProperty - } - - internal fun registerAction(name: String, builder: () -> Action): Action { - return actions.getOrPut(name, builder) + internal fun registerAction(name: String, action: Action) { + if (_actions.contains(name)) error("Action with name $name already registered") + _actions[name] = action } override suspend fun getProperty(propertyName: String): MetaItem<*> = - (properties[propertyName] ?: error("Property with name $propertyName not defined")).read() + (_properties[propertyName] ?: error("Property with name $propertyName not defined")).read() override suspend fun invalidateProperty(propertyName: String) { - (properties[propertyName] ?: error("Property with name $propertyName not defined")).invalidate() + (_properties[propertyName] ?: error("Property with name $propertyName not defined")).invalidate() } override suspend fun setProperty(propertyName: String, value: MetaItem<*>) { - (properties[propertyName] as? DeviceProperty ?: error("Property with name $propertyName not defined")).write( + (_properties[propertyName] as? DeviceProperty ?: error("Property with name $propertyName not defined")).write( value ) } override suspend fun execute(command: String, argument: MetaItem<*>?): MetaItem<*>? = - (actions[command] ?: error("Request with name $command not defined")).invoke(argument) + (_actions[command] ?: error("Request with name $command not defined")).invoke(argument) + @OptIn(ExperimentalCoroutinesApi::class) + private open inner class BasicReadOnlyDeviceProperty( + override val name: String, + default: MetaItem<*>?, + override val descriptor: PropertyDescriptor, + private val getter: suspend (before: MetaItem<*>?) -> MetaItem<*>, + ) : ReadOnlyDeviceProperty { - companion object { + override val scope: CoroutineScope get() = this@DeviceBase.scope + + private val state: MutableStateFlow?> = MutableStateFlow(default) + override val value: MetaItem<*>? get() = state.value + + override suspend fun invalidate() { + state.value = null + } + + override fun updateLogical(item: MetaItem<*>) { + state.value = item + notifyListeners { + propertyChanged(name, item) + } + } + + override suspend fun read(force: Boolean): MetaItem<*> { + //backup current value + val currentValue = value + return if (force || currentValue == null) { + val res = withContext(scope.coroutineContext) { + //all device operations should be run on device context + //TODO add error catching + getter(currentValue) + } + updateLogical(res) + res + } else { + currentValue + } + } + + override fun flow(): StateFlow?> = state + } + + /** + * Create a bound read-only property with given [getter] + */ + public fun newReadOnlyProperty( + name: String, + default: MetaItem<*>?, + descriptorBuilder: PropertyDescriptor.() -> Unit = {}, + getter: suspend (MetaItem<*>?) -> MetaItem<*>, + ): ReadOnlyDeviceProperty { + val property = BasicReadOnlyDeviceProperty( + name, + default, + PropertyDescriptor(name).apply(descriptorBuilder), + getter + ) + registerProperty(name, property) + return property + } + + @OptIn(ExperimentalCoroutinesApi::class) + private inner class BasicDeviceProperty( + name: String, + default: MetaItem<*>?, + descriptor: PropertyDescriptor, + getter: suspend (MetaItem<*>?) -> MetaItem<*>, + private val setter: suspend (oldValue: MetaItem<*>?, newValue: MetaItem<*>) -> MetaItem<*>?, + ) : BasicReadOnlyDeviceProperty(name, default, descriptor, getter), DeviceProperty { + + override var value: MetaItem<*>? + get() = super.value + set(value) { + scope.launch { + if (value == null) { + invalidate() + } else { + write(value) + } + } + } + + private val writeLock = Mutex() + + override suspend fun write(item: MetaItem<*>) { + writeLock.withLock { + //fast return if value is not changed + if (item == value) return@withLock + val oldValue = value + //all device operations should be run on device context + withContext(scope.coroutineContext) { + //TODO add error catching + setter(oldValue, item)?.let { + updateLogical(it) + } + } + } + } + } + + /** + * Create a bound mutable property with given [getter] and [setter] + */ + public fun newMutableProperty( + name: String, + default: MetaItem<*>?, + descriptorBuilder: PropertyDescriptor.() -> Unit = {}, + getter: suspend (MetaItem<*>?) -> MetaItem<*>, + setter: suspend (oldValue: MetaItem<*>?, newValue: MetaItem<*>) -> MetaItem<*>?, + ): DeviceProperty { + val property = BasicDeviceProperty( + name, + default, + PropertyDescriptor(name).apply(descriptorBuilder), + getter, + setter + ) + registerProperty(name, property) + return property + } + + /** + * A stand-alone action + */ + private inner class BasicAction( + override val name: String, + override val descriptor: ActionDescriptor, + private val block: suspend (MetaItem<*>?) -> MetaItem<*>?, + ) : Action { + override suspend fun invoke(arg: MetaItem<*>?): MetaItem<*>? = block(arg).also { + notifyListeners { + actionExecuted(name, arg, it) + } + } + } + + /** + * Create a new bound action + */ + public fun newAction( + name: String, + descriptorBuilder: ActionDescriptor.() -> Unit = {}, + block: suspend (MetaItem<*>?) -> MetaItem<*>?, + ): Action { + val action = BasicAction(name, ActionDescriptor(name).apply(descriptorBuilder), block) + registerAction(name, action) + return action + } + + public companion object { } } diff --git a/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/base/IsolatedDeviceProperty.kt b/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/base/IsolatedDeviceProperty.kt deleted file mode 100644 index 44cbd15..0000000 --- a/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/base/IsolatedDeviceProperty.kt +++ /dev/null @@ -1,329 +0,0 @@ -package hep.dataforge.control.base - -import hep.dataforge.control.api.PropertyDescriptor -import hep.dataforge.meta.* -import hep.dataforge.values.Null -import hep.dataforge.values.Value -import hep.dataforge.values.asValue -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.launch -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock -import kotlinx.coroutines.withContext -import kotlin.properties.ReadOnlyProperty -import kotlin.reflect.KProperty - -private fun DeviceBase.propertyChanged(name: String, item: MetaItem<*>?){ - notifyListeners { propertyChanged(name, item) } -} - -/** - * A stand-alone [ReadOnlyDeviceProperty] implementation not directly attached to a device - */ -@OptIn(ExperimentalCoroutinesApi::class) -public open class IsolatedReadOnlyDeviceProperty( - override val name: String, - default: MetaItem<*>?, - override val descriptor: PropertyDescriptor, - override val scope: CoroutineScope, - private val callback: (name: String, item: MetaItem<*>) -> Unit, - private val getter: suspend (before: MetaItem<*>?) -> MetaItem<*> -) : ReadOnlyDeviceProperty { - - private val state: MutableStateFlow?> = MutableStateFlow(default) - override val value: MetaItem<*>? get() = state.value - - override suspend fun invalidate() { - state.value = null - } - - override fun updateLogical(item: MetaItem<*>) { - state.value = item - callback(name, item) - } - - override suspend fun read(force: Boolean): MetaItem<*> { - //backup current value - val currentValue = value - return if (force || currentValue == null) { - val res = withContext(scope.coroutineContext) { - //all device operations should be run on device context - //TODO add error catching - getter(currentValue) - } - updateLogical(res) - res - } else { - currentValue - } - } - - override fun flow(): StateFlow?> = state -} - -public fun DeviceBase.readOnlyProperty( - name: String, - default: MetaItem<*>?, - descriptorBuilder: PropertyDescriptor.() -> Unit = {}, - getter: suspend (MetaItem<*>?) -> MetaItem<*> -): ReadOnlyDeviceProperty = registerProperty(name) { - IsolatedReadOnlyDeviceProperty( - name, - default, - PropertyDescriptor(name).apply(descriptorBuilder), - scope, - ::propertyChanged, - getter - ) -} - -private class ReadOnlyDevicePropertyDelegate( - val owner: D, - val default: MetaItem<*>?, - val descriptorBuilder: PropertyDescriptor.() -> Unit = {}, - private val getter: suspend (MetaItem<*>?) -> MetaItem<*> -) : ReadOnlyProperty { - - override fun getValue(thisRef: D, property: KProperty<*>): ReadOnlyDeviceProperty { - val name = property.name - - return owner.registerProperty(name) { - @OptIn(ExperimentalCoroutinesApi::class) - IsolatedReadOnlyDeviceProperty( - name, - default, - PropertyDescriptor(name).apply(descriptorBuilder), - owner.scope, - owner::propertyChanged, - getter - ) - } - } -} - -public fun D.reading( - default: MetaItem<*>? = null, - descriptorBuilder: PropertyDescriptor.() -> Unit = {}, - getter: suspend (MetaItem<*>?) -> MetaItem<*> -): ReadOnlyProperty = ReadOnlyDevicePropertyDelegate( - this, - default, - descriptorBuilder, - getter -) - -public fun D.readingValue( - default: Value? = null, - descriptorBuilder: PropertyDescriptor.() -> Unit = {}, - getter: suspend () -> Any? -): ReadOnlyProperty = ReadOnlyDevicePropertyDelegate( - this, - default?.let { MetaItem.ValueItem(it) }, - descriptorBuilder, - getter = { MetaItem.ValueItem(Value.of(getter())) } -) - -public fun D.readingNumber( - default: Number? = null, - descriptorBuilder: PropertyDescriptor.() -> Unit = {}, - getter: suspend () -> Number -): ReadOnlyProperty = ReadOnlyDevicePropertyDelegate( - this, - default?.let { MetaItem.ValueItem(it.asValue()) }, - descriptorBuilder, - getter = { - val number = getter() - MetaItem.ValueItem(number.asValue()) - } -) - -public fun D.readingString( - default: Number? = null, - descriptorBuilder: PropertyDescriptor.() -> Unit = {}, - getter: suspend () -> String -): ReadOnlyProperty = ReadOnlyDevicePropertyDelegate( - this, - default?.let { MetaItem.ValueItem(it.asValue()) }, - descriptorBuilder, - getter = { - val number = getter() - MetaItem.ValueItem(number.asValue()) - } -) - -public fun D.readingMeta( - default: Meta? = null, - descriptorBuilder: PropertyDescriptor.() -> Unit = {}, - getter: suspend MetaBuilder.() -> Unit -): ReadOnlyProperty = ReadOnlyDevicePropertyDelegate( - this, - default?.let { MetaItem.NodeItem(it) }, - descriptorBuilder, - getter = { - MetaItem.NodeItem(MetaBuilder().apply { getter() }) - } -) - -@OptIn(ExperimentalCoroutinesApi::class) -public class IsolatedDeviceProperty( - name: String, - default: MetaItem<*>?, - descriptor: PropertyDescriptor, - scope: CoroutineScope, - updateCallback: (name: String, item: MetaItem<*>?) -> Unit, - getter: suspend (MetaItem<*>?) -> MetaItem<*>, - private val setter: suspend (oldValue: MetaItem<*>?, newValue: MetaItem<*>) -> MetaItem<*>? -) : IsolatedReadOnlyDeviceProperty(name, default, descriptor, scope, updateCallback, getter), DeviceProperty { - - override var value: MetaItem<*>? - get() = super.value - set(value) { - scope.launch { - if (value == null) { - invalidate() - } else { - write(value) - } - } - } - - private val writeLock = Mutex() - - override suspend fun write(item: MetaItem<*>) { - writeLock.withLock { - //fast return if value is not changed - if (item == value) return@withLock - val oldValue = value - //all device operations should be run on device context - withContext(scope.coroutineContext) { - //TODO add error catching - setter(oldValue, item)?.let { - updateLogical(it) - } - } - } - } -} - -public fun DeviceBase.mutableProperty( - name: String, - default: MetaItem<*>?, - descriptorBuilder: PropertyDescriptor.() -> Unit = {}, - getter: suspend (MetaItem<*>?) -> MetaItem<*>, - setter: suspend (oldValue: MetaItem<*>?, newValue: MetaItem<*>) -> MetaItem<*>? -): DeviceProperty = registerMutableProperty(name) { - IsolatedDeviceProperty( - name, - default, - PropertyDescriptor(name).apply(descriptorBuilder), - scope, - ::propertyChanged, - getter, - setter - ) -} - -private class DevicePropertyDelegate( - val owner: D, - val default: MetaItem<*>?, - val descriptorBuilder: PropertyDescriptor.() -> Unit = {}, - private val getter: suspend (MetaItem<*>?) -> MetaItem<*>, - private val setter: suspend (oldValue: MetaItem<*>?, newValue: MetaItem<*>) -> MetaItem<*>? -) : ReadOnlyProperty { - - override fun getValue(thisRef: D, property: KProperty<*>): IsolatedDeviceProperty { - val name = property.name - return owner.registerMutableProperty(name) { - @OptIn(ExperimentalCoroutinesApi::class) - IsolatedDeviceProperty( - name, - default, - PropertyDescriptor(name).apply(descriptorBuilder), - owner.scope, - owner::propertyChanged, - getter, - setter - ) - } as IsolatedDeviceProperty - } -} - -public fun D.writing( - default: MetaItem<*>? = null, - descriptorBuilder: PropertyDescriptor.() -> Unit = {}, - getter: suspend (MetaItem<*>?) -> MetaItem<*>, - setter: suspend (oldValue: MetaItem<*>?, newValue: MetaItem<*>) -> MetaItem<*>? -): ReadOnlyProperty = DevicePropertyDelegate( - this, - default, - descriptorBuilder, - getter, - setter -) - -public fun D.writingVirtual( - default: MetaItem<*>, - descriptorBuilder: PropertyDescriptor.() -> Unit = {} -): ReadOnlyProperty = writing( - default, - descriptorBuilder, - getter = { it ?: default }, - setter = { _, newItem -> newItem } -) - -public fun D.writingVirtual( - default: Value, - descriptorBuilder: PropertyDescriptor.() -> Unit = {} -): ReadOnlyProperty = writing( - MetaItem.ValueItem(default), - descriptorBuilder, - getter = { it ?: MetaItem.ValueItem(default) }, - setter = { _, newItem -> newItem } -) - -public fun D.writingDouble( - descriptorBuilder: PropertyDescriptor.() -> Unit = {}, - getter: suspend (Double) -> Double, - setter: suspend (oldValue: Double?, newValue: Double) -> Double? -): ReadOnlyProperty { - val innerGetter: suspend (MetaItem<*>?) -> MetaItem<*> = { - MetaItem.ValueItem(getter(it.double ?: Double.NaN).asValue()) - } - - val innerSetter: suspend (oldValue: MetaItem<*>?, newValue: MetaItem<*>) -> MetaItem<*>? = { oldValue, newValue -> - setter(oldValue.double, newValue.double ?: Double.NaN)?.asMetaItem() - } - - return DevicePropertyDelegate( - this, - MetaItem.ValueItem(Double.NaN.asValue()), - descriptorBuilder, - innerGetter, - innerSetter - ) -} - -public fun D.writingBoolean( - descriptorBuilder: PropertyDescriptor.() -> Unit = {}, - getter: suspend (Boolean?) -> Boolean, - setter: suspend (oldValue: Boolean?, newValue: Boolean) -> Boolean? -): ReadOnlyProperty { - val innerGetter: suspend (MetaItem<*>?) -> MetaItem<*> = { - MetaItem.ValueItem(getter(it.boolean).asValue()) - } - - val innerSetter: suspend (oldValue: MetaItem<*>?, newValue: MetaItem<*>) -> MetaItem<*>? = { oldValue, newValue -> - setter(oldValue.boolean, newValue.boolean?: error("Can't convert $newValue to boolean"))?.asValue()?.asMetaItem() - } - - return DevicePropertyDelegate( - this, - MetaItem.ValueItem(Null), - descriptorBuilder, - innerGetter, - innerSetter - ) -} \ No newline at end of file diff --git a/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/base/actionDelegates.kt b/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/base/actionDelegates.kt new file mode 100644 index 0000000..7b34c42 --- /dev/null +++ b/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/base/actionDelegates.kt @@ -0,0 +1,59 @@ +package hep.dataforge.control.base + +import hep.dataforge.control.api.ActionDescriptor +import hep.dataforge.meta.MetaBuilder +import hep.dataforge.meta.MetaItem +import hep.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 (MetaItem<*>?) -> MetaItem<*>?, +) : PropertyDelegateProvider { + override operator fun provideDelegate(thisRef: D, property: KProperty<*>): ActionDelegate { + val name = property.name + owner.newAction(name, descriptorBuilder, block) + return owner.provideAction() + } +} + +public fun DeviceBase.requesting( + descriptorBuilder: ActionDescriptor.() -> Unit = {}, + block: suspend (MetaItem<*>?) -> MetaItem<*>?, +): PropertyDelegateProvider = ActionProvider(this, descriptorBuilder, block) + +public fun D.requestingValue( + descriptorBuilder: ActionDescriptor.() -> Unit = {}, + block: suspend (MetaItem<*>?) -> Any?, +): PropertyDelegateProvider = ActionProvider(this, descriptorBuilder) { + val res = block(it) + MetaItem.ValueItem(Value.of(res)) +} + +public fun D.requestingMeta( + descriptorBuilder: ActionDescriptor.() -> Unit = {}, + block: suspend MetaBuilder.(MetaItem<*>?) -> Unit, +): PropertyDelegateProvider = ActionProvider(this, descriptorBuilder) { + val res = MetaBuilder().apply { block(it) } + MetaItem.NodeItem(res) +} + +public fun DeviceBase.acting( + descriptorBuilder: ActionDescriptor.() -> Unit = {}, + block: suspend (MetaItem<*>?) -> Unit, +): PropertyDelegateProvider = ActionProvider(this, descriptorBuilder) { + block(it) + null +} \ No newline at end of file diff --git a/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/base/devicePropertyDelegates.kt b/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/base/devicePropertyDelegates.kt new file mode 100644 index 0000000..3cf044e --- /dev/null +++ b/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/base/devicePropertyDelegates.kt @@ -0,0 +1,196 @@ +package hep.dataforge.control.base + +import hep.dataforge.control.api.PropertyDescriptor +import hep.dataforge.meta.* +import hep.dataforge.values.Null +import hep.dataforge.values.Value +import hep.dataforge.values.asValue +import kotlin.properties.PropertyDelegateProvider +import kotlin.properties.ReadOnlyProperty +import kotlin.reflect.KProperty + +private fun D.provideProperty(): ReadOnlyProperty = + ReadOnlyProperty { _: D, property: KProperty<*> -> + val name = property.name + return@ReadOnlyProperty properties[name]!! + } + +public typealias ReadOnlyPropertyDelegate = ReadOnlyProperty + +private class ReadOnlyDevicePropertyProvider( + val owner: D, + val default: MetaItem<*>?, + val descriptorBuilder: PropertyDescriptor.() -> Unit = {}, + private val getter: suspend (MetaItem<*>?) -> MetaItem<*>, +) : PropertyDelegateProvider { + + override operator fun provideDelegate(thisRef: D, property: KProperty<*>): ReadOnlyPropertyDelegate { + val name = property.name + owner.newReadOnlyProperty(name, default, descriptorBuilder, getter) + return owner.provideProperty() + } +} + +public fun DeviceBase.reading( + default: MetaItem<*>? = null, + descriptorBuilder: PropertyDescriptor.() -> Unit = {}, + getter: suspend (MetaItem<*>?) -> MetaItem<*>, +): PropertyDelegateProvider = ReadOnlyDevicePropertyProvider( + this, + default, + descriptorBuilder, + getter +) + +public fun DeviceBase.readingValue( + default: Value? = null, + descriptorBuilder: PropertyDescriptor.() -> Unit = {}, + getter: suspend () -> Any?, +): PropertyDelegateProvider = ReadOnlyDevicePropertyProvider( + this, + default?.let { MetaItem.ValueItem(it) }, + descriptorBuilder, + getter = { MetaItem.ValueItem(Value.of(getter())) } +) + +public fun DeviceBase.readingNumber( + default: Number? = null, + descriptorBuilder: PropertyDescriptor.() -> Unit = {}, + getter: suspend () -> Number, +): PropertyDelegateProvider = ReadOnlyDevicePropertyProvider( + this, + default?.let { MetaItem.ValueItem(it.asValue()) }, + descriptorBuilder, + getter = { + val number = getter() + MetaItem.ValueItem(number.asValue()) + } +) + +public fun DeviceBase.readingString( + default: Number? = null, + descriptorBuilder: PropertyDescriptor.() -> Unit = {}, + getter: suspend () -> String, +): PropertyDelegateProvider = ReadOnlyDevicePropertyProvider( + this, + default?.let { MetaItem.ValueItem(it.asValue()) }, + descriptorBuilder, + getter = { + val number = getter() + MetaItem.ValueItem(number.asValue()) + } +) + +public fun DeviceBase.readingMeta( + default: Meta? = null, + descriptorBuilder: PropertyDescriptor.() -> Unit = {}, + getter: suspend MetaBuilder.() -> Unit, +): PropertyDelegateProvider = ReadOnlyDevicePropertyProvider( + this, + default?.let { MetaItem.NodeItem(it) }, + descriptorBuilder, + getter = { + MetaItem.NodeItem(MetaBuilder().apply { getter() }) + } +) + +private fun DeviceBase.provideMutableProperty(): ReadOnlyProperty = + ReadOnlyProperty { _: DeviceBase, property: KProperty<*> -> + val name = property.name + return@ReadOnlyProperty properties[name] as DeviceProperty + } + +public typealias PropertyDelegate = ReadOnlyProperty + +private class DevicePropertyProvider( + val owner: D, + val default: MetaItem<*>?, + val descriptorBuilder: PropertyDescriptor.() -> Unit = {}, + private val getter: suspend (MetaItem<*>?) -> MetaItem<*>, + private val setter: suspend (oldValue: MetaItem<*>?, newValue: MetaItem<*>) -> MetaItem<*>?, +) : PropertyDelegateProvider { + + override operator fun provideDelegate(thisRef: D, property: KProperty<*>): PropertyDelegate { + val name = property.name + owner.newMutableProperty(name, default, descriptorBuilder, getter, setter) + return owner.provideMutableProperty() + } +} + +public fun DeviceBase.writing( + default: MetaItem<*>? = null, + descriptorBuilder: PropertyDescriptor.() -> Unit = {}, + getter: suspend (MetaItem<*>?) -> MetaItem<*>, + setter: suspend (oldValue: MetaItem<*>?, newValue: MetaItem<*>) -> MetaItem<*>?, +): PropertyDelegateProvider = DevicePropertyProvider( + this, + default, + descriptorBuilder, + getter, + setter +) + +public fun DeviceBase.writingVirtual( + default: MetaItem<*>, + descriptorBuilder: PropertyDescriptor.() -> Unit = {}, +): PropertyDelegateProvider = writing( + default, + descriptorBuilder, + getter = { it ?: default }, + setter = { _, newItem -> newItem } +) + +public fun DeviceBase.writingVirtual( + default: Value, + descriptorBuilder: PropertyDescriptor.() -> Unit = {}, +): PropertyDelegateProvider = writing( + MetaItem.ValueItem(default), + descriptorBuilder, + getter = { it ?: MetaItem.ValueItem(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 (MetaItem<*>?) -> MetaItem<*> = { + MetaItem.ValueItem(getter(it.double ?: Double.NaN).asValue()) + } + + val innerSetter: suspend (oldValue: MetaItem<*>?, newValue: MetaItem<*>) -> MetaItem<*>? = { oldValue, newValue -> + setter(oldValue.double, newValue.double ?: Double.NaN)?.asMetaItem() + } + + return DevicePropertyProvider( + this, + MetaItem.ValueItem(Double.NaN.asValue()), + 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 (MetaItem<*>?) -> MetaItem<*> = { + MetaItem.ValueItem(getter(it.boolean).asValue()) + } + + val innerSetter: suspend (oldValue: MetaItem<*>?, newValue: MetaItem<*>) -> MetaItem<*>? = { oldValue, newValue -> + setter(oldValue.boolean, newValue.boolean ?: error("Can't convert $newValue to boolean"))?.asValue() + ?.asMetaItem() + } + + return DevicePropertyProvider( + this, + MetaItem.ValueItem(Null), + descriptorBuilder, + innerGetter, + innerSetter + ) +} \ No newline at end of file diff --git a/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/controllers/DeviceController.kt b/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/controllers/DeviceController.kt index 6e4465f..6607c5c 100644 --- a/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/controllers/DeviceController.kt +++ b/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/controllers/DeviceController.kt @@ -78,7 +78,7 @@ class DeviceController( } else if (target != null && target != deviceTarget) { error("Wrong target name $deviceTarget expected but $target found") } else { - val response = device.respond(request).apply { + val response = device.respondWithData(request).apply { meta { "target" put request.meta["source"].string "source" put deviceTarget diff --git a/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/controllers/delegates.kt b/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/controllers/delegates.kt index 122c917..7f1b41c 100644 --- a/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/controllers/delegates.kt +++ b/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/controllers/delegates.kt @@ -8,6 +8,7 @@ import hep.dataforge.values.Null import kotlin.properties.ReadOnlyProperty import kotlin.properties.ReadWriteProperty import kotlin.reflect.KProperty +import kotlin.time.Duration operator fun ReadOnlyDeviceProperty.getValue(thisRef: Any?, property: KProperty<*>): MetaItem<*> = value ?: MetaItem.ValueItem(Null) @@ -17,10 +18,8 @@ operator fun DeviceProperty.setValue(thisRef: Any?, property: KProperty<*>, valu } fun ReadOnlyDeviceProperty.convert(metaConverter: MetaConverter): ReadOnlyProperty { - return object : ReadOnlyProperty { - override fun getValue(thisRef: Any?, property: KProperty<*>): T { - return this@convert.getValue(thisRef, property).let { metaConverter.itemToObject(it) } - } + return ReadOnlyProperty { thisRef, property -> + getValue(thisRef, property).let { metaConverter.itemToObject(it) } } } @@ -43,4 +42,7 @@ fun ReadOnlyDeviceProperty.int() = convert(MetaConverter.int) fun DeviceProperty.int() = convert(MetaConverter.int) fun ReadOnlyDeviceProperty.string() = convert(MetaConverter.string) -fun DeviceProperty.string() = convert(MetaConverter.string) \ No newline at end of file +fun DeviceProperty.string() = convert(MetaConverter.string) + +fun ReadOnlyDeviceProperty.duration(): ReadOnlyProperty = TODO() +fun DeviceProperty.duration(): ReadWriteProperty = TODO() \ No newline at end of file diff --git a/dataforge-device-serial/build.gradle.kts b/dataforge-device-serial/build.gradle.kts index 6d033ae..a5036d4 100644 --- a/dataforge-device-serial/build.gradle.kts +++ b/dataforge-device-serial/build.gradle.kts @@ -1,6 +1,6 @@ plugins { - id("kscience.jvm") - id("kscience.publish") + id("ru.mipt.npm.jvm") + id("ru.mipt.npm.publish") } dependencies{ diff --git a/dataforge-device-server/build.gradle.kts b/dataforge-device-server/build.gradle.kts index 7e0c1ce..8f7428e 100644 --- a/dataforge-device-server/build.gradle.kts +++ b/dataforge-device-server/build.gradle.kts @@ -1,6 +1,6 @@ plugins { - id("kscience.jvm") - id("kscience.publish") + id("ru.mipt.npm.jvm") + id("ru.mipt.npm.publish") } kscience { @@ -8,7 +8,7 @@ kscience { } val dataforgeVersion: String by rootProject.extra -val ktorVersion: String by extra("1.3.2") +val ktorVersion: String by extra("1.4.0") dependencies{ implementation(project(":dataforge-device-core")) diff --git a/demo/build.gradle.kts b/demo/build.gradle.kts index d104cf4..a7c39a4 100644 --- a/demo/build.gradle.kts +++ b/demo/build.gradle.kts @@ -6,6 +6,7 @@ plugins { repositories{ + mavenLocal() jcenter() maven("https://kotlin.bintray.com/kotlinx") maven("https://dl.bintray.com/kotlin/kotlin-eap") @@ -21,7 +22,7 @@ dependencies{ implementation(project(":dataforge-device-client")) implementation("no.tornado:tornadofx:1.7.20") implementation(kotlin("stdlib-jdk8")) - implementation("kscience.plotlykt:plotlykt-server:0.2.0") + implementation("kscience.plotlykt:plotlykt-server:0.3.0-dev-2") } tasks.withType().configureEach { diff --git a/demo/src/main/kotlin/hep/dataforge/control/demo/DemoDevice.kt b/demo/src/main/kotlin/hep/dataforge/control/demo/DemoDevice.kt index 7ff95d3..220d0a9 100644 --- a/demo/src/main/kotlin/hep/dataforge/control/demo/DemoDevice.kt +++ b/demo/src/main/kotlin/hep/dataforge/control/demo/DemoDevice.kt @@ -50,7 +50,7 @@ class DemoDevice(parentScope: CoroutineScope) : DeviceBase() { } - val resetScale: Action by action { + val resetScale: Action by acting { timeScaleValue = 5000.0 sinScaleValue = 1.0 cosScaleValue = 1.0 diff --git a/docs/schemes/direct-vs-loop.vsdx b/docs/schemes/direct-vs-loop.vsdx new file mode 100644 index 0000000000000000000000000000000000000000..812a47e4f62df86d762ecdaf1176b9d3ed12e547 GIT binary patch literal 41268 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
X7BH3W^>Q(%+ATOA!utV-BU$dRSTJj}{bf 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 Fe*hMEqWJ&- literal 0 HcmV?d00001 diff --git a/motors/build.gradle.kts b/motors/build.gradle.kts index cb0dbd8..4b747ef 100644 --- a/motors/build.gradle.kts +++ b/motors/build.gradle.kts @@ -1,6 +1,6 @@ plugins { - id("kscience.jvm") - id("kscience.publish") + id("ru.mipt.npm.jvm") + id("ru.mipt.npm.publish") } //TODO to be moved to a separate project 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 index 56998ea..af5d831 100644 --- a/motors/src/main/kotlin/ru/mipt/npm/devices/pimotionmaster/PiMotionMasterDevice.kt +++ b/motors/src/main/kotlin/ru/mipt/npm/devices/pimotionmaster/PiMotionMasterDevice.kt @@ -1,11 +1,14 @@ package ru.mipt.npm.devices.pimotionmaster +import hep.dataforge.control.api.DeviceHub import hep.dataforge.control.base.* +import hep.dataforge.control.controllers.duration import hep.dataforge.control.ports.Port import hep.dataforge.control.ports.PortProxy import hep.dataforge.control.ports.send import hep.dataforge.control.ports.withDelimiter import hep.dataforge.meta.MetaItem +import hep.dataforge.names.NameToken import hep.dataforge.values.Null import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job @@ -14,11 +17,15 @@ import kotlinx.coroutines.flow.takeWhile import kotlinx.coroutines.flow.toList import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock +import kotlinx.coroutines.withTimeout +import kotlin.time.Duration + public class PiMotionMasterDevice( parentScope: CoroutineScope, + axes: List, private val portFactory: suspend (MetaItem<*>?) -> Port, -) : DeviceBase() { +) : DeviceBase(), DeviceHub { override val scope: CoroutineScope = CoroutineScope( parentScope.coroutineContext + Job(parentScope.coroutineContext[Job]) @@ -28,11 +35,17 @@ public class PiMotionMasterDevice( info = "The port for TCP connector" } + public val timeout: DeviceProperty by writingVirtual(Null) { + info = "Timeout" + } + + public var timeoutValue: Duration by timeout.duration() + private val connector = PortProxy { portFactory(port.value) } private val mutex = Mutex() - private suspend fun sendCommand(command: String, vararg arguments: String) { + private suspend fun sendCommandInternal(command: String, vararg arguments: String) { val joinedArguments = if (arguments.isEmpty()) { "" } else { @@ -46,9 +59,11 @@ public class PiMotionMasterDevice( * Send a synchronous request and receive a list of lines as a response */ private suspend fun request(command: String, vararg arguments: String): List = mutex.withLock { - sendCommand(command, *arguments) - val phrases = connector.receiving().withDelimiter("\n") - return@withLock phrases.takeWhile { it.endsWith(" \n") }.toList() + phrases.first() + withTimeout(timeoutValue) { + sendCommandInternal(command, *arguments) + val phrases = connector.receiving().withDelimiter("\n") + phrases.takeWhile { it.endsWith(" \n") }.toList() + phrases.first() + } } private suspend fun requestAndParse(command: String, vararg arguments: String): Map = buildMap { @@ -63,11 +78,13 @@ public class PiMotionMasterDevice( */ private suspend fun send(command: String, vararg arguments: String) { mutex.withLock { - sendCommand(command, *arguments) + withTimeout(timeoutValue) { + sendCommandInternal(command, *arguments) + } } } - public val initialize: Action by action { + public val initialize: Action by acting { send("INI") } @@ -79,32 +96,37 @@ public class PiMotionMasterDevice( override val scope: CoroutineScope get() = this@PiMotionMasterDevice.scope public val enabled: DeviceProperty by writingBoolean( getter = { - val result = requestAndParse("EAX?", axisId)[axisId]?.toIntOrNull() - ?: error("Malformed response. Should include integer value for $axisId") - result != 0 + val eax = requestAndParse("EAX?", axisId)[axisId]?.toIntOrNull() + ?: error("Malformed EAX response. Should include integer value for $axisId") + eax != 0 }, - setter = { oldValue, newValue -> - val value = if(newValue){ + setter = { _, newValue -> + val value = if (newValue) { "1" } else { "0" } send("EAX", axisId, value) - oldValue + newValue } ) - public val halt: Action by action { + public val halt: Action by acting { send("HLT", axisId) } + + public val targetPosition: DeviceProperty by writingDouble( + getter = { + requestAndParse("MOV?", axisId)[axisId]?.toDoubleOrNull() + ?: error("Malformed MOV response. Should include float value for $axisId") + }, + setter = { _, newValue -> + send("MOV", axisId, newValue.toString()) + newValue + } + ) } - init { - //list everything here to ensure it is initialized - initialize - firmwareVersion - - } - + override val devices: Map = axes.associate { NameToken(it) to Axis(it) } } \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 325f4d6..40fd8a0 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,6 +1,6 @@ pluginManagement { val kotlinVersion = "1.4.0" - val toolsVersion = "0.6.0-dev-1" + val toolsVersion = "0.6.0-dev-4" repositories { mavenLocal() @@ -9,25 +9,17 @@ pluginManagement { maven("https://kotlin.bintray.com/kotlinx") maven("https://dl.bintray.com/kotlin/kotlin-eap") maven("https://dl.bintray.com/mipt-npm/dataforge") - maven("https://dl.bintray.com/mipt-npm/scientifik") + maven("https://dl.bintray.com/mipt-npm/kscience") maven("https://dl.bintray.com/mipt-npm/dev") } plugins { + id("ru.mipt.npm.mpp") version toolsVersion + id("ru.mipt.npm.jvm") version toolsVersion + id("ru.mipt.npm.js") version toolsVersion + id("ru.mipt.npm.publish") version toolsVersion kotlin("jvm") version kotlinVersion - id("scientifik.mpp") version toolsVersion - id("scientifik.jvm") version toolsVersion - id("scientifik.js") version toolsVersion - id("scientifik.publish") version toolsVersion - } - - resolutionStrategy { - eachPlugin { - when (requested.id.id) { - "kscience.publish", "kscience.mpp", "kscience.jvm", "kscience.js" -> useModule("ru.mipt.npm:gradle-tools:${toolsVersion}") - "kotlinx-atomicfu" -> useModule("org.jetbrains.kotlinx:atomicfu-gradle-plugin:${requested.version}") - } - } + kotlin("js") version kotlinVersion } }