diff --git a/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/api/Device.kt b/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/api/Device.kt index 6298657..eeed7f9 100644 --- a/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/api/Device.kt +++ b/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/api/Device.kt @@ -57,7 +57,7 @@ public interface Device : Closeable, ContextAware, CoroutineScope { /** * 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 +67,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,7 +93,7 @@ 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 { for (descriptor in propertyDescriptors) { @@ -97,4 +105,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/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/properties/DeviceBySpec.kt b/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/spec/DeviceBase.kt similarity index 79% rename from controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/properties/DeviceBySpec.kt rename to controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/spec/DeviceBase.kt index c569cc3..dd00a67 100644 --- a/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/properties/DeviceBySpec.kt +++ b/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/spec/DeviceBase.kt @@ -1,4 +1,4 @@ -package ru.mipt.npm.controls.properties +package ru.mipt.npm.controls.spec import kotlinx.coroutines.Job import kotlinx.coroutines.SupervisorJob @@ -13,24 +13,15 @@ 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, + public val meta: Meta = Meta.EMPTY ) : Device { - override var context: Context = context - internal set - public var meta: Meta = meta - internal set - - public val properties: Map> get() = spec.properties - public val actions: Map> get() = spec.actions + public abstract val properties: Map> //get() = spec.properties + public abstract val actions: Map> //get() = spec.actions override val propertyDescriptors: Collection get() = properties.values.map { it.descriptor } @@ -68,6 +59,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,7 +96,7 @@ 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 @@ -123,19 +121,20 @@ public open class DeviceBySpec>( } } - override fun close() { - with(spec) { self.onShutdown() } - super.close() - } + public suspend operator fun DeviceActionSpec.invoke(input: I? = null): O? = execute(self, input) + } -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) +/** + * 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 } + 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/spec/DevicePropertySpec.kt similarity index 59% rename from controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/properties/DevicePropertySpec.kt rename to controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/spec/DevicePropertySpec.kt index 23ceffb..a6fb9f6 100644 --- a/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/properties/DevicePropertySpec.kt +++ b/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/spec/DevicePropertySpec.kt @@ -1,7 +1,14 @@ -package ru.mipt.npm.controls.properties +package ru.mipt.npm.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 ru.mipt.npm.controls.api.ActionDescriptor import ru.mipt.npm.controls.api.Device +import ru.mipt.npm.controls.api.PropertyChangedMessage import ru.mipt.npm.controls.api.PropertyDescriptor import space.kscience.dataforge.meta.Meta import space.kscience.dataforge.meta.transformations.MetaConverter @@ -75,11 +82,48 @@ public interface DeviceActionSpec { public suspend fun execute(device: D, input: I?): O? } -public suspend fun DeviceActionSpec.executeMeta( +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) } -} \ No newline at end of file +} + + +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/ru/mipt/npm/controls/spec/DeviceSpec.kt similarity index 85% rename from controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/properties/DeviceSpec.kt rename to controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/spec/DeviceSpec.kt index 934220f..cba14ec 100644 --- a/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/properties/DeviceSpec.kt +++ b/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/spec/DeviceSpec.kt @@ -1,10 +1,9 @@ -package ru.mipt.npm.controls.properties +package ru.mipt.npm.controls.spec import kotlinx.coroutines.withContext 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.context.Context -import space.kscience.dataforge.context.Factory import space.kscience.dataforge.meta.Meta import space.kscience.dataforge.meta.transformations.MetaConverter import kotlin.properties.PropertyDelegateProvider @@ -14,9 +13,7 @@ import kotlin.reflect.KProperty import kotlin.reflect.KProperty1 @OptIn(InternalDeviceAPI::class) -public abstract class DeviceSpec>( - private val buildDevice: () -> D -) : Factory { +public abstract class DeviceSpec { private val _properties = HashMap>() public val properties: Map> get() = _properties @@ -75,8 +72,8 @@ public abstract class DeviceSpec>( public fun property( converter: MetaConverter, - name: String? = null, descriptorBuilder: PropertyDescriptor.() -> Unit = {}, + name: String? = null, read: suspend D.() -> T ): PropertyDelegateProvider, ReadOnlyProperty, DevicePropertySpec>> = PropertyDelegateProvider { _: DeviceSpec, property -> @@ -96,8 +93,8 @@ public abstract class DeviceSpec>( public fun property( converter: MetaConverter, - name: String? = null, descriptorBuilder: PropertyDescriptor.() -> Unit = {}, + name: String? = null, read: suspend D.() -> T, write: suspend D.(T) -> Unit ): PropertyDelegateProvider, ReadOnlyProperty, WritableDevicePropertySpec>> = @@ -129,8 +126,8 @@ public abstract class DeviceSpec>( public fun action( inputConverter: MetaConverter, outputConverter: MetaConverter, - name: String? = null, descriptorBuilder: ActionDescriptor.() -> Unit = {}, + name: String? = null, execute: suspend D.(I?) -> O? ): PropertyDelegateProvider, ReadOnlyProperty, DeviceActionSpec>> = PropertyDelegateProvider { _: DeviceSpec, property -> @@ -153,19 +150,35 @@ 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 } } diff --git a/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/properties/deviceExtensions.kt b/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/spec/deviceExtensions.kt similarity index 74% rename from controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/properties/deviceExtensions.kt rename to controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/spec/deviceExtensions.kt index 582c8a7..da83fab 100644 --- a/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/properties/deviceExtensions.kt +++ b/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/spec/deviceExtensions.kt @@ -1,4 +1,4 @@ -package ru.mipt.npm.controls.properties +package ru.mipt.npm.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()) @@ -24,7 +24,7 @@ public fun , R> D.readRecurring(interval: Duration, reader: /** * Do a recurring task on a device. The task could */ -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/ru/mipt/npm/controls/spec/misc.kt similarity index 94% rename from controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/base/misc.kt rename to controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/spec/misc.kt index 111789f..345f453 100644 --- a/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/base/misc.kt +++ b/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/spec/misc.kt @@ -1,4 +1,4 @@ -package ru.mipt.npm.controls.base +package ru.mipt.npm.controls.spec import space.kscience.dataforge.meta.Meta import space.kscience.dataforge.meta.double diff --git a/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/properties/propertySpecDelegates.kt b/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/spec/propertySpecDelegates.kt similarity index 80% rename from controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/properties/propertySpecDelegates.kt rename to controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/spec/propertySpecDelegates.kt index d087505..7ac8039 100644 --- a/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/properties/propertySpecDelegates.kt +++ b/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/spec/propertySpecDelegates.kt @@ -1,4 +1,4 @@ -package ru.mipt.npm.controls.properties +package ru.mipt.npm.controls.spec import ru.mipt.npm.controls.api.PropertyDescriptor import ru.mipt.npm.controls.api.metaDescriptor @@ -10,19 +10,19 @@ import kotlin.properties.ReadOnlyProperty //read only delegates -public fun > DeviceSpec.booleanProperty( - name: String? = null, +public fun > DeviceSpec.booleanProperty( descriptorBuilder: PropertyDescriptor.() -> Unit = {}, + 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 ): 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 = {}, + 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 = {}, + 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 = {}, + 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 = {}, + name: String? = null, read: suspend D.() -> Boolean, write: suspend D.(Boolean) -> Unit ): PropertyDelegateProvider, ReadOnlyProperty, WritableDevicePropertySpec>> = property( 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) + property(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) + property(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) + property(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 + property(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/spec/getDeviceProperty.kt b/controls-core/src/jvmMain/kotlin/ru/mipt/npm/controls/spec/getDeviceProperty.kt new file mode 100644 index 0000000..39c92e9 --- /dev/null +++ b/controls-core/src/jvmMain/kotlin/ru/mipt/npm/controls/spec/getDeviceProperty.kt @@ -0,0 +1,10 @@ +package ru.mipt.npm.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-opcua/src/main/kotlin/ru/mipt/npm/controls/opcua/client/MiloDeviceBySpec.kt b/controls-opcua/src/main/kotlin/ru/mipt/npm/controls/opcua/client/MiloDeviceBySpec.kt index 351115c..d76fc75 100644 --- a/controls-opcua/src/main/kotlin/ru/mipt/npm/controls/opcua/client/MiloDeviceBySpec.kt +++ b/controls-opcua/src/main/kotlin/ru/mipt/npm/controls/opcua/client/MiloDeviceBySpec.kt @@ -4,8 +4,8 @@ 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 ru.mipt.npm.controls.spec.DeviceBySpec +import ru.mipt.npm.controls.spec.DeviceSpec import space.kscience.dataforge.context.Context import space.kscience.dataforge.context.Global import space.kscience.dataforge.meta.Meta diff --git a/demo/build.gradle.kts b/demo/build.gradle.kts index 0fb18d9..522357e 100644 --- a/demo/build.gradle.kts +++ b/demo/build.gradle.kts @@ -5,7 +5,7 @@ plugins { } -repositories{ +repositories { mavenCentral() jcenter() maven("https://repo.kotlin.link") @@ -15,7 +15,7 @@ repositories{ val ktorVersion: String by rootProject.extra val rsocketVersion: String by rootProject.extra -dependencies{ +dependencies { implementation(projects.controlsCore) //implementation(projects.controlsServer) implementation(projects.magix.magixServer) @@ -34,15 +34,15 @@ dependencies{ tasks.withType().configureEach { kotlinOptions { jvmTarget = "11" - freeCompilerArgs = freeCompilerArgs + "-Xjvm-default=all" + freeCompilerArgs = freeCompilerArgs + listOf("-Xjvm-default=all", "-Xopt-in=kotlin.RequiresOptIn") } } -javafx{ +javafx { version = "14" modules("javafx.controls") } -application{ +application { mainClass.set("ru.mipt.npm.controls.demo.DemoControllerViewKt") } \ No newline at end of file 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 index 7882cbc..be80a40 100644 --- a/demo/src/main/kotlin/ru/mipt/npm/controls/demo/DemoDevice.kt +++ b/demo/src/main/kotlin/ru/mipt/npm/controls/demo/DemoDevice.kt @@ -1,22 +1,47 @@ package ru.mipt.npm.controls.demo import kotlinx.coroutines.launch -import ru.mipt.npm.controls.properties.* +import ru.mipt.npm.controls.api.metaDescriptor +import ru.mipt.npm.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.descriptors.value import space.kscience.dataforge.meta.transformations.MetaConverter +import space.kscience.dataforge.values.ValueType import java.time.Instant import kotlin.time.Duration import kotlin.time.ExperimentalTime -class DemoDevice : DeviceBySpec(DemoDevice) { +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(::DemoDevice) { + @OptIn(ExperimentalTime::class) + override suspend fun open() { + super.open() + launch { + sinScale.read() + cosScale.read() + timeScale.read() + } + doRecurring(Duration.milliseconds(50)) { + coordinates.read() + } + } + + + companion object : DeviceSpec(), Factory { // register virtual properties based on actual object state - val timeScale by property(MetaConverter.double, DemoDevice::timeScaleState) + val timeScale by property(MetaConverter.double, DemoDevice::timeScaleState) { + metaDescriptor { + type(ValueType.NUMBER) + } + info = "Real to virtual time scale" + } + val sinScale by property(MetaConverter.double, DemoDevice::sinScaleState) val cosScale by property(MetaConverter.double, DemoDevice::cosScaleState) @@ -30,7 +55,13 @@ class DemoDevice : DeviceBySpec(DemoDevice) { kotlin.math.cos(time.toEpochMilli().toDouble() / timeScaleState) * sinScaleState } - val coordinates by metaProperty { + val coordinates by metaProperty( + descriptorBuilder = { + metaDescriptor { + value("time", ValueType.NUMBER) + } + } + ) { Meta { val time = Instant.now() "time" put time.toEpochMilli() @@ -46,15 +77,6 @@ class DemoDevice : DeviceBySpec(DemoDevice) { null } - @OptIn(ExperimentalTime::class) - override fun DemoDevice.onStartup() { - launch { - sinScale.read() - cosScale.read() - } - doRecurring(Duration.milliseconds(50)){ - coordinates.read() - } - } + override fun invoke(meta: Meta, context: Context): DemoDevice = DemoDevice(context, meta) } } \ No newline at end of file diff --git a/motors/src/main/kotlin/ru/mipt/npm/devices/pimotionmaster/PiMotionMasterApp.kt b/motors/src/main/kotlin/ru/mipt/npm/devices/pimotionmaster/PiMotionMasterApp.kt index 67d475f..cf66c82 100644 --- a/motors/src/main/kotlin/ru/mipt/npm/devices/pimotionmaster/PiMotionMasterApp.kt +++ b/motors/src/main/kotlin/ru/mipt/npm/devices/pimotionmaster/PiMotionMasterApp.kt @@ -13,6 +13,9 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.launch import ru.mipt.npm.controls.controllers.DeviceManager import ru.mipt.npm.controls.controllers.installing +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.dataforge.context.Global import space.kscience.dataforge.context.fetch import tornadofx.* @@ -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 = minPosition.read() + val max = 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/motors/src/main/kotlin/ru/mipt/npm/devices/pimotionmaster/PiMotionMasterDevice.kt b/motors/src/main/kotlin/ru/mipt/npm/devices/pimotionmaster/PiMotionMasterDevice.kt index 20c5dd1..f08bea2 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 @@ -13,13 +13,13 @@ 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 ru.mipt.npm.controls.spec.* 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.meta.transformations.MetaConverter import space.kscience.dataforge.names.NameToken import space.kscience.dataforge.values.asValue import kotlin.collections.component1 @@ -29,70 +29,20 @@ import kotlin.time.Duration class PiMotionMasterDevice( context: Context, private val portFactory: PortFactory = KtorTcpPort, -) : DeviceBase(context), DeviceHub { +) : DeviceBySpec(PiMotionMasterDevice, 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() + var timeoutValue: Duration = Duration.microseconds(200) /** * Name-friendly accessor for axis @@ -182,166 +132,225 @@ class PiMotionMasterDevice( } } - val initialize: DeviceAction by acting { - send("INI") - } + companion object : DeviceSpec(), Factory { - val identity: ReadOnlyDeviceProperty by readingString { - request("*IDN?").first() - } + override fun invoke(meta: Meta, context: Context): PiMotionMasterDevice = PiMotionMasterDevice(context) - val firmwareVersion: ReadOnlyDeviceProperty by readingString { - request("VER?").first() - } + val connected by booleanProperty(descriptorBuilder = { + info = "True if the connection address is defined and the device is initialized" + }) { + port != null + } - val stop: DeviceAction by acting( - descriptorBuilder = { + + 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" - }, - action = { send("STP") } - ) + }) { + send("STP") + } - inner class Axis(val axisId: String) : DeviceBase(context) { + 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" + }) { + if (port != null) { + stop() + port?.close() + } + port = null + updateLogical(connected, false) + null + } + + + val timeout by property(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 = - requestAndParse(command, axisId)[axisId]?.toIntOrNull() - ?: error("Malformed $command response. Should include integer value for $axisId") != 0 + (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" } - send(command, axisId, boolean) - failIfError() + mm.send(command, axisId, boolean) + mm.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) + 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/fxDeviceProperties.kt b/motors/src/main/kotlin/ru/mipt/npm/devices/pimotionmaster/fxDeviceProperties.kt index 48ef6d2..db5e60a 100644 --- a/motors/src/main/kotlin/ru/mipt/npm/devices/pimotionmaster/fxDeviceProperties.kt +++ b/motors/src/main/kotlin/ru/mipt/npm/devices/pimotionmaster/fxDeviceProperties.kt @@ -3,58 +3,67 @@ 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 ru.mipt.npm.controls.spec.DevicePropertySpec +import ru.mipt.npm.controls.spec.WritableDevicePropertySpec +import ru.mipt.npm.controls.spec.onPropertyChange +import ru.mipt.npm.controls.spec.write 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 +/** + * 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 - flowTyped().onEach { - if (it != null) { - runLater { + 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() } - }.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 + } 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/settings.gradle.kts b/settings.gradle.kts index cf5e79e..1f0f7a3 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -4,7 +4,7 @@ enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") enableFeaturePreview("VERSION_CATALOGS") pluginManagement { - val toolsVersion = "0.10.4" + val toolsVersion = "0.10.5" repositories { maven("https://repo.kotlin.link") @@ -28,7 +28,7 @@ dependencyResolutionManagement { versionCatalogs { create("npm") { - from("ru.mipt.npm:version-catalog:0.10.4") + from("ru.mipt.npm:version-catalog:0.10.5") } } }