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 * 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> 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? 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() { override fun close() {
cancel("The device is closed") 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 * Get a snapshot of logical state of the device
* *
* TODO currently this * TODO currently this
*/ */
public fun Device.getProperties(): Meta = Meta { public fun Device.getProperties(): Meta = Meta {
for (descriptor in propertyDescriptors) { for (descriptor in propertyDescriptors) {
@ -97,4 +105,4 @@ public fun Device.getProperties(): Meta = Meta {
* Subscribe on property changes for the whole device * Subscribe on property changes for the whole device
*/ */
public fun Device.onPropertyChange(callback: suspend PropertyChangedMessage.() -> Unit): Job = public fun Device.onPropertyChange(callback: suspend PropertyChangedMessage.() -> Unit): Job =
messageFlow.filterIsInstance<PropertyChangedMessage>().onEach(callback).launchIn(this) messageFlow.filterIsInstance<PropertyChangedMessage>().onEach(callback).launchIn(this)

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.Job
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
@ -13,24 +13,15 @@ import space.kscience.dataforge.context.Global
import space.kscience.dataforge.meta.Meta import space.kscience.dataforge.meta.Meta
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
/**
* A device generated from specification
* @param D recursive self-type for properties and actions
*/
@OptIn(InternalDeviceAPI::class) @OptIn(InternalDeviceAPI::class)
public open class DeviceBySpec<D : DeviceBySpec<D>>( public abstract class DeviceBase<D : DeviceBase<D>>(
public val spec: DeviceSpec<D>, override val context: Context = Global,
context: Context = Global, public val meta: Meta = Meta.EMPTY
meta: Meta = Meta.EMPTY
) : Device { ) : Device {
override var context: Context = context
internal set
public var meta: Meta = meta public abstract val properties: Map<String, DevicePropertySpec<D, *>> //get() = spec.properties
internal set public abstract val actions: Map<String, DeviceActionSpec<D, *, *>> //get() = spec.actions
public val properties: Map<String, DevicePropertySpec<D, *>> get() = spec.properties
public val actions: Map<String, DeviceActionSpec<D, *, *>> get() = spec.actions
override val propertyDescriptors: Collection<PropertyDescriptor> override val propertyDescriptors: Collection<PropertyDescriptor>
get() = properties.values.map { it.descriptor } 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. * 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 * 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? = override suspend fun execute(action: String, argument: Meta?): Meta? =
actions[action]?.executeMeta(self, argument) actions[action]?.executeWithMeta(self, argument)
/** /**
* Read typed value and update/push event if needed * Read typed value and update/push event if needed
@ -123,19 +121,20 @@ public open class DeviceBySpec<D : DeviceBySpec<D>>(
} }
} }
override fun close() { public suspend operator fun <I, O> DeviceActionSpec<D, I, O>.invoke(input: I? = null): O? = execute(self, input)
with(spec) { self.onShutdown() }
super.close()
}
} }
public suspend fun <D : DeviceBySpec<D>, T : Any> D.read( /**
propertySpec: DevicePropertySpec<D, T> * A device generated from specification
): T = propertySpec.read() * @param D recursive self-type for properties and actions
*/
public fun <D : DeviceBySpec<D>, T> D.write( public open class DeviceBySpec<D : DeviceBySpec<D>>(
propertySpec: WritableDevicePropertySpec<D, T>, public val spec: DeviceSpec<D>,
value: T context: Context = Global,
): Job = launch { meta: Meta = Meta.EMPTY
propertySpec.write(value) ) : 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.ActionDescriptor
import ru.mipt.npm.controls.api.Device import ru.mipt.npm.controls.api.Device
import ru.mipt.npm.controls.api.PropertyChangedMessage
import ru.mipt.npm.controls.api.PropertyDescriptor import ru.mipt.npm.controls.api.PropertyDescriptor
import space.kscience.dataforge.meta.Meta import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.transformations.MetaConverter import space.kscience.dataforge.meta.transformations.MetaConverter
@ -75,11 +82,48 @@ public interface DeviceActionSpec<in D : Device, I, O> {
public suspend fun execute(device: D, input: 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, device: D,
item: Meta? item: Meta?
): Meta? { ): Meta? {
val arg = item?.let { inputConverter.metaToObject(item) } val arg = item?.let { inputConverter.metaToObject(item) }
val res = execute(device, arg) val res = execute(device, arg)
return res?.let { outputConverter.objectToMeta(res) } 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 kotlinx.coroutines.withContext
import ru.mipt.npm.controls.api.ActionDescriptor import ru.mipt.npm.controls.api.ActionDescriptor
import ru.mipt.npm.controls.api.Device
import ru.mipt.npm.controls.api.PropertyDescriptor 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.Meta
import space.kscience.dataforge.meta.transformations.MetaConverter import space.kscience.dataforge.meta.transformations.MetaConverter
import kotlin.properties.PropertyDelegateProvider import kotlin.properties.PropertyDelegateProvider
@ -14,9 +13,7 @@ import kotlin.reflect.KProperty
import kotlin.reflect.KProperty1 import kotlin.reflect.KProperty1
@OptIn(InternalDeviceAPI::class) @OptIn(InternalDeviceAPI::class)
public abstract class DeviceSpec<D : DeviceBySpec<D>>( public abstract class DeviceSpec<D : Device> {
private val buildDevice: () -> D
) : Factory<D> {
private val _properties = HashMap<String, DevicePropertySpec<D, *>>() private val _properties = HashMap<String, DevicePropertySpec<D, *>>()
public val properties: Map<String, DevicePropertySpec<D, *>> get() = _properties 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( public fun <T : Any> property(
converter: MetaConverter<T>, converter: MetaConverter<T>,
name: String? = null,
descriptorBuilder: PropertyDescriptor.() -> Unit = {}, descriptorBuilder: PropertyDescriptor.() -> Unit = {},
name: String? = null,
read: suspend D.() -> T read: suspend D.() -> T
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DevicePropertySpec<D, T>>> = ): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DevicePropertySpec<D, T>>> =
PropertyDelegateProvider { _: DeviceSpec<D>, property -> PropertyDelegateProvider { _: DeviceSpec<D>, property ->
@ -96,8 +93,8 @@ public abstract class DeviceSpec<D : DeviceBySpec<D>>(
public fun <T : Any> property( public fun <T : Any> property(
converter: MetaConverter<T>, converter: MetaConverter<T>,
name: String? = null,
descriptorBuilder: PropertyDescriptor.() -> Unit = {}, descriptorBuilder: PropertyDescriptor.() -> Unit = {},
name: String? = null,
read: suspend D.() -> T, read: suspend D.() -> T,
write: suspend D.(T) -> Unit write: suspend D.(T) -> Unit
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, WritableDevicePropertySpec<D, T>>> = ): 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( public fun <I : Any, O : Any> action(
inputConverter: MetaConverter<I>, inputConverter: MetaConverter<I>,
outputConverter: MetaConverter<O>, outputConverter: MetaConverter<O>,
name: String? = null,
descriptorBuilder: ActionDescriptor.() -> Unit = {}, descriptorBuilder: ActionDescriptor.() -> Unit = {},
name: String? = null,
execute: suspend D.(I?) -> O? execute: suspend D.(I?) -> O?
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DeviceActionSpec<D, I, O>>> = ): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DeviceActionSpec<D, I, O>>> =
PropertyDelegateProvider { _: DeviceSpec<D>, property -> 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() {} public fun unitAction(
descriptorBuilder: ActionDescriptor.() -> Unit = {},
name: String? = null,
override fun invoke(meta: Meta, context: Context): D = buildDevice().apply { execute: suspend D.() -> Unit
this.context = context ): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DeviceActionSpec<D, Meta, Meta>>> = action(
this.meta = meta MetaConverter.Companion.meta,
onStartup() 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.Job
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@ -14,7 +14,7 @@ import kotlin.time.Duration
* *
* The flow is canceled when the device scope is canceled * 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) { while (isActive) {
kotlinx.coroutines.delay(interval) kotlinx.coroutines.delay(interval)
emit(reader()) 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 * 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) { while (isActive) {
kotlinx.coroutines.delay(interval) kotlinx.coroutines.delay(interval)
task() 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.Meta
import space.kscience.dataforge.meta.double 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.PropertyDescriptor
import ru.mipt.npm.controls.api.metaDescriptor import ru.mipt.npm.controls.api.metaDescriptor
@ -10,19 +10,19 @@ import kotlin.properties.ReadOnlyProperty
//read only delegates //read only delegates
public fun <D : DeviceBySpec<D>> DeviceSpec<D>.booleanProperty( public fun <D : DeviceBase<D>> DeviceSpec<D>.booleanProperty(
name: String? = null,
descriptorBuilder: PropertyDescriptor.() -> Unit = {}, descriptorBuilder: PropertyDescriptor.() -> Unit = {},
name: String? = null,
read: suspend D.() -> Boolean read: suspend D.() -> Boolean
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DevicePropertySpec<D, Boolean>>> = property( ): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DevicePropertySpec<D, Boolean>>> = property(
MetaConverter.boolean, MetaConverter.boolean,
name,
{ {
metaDescriptor { metaDescriptor {
type(ValueType.BOOLEAN) type(ValueType.BOOLEAN)
} }
descriptorBuilder() descriptorBuilder()
}, },
name,
read read
) )
@ -35,110 +35,110 @@ private inline fun numberDescriptor(
descriptorBuilder() descriptorBuilder()
} }
public fun <D : DeviceBySpec<D>> DeviceSpec<D>.numberProperty( public fun <D : DeviceBase<D>> DeviceSpec<D>.numberProperty(
name: String? = null, name: String? = null,
descriptorBuilder: PropertyDescriptor.() -> Unit = {}, descriptorBuilder: PropertyDescriptor.() -> Unit = {},
read: suspend D.() -> Number read: suspend D.() -> Number
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DevicePropertySpec<D, Number>>> = property( ): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DevicePropertySpec<D, Number>>> = property(
MetaConverter.number, MetaConverter.number,
name,
numberDescriptor(descriptorBuilder), numberDescriptor(descriptorBuilder),
name,
read read
) )
public fun <D : DeviceBySpec<D>> DeviceSpec<D>.doubleProperty( public fun <D : DeviceBase<D>> DeviceSpec<D>.doubleProperty(
name: String? = null,
descriptorBuilder: PropertyDescriptor.() -> Unit = {}, descriptorBuilder: PropertyDescriptor.() -> Unit = {},
name: String? = null,
read: suspend D.() -> Double read: suspend D.() -> Double
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DevicePropertySpec<D, Double>>> = property( ): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DevicePropertySpec<D, Double>>> = property(
MetaConverter.double, MetaConverter.double,
name,
numberDescriptor(descriptorBuilder), numberDescriptor(descriptorBuilder),
name,
read read
) )
public fun <D : DeviceBySpec<D>> DeviceSpec<D>.stringProperty( public fun <D : DeviceBase<D>> DeviceSpec<D>.stringProperty(
name: String? = null,
descriptorBuilder: PropertyDescriptor.() -> Unit = {}, descriptorBuilder: PropertyDescriptor.() -> Unit = {},
name: String? = null,
read: suspend D.() -> String read: suspend D.() -> String
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DevicePropertySpec<D, String>>> = property( ): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DevicePropertySpec<D, String>>> = property(
MetaConverter.string, MetaConverter.string,
name,
{ {
metaDescriptor { metaDescriptor {
type(ValueType.STRING) type(ValueType.STRING)
} }
descriptorBuilder() descriptorBuilder()
}, },
name,
read read
) )
public fun <D : DeviceBySpec<D>> DeviceSpec<D>.metaProperty( public fun <D : DeviceBase<D>> DeviceSpec<D>.metaProperty(
name: String? = null,
descriptorBuilder: PropertyDescriptor.() -> Unit = {}, descriptorBuilder: PropertyDescriptor.() -> Unit = {},
name: String? = null,
read: suspend D.() -> Meta read: suspend D.() -> Meta
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DevicePropertySpec<D, Meta>>> = property( ): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DevicePropertySpec<D, Meta>>> = property(
MetaConverter.meta, MetaConverter.meta,
name,
{ {
metaDescriptor { metaDescriptor {
type(ValueType.STRING) type(ValueType.STRING)
} }
descriptorBuilder() descriptorBuilder()
}, },
name,
read read
) )
//read-write delegates //read-write delegates
public fun <D : DeviceBySpec<D>> DeviceSpec<D>.booleanProperty( public fun <D : DeviceBase<D>> DeviceSpec<D>.booleanProperty(
name: String? = null,
descriptorBuilder: PropertyDescriptor.() -> Unit = {}, descriptorBuilder: PropertyDescriptor.() -> Unit = {},
name: String? = null,
read: suspend D.() -> Boolean, read: suspend D.() -> Boolean,
write: suspend D.(Boolean) -> Unit write: suspend D.(Boolean) -> Unit
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, WritableDevicePropertySpec<D, Boolean>>> = ): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, WritableDevicePropertySpec<D, Boolean>>> =
property( property(
MetaConverter.boolean, MetaConverter.boolean,
name,
{ {
metaDescriptor { metaDescriptor {
type(ValueType.BOOLEAN) type(ValueType.BOOLEAN)
} }
descriptorBuilder() descriptorBuilder()
}, },
name,
read, read,
write write
) )
public fun <D : DeviceBySpec<D>> DeviceSpec<D>.numberProperty( public fun <D : DeviceBase<D>> DeviceSpec<D>.numberProperty(
name: String? = null,
descriptorBuilder: PropertyDescriptor.() -> Unit = {}, descriptorBuilder: PropertyDescriptor.() -> Unit = {},
name: String? = null,
read: suspend D.() -> Number, read: suspend D.() -> Number,
write: suspend D.(Number) -> Unit write: suspend D.(Number) -> Unit
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, WritableDevicePropertySpec<D, Number>>> = ): 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( public fun <D : DeviceBase<D>> DeviceSpec<D>.doubleProperty(
name: String? = null,
descriptorBuilder: PropertyDescriptor.() -> Unit = {}, descriptorBuilder: PropertyDescriptor.() -> Unit = {},
name: String? = null,
read: suspend D.() -> Double, read: suspend D.() -> Double,
write: suspend D.(Double) -> Unit write: suspend D.(Double) -> Unit
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, WritableDevicePropertySpec<D, Double>>> = ): 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( public fun <D : DeviceBase<D>> DeviceSpec<D>.stringProperty(
name: String? = null,
descriptorBuilder: PropertyDescriptor.() -> Unit = {}, descriptorBuilder: PropertyDescriptor.() -> Unit = {},
name: String? = null,
read: suspend D.() -> String, read: suspend D.() -> String,
write: suspend D.(String) -> Unit write: suspend D.(String) -> Unit
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, WritableDevicePropertySpec<D, String>>> = ): 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( public fun <D : DeviceBase<D>> DeviceSpec<D>.metaProperty(
name: String? = null,
descriptorBuilder: PropertyDescriptor.() -> Unit = {}, descriptorBuilder: PropertyDescriptor.() -> Unit = {},
name: String? = null,
read: suspend D.() -> Meta, read: suspend D.() -> Meta,
write: suspend D.(Meta) -> Unit write: suspend D.(Meta) -> Unit
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, WritableDevicePropertySpec<D, Meta>>> = ): 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 kotlinx.coroutines.runBlocking
import org.eclipse.milo.opcua.sdk.client.OpcUaClient import org.eclipse.milo.opcua.sdk.client.OpcUaClient
import org.eclipse.milo.opcua.stack.core.types.builtin.NodeId import org.eclipse.milo.opcua.stack.core.types.builtin.NodeId
import ru.mipt.npm.controls.properties.DeviceBySpec import ru.mipt.npm.controls.spec.DeviceBySpec
import ru.mipt.npm.controls.properties.DeviceSpec import ru.mipt.npm.controls.spec.DeviceSpec
import space.kscience.dataforge.context.Context import space.kscience.dataforge.context.Context
import space.kscience.dataforge.context.Global import space.kscience.dataforge.context.Global
import space.kscience.dataforge.meta.Meta import space.kscience.dataforge.meta.Meta

View File

@ -5,7 +5,7 @@ plugins {
} }
repositories{ repositories {
mavenCentral() mavenCentral()
jcenter() jcenter()
maven("https://repo.kotlin.link") maven("https://repo.kotlin.link")
@ -15,7 +15,7 @@ repositories{
val ktorVersion: String by rootProject.extra val ktorVersion: String by rootProject.extra
val rsocketVersion: String by rootProject.extra val rsocketVersion: String by rootProject.extra
dependencies{ dependencies {
implementation(projects.controlsCore) implementation(projects.controlsCore)
//implementation(projects.controlsServer) //implementation(projects.controlsServer)
implementation(projects.magix.magixServer) implementation(projects.magix.magixServer)
@ -34,15 +34,15 @@ dependencies{
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach { tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach {
kotlinOptions { kotlinOptions {
jvmTarget = "11" jvmTarget = "11"
freeCompilerArgs = freeCompilerArgs + "-Xjvm-default=all" freeCompilerArgs = freeCompilerArgs + listOf("-Xjvm-default=all", "-Xopt-in=kotlin.RequiresOptIn")
} }
} }
javafx{ javafx {
version = "14" version = "14"
modules("javafx.controls") modules("javafx.controls")
} }
application{ application {
mainClass.set("ru.mipt.npm.controls.demo.DemoControllerViewKt") mainClass.set("ru.mipt.npm.controls.demo.DemoControllerViewKt")
} }

View File

@ -1,22 +1,47 @@
package ru.mipt.npm.controls.demo package ru.mipt.npm.controls.demo
import kotlinx.coroutines.launch 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.Meta
import space.kscience.dataforge.meta.descriptors.value
import space.kscience.dataforge.meta.transformations.MetaConverter import space.kscience.dataforge.meta.transformations.MetaConverter
import space.kscience.dataforge.values.ValueType
import java.time.Instant import java.time.Instant
import kotlin.time.Duration import kotlin.time.Duration
import kotlin.time.ExperimentalTime 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 timeScaleState = 5000.0
private var sinScaleState = 1.0 private var sinScaleState = 1.0
private var cosScaleState = 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 // 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 sinScale by property(MetaConverter.double, DemoDevice::sinScaleState)
val cosScale by property(MetaConverter.double, DemoDevice::cosScaleState) 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 kotlin.math.cos(time.toEpochMilli().toDouble() / timeScaleState) * sinScaleState
} }
val coordinates by metaProperty { val coordinates by metaProperty(
descriptorBuilder = {
metaDescriptor {
value("time", ValueType.NUMBER)
}
}
) {
Meta { Meta {
val time = Instant.now() val time = Instant.now()
"time" put time.toEpochMilli() "time" put time.toEpochMilli()
@ -46,15 +77,6 @@ class DemoDevice : DeviceBySpec<DemoDevice>(DemoDevice) {
null null
} }
@OptIn(ExperimentalTime::class) override fun invoke(meta: Meta, context: Context): DemoDevice = DemoDevice(context, meta)
override fun DemoDevice.onStartup() {
launch {
sinScale.read()
cosScale.read()
}
doRecurring(Duration.milliseconds(50)){
coordinates.read()
}
}
} }
} }

View File

@ -13,6 +13,9 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import ru.mipt.npm.controls.controllers.DeviceManager import ru.mipt.npm.controls.controllers.DeviceManager
import ru.mipt.npm.controls.controllers.installing 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.Global
import space.kscience.dataforge.context.fetch import space.kscience.dataforge.context.fetch
import tornadofx.* import tornadofx.*
@ -40,28 +43,30 @@ fun VBox.piMotionMasterAxis(
alignment = Pos.CENTER alignment = Pos.CENTER
label(axisName) label(axisName)
coroutineScope.launch { coroutineScope.launch {
val min = axis.minPosition.readTyped(true) with(axis) {
val max = axis.maxPosition.readTyped(true) val min = minPosition.read()
val positionProperty = axis.position.fxProperty(axis) val max = maxPosition.read()
val startPosition = axis.position.readTyped(true) val positionProperty = fxProperty(position)
runLater { val startPosition = position.read()
vbox { runLater {
hgrow = Priority.ALWAYS vbox {
slider(min..max, startPosition) { hgrow = Priority.ALWAYS
minWidth = 300.0 slider(min..max, startPosition) {
isShowTickLabels = true minWidth = 300.0
isShowTickMarks = true isShowTickLabels = true
minorTickCount = 10 isShowTickMarks = true
majorTickUnit = 1.0 minorTickCount = 10
valueProperty().onChange { majorTickUnit = 1.0
coroutineScope.launch { valueProperty().onChange {
axis.move(value) coroutineScope.launch {
axis.move(value)
}
} }
} }
} slider(min..max) {
slider(min..max) { isDisable = true
isDisable = true valueProperty().bind(positionProperty)
valueProperty().bind(positionProperty) }
} }
} }
} }
@ -82,7 +87,7 @@ class PiMotionMasterView : View() {
private val controller: PiMotionMasterController by inject() private val controller: PiMotionMasterController by inject()
val device = controller.motionMaster 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 debugServerJobProperty = SimpleObjectProperty<Job>()
private val debugServerStarted = debugServerJobProperty.booleanBinding { it != null } private val debugServerStarted = debugServerJobProperty.booleanBinding { it != null }
//private val axisList = FXCollections.observableArrayList<Map.Entry<String, PiMotionMasterDevice.Axis>>() //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 kotlinx.coroutines.withTimeout
import ru.mipt.npm.controls.api.DeviceHub import ru.mipt.npm.controls.api.DeviceHub
import ru.mipt.npm.controls.api.PropertyDescriptor import ru.mipt.npm.controls.api.PropertyDescriptor
import ru.mipt.npm.controls.base.*
import ru.mipt.npm.controls.controllers.duration
import ru.mipt.npm.controls.ports.* import ru.mipt.npm.controls.ports.*
import ru.mipt.npm.controls.spec.*
import space.kscience.dataforge.context.* import space.kscience.dataforge.context.*
import space.kscience.dataforge.meta.Meta import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.double import space.kscience.dataforge.meta.double
import space.kscience.dataforge.meta.get import space.kscience.dataforge.meta.get
import space.kscience.dataforge.meta.transformations.MetaConverter
import space.kscience.dataforge.names.NameToken import space.kscience.dataforge.names.NameToken
import space.kscience.dataforge.values.asValue import space.kscience.dataforge.values.asValue
import kotlin.collections.component1 import kotlin.collections.component1
@ -29,70 +29,20 @@ import kotlin.time.Duration
class PiMotionMasterDevice( class PiMotionMasterDevice(
context: Context, context: Context,
private val portFactory: PortFactory = KtorTcpPort, private val portFactory: PortFactory = KtorTcpPort,
) : DeviceBase(context), DeviceHub { ) : DeviceBySpec<PiMotionMasterDevice>(PiMotionMasterDevice, context), DeviceHub {
private var port: Port? = null private var port: Port? = null
//TODO make proxy work //TODO make proxy work
//PortProxy { portFactory(address ?: error("The device is not connected"), context) } //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() { fun disconnect() {
runBlocking { runBlocking {
disconnect.invoke() disconnect.invoke()
} }
} }
val timeout: DeviceProperty by writingVirtual(200.asValue()) { var timeoutValue: Duration = Duration.microseconds(200)
info = "Timeout"
}
var timeoutValue: Duration by timeout.duration()
/** /**
* Name-friendly accessor for axis * Name-friendly accessor for axis
@ -182,166 +132,225 @@ class PiMotionMasterDevice(
} }
} }
val initialize: DeviceAction by acting { companion object : DeviceSpec<PiMotionMasterDevice>(), Factory<PiMotionMasterDevice> {
send("INI")
}
val identity: ReadOnlyDeviceProperty by readingString { override fun invoke(meta: Meta, context: Context): PiMotionMasterDevice = PiMotionMasterDevice(context)
request("*IDN?").first()
}
val firmwareVersion: ReadOnlyDeviceProperty by readingString { val connected by booleanProperty(descriptorBuilder = {
request("VER?").first() 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" 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 = private suspend fun readAxisBoolean(command: String): Boolean =
requestAndParse(command, axisId)[axisId]?.toIntOrNull() (mm.requestAndParse(command, axisId)[axisId]?.toIntOrNull()
?: error("Malformed $command response. Should include integer value for $axisId") != 0 ?: 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 { private suspend fun writeAxisBoolean(command: String, value: Boolean): Boolean {
val boolean = if (value) { val boolean = if (value) {
"1" "1"
} else { } else {
"0" "0"
} }
send(command, axisId, boolean) mm.send(command, axisId, boolean)
failIfError() mm.failIfError()
return value 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) { suspend fun move(target: Double) {
move(target.asMeta()) move(target.asMeta())
} }
}
companion object : Factory<PiMotionMasterDevice> { companion object : DeviceSpec<Axis>() {
override fun invoke(meta: Meta, context: Context): PiMotionMasterDevice = PiMotionMasterDevice(context)
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.ObjectPropertyBase
import javafx.beans.property.Property import javafx.beans.property.Property
import javafx.beans.property.ReadOnlyProperty 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.api.Device
import ru.mipt.npm.controls.base.TypedDeviceProperty import ru.mipt.npm.controls.spec.DevicePropertySpec
import ru.mipt.npm.controls.base.TypedReadOnlyDeviceProperty 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.info
import space.kscience.dataforge.context.logger import space.kscience.dataforge.context.logger
import tornadofx.* import tornadofx.*
fun <T : Any> TypedReadOnlyDeviceProperty<T>.fxProperty(ownerDevice: Device?): ReadOnlyProperty<T> = /**
object : ObjectPropertyBase<T>() { * Bind a FX property to a device property with a given [spec]
override fun getBean(): Any? = ownerDevice */
override fun getName(): String = this@fxProperty.name 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 { init {
//Read incoming changes //Read incoming changes
flowTyped().onEach { onPropertyChange(spec) {
if (it != null) { if (it != null) {
runLater { runLater {
try {
set(it) set(it)
} catch (ex: Throwable) {
logger.info { "Failed to set property $name to $it" }
} }
} else {
invalidated()
} }
}.catch { } else {
ownerDevice?.logger?.info { "Failed to set property $name to $it" } invalidated()
}.launchIn(scope) }
} }
} }
}
fun <T : Any> TypedDeviceProperty<T>.fxProperty(ownerDevice: Device?): Property<T> =
object : ObjectPropertyBase<T>() { fun <D : Device, T : Any> D.fxProperty(spec: WritableDevicePropertySpec<D, T>): Property<T> =
override fun getBean(): Any? = ownerDevice object : ObjectPropertyBase<T>() {
override fun getName(): String = this@fxProperty.name override fun getBean(): Any = this
override fun getName(): String = spec.name
init {
//Read incoming changes init {
flowTyped().onEach { //Read incoming changes
if (it != null) { onPropertyChange(spec) {
runLater { if (it != null) {
set(it) runLater {
} try {
} else { set(it)
invalidated() } catch (ex: Throwable) {
} logger.info { "Failed to set property $name to $it" }
}.catch { }
ownerDevice?.logger?.info { "Failed to set property $name to $it" } }
}.launchIn(scope) } else {
invalidated()
onChange { }
typedValue = it }
onChange { newValue ->
if (newValue != null) {
write(spec, newValue)
}
} }
} }
} }

View File

@ -4,7 +4,7 @@ enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")
enableFeaturePreview("VERSION_CATALOGS") enableFeaturePreview("VERSION_CATALOGS")
pluginManagement { pluginManagement {
val toolsVersion = "0.10.4" val toolsVersion = "0.10.5"
repositories { repositories {
maven("https://repo.kotlin.link") maven("https://repo.kotlin.link")
@ -28,7 +28,7 @@ dependencyResolutionManagement {
versionCatalogs { versionCatalogs {
create("npm") { create("npm") {
from("ru.mipt.npm:version-catalog:0.10.4") from("ru.mipt.npm:version-catalog:0.10.5")
} }
} }
} }