Remove stand-alone properties and use specs instead

This commit is contained in:
Alexander Nozik 2021-10-23 19:44:13 +03:00
parent 1b021ca5ca
commit ed2a2a29af
23 changed files with 489 additions and 1204 deletions

View File

@ -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<DeviceMessage>
@ -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")
}

View File

@ -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?
}

View File

@ -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<Meta?> = 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<Meta?> = 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<String, ReadOnlyDeviceProperty>()
public val properties: Map<String, ReadOnlyDeviceProperty> get() = _properties
private val _actions = HashMap<String, DeviceAction>()
public val actions: Map<String, DeviceAction> get() = _actions
internal val sharedMessageFlow = MutableSharedFlow<DeviceMessage>()
override val messageFlow: SharedFlow<DeviceMessage> get() = sharedMessageFlow
private val sharedLogFlow = MutableSharedFlow<LogEntry>()
/**
* The [SharedFlow] of log messages
*/
@DFExperimental
public val logFlow: SharedFlow<LogEntry>
get() = sharedLogFlow
protected suspend fun log(message: String, priority: Int = 0) {
sharedLogFlow.emit(LogEntry(message, priority))
}
override val propertyDescriptors: Collection<PropertyDescriptor>
get() = _properties.values.map { it.descriptor }
override val actionDescriptors: Collection<ActionDescriptor>
get() = _actions.values.map { it.descriptor }
private fun <P : ReadOnlyDeviceProperty> 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 {
}
}

View File

@ -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<Meta?>
}
/**
* 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)
}

View File

@ -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<T : Any>(
private val property: ReadOnlyDeviceProperty,
protected val converter: MetaConverter<T>,
) : 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<T?> = flow().map { it?.let { converter.metaToObject(it) } }
}
/**
* A type-safe wrapper for a read-write device property
*/
public class TypedDeviceProperty<T : Any>(
private val property: DeviceProperty,
converter: MetaConverter<T>,
) : TypedReadOnlyDeviceProperty<T>(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))
}
}

View File

@ -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 : DeviceBase> D.provideAction(): ReadOnlyProperty<D, DeviceAction> =
ReadOnlyProperty { _: D, property: KProperty<*> ->
val name = property.name
return@ReadOnlyProperty actions[name]!!
}
public typealias ActionDelegate = ReadOnlyProperty<DeviceBase, DeviceAction>
private class ActionProvider<D : DeviceBase>(
val owner: D,
val descriptorBuilder: ActionDescriptor.() -> Unit = {},
val block: suspend (Meta?) -> Meta?,
) : PropertyDelegateProvider<D, ActionDelegate> {
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<DeviceBase, ActionDelegate> = ActionProvider(this, descriptorBuilder, action)
public fun <D : DeviceBase> D.requestingValue(
descriptorBuilder: ActionDescriptor.() -> Unit = {},
action: suspend (Meta?) -> Any?,
): PropertyDelegateProvider<D, ActionDelegate> = ActionProvider(this, descriptorBuilder) {
val res = action(it)
Meta(Value.of(res))
}
public fun <D : DeviceBase> D.requestingMeta(
descriptorBuilder: ActionDescriptor.() -> Unit = {},
action: suspend MutableMeta.(Meta?) -> Unit,
): PropertyDelegateProvider<D, ActionDelegate> = ActionProvider(this, descriptorBuilder) {
Meta { action(it) }
}
public fun DeviceBase.acting(
descriptorBuilder: ActionDescriptor.() -> Unit = {},
action: suspend (Meta?) -> Unit,
): PropertyDelegateProvider<DeviceBase, ActionDelegate> = ActionProvider(this, descriptorBuilder) {
action(it)
null
}

View File

@ -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 : DeviceBase> D.provideProperty(name: String): ReadOnlyProperty<D, ReadOnlyDeviceProperty> =
ReadOnlyProperty { _: D, _: KProperty<*> ->
return@ReadOnlyProperty properties.getValue(name)
}
private fun <D : DeviceBase, T : Any> D.provideProperty(
name: String,
converter: MetaConverter<T>,
): ReadOnlyProperty<D, TypedReadOnlyDeviceProperty<T>> =
ReadOnlyProperty { _: D, _: KProperty<*> ->
return@ReadOnlyProperty TypedReadOnlyDeviceProperty(properties.getValue(name), converter)
}
public typealias ReadOnlyPropertyDelegate = ReadOnlyProperty<DeviceBase, ReadOnlyDeviceProperty>
public typealias TypedReadOnlyPropertyDelegate<T> = ReadOnlyProperty<DeviceBase, TypedReadOnlyDeviceProperty<T>>
private class ReadOnlyDevicePropertyProvider<D : DeviceBase>(
val owner: D,
val default: Meta?,
val descriptorBuilder: PropertyDescriptor.() -> Unit = {},
private val getter: suspend (Meta?) -> Meta,
) : PropertyDelegateProvider<D, ReadOnlyPropertyDelegate> {
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<D : DeviceBase, T : Any>(
val owner: D,
val default: Meta?,
val converter: MetaConverter<T>,
val descriptorBuilder: PropertyDescriptor.() -> Unit = {},
private val getter: suspend (Meta?) -> Meta,
) : PropertyDelegateProvider<D, TypedReadOnlyPropertyDelegate<T>> {
override operator fun provideDelegate(thisRef: D, property: KProperty<*>): TypedReadOnlyPropertyDelegate<T> {
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<DeviceBase, ReadOnlyPropertyDelegate> = ReadOnlyDevicePropertyProvider(
this,
default,
descriptorBuilder,
getter
)
public fun DeviceBase.readingValue(
default: Value? = null,
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
getter: suspend () -> Any?,
): PropertyDelegateProvider<DeviceBase, ReadOnlyPropertyDelegate> = 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<DeviceBase, TypedReadOnlyPropertyDelegate<Number>> = 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<DeviceBase, TypedReadOnlyPropertyDelegate<Double>> = 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<DeviceBase, TypedReadOnlyPropertyDelegate<String>> = 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<DeviceBase, TypedReadOnlyPropertyDelegate<Boolean>> = 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<DeviceBase, TypedReadOnlyPropertyDelegate<Meta>> = TypedReadOnlyDevicePropertyProvider(
this,
default,
MetaConverter.meta,
descriptorBuilder,
getter = {
Meta { getter() }
}
)
private fun DeviceBase.provideMutableProperty(name: String): ReadOnlyProperty<DeviceBase, DeviceProperty> =
ReadOnlyProperty { _: DeviceBase, _: KProperty<*> ->
return@ReadOnlyProperty properties[name] as DeviceProperty
}
private fun <T : Any> DeviceBase.provideMutableProperty(
name: String,
converter: MetaConverter<T>,
): ReadOnlyProperty<DeviceBase, TypedDeviceProperty<T>> =
ReadOnlyProperty { _: DeviceBase, _: KProperty<*> ->
return@ReadOnlyProperty TypedDeviceProperty(properties[name] as DeviceProperty, converter)
}
public typealias PropertyDelegate = ReadOnlyProperty<DeviceBase, DeviceProperty>
public typealias TypedPropertyDelegate<T> = ReadOnlyProperty<DeviceBase, TypedDeviceProperty<T>>
private class DevicePropertyProvider<D : DeviceBase>(
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<D, PropertyDelegate> {
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<D : DeviceBase, T : Any>(
val owner: D,
val default: Meta?,
val converter: MetaConverter<T>,
val descriptorBuilder: PropertyDescriptor.() -> Unit = {},
private val getter: suspend (Meta?) -> Meta,
private val setter: suspend (oldValue: Meta?, newValue: Meta) -> Meta?,
) : PropertyDelegateProvider<D, TypedPropertyDelegate<T>> {
override operator fun provideDelegate(thisRef: D, property: KProperty<*>): TypedPropertyDelegate<T> {
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<DeviceBase, PropertyDelegate> = DevicePropertyProvider(
this,
default,
descriptorBuilder,
getter,
setter
)
public fun DeviceBase.writingVirtual(
default: Meta,
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
): PropertyDelegateProvider<DeviceBase, PropertyDelegate> = writing(
default,
descriptorBuilder,
getter = { it ?: default },
setter = { _, newItem -> newItem }
)
public fun DeviceBase.writingVirtual(
default: Value,
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
): PropertyDelegateProvider<DeviceBase, PropertyDelegate> = writing(
Meta(default),
descriptorBuilder,
getter = { it ?: Meta(default) },
setter = { _, newItem -> newItem }
)
public fun <D : DeviceBase> D.writingDouble(
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
getter: suspend (Double) -> Double,
setter: suspend (oldValue: Double?, newValue: Double) -> Double?,
): PropertyDelegateProvider<D, TypedPropertyDelegate<Double>> {
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 : DeviceBase> D.writingBoolean(
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
getter: suspend (Boolean?) -> Boolean,
setter: suspend (oldValue: Boolean?, newValue: Boolean) -> Boolean?,
): PropertyDelegateProvider<D, TypedPropertyDelegate<Boolean>> {
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
)
}

View File

@ -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<D : DeviceBySpec<D>>(
public val spec: DeviceSpec<D>,
context: Context = Global,
meta: Meta = Meta.EMPTY
public abstract class DeviceBase<D : DeviceBase<D>>(
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<String, DevicePropertySpec<D, *>> get() = spec.properties
public val actions: Map<String, DeviceActionSpec<D, *, *>> get() = spec.actions
public abstract val properties: Map<String, DevicePropertySpec<D, *>> //get() = spec.properties
public abstract val actions: Map<String, DeviceActionSpec<D, *, *>> //get() = spec.actions
override val propertyDescriptors: Collection<PropertyDescriptor>
get() = properties.values.map { it.descriptor }
@ -68,6 +59,13 @@ public open class DeviceBySpec<D : DeviceBySpec<D>>(
}
}
/**
* Update logical state using given [spec] and its convertor
*/
protected suspend fun <T> updateLogical(spec: DevicePropertySpec<D, T>, 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<D : DeviceBySpec<D>>(
}
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<D : DeviceBySpec<D>>(
}
}
override fun close() {
with(spec) { self.onShutdown() }
super.close()
}
public suspend operator fun <I, O> DeviceActionSpec<D, I, O>.invoke(input: I? = null): O? = execute(self, input)
}
public suspend fun <D : DeviceBySpec<D>, T : Any> D.read(
propertySpec: DevicePropertySpec<D, T>
): T = propertySpec.read()
public fun <D : DeviceBySpec<D>, T> D.write(
propertySpec: WritableDevicePropertySpec<D, T>,
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<D : DeviceBySpec<D>>(
public val spec: DeviceSpec<D>,
context: Context = Global,
meta: Meta = Meta.EMPTY
) : DeviceBase<D>(context, meta) {
override val properties: Map<String, DevicePropertySpec<D, *>> get() = spec.properties
override val actions: Map<String, DeviceActionSpec<D, *, *>> get() = spec.actions
}

View File

@ -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,7 +82,7 @@ public interface DeviceActionSpec<in D : Device, I, O> {
public suspend fun execute(device: D, input: I?): O?
}
public suspend fun <D : Device, I, O> DeviceActionSpec<D, I, O>.executeMeta(
public suspend fun <D : Device, I, O> DeviceActionSpec<D, I, O>.executeWithMeta(
device: D,
item: Meta?
): Meta? {
@ -83,3 +90,40 @@ public suspend fun <D : Device, I, O> DeviceActionSpec<D, I, O>.executeMeta(
val res = execute(device, arg)
return res?.let { outputConverter.objectToMeta(res) }
}
public suspend fun <D : DeviceBase<D>, T : Any> D.read(
propertySpec: DevicePropertySpec<D, T>
): T = propertySpec.read()
public suspend fun <D : Device, T : Any> D.read(
propertySpec: DevicePropertySpec<D, T>
): T = propertySpec.converter.metaToObject(readProperty(propertySpec.name))
?: error("Property meta converter returned null")
public fun <D : Device, T> D.write(
propertySpec: WritableDevicePropertySpec<D, T>,
value: T
): Job = launch {
writeProperty(propertySpec.name, propertySpec.converter.objectToMeta(value))
}
public fun <D : DeviceBase<D>, T> D.write(
propertySpec: WritableDevicePropertySpec<D, T>,
value: T
): Job = launch {
propertySpec.write(value)
}
/**
* A type safe property change listener
*/
public fun <D : Device, T> Device.onPropertyChange(
spec: DevicePropertySpec<D, T>,
callback: suspend PropertyChangedMessage.(T?) -> Unit
): Job = messageFlow
.filterIsInstance<PropertyChangedMessage>()
.filter { it.property == spec.name }
.onEach { change ->
change.callback(spec.converter.metaToObject(change.value))
}.launchIn(this)

View File

@ -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<D : DeviceBySpec<D>>(
private val buildDevice: () -> D
) : Factory<D> {
public abstract class DeviceSpec<D : Device> {
private val _properties = HashMap<String, DevicePropertySpec<D, *>>()
public val properties: Map<String, DevicePropertySpec<D, *>> get() = _properties
@ -75,8 +72,8 @@ public abstract class DeviceSpec<D : DeviceBySpec<D>>(
public fun <T : Any> property(
converter: MetaConverter<T>,
name: String? = null,
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
name: String? = null,
read: suspend D.() -> T
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DevicePropertySpec<D, T>>> =
PropertyDelegateProvider { _: DeviceSpec<D>, property ->
@ -96,8 +93,8 @@ public abstract class DeviceSpec<D : DeviceBySpec<D>>(
public fun <T : Any> property(
converter: MetaConverter<T>,
name: String? = null,
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
name: String? = null,
read: suspend D.() -> T,
write: suspend D.(T) -> Unit
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, WritableDevicePropertySpec<D, T>>> =
@ -129,8 +126,8 @@ public abstract class DeviceSpec<D : DeviceBySpec<D>>(
public fun <I : Any, O : Any> action(
inputConverter: MetaConverter<I>,
outputConverter: MetaConverter<O>,
name: String? = null,
descriptorBuilder: ActionDescriptor.() -> Unit = {},
name: String? = null,
execute: suspend D.(I?) -> O?
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DeviceActionSpec<D, I, O>>> =
PropertyDelegateProvider { _: DeviceSpec<D>, property ->
@ -153,19 +150,35 @@ public abstract class DeviceSpec<D : DeviceBySpec<D>>(
}
/**
* 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<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DeviceActionSpec<D, Meta, Meta>>> = 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<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DeviceActionSpec<D, Meta, Meta>>> = action(
MetaConverter.Companion.meta,
MetaConverter.Companion.meta,
descriptorBuilder,
name
){
execute()
null
}
}

View File

@ -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 <D : DeviceBySpec<D>, R> D.readRecurring(interval: Duration, reader: suspend D.() -> R): Flow<R> = flow {
public fun <D : DeviceBase<D>, R> D.readRecurring(interval: Duration, reader: suspend D.() -> R): Flow<R> = flow {
while (isActive) {
kotlinx.coroutines.delay(interval)
emit(reader())
@ -24,7 +24,7 @@ public fun <D : DeviceBySpec<D>, R> D.readRecurring(interval: Duration, reader:
/**
* Do a recurring task on a device. The task could
*/
public fun <D : DeviceBySpec<D>> D.doRecurring(interval: Duration, task: suspend D.() -> Unit): Job = launch {
public fun <D : DeviceBase<D>> D.doRecurring(interval: Duration, task: suspend D.() -> Unit): Job = launch {
while (isActive) {
kotlinx.coroutines.delay(interval)
task()

View File

@ -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

View File

@ -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 <D : DeviceBySpec<D>> DeviceSpec<D>.booleanProperty(
name: String? = null,
public fun <D : DeviceBase<D>> DeviceSpec<D>.booleanProperty(
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
name: String? = null,
read: suspend D.() -> Boolean
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DevicePropertySpec<D, Boolean>>> = property(
MetaConverter.boolean,
name,
{
metaDescriptor {
type(ValueType.BOOLEAN)
}
descriptorBuilder()
},
name,
read
)
@ -35,110 +35,110 @@ private inline fun numberDescriptor(
descriptorBuilder()
}
public fun <D : DeviceBySpec<D>> DeviceSpec<D>.numberProperty(
public fun <D : DeviceBase<D>> DeviceSpec<D>.numberProperty(
name: String? = null,
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
read: suspend D.() -> Number
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DevicePropertySpec<D, Number>>> = property(
MetaConverter.number,
name,
numberDescriptor(descriptorBuilder),
name,
read
)
public fun <D : DeviceBySpec<D>> DeviceSpec<D>.doubleProperty(
name: String? = null,
public fun <D : DeviceBase<D>> DeviceSpec<D>.doubleProperty(
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
name: String? = null,
read: suspend D.() -> Double
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DevicePropertySpec<D, Double>>> = property(
MetaConverter.double,
name,
numberDescriptor(descriptorBuilder),
name,
read
)
public fun <D : DeviceBySpec<D>> DeviceSpec<D>.stringProperty(
name: String? = null,
public fun <D : DeviceBase<D>> DeviceSpec<D>.stringProperty(
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
name: String? = null,
read: suspend D.() -> String
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DevicePropertySpec<D, String>>> = property(
MetaConverter.string,
name,
{
metaDescriptor {
type(ValueType.STRING)
}
descriptorBuilder()
},
name,
read
)
public fun <D : DeviceBySpec<D>> DeviceSpec<D>.metaProperty(
name: String? = null,
public fun <D : DeviceBase<D>> DeviceSpec<D>.metaProperty(
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
name: String? = null,
read: suspend D.() -> Meta
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DevicePropertySpec<D, Meta>>> = property(
MetaConverter.meta,
name,
{
metaDescriptor {
type(ValueType.STRING)
}
descriptorBuilder()
},
name,
read
)
//read-write delegates
public fun <D : DeviceBySpec<D>> DeviceSpec<D>.booleanProperty(
name: String? = null,
public fun <D : DeviceBase<D>> DeviceSpec<D>.booleanProperty(
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
name: String? = null,
read: suspend D.() -> Boolean,
write: suspend D.(Boolean) -> Unit
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, WritableDevicePropertySpec<D, Boolean>>> =
property(
MetaConverter.boolean,
name,
{
metaDescriptor {
type(ValueType.BOOLEAN)
}
descriptorBuilder()
},
name,
read,
write
)
public fun <D : DeviceBySpec<D>> DeviceSpec<D>.numberProperty(
name: String? = null,
public fun <D : DeviceBase<D>> DeviceSpec<D>.numberProperty(
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
name: String? = null,
read: suspend D.() -> Number,
write: suspend D.(Number) -> Unit
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, WritableDevicePropertySpec<D, Number>>> =
property(MetaConverter.number, name, numberDescriptor(descriptorBuilder), read, write)
property(MetaConverter.number, numberDescriptor(descriptorBuilder), name, read, write)
public fun <D : DeviceBySpec<D>> DeviceSpec<D>.doubleProperty(
name: String? = null,
public fun <D : DeviceBase<D>> DeviceSpec<D>.doubleProperty(
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
name: String? = null,
read: suspend D.() -> Double,
write: suspend D.(Double) -> Unit
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, WritableDevicePropertySpec<D, Double>>> =
property(MetaConverter.double, name, numberDescriptor(descriptorBuilder), read, write)
property(MetaConverter.double, numberDescriptor(descriptorBuilder), name, read, write)
public fun <D : DeviceBySpec<D>> DeviceSpec<D>.stringProperty(
name: String? = null,
public fun <D : DeviceBase<D>> DeviceSpec<D>.stringProperty(
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
name: String? = null,
read: suspend D.() -> String,
write: suspend D.(String) -> Unit
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, WritableDevicePropertySpec<D, String>>> =
property(MetaConverter.string, name, descriptorBuilder, read, write)
property(MetaConverter.string, descriptorBuilder, name, read, write)
public fun <D : DeviceBySpec<D>> DeviceSpec<D>.metaProperty(
name: String? = null,
public fun <D : DeviceBase<D>> DeviceSpec<D>.metaProperty(
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
name: String? = null,
read: suspend D.() -> Meta,
write: suspend D.(Meta) -> Unit
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, WritableDevicePropertySpec<D, Meta>>> =
property(MetaConverter.meta, name, descriptorBuilder, read, write)
property(MetaConverter.meta, descriptorBuilder, name, read, write)

View File

@ -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 <T: Any> TypedReadOnlyDeviceProperty<T>.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 <T: Any> TypedDeviceProperty<T>.setValue(thisRef: Any?, property: KProperty<*>, value: T) {
this.typedValue = value
}
public fun <T : Any> ReadOnlyDeviceProperty.convert(
metaConverter: MetaConverter<T>,
forceRead: Boolean,
): ReadOnlyProperty<Any?, T> {
return ReadOnlyProperty { _, _ ->
runBlocking(scope.coroutineContext) {
val meta = read(forceRead)
metaConverter.metaToObject(meta)?: error("Meta $meta could not be converted by $metaConverter")
}
}
}
public fun <T : Any> DeviceProperty.convert(
metaConverter: MetaConverter<T>,
forceRead: Boolean,
): ReadWriteProperty<Any?, T> {
return object : ReadWriteProperty<Any?, T> {
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<Any?, Double> =
convert(MetaConverter.double, forceRead)
public fun DeviceProperty.double(forceRead: Boolean = false): ReadWriteProperty<Any?, Double> =
convert(MetaConverter.double, forceRead)
public fun ReadOnlyDeviceProperty.int(forceRead: Boolean = false): ReadOnlyProperty<Any?, Int> =
convert(MetaConverter.int, forceRead)
public fun DeviceProperty.int(forceRead: Boolean = false): ReadWriteProperty<Any?, Int> =
convert(MetaConverter.int, forceRead)
public fun ReadOnlyDeviceProperty.string(forceRead: Boolean = false): ReadOnlyProperty<Any?, String> =
convert(MetaConverter.string, forceRead)
public fun DeviceProperty.string(forceRead: Boolean = false): ReadWriteProperty<Any?, String> =
convert(MetaConverter.string, forceRead)
public fun ReadOnlyDeviceProperty.boolean(forceRead: Boolean = false): ReadOnlyProperty<Any?, Boolean> =
convert(MetaConverter.boolean, forceRead)
public fun DeviceProperty.boolean(forceRead: Boolean = false): ReadWriteProperty<Any?, Boolean> =
convert(MetaConverter.boolean, forceRead)
public fun ReadOnlyDeviceProperty.duration(forceRead: Boolean = false): ReadOnlyProperty<Any?, Duration> =
convert(DurationConverter, forceRead)
public fun DeviceProperty.duration(forceRead: Boolean = false): ReadWriteProperty<Any?, Duration> =
convert(DurationConverter, forceRead)

View File

@ -1,10 +0,0 @@
package ru.mipt.npm.controls.properties
import kotlinx.coroutines.runBlocking
/**
* Blocking property get call
*/
public operator fun <D : DeviceBySpec<D>, T : Any> D.get(
propertySpec: DevicePropertySpec<D, T>
): T = runBlocking { read(propertySpec) }

View File

@ -0,0 +1,10 @@
package ru.mipt.npm.controls.spec
import kotlinx.coroutines.runBlocking
/**
* Blocking property get call
*/
public operator fun <D : DeviceBase<D>, T : Any> D.get(
propertySpec: DevicePropertySpec<D, T>
): T = runBlocking { read(propertySpec) }

View File

@ -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

View File

@ -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<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().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")
}

View File

@ -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>(DemoDevice) {
class DemoDevice(context: Context, meta: Meta) : DeviceBySpec<DemoDevice>(DemoDevice, context, meta) {
private var timeScaleState = 5000.0
private var sinScaleState = 1.0
private var cosScaleState = 1.0
companion object : DeviceSpec<DemoDevice>(::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<DemoDevice>(), Factory<DemoDevice> {
// 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>(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>(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)
}
}

View File

@ -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<Boolean> = device.connected.fxProperty(device)
private val connectedProperty: ReadOnlyProperty<Boolean> = device.fxProperty(PiMotionMasterDevice.connected)
private val debugServerJobProperty = SimpleObjectProperty<Job>()
private val debugServerStarted = debugServerJobProperty.booleanBinding { it != null }
//private val axisList = FXCollections.observableArrayList<Map.Entry<String, PiMotionMasterDevice.Axis>>()

View File

@ -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>(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<PiMotionMasterDevice>(), Factory<PiMotionMasterDevice> {
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>(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<Boolean> 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<Boolean> by axisBooleanProperty("SVO") {
info = "Servo closed loop mode"
}
val velocity: TypedDeviceProperty<Double> 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<PiMotionMasterDevice> {
override fun invoke(meta: Meta, context: Context): PiMotionMasterDevice = PiMotionMasterDevice(context)
companion object : DeviceSpec<Axis>() {
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
}
}
}
}

View File

@ -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 <T : Any> TypedReadOnlyDeviceProperty<T>.fxProperty(ownerDevice: Device?): ReadOnlyProperty<T> =
object : ObjectPropertyBase<T>() {
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 <D : Device, T : Any> Device.fxProperty(
spec: DevicePropertySpec<D, T>
): ReadOnlyProperty<T> = object : ObjectPropertyBase<T>() {
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 <T : Any> TypedDeviceProperty<T>.fxProperty(ownerDevice: Device?): Property<T> =
object : ObjectPropertyBase<T>() {
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 : Device, T : Any> D.fxProperty(spec: WritableDevicePropertySpec<D, T>): Property<T> =
object : ObjectPropertyBase<T>() {
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)
}
}
}
}

View File

@ -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")
}
}
}