WIP: Composition of specifications and devices #13
@ -32,7 +32,7 @@ public class StateConstructorElement<T>(
|
||||
public val state: DeviceState<T>,
|
||||
) : ConstructorElement
|
||||
|
||||
public class ConnectionConstrucorElement(
|
||||
public class ConnectionConstructorElement(
|
||||
public val reads: Collection<DeviceState<*>>,
|
||||
public val writes: Collection<DeviceState<*>>,
|
||||
) : ConstructorElement
|
||||
@ -58,7 +58,7 @@ public interface StateContainer : ContextAware, CoroutineScope {
|
||||
reads: Collection<DeviceState<*>> = emptySet(),
|
||||
onChange: suspend (T) -> Unit,
|
||||
): Job = valueFlow.onEach(onChange).launchIn(this@StateContainer).also {
|
||||
registerElement(ConnectionConstrucorElement(reads + this, writes))
|
||||
registerElement(ConnectionConstructorElement(reads + this, writes))
|
||||
}
|
||||
|
||||
public fun <T> DeviceState<T>.onChange(
|
||||
@ -72,7 +72,7 @@ public interface StateContainer : ContextAware, CoroutineScope {
|
||||
onChange(pair.first, pair.second)
|
||||
}
|
||||
}.launchIn(this@StateContainer).also {
|
||||
registerElement(ConnectionConstrucorElement(reads + this, writes))
|
||||
registerElement(ConnectionConstructorElement(reads + this, writes))
|
||||
}
|
||||
}
|
||||
|
||||
@ -164,7 +164,7 @@ public fun <T1, T2, R> StateContainer.combineState(
|
||||
* On resulting [Job] cancel the binding is unregistered
|
||||
*/
|
||||
public fun <T> StateContainer.bind(sourceState: DeviceState<T>, targetState: MutableDeviceState<T>): Job {
|
||||
val descriptor = ConnectionConstrucorElement(setOf(sourceState), setOf(targetState))
|
||||
val descriptor = ConnectionConstructorElement(setOf(sourceState), setOf(targetState))
|
||||
registerElement(descriptor)
|
||||
return sourceState.valueFlow.onEach {
|
||||
targetState.value = it
|
||||
@ -186,7 +186,7 @@ public fun <T, R> StateContainer.transformTo(
|
||||
targetState: MutableDeviceState<R>,
|
||||
transformation: suspend (T) -> R,
|
||||
): Job {
|
||||
val descriptor = ConnectionConstrucorElement(setOf(sourceState), setOf(targetState))
|
||||
val descriptor = ConnectionConstructorElement(setOf(sourceState), setOf(targetState))
|
||||
registerElement(descriptor)
|
||||
return sourceState.valueFlow.onEach {
|
||||
targetState.value = transformation(it)
|
||||
@ -208,7 +208,7 @@ public fun <T1, T2, R> StateContainer.combineTo(
|
||||
targetState: MutableDeviceState<R>,
|
||||
transformation: suspend (T1, T2) -> R,
|
||||
): Job {
|
||||
val descriptor = ConnectionConstrucorElement(setOf(sourceState1, sourceState2), setOf(targetState))
|
||||
val descriptor = ConnectionConstructorElement(setOf(sourceState1, sourceState2), setOf(targetState))
|
||||
registerElement(descriptor)
|
||||
return kotlinx.coroutines.flow.combine(sourceState1.valueFlow, sourceState2.valueFlow, transformation).onEach {
|
||||
targetState.value = it
|
||||
@ -229,7 +229,7 @@ public inline fun <reified T, R> StateContainer.combineTo(
|
||||
targetState: MutableDeviceState<R>,
|
||||
noinline transformation: suspend (Array<T>) -> R,
|
||||
): Job {
|
||||
val descriptor = ConnectionConstrucorElement(sourceStates, setOf(targetState))
|
||||
val descriptor = ConnectionConstructorElement(sourceStates, setOf(targetState))
|
||||
registerElement(descriptor)
|
||||
return kotlinx.coroutines.flow.combine(sourceStates.map { it.valueFlow }, transformation).onEach {
|
||||
targetState.value = it
|
||||
|
@ -4,34 +4,109 @@ import kotlinx.serialization.Serializable
|
||||
import space.kscience.dataforge.meta.descriptors.MetaDescriptor
|
||||
import space.kscience.dataforge.meta.descriptors.MetaDescriptorBuilder
|
||||
|
||||
//TODO add proper builders
|
||||
//TODO check proper builders
|
||||
|
||||
/**
|
||||
* A descriptor for property
|
||||
* A descriptor for a property
|
||||
*/
|
||||
@Serializable
|
||||
public class PropertyDescriptor(
|
||||
public val name: String,
|
||||
public var description: String? = null,
|
||||
public var metaDescriptor: MetaDescriptor = MetaDescriptor(),
|
||||
public var readable: Boolean = true,
|
||||
public var mutable: Boolean = false,
|
||||
public val description: String? = null,
|
||||
public val metaDescriptor: MetaDescriptor = MetaDescriptor(),
|
||||
public val readable: Boolean = true,
|
||||
public val mutable: Boolean = false,
|
||||
)
|
||||
|
||||
public fun PropertyDescriptor.metaDescriptor(block: MetaDescriptorBuilder.() -> Unit) {
|
||||
metaDescriptor = MetaDescriptor {
|
||||
from(metaDescriptor)
|
||||
block()
|
||||
/**
|
||||
* A builder for PropertyDescriptor
|
||||
*/
|
||||
public class PropertyDescriptorBuilder(public val name: String) {
|
||||
public var description: String? = null
|
||||
public var metaDescriptor: MetaDescriptor = MetaDescriptor()
|
||||
public var readable: Boolean = true
|
||||
public var mutable: Boolean = false
|
||||
|
||||
/**
|
||||
* Configure the metaDescriptor using a block
|
||||
*/
|
||||
public fun metaDescriptor(block: MetaDescriptorBuilder.() -> Unit) {
|
||||
metaDescriptor = MetaDescriptor {
|
||||
from(metaDescriptor)
|
||||
block()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the PropertyDescriptor
|
||||
*/
|
||||
public fun build(): PropertyDescriptor = PropertyDescriptor(
|
||||
name = name,
|
||||
description = description,
|
||||
metaDescriptor = metaDescriptor,
|
||||
readable = readable,
|
||||
mutable = mutable
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* A descriptor for property
|
||||
* Create a PropertyDescriptor using a builder
|
||||
*/
|
||||
public fun propertyDescriptor(name: String, builder: PropertyDescriptorBuilder.() -> Unit = {}): PropertyDescriptor =
|
||||
PropertyDescriptorBuilder(name).apply(builder).build()
|
||||
|
||||
/**
|
||||
* A descriptor for an action
|
||||
*/
|
||||
@Serializable
|
||||
public class ActionDescriptor(
|
||||
public val name: String,
|
||||
public var description: String? = null,
|
||||
public var inputMetaDescriptor: MetaDescriptor = MetaDescriptor(),
|
||||
public var outputMetaDescriptor: MetaDescriptor = MetaDescriptor()
|
||||
public val description: String? = null,
|
||||
public val inputMetaDescriptor: MetaDescriptor = MetaDescriptor(),
|
||||
public val outputMetaDescriptor: MetaDescriptor = MetaDescriptor(),
|
||||
)
|
||||
|
||||
/**
|
||||
* A builder for ActionDescriptor
|
||||
*/
|
||||
public class ActionDescriptorBuilder(public val name: String) {
|
||||
public var description: String? = null
|
||||
public var inputMetaDescriptor: MetaDescriptor = MetaDescriptor()
|
||||
public var outputMetaDescriptor: MetaDescriptor = MetaDescriptor()
|
||||
|
||||
/**
|
||||
* Configure the inputMetaDescriptor using a block
|
||||
*/
|
||||
public fun inputMeta(block: MetaDescriptorBuilder.() -> Unit) {
|
||||
inputMetaDescriptor = MetaDescriptor {
|
||||
from(inputMetaDescriptor)
|
||||
block()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure the outputMetaDescriptor using a block
|
||||
*/
|
||||
public fun outputMeta(block: MetaDescriptorBuilder.() -> Unit) {
|
||||
outputMetaDescriptor = MetaDescriptor {
|
||||
from(outputMetaDescriptor)
|
||||
block()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the ActionDescriptor
|
||||
*/
|
||||
public fun build(): ActionDescriptor = ActionDescriptor(
|
||||
name = name,
|
||||
description = description,
|
||||
inputMetaDescriptor = inputMetaDescriptor,
|
||||
outputMetaDescriptor = outputMetaDescriptor
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an ActionDescriptor using a builder
|
||||
*/
|
||||
public fun actionDescriptor(name: String, builder: ActionDescriptorBuilder.() -> Unit = {}): ActionDescriptor =
|
||||
ActionDescriptorBuilder(name).apply(builder).build()
|
||||
|
@ -0,0 +1,809 @@
|
||||
package space.kscience.controls.spec
|
||||
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.CoroutineName
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.cancelAndJoin
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
import space.kscience.controls.api.*
|
||||
import space.kscience.dataforge.context.Context
|
||||
import space.kscience.dataforge.context.error
|
||||
import space.kscience.dataforge.context.logger
|
||||
import space.kscience.dataforge.meta.Meta
|
||||
import space.kscience.dataforge.meta.MetaConverter
|
||||
import space.kscience.dataforge.meta.MutableMeta
|
||||
import space.kscience.dataforge.names.Name
|
||||
import space.kscience.dataforge.names.asName
|
||||
import space.kscience.dataforge.names.parseAsName
|
||||
import space.kscience.dataforge.names.plus
|
||||
import kotlin.properties.PropertyDelegateProvider
|
||||
import kotlin.properties.ReadOnlyProperty
|
||||
import kotlin.reflect.KProperty
|
||||
import kotlin.time.Duration
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.filterIsInstance
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import space.kscience.dataforge.context.AbstractPlugin
|
||||
import space.kscience.dataforge.context.ContextAware
|
||||
import space.kscience.dataforge.context.ContextBuilder
|
||||
import space.kscience.dataforge.context.PluginFactory
|
||||
import space.kscience.dataforge.context.PluginTag
|
||||
|
||||
/**
|
||||
* Defines how child device lifecycle should be managed.
|
||||
*/
|
||||
public enum class LifecycleMode {
|
||||
/**
|
||||
* Device starts and stops with parent
|
||||
*/
|
||||
LINKED,
|
||||
|
||||
/**
|
||||
* Device is started and stopped independently
|
||||
*/
|
||||
INDEPENDENT,
|
||||
|
||||
/**
|
||||
* Device is created but starts only when explicitly requested
|
||||
*/
|
||||
LAZY
|
||||
}
|
||||
|
||||
public sealed class DeviceChangeEvent {
|
||||
public abstract val deviceName: Name
|
||||
|
||||
public data class Added(override val deviceName: Name, val device: Device) : DeviceChangeEvent()
|
||||
public data class Removed(override val deviceName: Name) : DeviceChangeEvent()
|
||||
}
|
||||
|
||||
public interface ComponentRegistry : ContextAware {
|
||||
public fun <D: ConfigurableCompositeControlComponent<*>> getSpec(name: Name): CompositeControlComponentSpec<D>?
|
||||
}
|
||||
|
||||
public class ComponentRegistryManager : AbstractPlugin(), ComponentRegistry {
|
||||
private val specs = mutableMapOf<Name, CompositeControlComponentSpec<*>>()
|
||||
|
||||
override val tag: PluginTag = Companion.tag
|
||||
|
||||
override fun <D : ConfigurableCompositeControlComponent<*>> getSpec(name: Name): CompositeControlComponentSpec<D>? {
|
||||
try {
|
||||
return specs[name] as? CompositeControlComponentSpec<D>
|
||||
} catch (e: ClassCastException) {
|
||||
logger.error(e) { "Failed to get spec $name" }
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
public fun registerSpec(spec: CompositeControlComponentSpec<*>) {
|
||||
specs[(spec as DevicePropertySpec<*, *>).name.asName()] = spec
|
||||
}
|
||||
|
||||
public companion object : PluginFactory<ComponentRegistryManager> {
|
||||
override val tag: PluginTag = PluginTag("controls.spechub", group = PluginTag.DATAFORGE_GROUP)
|
||||
|
||||
override fun build(context: Context, meta: Meta): ComponentRegistryManager = ComponentRegistryManager()
|
||||
|
||||
}
|
||||
}
|
||||
public val Context.componentRegistry: ComponentRegistry? get() = plugins[ComponentRegistryManager]
|
||||
|
||||
public fun ContextBuilder.withSpecHub() {
|
||||
plugin(ComponentRegistryManager)
|
||||
}
|
||||
|
||||
/**
|
||||
* Builder for [DeviceLifecycleConfig].
|
||||
*/
|
||||
public class DeviceLifecycleConfigBuilder {
|
||||
public var lifecycleMode: LifecycleMode = LifecycleMode.LINKED
|
||||
public var messageBuffer: Int = 1000
|
||||
public var startDelay: Duration = Duration.ZERO
|
||||
public var startTimeout: Duration? = null
|
||||
public var stopTimeout: Duration? = null
|
||||
public var coroutineScope: CoroutineScope? = null
|
||||
public var dispatcher: CoroutineDispatcher? = null
|
||||
public var onError: ChildDeviceErrorHandler = ChildDeviceErrorHandler.RESTART
|
||||
|
||||
public fun build(): DeviceLifecycleConfig = DeviceLifecycleConfig(
|
||||
lifecycleMode, messageBuffer, startDelay, startTimeout, stopTimeout, coroutineScope, dispatcher, onError
|
||||
)
|
||||
}
|
||||
|
||||
public fun DeviceLifecycleConfigBuilder.linked() {
|
||||
lifecycleMode = LifecycleMode.LINKED
|
||||
}
|
||||
|
||||
public fun DeviceLifecycleConfigBuilder.independent() {
|
||||
lifecycleMode = LifecycleMode.INDEPENDENT
|
||||
}
|
||||
|
||||
public fun DeviceLifecycleConfigBuilder.lazy() {
|
||||
lifecycleMode = LifecycleMode.LAZY
|
||||
}
|
||||
|
||||
public fun DeviceLifecycleConfigBuilder.restartOnError() {
|
||||
onError = ChildDeviceErrorHandler.RESTART
|
||||
}
|
||||
|
||||
public fun DeviceLifecycleConfigBuilder.propagateError() {
|
||||
onError = ChildDeviceErrorHandler.PROPAGATE
|
||||
}
|
||||
|
||||
public fun DeviceLifecycleConfigBuilder.withCustomTimeout(timeout: Duration) {
|
||||
startTimeout = timeout
|
||||
stopTimeout = timeout
|
||||
}
|
||||
|
||||
@OptIn(InternalDeviceAPI::class)
|
||||
public abstract class CompositeControlComponentSpec<D : Device>() : CompositeDeviceSpec<D> {
|
||||
|
||||
private val _properties = hashMapOf<String, DevicePropertySpec<D, *>>(
|
||||
DeviceMetaPropertySpec.name to DeviceMetaPropertySpec
|
||||
)
|
||||
|
||||
override val properties: Map<String, DevicePropertySpec<D, *>> get() = _properties
|
||||
|
||||
private val _actions = hashMapOf<String, DeviceActionSpec<D, *, *>>()
|
||||
|
||||
override val actions: Map<String, DeviceActionSpec<D, *, *>> get() = _actions
|
||||
|
||||
private val _childSpecs = mutableMapOf<String, ChildComponentConfig<*>>()
|
||||
|
||||
override val childSpecs: Map<String, ChildComponentConfig<*>> get() = _childSpecs
|
||||
|
||||
public fun <CDS: CompositeControlComponentSpec<CD>, CD: ConfigurableCompositeControlComponent<CD>> childSpec(
|
||||
deviceName: String? = null,
|
||||
specName: Name? = null,
|
||||
metaBuilder: (MutableMeta.() -> Unit)? = null,
|
||||
configBuilder: DeviceLifecycleConfigBuilder.() -> Unit = {},
|
||||
): PropertyDelegateProvider<CompositeControlComponentSpec<D>, ReadOnlyProperty<CompositeControlComponentSpec<D>, CompositeControlComponentSpec<CD>>> =
|
||||
PropertyDelegateProvider { thisRef, property ->
|
||||
ReadOnlyProperty { _, _ ->
|
||||
val childSpecName = specName ?: property.name.asName()
|
||||
val nameForDevice = deviceName?.asName() ?: property.name.asName()
|
||||
val config = DeviceLifecycleConfigBuilder().apply(configBuilder).build()
|
||||
val meta = metaBuilder?.let { Meta(it) }
|
||||
val spec = (thisRef as ConfigurableCompositeControlComponent<*>).context.componentRegistry?.getSpec<CD>(childSpecName) ?: error("Spec with name '$specName' is not found")
|
||||
val childComponentConfig = object : ChildComponentConfig<CD>{
|
||||
override val spec: CompositeControlComponentSpec<CD> = spec
|
||||
override val config: DeviceLifecycleConfig = config
|
||||
override val meta: Meta? = meta
|
||||
override val name: Name = nameForDevice
|
||||
}
|
||||
_childSpecs[property.name] = childComponentConfig
|
||||
childComponentConfig.spec
|
||||
}
|
||||
}
|
||||
|
||||
override fun validate(device: D) {
|
||||
properties.map { it.value.descriptor }.forEach { specProperty ->
|
||||
check(specProperty in device.propertyDescriptors) { "Property ${specProperty.name} not registered in ${device.id}" }
|
||||
}
|
||||
actions.map { it.value.descriptor }.forEach { specAction ->
|
||||
check(specAction in device.actionDescriptors) { "Action ${specAction.name} not registered in ${device.id}" }
|
||||
}
|
||||
}
|
||||
|
||||
override fun <T, P : DevicePropertySpec<D, T>> registerProperty(deviceProperty: P): P {
|
||||
_properties[deviceProperty.name] = deviceProperty
|
||||
return deviceProperty
|
||||
}
|
||||
|
||||
override fun <I, O> registerAction(deviceAction: DeviceActionSpec<D, I, O>): DeviceActionSpec<D, I, O> {
|
||||
_actions[deviceAction.name] = deviceAction
|
||||
return deviceAction
|
||||
}
|
||||
|
||||
override fun createPropertyDescriptorInternal(
|
||||
propertyName: String,
|
||||
converter: MetaConverter<*>,
|
||||
mutable: Boolean,
|
||||
property: KProperty<*>,
|
||||
descriptorBuilder: PropertyDescriptorBuilder.() -> Unit,
|
||||
): PropertyDescriptor {
|
||||
return propertyDescriptor(propertyName) {
|
||||
this.mutable = mutable
|
||||
converter.descriptor?.let { converterDescriptor ->
|
||||
metaDescriptor {
|
||||
from(converterDescriptor)
|
||||
}
|
||||
}
|
||||
fromSpec(property)
|
||||
descriptorBuilder()
|
||||
}
|
||||
}
|
||||
|
||||
override fun createActionDescriptor(
|
||||
actionName: String,
|
||||
inputConverter: MetaConverter<*>,
|
||||
outputConverter: MetaConverter<*>,
|
||||
property: KProperty<*>,
|
||||
descriptorBuilder: ActionDescriptorBuilder.() -> Unit,
|
||||
): ActionDescriptor {
|
||||
return actionDescriptor(actionName) {
|
||||
inputConverter.descriptor?.let { converterDescriptor ->
|
||||
inputMeta {
|
||||
from(converterDescriptor)
|
||||
}
|
||||
}
|
||||
outputConverter.descriptor?.let { converterDescriptor ->
|
||||
outputMeta {
|
||||
from(converterDescriptor)
|
||||
}
|
||||
}
|
||||
fromSpec(property)
|
||||
descriptorBuilder()
|
||||
}
|
||||
}
|
||||
|
||||
override fun <T> property(
|
||||
converter: MetaConverter<T>,
|
||||
descriptorBuilder: PropertyDescriptorBuilder.() -> Unit,
|
||||
name: String?,
|
||||
read: suspend D.(propertyName: String) -> T?,
|
||||
): PropertyDelegateProvider<CompositeControlComponentSpec<D>, ReadOnlyProperty<CompositeControlComponentSpec<D>, DevicePropertySpec<D, T>>> =
|
||||
PropertyDelegateProvider { _: CompositeControlComponentSpec<D>, property ->
|
||||
val propertyName = name ?: property.name
|
||||
val descriptor = createPropertyDescriptorInternal(
|
||||
propertyName = propertyName,
|
||||
converter = converter,
|
||||
mutable = false,
|
||||
property = property,
|
||||
descriptorBuilder = descriptorBuilder
|
||||
)
|
||||
val deviceProperty = registerProperty(object : DevicePropertySpec<D, T> {
|
||||
override val descriptor = descriptor
|
||||
override val converter = converter
|
||||
|
||||
override suspend fun read(device: D): T? =
|
||||
withContext(device.coroutineContext) { device.read(propertyName) }
|
||||
})
|
||||
ReadOnlyProperty { _, _ ->
|
||||
deviceProperty
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override fun <T> mutableProperty(
|
||||
converter: MetaConverter<T>,
|
||||
descriptorBuilder: PropertyDescriptorBuilder.() -> Unit,
|
||||
name: String?,
|
||||
read: suspend D.(propertyName: String) -> T?,
|
||||
write: suspend D.(propertyName: String, value: T) -> Unit
|
||||
): PropertyDelegateProvider<CompositeControlComponentSpec<D>, ReadOnlyProperty<CompositeControlComponentSpec<D>, MutableDevicePropertySpec<D, T>>> =
|
||||
PropertyDelegateProvider { _: CompositeControlComponentSpec<D>, property ->
|
||||
val propertyName = name ?: property.name
|
||||
val descriptor = createPropertyDescriptorInternal(
|
||||
propertyName = propertyName,
|
||||
converter = converter,
|
||||
mutable = true,
|
||||
property = property,
|
||||
descriptorBuilder = descriptorBuilder
|
||||
)
|
||||
val deviceProperty = registerProperty(object : MutableDevicePropertySpec<D, T> {
|
||||
override val descriptor = descriptor
|
||||
override val converter = converter
|
||||
override suspend fun read(device: D): T? =
|
||||
withContext(device.coroutineContext) { device.read(propertyName) }
|
||||
|
||||
override suspend fun write(device: D, value: T): Unit = withContext(device.coroutineContext) {
|
||||
device.write(propertyName, value)
|
||||
}
|
||||
})
|
||||
ReadOnlyProperty { _, _ ->
|
||||
deviceProperty
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override fun <I, O> action(
|
||||
inputConverter: MetaConverter<I>,
|
||||
outputConverter: MetaConverter<O>,
|
||||
descriptorBuilder: ActionDescriptorBuilder.() -> Unit,
|
||||
name: String?,
|
||||
execute: suspend D.(I) -> O,
|
||||
): PropertyDelegateProvider<CompositeControlComponentSpec<D>, ReadOnlyProperty<CompositeControlComponentSpec<D>, DeviceActionSpec<D, I, O>>> =
|
||||
PropertyDelegateProvider { _: CompositeControlComponentSpec<D>, property ->
|
||||
val actionName = name ?: property.name
|
||||
val descriptor = createActionDescriptor(
|
||||
actionName = actionName,
|
||||
inputConverter = inputConverter,
|
||||
outputConverter = outputConverter,
|
||||
property = property,
|
||||
descriptorBuilder = descriptorBuilder
|
||||
)
|
||||
val deviceAction = registerAction(object : DeviceActionSpec<D, I, O> {
|
||||
override val descriptor = descriptor
|
||||
override val inputConverter = inputConverter
|
||||
override val outputConverter = outputConverter
|
||||
override suspend fun execute(device: D, input: I): O = withContext(device.coroutineContext) {
|
||||
device.execute(input)
|
||||
}
|
||||
})
|
||||
|
||||
ReadOnlyProperty { _, _ ->
|
||||
deviceAction
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun D.onOpen() {}
|
||||
override suspend fun D.onClose() {}
|
||||
|
||||
}
|
||||
|
||||
public fun <D : Device> CompositeControlComponentSpec<D>.unitAction(
|
||||
descriptorBuilder: ActionDescriptorBuilder.() -> Unit = {},
|
||||
name: String? = null,
|
||||
execute: suspend D.() -> Unit,
|
||||
): PropertyDelegateProvider<CompositeControlComponentSpec<D>, ReadOnlyProperty<CompositeControlComponentSpec<D>, DeviceActionSpec<D, Unit, Unit>>> =
|
||||
action(
|
||||
MetaConverter.unit,
|
||||
MetaConverter.unit,
|
||||
descriptorBuilder,
|
||||
name
|
||||
) {
|
||||
execute()
|
||||
}
|
||||
|
||||
|
||||
public fun <D : Device> CompositeControlComponentSpec<D>.metaAction(
|
||||
descriptorBuilder: ActionDescriptorBuilder.() -> Unit = {},
|
||||
name: String? = null,
|
||||
execute: suspend D.(Meta) -> Meta,
|
||||
): PropertyDelegateProvider<CompositeControlComponentSpec<D>, ReadOnlyProperty<CompositeControlComponentSpec<D>, DeviceActionSpec<D, Meta, Meta>>> =
|
||||
action(
|
||||
MetaConverter.meta,
|
||||
MetaConverter.meta,
|
||||
descriptorBuilder,
|
||||
name
|
||||
) {
|
||||
execute(it)
|
||||
}
|
||||
|
||||
/**
|
||||
* Basic interface for device description
|
||||
*/
|
||||
public interface CompositeDeviceSpec<D : Device> {
|
||||
public val properties: Map<String, DevicePropertySpec<D, *>>
|
||||
|
||||
public val actions: Map<String, DeviceActionSpec<D, *, *>>
|
||||
|
||||
public val childSpecs: Map<String, ChildComponentConfig<*>>
|
||||
|
||||
/**
|
||||
* Called on `start()`
|
||||
*/
|
||||
public suspend fun D.onOpen()
|
||||
|
||||
/**
|
||||
* Called on `stop()`
|
||||
*/
|
||||
public suspend fun D.onClose()
|
||||
|
||||
/**
|
||||
* Registers a property in the spec.
|
||||
*/
|
||||
public fun <T, P : DevicePropertySpec<D, T>> registerProperty(deviceProperty: P): P
|
||||
|
||||
/**
|
||||
* Registers an action in the spec.
|
||||
*/
|
||||
public fun <I, O> registerAction(deviceAction: DeviceActionSpec<D, I, O>): DeviceActionSpec<D, I, O>
|
||||
|
||||
public fun validate(device: D)
|
||||
|
||||
public fun createPropertyDescriptorInternal(
|
||||
propertyName: String,
|
||||
converter: MetaConverter<*>,
|
||||
mutable: Boolean,
|
||||
property: KProperty<*>,
|
||||
descriptorBuilder: PropertyDescriptorBuilder.() -> Unit
|
||||
): PropertyDescriptor
|
||||
|
||||
public fun createActionDescriptor(
|
||||
actionName: String,
|
||||
inputConverter: MetaConverter<*>,
|
||||
outputConverter: MetaConverter<*>,
|
||||
property: KProperty<*>,
|
||||
descriptorBuilder: ActionDescriptorBuilder.() -> Unit
|
||||
): ActionDescriptor
|
||||
|
||||
public fun <T> property(
|
||||
converter: MetaConverter<T>,
|
||||
descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {},
|
||||
name: String? = null,
|
||||
read: suspend D.(propertyName: String) -> T?,
|
||||
): PropertyDelegateProvider<CompositeControlComponentSpec<D>, ReadOnlyProperty<CompositeControlComponentSpec<D>, DevicePropertySpec<D, T>>>
|
||||
|
||||
public fun <T> mutableProperty(
|
||||
converter: MetaConverter<T>,
|
||||
descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {},
|
||||
name: String? = null,
|
||||
read: suspend D.(propertyName: String) -> T?,
|
||||
write: suspend D.(propertyName: String, value: T) -> Unit,
|
||||
): PropertyDelegateProvider<CompositeControlComponentSpec<D>, ReadOnlyProperty<CompositeControlComponentSpec<D>, MutableDevicePropertySpec<D, T>>>
|
||||
|
||||
public fun <I, O> action(
|
||||
inputConverter: MetaConverter<I>,
|
||||
outputConverter: MetaConverter<O>,
|
||||
descriptorBuilder: ActionDescriptorBuilder.() -> Unit = {},
|
||||
name: String? = null,
|
||||
execute: suspend D.(I) -> O,
|
||||
): PropertyDelegateProvider<CompositeControlComponentSpec<D>, ReadOnlyProperty<CompositeControlComponentSpec<D>, DeviceActionSpec<D, I, O>>>
|
||||
}
|
||||
|
||||
public data class DeviceLifecycleConfig(
|
||||
val lifecycleMode: LifecycleMode = LifecycleMode.LINKED,
|
||||
val messageBuffer: Int = 1000,
|
||||
val startDelay: Duration = Duration.ZERO,
|
||||
val startTimeout: Duration? = null,
|
||||
val stopTimeout: Duration? = null,
|
||||
val coroutineScope: CoroutineScope? = null,
|
||||
val dispatcher: CoroutineDispatcher? = null,
|
||||
val onError: ChildDeviceErrorHandler = ChildDeviceErrorHandler.RESTART
|
||||
) {
|
||||
init {
|
||||
require(messageBuffer > 0) { "Message buffer size must be positive." }
|
||||
startTimeout?.let { require(it.isPositive()) { "Start timeout must be positive." } }
|
||||
stopTimeout?.let { require(it.isPositive()) { "Stop timeout must be positive." } }
|
||||
}
|
||||
}
|
||||
|
||||
public enum class ChildDeviceErrorHandler {
|
||||
IGNORE,
|
||||
RESTART,
|
||||
STOP_PARENT,
|
||||
PROPAGATE
|
||||
}
|
||||
|
||||
/**
|
||||
* Base class for managing child devices. Manages lifecycle and message flow.
|
||||
*/
|
||||
public abstract class AbstractDeviceHubManager(
|
||||
public val context: Context,
|
||||
private val dispatcher: CoroutineDispatcher = Dispatchers.Default,
|
||||
) {
|
||||
internal val childrenJobs: MutableMap<Name, ChildJob> = mutableMapOf()
|
||||
public val devices: Map<Name, Device> get() = childrenJobs.mapValues { it.value.device }
|
||||
|
||||
internal data class ChildJob(
|
||||
val device: Device,
|
||||
val job: Job,
|
||||
val lifecycleMode: LifecycleMode,
|
||||
val messageBus: MutableSharedFlow<DeviceMessage>,
|
||||
val meta: Meta? = null
|
||||
)
|
||||
|
||||
/**
|
||||
* A centralized bus for messages
|
||||
*/
|
||||
public abstract val messageBus: MutableSharedFlow<DeviceMessage>
|
||||
|
||||
/**
|
||||
* A centralized bus for device change events
|
||||
*/
|
||||
public abstract val deviceChanges: MutableSharedFlow<DeviceChangeEvent>
|
||||
|
||||
|
||||
/**
|
||||
* Launches a child device with a specific lifecycle mode and error handling.
|
||||
*/
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
internal fun launchChild(name: Name, device: Device, config: DeviceLifecycleConfig, meta: Meta? = null): ChildJob {
|
||||
val childMessageBus = MutableSharedFlow<DeviceMessage>(
|
||||
replay = config.messageBuffer,
|
||||
onBufferOverflow = BufferOverflow.DROP_OLDEST
|
||||
)
|
||||
val childScope = config.coroutineScope ?: context
|
||||
val job = childScope.launch(dispatcher + CoroutineName("Child device $name")) {
|
||||
try {
|
||||
if (config.lifecycleMode != LifecycleMode.INDEPENDENT) {
|
||||
if (config.lifecycleMode == LifecycleMode.LINKED || device.lifecycleState == LifecycleState.STARTING){
|
||||
delay(config.startDelay)
|
||||
withTimeoutOrNull(config.startTimeout ?: Duration.INFINITE) {
|
||||
device.start()
|
||||
} ?: error("Timeout on start for $name")
|
||||
}
|
||||
}
|
||||
|
||||
device.messageFlow.collect { message ->
|
||||
childMessageBus.emit(message.changeSource { name.plus(it) })
|
||||
messageBus.emit(message.changeSource { name.plus(it) })
|
||||
}
|
||||
} catch (ex: Exception) {
|
||||
val errorMessage = DeviceMessage.error(ex, name)
|
||||
messageBus.emit(errorMessage)
|
||||
|
||||
when (config.onError) {
|
||||
ChildDeviceErrorHandler.IGNORE -> context.logger.error(ex) { "Error in child device $name ignored" }
|
||||
ChildDeviceErrorHandler.RESTART -> {
|
||||
context.logger.error(ex) { "Error in child device $name, restarting" }
|
||||
removeDevice(name)
|
||||
childrenJobs[name] = launchChild(name, device, config, meta)
|
||||
}
|
||||
ChildDeviceErrorHandler.STOP_PARENT -> {
|
||||
context.logger.error(ex) { "Error in child device $name, stopping parent" }
|
||||
coroutineContext[Job]?.cancelAndJoin()
|
||||
}
|
||||
ChildDeviceErrorHandler.PROPAGATE -> {
|
||||
context.logger.error(ex) { "Error in child device $name propagated to parent" }
|
||||
throw ex
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
childrenJobs.remove(name)
|
||||
clearReplayCache(childMessageBus)
|
||||
deviceChanges.emit(DeviceChangeEvent.Removed(name))
|
||||
messageBus.emit(DeviceLogMessage("Device $name stopped", sourceDevice = name))
|
||||
if (device is ConfigurableCompositeControlComponent<*>) {
|
||||
device.onChildStop()
|
||||
}
|
||||
}
|
||||
}
|
||||
return ChildJob(device, job, config.lifecycleMode, childMessageBus, meta)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
private fun <T> clearReplayCache(mutableSharedFlow: MutableSharedFlow<T>){
|
||||
val cached = mutableSharedFlow.replayCache
|
||||
mutableSharedFlow.resetReplayCache()
|
||||
cached.forEach { mutableSharedFlow.tryEmit(it) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a device to the hub and manage its lifecycle according to its spec
|
||||
*/
|
||||
public fun addDevice(name: Name, device: Device, config: DeviceLifecycleConfig, meta: Meta? = null) {
|
||||
val existingDevice = devices[name]
|
||||
if (existingDevice != null) {
|
||||
if(existingDevice == device) {
|
||||
error("Device with name $name is already installed")
|
||||
}
|
||||
context.launch(device.coroutineContext) { existingDevice.stopWithTimeout(config.stopTimeout ?: Duration.INFINITE) }
|
||||
}
|
||||
childrenJobs[name] = launchChild(name, device, config, meta)
|
||||
|
||||
context.launch {
|
||||
deviceChanges.emit(DeviceChangeEvent.Added(name, device))
|
||||
messageBus.emit(DeviceLogMessage("Device $name added", sourceDevice = name))
|
||||
}
|
||||
}
|
||||
|
||||
public fun removeDevice(name: Name) {
|
||||
childrenJobs[name]?.let { childJob ->
|
||||
context.launch(childJob.device.coroutineContext) {
|
||||
val timeout = when (childJob.lifecycleMode) {
|
||||
LifecycleMode.INDEPENDENT -> childJob.device.meta["stopTimeout".asName()]?.value?.let {
|
||||
Duration.parse(it.toString())
|
||||
} ?: Duration.INFINITE
|
||||
|
||||
else -> Duration.INFINITE
|
||||
}
|
||||
withTimeoutOrNull(timeout) {
|
||||
childJob.job.cancelAndJoin()
|
||||
childJob.device.stop()
|
||||
} ?: error("Timeout on stop for $name")
|
||||
}
|
||||
childrenJobs.remove(name)
|
||||
context.launch {
|
||||
messageBus.emit(DeviceLogMessage("Device $name removed", sourceDevice = name))
|
||||
deviceChanges.emit(DeviceChangeEvent.Removed(name))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Change lifecycle mode of a child device
|
||||
*/
|
||||
public fun changeLifecycleMode(name: Name, mode: LifecycleMode) {
|
||||
val job = childrenJobs[name] ?: error("Device with name '$name' is not found")
|
||||
val config = DeviceLifecycleConfig(lifecycleMode = mode, messageBuffer = job.messageBus.replayCache.size)
|
||||
context.launch {
|
||||
job.job.cancelAndJoin()
|
||||
childrenJobs[name] = launchChild(name, job.device, config, job.meta)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get local message bus for a child device
|
||||
*/
|
||||
public fun getChildMessageBus(name: Name) : MutableSharedFlow<DeviceMessage>? = childrenJobs[name]?.messageBus
|
||||
|
||||
/**
|
||||
* Method for explicit call when child device is stopped.
|
||||
*/
|
||||
internal open fun onChildStop(){
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private class DeviceHubManagerImpl(context: Context, dispatcher: CoroutineDispatcher = Dispatchers.Default) : AbstractDeviceHubManager(context, dispatcher){
|
||||
override val messageBus: MutableSharedFlow<DeviceMessage> = MutableSharedFlow(
|
||||
replay = 1000,
|
||||
onBufferOverflow = BufferOverflow.DROP_OLDEST
|
||||
)
|
||||
override val deviceChanges: MutableSharedFlow<DeviceChangeEvent> = MutableSharedFlow(replay = 1)
|
||||
}
|
||||
|
||||
/**
|
||||
* An interface for a device that contains other devices as children.
|
||||
*/
|
||||
public interface CompositeControlComponent : Device {
|
||||
/**
|
||||
* A centralized flow of all device messages from this node and all its children
|
||||
*/
|
||||
public val messageBus: SharedFlow<DeviceMessage>
|
||||
|
||||
/**
|
||||
* A map of child devices
|
||||
*/
|
||||
public val devices: Map<Name, Device>
|
||||
}
|
||||
|
||||
/**
|
||||
* A base class for devices created from specification, using AbstractDeviceHubManager for children management
|
||||
*/
|
||||
public open class ConfigurableCompositeControlComponent<D : Device>(
|
||||
public open val spec: CompositeControlComponentSpec<D>,
|
||||
context: Context,
|
||||
meta: Meta = Meta.EMPTY,
|
||||
config: DeviceLifecycleConfig = DeviceLifecycleConfig(),
|
||||
private val hubManager: AbstractDeviceHubManager = DeviceHubManagerImpl(context, config.dispatcher ?: Dispatchers.Default),
|
||||
) : DeviceBase<D>(context, meta), CompositeControlComponent {
|
||||
|
||||
override val properties: Map<String, DevicePropertySpec<D, *>>
|
||||
get() = spec.properties
|
||||
|
||||
override val actions: Map<String, DeviceActionSpec<D, *, *>>
|
||||
get() = spec.actions
|
||||
|
||||
final override val messageBus: MutableSharedFlow<DeviceMessage>
|
||||
get() = hubManager.messageBus
|
||||
|
||||
public val deviceChanges: MutableSharedFlow<DeviceChangeEvent>
|
||||
get() = hubManager.deviceChanges
|
||||
|
||||
public val aggregatedMessageFlow: SharedFlow<DeviceMessage>
|
||||
get() = hubManager.messageBus
|
||||
|
||||
init {
|
||||
spec.childSpecs.forEach{ (name, childSpec) ->
|
||||
val childDevice = ConfigurableCompositeControlComponent(childSpec.spec, context, childSpec.meta ?: Meta.EMPTY, childSpec.config)
|
||||
addDevice(childSpec.name, childDevice, childSpec.config, childSpec.meta)
|
||||
}
|
||||
|
||||
spec.actions.values.forEach { actionSpec ->
|
||||
launch {
|
||||
val actionName = actionSpec.name
|
||||
messageFlow.filterIsInstance<ActionExecuteMessage>().filter { it.action == actionName }.onEach {
|
||||
val result = execute(actionName, it.argument)
|
||||
messageBus.emit(
|
||||
ActionResultMessage(
|
||||
action = actionName,
|
||||
result = result,
|
||||
requestId = it.requestId,
|
||||
sourceDevice = id.asName()
|
||||
)
|
||||
)
|
||||
}.launchIn(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun onStart() {
|
||||
with(spec) {
|
||||
self.onOpen()
|
||||
validate(self)
|
||||
}
|
||||
hubManager.devices.values.filter {
|
||||
it.lifecycleState != LifecycleState.STARTED && it.lifecycleState != LifecycleState.STARTING
|
||||
}.forEach {
|
||||
if (hubManager.childrenJobs[it.id.parseAsName()]?.lifecycleMode != LifecycleMode.LAZY) {
|
||||
it.start()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getTimeout(device: Device): Duration {
|
||||
return (hubManager.childrenJobs[device.id.parseAsName()]?.lifecycleMode ?: LifecycleMode.LINKED).let{
|
||||
if(it == LifecycleMode.INDEPENDENT)
|
||||
it.name.let{ meta["stopTimeout".parseAsName()]?.let { durationMeta ->
|
||||
Duration.parse(durationMeta.value.toString())
|
||||
} ?: Duration.INFINITE }
|
||||
else Duration.INFINITE
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun onStop() {
|
||||
hubManager.devices.values.forEach {
|
||||
launch(it.coroutineContext){
|
||||
withTimeoutOrNull(getTimeout(it)){
|
||||
it.stop()
|
||||
}
|
||||
}
|
||||
}
|
||||
with(spec) {
|
||||
self.onClose()
|
||||
}
|
||||
}
|
||||
|
||||
override fun toString(): String = "Device(spec=$spec)"
|
||||
|
||||
internal open fun onChildStop() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Add existing device to this hub
|
||||
*/
|
||||
private fun addDevice(name: Name = device.id.asName(), device: Device, config: DeviceLifecycleConfig = DeviceLifecycleConfig(), meta: Meta? = null) {
|
||||
hubManager.addDevice(name, device, config, meta)
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a child device from the hub by name.
|
||||
*/
|
||||
private fun removeDevice(name: Name) {
|
||||
hubManager.removeDevice(name)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of all children devices
|
||||
*/
|
||||
public override val devices: Map<Name, Device>
|
||||
get() = hubManager.devices
|
||||
|
||||
/**
|
||||
* Get child device from this hub by name
|
||||
*/
|
||||
public fun <CD : ConfigurableCompositeControlComponent<CD>> getChildDevice(name: Name): ConfigurableCompositeControlComponent<CD> =
|
||||
hubManager.devices[name] as? ConfigurableCompositeControlComponent<CD>? ?: error("Device $name not found")
|
||||
|
||||
public fun <CD: ConfigurableCompositeControlComponent<CD>> childDevice(name: Name? = null):
|
||||
PropertyDelegateProvider<ConfigurableCompositeControlComponent<D>, ReadOnlyProperty<ConfigurableCompositeControlComponent<D>, CD>>
|
||||
= PropertyDelegateProvider{ thisRef, property ->
|
||||
ReadOnlyProperty{ _, _ ->
|
||||
val deviceName = name ?: property.name.asName()
|
||||
thisRef.devices[deviceName] as? CD ?: error("Device $deviceName not found")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get child device message bus by name
|
||||
*/
|
||||
public fun getChildMessageBus(name: Name): SharedFlow<DeviceMessage>? = hubManager.getChildMessageBus(name)
|
||||
|
||||
/**
|
||||
* Get device, using delegate method
|
||||
*/
|
||||
public inline operator fun <reified D : Device> get(name: Name): D? = devices[name] as? D
|
||||
|
||||
/**
|
||||
* Get device, using delegate method
|
||||
*/
|
||||
public inline operator fun <reified D : Device> get(name: String): D? = devices[name.asName()] as? D
|
||||
|
||||
}
|
||||
|
||||
public suspend fun WithLifeCycle.stopWithTimeout(timeout: Duration = Duration.INFINITE) {
|
||||
withTimeoutOrNull(timeout) {
|
||||
stop()
|
||||
}
|
||||
}
|
||||
|
||||
public interface ChildComponentConfig<CD: Device>{
|
||||
public val spec: CompositeControlComponentSpec<CD>
|
||||
public val config: DeviceLifecycleConfig
|
||||
public val meta: Meta?
|
||||
public val name: Name
|
||||
}
|
@ -0,0 +1,384 @@
|
||||
package space.kscience.controls.spec
|
||||
|
||||
import kotlinx.coroutines.Deferred
|
||||
import space.kscience.controls.api.ActionDescriptorBuilder
|
||||
import space.kscience.controls.api.Device
|
||||
import space.kscience.controls.api.PropertyDescriptorBuilder
|
||||
import space.kscience.controls.api.id
|
||||
import space.kscience.dataforge.meta.Meta
|
||||
import space.kscience.dataforge.meta.MetaConverter
|
||||
import space.kscience.dataforge.meta.ValueType
|
||||
import space.kscience.dataforge.meta.descriptors.MetaDescriptor
|
||||
import space.kscience.dataforge.meta.string
|
||||
import kotlin.properties.PropertyDelegateProvider
|
||||
import kotlin.properties.ReadOnlyProperty
|
||||
import kotlin.reflect.KMutableProperty1
|
||||
import kotlin.reflect.KProperty1
|
||||
|
||||
/**
|
||||
* Create a [MetaConverter] for enum values
|
||||
*/
|
||||
public fun <E : Enum<E>> createEnumConverter(enumValues: Array<E>): MetaConverter<E> = object : MetaConverter<E> {
|
||||
override val descriptor: MetaDescriptor = MetaDescriptor {
|
||||
valueType(ValueType.STRING)
|
||||
allowedValues(enumValues.map { it.name })
|
||||
}
|
||||
|
||||
override fun readOrNull(source: Meta): E? {
|
||||
val value = source.value ?: return null
|
||||
return enumValues.firstOrNull { it.name == value.string }
|
||||
}
|
||||
|
||||
override fun convert(obj: E): Meta = Meta(obj.name)
|
||||
}
|
||||
|
||||
/**
|
||||
* A read-only device property that delegates reading to a device [KProperty1]
|
||||
*/
|
||||
public fun <T, D : Device> CompositeControlComponentSpec<D>.property(
|
||||
converter: MetaConverter<T>,
|
||||
descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {},
|
||||
name: String? = null,
|
||||
read: suspend D.(propertyName: String) -> T?,
|
||||
): PropertyDelegateProvider<CompositeControlComponentSpec<D>, ReadOnlyProperty<CompositeControlComponentSpec<D>, DevicePropertySpec<D, T>>> {
|
||||
return property(converter, descriptorBuilder, name, read)
|
||||
}
|
||||
|
||||
/**
|
||||
* Mutable property that delegates reading and writing to a device [KMutableProperty1]
|
||||
*/
|
||||
public fun <T, D : Device> CompositeControlComponentSpec<D>.mutableProperty(
|
||||
converter: MetaConverter<T>,
|
||||
descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {},
|
||||
name: String? = null,
|
||||
read: suspend D.(propertyName: String) -> T?,
|
||||
write: suspend D.(propertyName: String, value: T) -> Unit,
|
||||
): PropertyDelegateProvider<CompositeControlComponentSpec<D>, ReadOnlyProperty<CompositeControlComponentSpec<D>, MutableDevicePropertySpec<D, T>>> {
|
||||
return mutableProperty(converter, descriptorBuilder, name, read, write)
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a mutable logical property (without a corresponding physical state) for a device
|
||||
*/
|
||||
public fun <T, D : DeviceBase<D>> CompositeControlComponentSpec<D>.logical(
|
||||
converter: MetaConverter<T>,
|
||||
descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {},
|
||||
name: String? = null,
|
||||
): PropertyDelegateProvider<CompositeControlComponentSpec<D>, ReadOnlyProperty<CompositeControlComponentSpec<D>, MutableDevicePropertySpec<D, T>>> =
|
||||
mutableProperty(
|
||||
converter,
|
||||
descriptorBuilder,
|
||||
name,
|
||||
read = { propertyName -> getProperty(propertyName)?.let(converter::readOrNull) },
|
||||
write = { propertyName, value -> writeProperty(propertyName, converter.convert(value)) }
|
||||
)
|
||||
|
||||
/**
|
||||
* Creates a boolean property for a device.
|
||||
*/
|
||||
public fun <D : Device> CompositeControlComponentSpec<D>.boolean(
|
||||
descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {},
|
||||
name: String? = null,
|
||||
read: suspend D.(propertyName: String) -> Boolean?,
|
||||
): PropertyDelegateProvider<CompositeControlComponentSpec<D>, ReadOnlyProperty<CompositeControlComponentSpec<D>, DevicePropertySpec<D, Boolean>>> =
|
||||
property(MetaConverter.boolean, descriptorBuilder, name, read)
|
||||
|
||||
/**
|
||||
* Creates a mutable boolean property for a device.
|
||||
*/
|
||||
public fun <D : Device> CompositeControlComponentSpec<D>.booleanMutable(
|
||||
descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {},
|
||||
name: String? = null,
|
||||
read: suspend D.(propertyName: String) -> Boolean?,
|
||||
write: suspend D.(propertyName: String, value: Boolean) -> Unit
|
||||
): PropertyDelegateProvider<CompositeControlComponentSpec<D>, ReadOnlyProperty<CompositeControlComponentSpec<D>, MutableDevicePropertySpec<D, Boolean>>> =
|
||||
mutableProperty(MetaConverter.boolean, descriptorBuilder, name, read, write)
|
||||
|
||||
/**
|
||||
* Creates a read-only number property for a device.
|
||||
*/
|
||||
public fun <D : Device> CompositeControlComponentSpec<D>.number(
|
||||
descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {},
|
||||
name: String? = null,
|
||||
read: suspend D.(propertyName: String) -> Number?,
|
||||
): PropertyDelegateProvider<CompositeControlComponentSpec<D>, ReadOnlyProperty<CompositeControlComponentSpec<D>, DevicePropertySpec<D, Number>>> =
|
||||
property(MetaConverter.number, descriptorBuilder, name, read)
|
||||
|
||||
/**
|
||||
* Creates a mutable number property for a device.
|
||||
*/
|
||||
public fun <D : Device> CompositeControlComponentSpec<D>.numberMutable(
|
||||
descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {},
|
||||
name: String? = null,
|
||||
read: suspend D.(propertyName: String) -> Number?,
|
||||
write: suspend D.(propertyName: String, value: Number) -> Unit
|
||||
): PropertyDelegateProvider<CompositeControlComponentSpec<D>, ReadOnlyProperty<CompositeControlComponentSpec<D>, MutableDevicePropertySpec<D, Number>>> =
|
||||
mutableProperty(MetaConverter.number, descriptorBuilder, name, read, write)
|
||||
|
||||
|
||||
/**
|
||||
* Creates a read-only double property for a device.
|
||||
*/
|
||||
public fun <D : Device> CompositeControlComponentSpec<D>.double(
|
||||
descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {},
|
||||
name: String? = null,
|
||||
read: suspend D.(propertyName: String) -> Double?,
|
||||
): PropertyDelegateProvider<CompositeControlComponentSpec<D>, ReadOnlyProperty<CompositeControlComponentSpec<D>, DevicePropertySpec<D, Double>>> =
|
||||
property(MetaConverter.double, descriptorBuilder, name, read)
|
||||
|
||||
/**
|
||||
* Creates a mutable double property for a device.
|
||||
*/
|
||||
public fun <D : Device> CompositeControlComponentSpec<D>.doubleMutable(
|
||||
descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {},
|
||||
name: String? = null,
|
||||
read: suspend D.(propertyName: String) -> Double?,
|
||||
write: suspend D.(propertyName: String, value: Double) -> Unit
|
||||
): PropertyDelegateProvider<CompositeControlComponentSpec<D>, ReadOnlyProperty<CompositeControlComponentSpec<D>, MutableDevicePropertySpec<D, Double>>> =
|
||||
mutableProperty(MetaConverter.double, descriptorBuilder, name, read, write)
|
||||
|
||||
/**
|
||||
* Creates a read-only string property for a device.
|
||||
*/
|
||||
public fun <D : Device> CompositeControlComponentSpec<D>.string(
|
||||
descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {},
|
||||
name: String? = null,
|
||||
read: suspend D.(propertyName: String) -> String?
|
||||
): PropertyDelegateProvider<CompositeControlComponentSpec<D>, ReadOnlyProperty<CompositeControlComponentSpec<D>, DevicePropertySpec<D, String>>> =
|
||||
property(MetaConverter.string, descriptorBuilder, name, read)
|
||||
|
||||
/**
|
||||
* Creates a mutable string property for a device.
|
||||
*/
|
||||
public fun <D : Device> CompositeControlComponentSpec<D>.stringMutableProperty(
|
||||
descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {},
|
||||
name: String? = null,
|
||||
read: suspend D.(propertyName: String) -> String?,
|
||||
write: suspend D.(propertyName: String, value: String) -> Unit
|
||||
): PropertyDelegateProvider<CompositeControlComponentSpec<D>, ReadOnlyProperty<CompositeControlComponentSpec<D>, MutableDevicePropertySpec<D, String>>> =
|
||||
mutableProperty(MetaConverter.string, descriptorBuilder, name, read, write)
|
||||
|
||||
/**
|
||||
* Creates a read-only meta property for a device.
|
||||
*/
|
||||
public fun <D : Device> CompositeControlComponentSpec<D>.meta(
|
||||
descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {},
|
||||
name: String? = null,
|
||||
read: suspend D.(propertyName: String) -> Meta?,
|
||||
): PropertyDelegateProvider<CompositeControlComponentSpec<D>, ReadOnlyProperty<CompositeControlComponentSpec<D>, DevicePropertySpec<D, Meta>>> =
|
||||
property(MetaConverter.meta, descriptorBuilder, name, read)
|
||||
|
||||
/**
|
||||
* Creates a mutable meta property for a device.
|
||||
*/
|
||||
public fun <D : Device> CompositeControlComponentSpec<D>.metaMutable(
|
||||
descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {},
|
||||
name: String? = null,
|
||||
read: suspend D.(propertyName: String) -> Meta?,
|
||||
write: suspend D.(propertyName: String, value: Meta) -> Unit
|
||||
): PropertyDelegateProvider<CompositeControlComponentSpec<D>, ReadOnlyProperty<CompositeControlComponentSpec<D>, MutableDevicePropertySpec<D, Meta>>> =
|
||||
mutableProperty(MetaConverter.meta, descriptorBuilder, name, read, write)
|
||||
|
||||
/**
|
||||
* Creates a read-only enum property for a device.
|
||||
*/
|
||||
public fun <E : Enum<E>, D : Device> CompositeControlComponentSpec<D>.enum(
|
||||
enumValues: Array<E>,
|
||||
descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {},
|
||||
name: String? = null,
|
||||
read: suspend D.(propertyName: String) -> E?,
|
||||
): PropertyDelegateProvider<CompositeControlComponentSpec<D>, ReadOnlyProperty<CompositeControlComponentSpec<D>, DevicePropertySpec<D, E>>> {
|
||||
val converter = createEnumConverter(enumValues)
|
||||
return property(converter, descriptorBuilder, name, read)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a mutable enum property for a device.
|
||||
*/
|
||||
public fun <E : Enum<E>, D : Device> CompositeControlComponentSpec<D>.enumMutable(
|
||||
enumValues: Array<E>,
|
||||
descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {},
|
||||
name: String? = null,
|
||||
read: suspend D.(propertyName: String) -> E?,
|
||||
write: suspend D.(propertyName: String, value: E) -> Unit
|
||||
): PropertyDelegateProvider<CompositeControlComponentSpec<D>, ReadOnlyProperty<CompositeControlComponentSpec<D>, MutableDevicePropertySpec<D, E>>> {
|
||||
val converter = createEnumConverter(enumValues)
|
||||
return mutableProperty(converter, descriptorBuilder, name, read, write)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a read-only float property for a device.
|
||||
*/
|
||||
public fun <D : Device> CompositeControlComponentSpec<D>.float(
|
||||
descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {},
|
||||
name: String? = null,
|
||||
read: suspend D.(propertyName: String) -> Float?,
|
||||
): PropertyDelegateProvider<CompositeControlComponentSpec<D>, ReadOnlyProperty<CompositeControlComponentSpec<D>, DevicePropertySpec<D, Float>>> =
|
||||
property(MetaConverter.float, descriptorBuilder, name, read)
|
||||
|
||||
/**
|
||||
* Creates a mutable float property for a device.
|
||||
*/
|
||||
public fun <D : Device> CompositeControlComponentSpec<D>.floatMutable(
|
||||
descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {},
|
||||
name: String? = null,
|
||||
read: suspend D.(propertyName: String) -> Float?,
|
||||
write: suspend D.(propertyName: String, value: Float) -> Unit
|
||||
): PropertyDelegateProvider<CompositeControlComponentSpec<D>, ReadOnlyProperty<CompositeControlComponentSpec<D>, MutableDevicePropertySpec<D, Float>>> =
|
||||
mutableProperty(MetaConverter.float, descriptorBuilder, name, read, write)
|
||||
|
||||
/**
|
||||
* Creates a read-only long property for a device.
|
||||
*/
|
||||
public fun <D : Device> CompositeControlComponentSpec<D>.long(
|
||||
descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {},
|
||||
name: String? = null,
|
||||
read: suspend D.(propertyName: String) -> Long?,
|
||||
): PropertyDelegateProvider<CompositeControlComponentSpec<D>, ReadOnlyProperty<CompositeControlComponentSpec<D>, DevicePropertySpec<D, Long>>> =
|
||||
property(MetaConverter.long, descriptorBuilder, name, read)
|
||||
|
||||
/**
|
||||
* Creates a mutable long property for a device.
|
||||
*/
|
||||
public fun <D : Device> CompositeControlComponentSpec<D>.longMutable(
|
||||
descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {},
|
||||
name: String? = null,
|
||||
read: suspend D.(propertyName: String) -> Long?,
|
||||
write: suspend D.(propertyName: String, value: Long) -> Unit
|
||||
): PropertyDelegateProvider<CompositeControlComponentSpec<D>, ReadOnlyProperty<CompositeControlComponentSpec<D>, MutableDevicePropertySpec<D, Long>>> =
|
||||
mutableProperty(MetaConverter.long, descriptorBuilder, name, read, write)
|
||||
|
||||
/**
|
||||
* Creates a read-only int property for a device.
|
||||
*/
|
||||
public fun <D : Device> CompositeControlComponentSpec<D>.int(
|
||||
descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {},
|
||||
name: String? = null,
|
||||
read: suspend D.(propertyName: String) -> Int?,
|
||||
): PropertyDelegateProvider<CompositeControlComponentSpec<D>, ReadOnlyProperty<CompositeControlComponentSpec<D>, DevicePropertySpec<D, Int>>> =
|
||||
property(MetaConverter.int, descriptorBuilder, name, read)
|
||||
|
||||
/**
|
||||
* Creates a mutable int property for a device.
|
||||
*/
|
||||
public fun <D : Device> CompositeControlComponentSpec<D>.intMutable(
|
||||
descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {},
|
||||
name: String? = null,
|
||||
read: suspend D.(propertyName: String) -> Int?,
|
||||
write: suspend D.(propertyName: String, value: Int) -> Unit
|
||||
): PropertyDelegateProvider<CompositeControlComponentSpec<D>, ReadOnlyProperty<CompositeControlComponentSpec<D>, MutableDevicePropertySpec<D, Int>>> =
|
||||
mutableProperty(MetaConverter.int, descriptorBuilder, name, read, write)
|
||||
|
||||
/**
|
||||
* Creates a read-only list property for a device.
|
||||
*/
|
||||
public fun <T, D : Device> CompositeControlComponentSpec<D>.list(
|
||||
converter: MetaConverter<List<T>>,
|
||||
descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {},
|
||||
name: String? = null,
|
||||
read: suspend D.(propertyName: String) -> List<T>?,
|
||||
): PropertyDelegateProvider<CompositeControlComponentSpec<D>, ReadOnlyProperty<CompositeControlComponentSpec<D>, DevicePropertySpec<D, List<T>>>> =
|
||||
property(converter, descriptorBuilder, name, read)
|
||||
|
||||
/**
|
||||
* Creates a mutable list property for a device.
|
||||
*/
|
||||
public fun <T, D : Device> CompositeControlComponentSpec<D>.listMutable(
|
||||
converter: MetaConverter<List<T>>,
|
||||
descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {},
|
||||
name: String? = null,
|
||||
read: suspend D.(propertyName: String) -> List<T>?,
|
||||
write: suspend D.(propertyName: String, value: List<T>) -> Unit
|
||||
): PropertyDelegateProvider<CompositeControlComponentSpec<D>, ReadOnlyProperty<CompositeControlComponentSpec<D>, MutableDevicePropertySpec<D, List<T>>>> =
|
||||
mutableProperty(converter, descriptorBuilder, name, read, write)
|
||||
|
||||
|
||||
public fun <I, O, D : Device> CompositeControlComponentSpec<D>.asyncActionProperty(
|
||||
inputConverter: MetaConverter<I>,
|
||||
outputConverter: MetaConverter<O>,
|
||||
descriptorBuilder: ActionDescriptorBuilder.() -> Unit = {},
|
||||
name: String? = null,
|
||||
execute: suspend D.(I) -> Deferred<O>,
|
||||
): PropertyDelegateProvider<CompositeControlComponentSpec<D>, ReadOnlyProperty<CompositeControlComponentSpec<D>, DeviceActionSpec<D, I, O>>> =
|
||||
action(inputConverter, outputConverter, descriptorBuilder, name) { input ->
|
||||
execute(input).await()
|
||||
}
|
||||
|
||||
public fun <T, D : Device> CompositeControlComponentSpec<D>.metaProperty(
|
||||
descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {},
|
||||
name: String? = null,
|
||||
read: suspend D.(propertyName: String) -> Meta?,
|
||||
): PropertyDelegateProvider<CompositeControlComponentSpec<D>, ReadOnlyProperty<CompositeControlComponentSpec<D>, DevicePropertySpec<D, Meta>>> =
|
||||
property(MetaConverter.meta, descriptorBuilder, name, read)
|
||||
|
||||
|
||||
public fun <T, D : Device> CompositeControlComponentSpec<D>.mutableMetaProperty(
|
||||
descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {},
|
||||
name: String? = null,
|
||||
read: suspend D.(propertyName: String) -> Meta?,
|
||||
write: suspend D.(propertyName: String, value: Meta) -> Unit
|
||||
): PropertyDelegateProvider<CompositeControlComponentSpec<D>, ReadOnlyProperty<CompositeControlComponentSpec<D>, MutableDevicePropertySpec<D, Meta>>> =
|
||||
mutableProperty(MetaConverter.meta, descriptorBuilder, name, read, write)
|
||||
|
||||
/**
|
||||
* An action that takes no parameters and returns no values
|
||||
*/
|
||||
public fun <T, D : Device> CompositeControlComponentSpec<D>.unitAction(
|
||||
descriptorBuilder: ActionDescriptorBuilder.() -> Unit = {},
|
||||
name: String? = null,
|
||||
execute: suspend D.() -> Unit,
|
||||
): PropertyDelegateProvider<CompositeControlComponentSpec<D>, ReadOnlyProperty<CompositeControlComponentSpec<D>, DeviceActionSpec<D, Unit, Unit>>> =
|
||||
action(
|
||||
MetaConverter.unit,
|
||||
MetaConverter.unit,
|
||||
descriptorBuilder,
|
||||
name
|
||||
) {
|
||||
execute()
|
||||
}
|
||||
|
||||
public fun <I, O, D : Device> CompositeControlComponentSpec<D>.asyncAction(
|
||||
inputConverter: MetaConverter<I>,
|
||||
outputConverter: MetaConverter<O>,
|
||||
descriptorBuilder: ActionDescriptorBuilder.() -> Unit = {},
|
||||
name: String? = null,
|
||||
execute: suspend D.(I) -> Deferred<O>,
|
||||
): PropertyDelegateProvider<CompositeControlComponentSpec<D>, ReadOnlyProperty<CompositeControlComponentSpec<D>, DeviceActionSpec<D, I, O>>> =
|
||||
action(
|
||||
inputConverter,
|
||||
outputConverter,
|
||||
descriptorBuilder,
|
||||
name
|
||||
) {
|
||||
execute(it).await()
|
||||
}
|
||||
|
||||
/**
|
||||
* An action that takes [Meta] and returns [Meta]. No conversions are done
|
||||
*/
|
||||
public fun <T, D : Device> CompositeControlComponentSpec<D>.metaAction(
|
||||
descriptorBuilder: ActionDescriptorBuilder.() -> Unit = {},
|
||||
name: String? = null,
|
||||
execute: suspend D.(Meta) -> Meta,
|
||||
): PropertyDelegateProvider<CompositeControlComponentSpec<D>, ReadOnlyProperty<CompositeControlComponentSpec<D>, DeviceActionSpec<D, Meta, Meta>>> =
|
||||
action(
|
||||
MetaConverter.meta,
|
||||
MetaConverter.meta,
|
||||
descriptorBuilder,
|
||||
name
|
||||
) {
|
||||
execute(it)
|
||||
}
|
||||
|
||||
/**
|
||||
* Throw an exception if device does not have all properties and actions defined by this specification
|
||||
*/
|
||||
public fun CompositeControlComponentSpec<*>.validate(device: Device) {
|
||||
properties.map { it.value.descriptor }.forEach { specProperty ->
|
||||
check(specProperty in device.propertyDescriptors) { "Property ${specProperty.name} not registered in ${device.id}" }
|
||||
}
|
||||
|
||||
actions.map { it.value.descriptor }.forEach { specAction ->
|
||||
check(specAction in device.actionDescriptors) { "Action ${specAction.name} not registered in ${device.id}" }
|
||||
}
|
||||
}
|
@ -44,24 +44,24 @@ public abstract class DeviceSpec<D : Device> {
|
||||
|
||||
public fun <T> property(
|
||||
converter: MetaConverter<T>,
|
||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||
descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {},
|
||||
name: String? = null,
|
||||
read: suspend D.(propertyName: String) -> T?,
|
||||
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DevicePropertySpec<D, T>>> =
|
||||
PropertyDelegateProvider { _: DeviceSpec<D>, property ->
|
||||
val propertyName = name ?: property.name
|
||||
val deviceProperty = object : DevicePropertySpec<D, T> {
|
||||
|
||||
override val descriptor: PropertyDescriptor = PropertyDescriptor(propertyName).apply {
|
||||
converter.descriptor?.let { converterDescriptor ->
|
||||
metaDescriptor {
|
||||
from(converterDescriptor)
|
||||
}
|
||||
val descriptor = propertyDescriptor(propertyName) {
|
||||
mutable = false
|
||||
converter.descriptor?.let { converterDescriptor ->
|
||||
metaDescriptor {
|
||||
from(converterDescriptor)
|
||||
}
|
||||
fromSpec(property)
|
||||
descriptorBuilder()
|
||||
}
|
||||
|
||||
fromSpec(property)
|
||||
descriptorBuilder()
|
||||
}
|
||||
val deviceProperty = object : DevicePropertySpec<D, T> {
|
||||
override val descriptor = descriptor
|
||||
override val converter: MetaConverter<T> = converter
|
||||
|
||||
override suspend fun read(device: D): T? =
|
||||
@ -75,26 +75,25 @@ public abstract class DeviceSpec<D : Device> {
|
||||
|
||||
public fun <T> mutableProperty(
|
||||
converter: MetaConverter<T>,
|
||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||
descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {},
|
||||
name: String? = null,
|
||||
read: suspend D.(propertyName: String) -> T?,
|
||||
write: suspend D.(propertyName: String, value: T) -> Unit,
|
||||
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, MutableDevicePropertySpec<D, T>>> =
|
||||
PropertyDelegateProvider { _: DeviceSpec<D>, property: KProperty<*> ->
|
||||
val propertyName = name ?: property.name
|
||||
val deviceProperty = object : MutableDevicePropertySpec<D, T> {
|
||||
override val descriptor: PropertyDescriptor = PropertyDescriptor(
|
||||
propertyName,
|
||||
mutable = true
|
||||
).apply {
|
||||
converter.descriptor?.let { converterDescriptor ->
|
||||
metaDescriptor {
|
||||
from(converterDescriptor)
|
||||
}
|
||||
val descriptor = propertyDescriptor(propertyName) {
|
||||
this.mutable = true
|
||||
converter.descriptor?.let { converterDescriptor ->
|
||||
metaDescriptor {
|
||||
from(converterDescriptor)
|
||||
}
|
||||
fromSpec(property)
|
||||
descriptorBuilder()
|
||||
}
|
||||
fromSpec(property)
|
||||
descriptorBuilder()
|
||||
}
|
||||
val deviceProperty = object : MutableDevicePropertySpec<D, T> {
|
||||
override val descriptor = descriptor
|
||||
override val converter: MetaConverter<T> = converter
|
||||
|
||||
override suspend fun read(device: D): T? =
|
||||
@ -119,30 +118,28 @@ public abstract class DeviceSpec<D : Device> {
|
||||
public fun <I, O> action(
|
||||
inputConverter: MetaConverter<I>,
|
||||
outputConverter: MetaConverter<O>,
|
||||
descriptorBuilder: ActionDescriptor.() -> Unit = {},
|
||||
descriptorBuilder: ActionDescriptorBuilder.() -> Unit = {},
|
||||
name: String? = null,
|
||||
execute: suspend D.(I) -> O,
|
||||
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DeviceActionSpec<D, I, O>>> =
|
||||
PropertyDelegateProvider { _: DeviceSpec<D>, property: KProperty<*> ->
|
||||
val actionName = name ?: property.name
|
||||
val deviceAction = object : DeviceActionSpec<D, I, O> {
|
||||
override val descriptor: ActionDescriptor = ActionDescriptor(actionName).apply {
|
||||
inputConverter.descriptor?.let { converterDescriptor ->
|
||||
inputMetaDescriptor = MetaDescriptor {
|
||||
from(converterDescriptor)
|
||||
from(inputMetaDescriptor)
|
||||
}
|
||||
val descriptor = actionDescriptor(actionName) {
|
||||
inputConverter.descriptor?.let { converterDescriptor ->
|
||||
inputMeta {
|
||||
from(converterDescriptor)
|
||||
}
|
||||
outputConverter.descriptor?.let { converterDescriptor ->
|
||||
outputMetaDescriptor = MetaDescriptor {
|
||||
from(converterDescriptor)
|
||||
from(outputMetaDescriptor)
|
||||
}
|
||||
}
|
||||
|
||||
fromSpec(property)
|
||||
descriptorBuilder()
|
||||
}
|
||||
outputConverter.descriptor?.let { converterDescriptor ->
|
||||
outputMeta {
|
||||
from(converterDescriptor)
|
||||
}
|
||||
}
|
||||
fromSpec(property)
|
||||
descriptorBuilder()
|
||||
}
|
||||
val deviceAction = object : DeviceActionSpec<D, I, O> {
|
||||
override val descriptor: ActionDescriptor = descriptor
|
||||
|
||||
override val inputConverter: MetaConverter<I> = inputConverter
|
||||
override val outputConverter: MetaConverter<O> = outputConverter
|
||||
@ -162,7 +159,7 @@ public abstract class DeviceSpec<D : Device> {
|
||||
* An action that takes no parameters and returns no values
|
||||
*/
|
||||
public fun <D : Device> DeviceSpec<D>.unitAction(
|
||||
descriptorBuilder: ActionDescriptor.() -> Unit = {},
|
||||
descriptorBuilder: ActionDescriptorBuilder.() -> Unit = {},
|
||||
name: String? = null,
|
||||
execute: suspend D.() -> Unit,
|
||||
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DeviceActionSpec<D, Unit, Unit>>> =
|
||||
@ -179,7 +176,7 @@ public fun <D : Device> DeviceSpec<D>.unitAction(
|
||||
* An action that takes [Meta] and returns [Meta]. No conversions are done
|
||||
*/
|
||||
public fun <D : Device> DeviceSpec<D>.metaAction(
|
||||
descriptorBuilder: ActionDescriptor.() -> Unit = {},
|
||||
descriptorBuilder: ActionDescriptorBuilder.() -> Unit = {},
|
||||
name: String? = null,
|
||||
execute: suspend D.(Meta) -> Meta,
|
||||
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DeviceActionSpec<D, Meta, Meta>>> =
|
||||
|
@ -1,10 +1,10 @@
|
||||
package space.kscience.controls.spec
|
||||
|
||||
import space.kscience.controls.api.ActionDescriptor
|
||||
import space.kscience.controls.api.PropertyDescriptor
|
||||
import space.kscience.controls.api.ActionDescriptorBuilder
|
||||
import space.kscience.controls.api.PropertyDescriptorBuilder
|
||||
import kotlin.reflect.KProperty
|
||||
|
||||
|
||||
internal expect fun PropertyDescriptor.fromSpec(property: KProperty<*>)
|
||||
internal expect fun PropertyDescriptorBuilder.fromSpec(property: KProperty<*>)
|
||||
|
||||
internal expect fun ActionDescriptor.fromSpec(property: KProperty<*>)
|
||||
internal expect fun ActionDescriptorBuilder.fromSpec(property: KProperty<*>)
|
@ -1,8 +1,7 @@
|
||||
package space.kscience.controls.spec
|
||||
|
||||
import space.kscience.controls.api.Device
|
||||
import space.kscience.controls.api.PropertyDescriptor
|
||||
import space.kscience.controls.api.metaDescriptor
|
||||
import space.kscience.controls.api.PropertyDescriptorBuilder
|
||||
import space.kscience.dataforge.meta.Meta
|
||||
import space.kscience.dataforge.meta.MetaConverter
|
||||
import space.kscience.dataforge.meta.ValueType
|
||||
@ -17,7 +16,7 @@ import kotlin.reflect.KProperty1
|
||||
public fun <T, D : Device> DeviceSpec<D>.property(
|
||||
converter: MetaConverter<T>,
|
||||
readOnlyProperty: KProperty1<D, T>,
|
||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||
descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {},
|
||||
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DevicePropertySpec<D, T>>> = property(
|
||||
converter,
|
||||
descriptorBuilder,
|
||||
@ -31,7 +30,7 @@ public fun <T, D : Device> DeviceSpec<D>.property(
|
||||
public fun <T, D : Device> DeviceSpec<D>.mutableProperty(
|
||||
converter: MetaConverter<T>,
|
||||
readWriteProperty: KMutableProperty1<D, T>,
|
||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||
descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {},
|
||||
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, MutableDevicePropertySpec<D, T>>> =
|
||||
mutableProperty(
|
||||
converter,
|
||||
@ -49,7 +48,7 @@ public fun <T, D : Device> DeviceSpec<D>.mutableProperty(
|
||||
*/
|
||||
public fun <T, D : DeviceBase<D>> DeviceSpec<D>.property(
|
||||
converter: MetaConverter<T>,
|
||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||
descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {},
|
||||
name: String? = null,
|
||||
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DevicePropertySpec<D, T>>> =
|
||||
property(
|
||||
@ -60,7 +59,7 @@ public fun <T, D : DeviceBase<D>> DeviceSpec<D>.property(
|
||||
)
|
||||
|
||||
public fun <D : Device> DeviceSpec<D>.booleanProperty(
|
||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||
descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {},
|
||||
name: String? = null,
|
||||
read: suspend D.(propertyName: String) -> Boolean?
|
||||
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DevicePropertySpec<D, Boolean>>> = property(
|
||||
@ -76,8 +75,8 @@ public fun <D : Device> DeviceSpec<D>.booleanProperty(
|
||||
)
|
||||
|
||||
private inline fun numberDescriptor(
|
||||
crossinline descriptorBuilder: PropertyDescriptor.() -> Unit = {}
|
||||
): PropertyDescriptor.() -> Unit = {
|
||||
crossinline descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {}
|
||||
): PropertyDescriptorBuilder.() -> Unit = {
|
||||
metaDescriptor {
|
||||
valueType(ValueType.NUMBER)
|
||||
}
|
||||
@ -85,7 +84,7 @@ private inline fun numberDescriptor(
|
||||
}
|
||||
|
||||
public fun <D : Device> DeviceSpec<D>.numberProperty(
|
||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||
descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {},
|
||||
name: String? = null,
|
||||
read: suspend D.(propertyName: String) -> Number?
|
||||
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DevicePropertySpec<D, Number>>> = property(
|
||||
@ -96,7 +95,7 @@ public fun <D : Device> DeviceSpec<D>.numberProperty(
|
||||
)
|
||||
|
||||
public fun <D : Device> DeviceSpec<D>.doubleProperty(
|
||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||
descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {},
|
||||
name: String? = null,
|
||||
read: suspend D.(propertyName: String) -> Double?
|
||||
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DevicePropertySpec<D, Double>>> = property(
|
||||
@ -107,7 +106,7 @@ public fun <D : Device> DeviceSpec<D>.doubleProperty(
|
||||
)
|
||||
|
||||
public fun <D : Device> DeviceSpec<D>.stringProperty(
|
||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||
descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {},
|
||||
name: String? = null,
|
||||
read: suspend D.(propertyName: String) -> String?
|
||||
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DevicePropertySpec<D, String>>> = property(
|
||||
@ -123,7 +122,7 @@ public fun <D : Device> DeviceSpec<D>.stringProperty(
|
||||
)
|
||||
|
||||
public fun <D : Device> DeviceSpec<D>.metaProperty(
|
||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||
descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {},
|
||||
name: String? = null,
|
||||
read: suspend D.(propertyName: String) -> Meta?
|
||||
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DevicePropertySpec<D, Meta>>> = property(
|
||||
@ -147,7 +146,7 @@ public fun <D : Device> DeviceSpec<D>.metaProperty(
|
||||
*/
|
||||
public fun <T, D : DeviceBase<D>> DeviceSpec<D>.mutableProperty(
|
||||
converter: MetaConverter<T>,
|
||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||
descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {},
|
||||
name: String? = null,
|
||||
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, MutableDevicePropertySpec<D, T>>> =
|
||||
mutableProperty(
|
||||
@ -159,7 +158,7 @@ public fun <T, D : DeviceBase<D>> DeviceSpec<D>.mutableProperty(
|
||||
)
|
||||
|
||||
public fun <D : Device> DeviceSpec<D>.mutableBooleanProperty(
|
||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||
descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {},
|
||||
name: String? = null,
|
||||
read: suspend D.(propertyName: String) -> Boolean?,
|
||||
write: suspend D.(propertyName: String, value: Boolean) -> Unit
|
||||
@ -179,7 +178,7 @@ public fun <D : Device> DeviceSpec<D>.mutableBooleanProperty(
|
||||
|
||||
|
||||
public fun <D : Device> DeviceSpec<D>.mutableNumberProperty(
|
||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||
descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {},
|
||||
name: String? = null,
|
||||
read: suspend D.(propertyName: String) -> Number,
|
||||
write: suspend D.(propertyName: String, value: Number) -> Unit
|
||||
@ -187,7 +186,7 @@ public fun <D : Device> DeviceSpec<D>.mutableNumberProperty(
|
||||
mutableProperty(MetaConverter.number, numberDescriptor(descriptorBuilder), name, read, write)
|
||||
|
||||
public fun <D : Device> DeviceSpec<D>.mutableDoubleProperty(
|
||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||
descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {},
|
||||
name: String? = null,
|
||||
read: suspend D.(propertyName: String) -> Double,
|
||||
write: suspend D.(propertyName: String, value: Double) -> Unit
|
||||
@ -195,7 +194,7 @@ public fun <D : Device> DeviceSpec<D>.mutableDoubleProperty(
|
||||
mutableProperty(MetaConverter.double, numberDescriptor(descriptorBuilder), name, read, write)
|
||||
|
||||
public fun <D : Device> DeviceSpec<D>.mutableStringProperty(
|
||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||
descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {},
|
||||
name: String? = null,
|
||||
read: suspend D.(propertyName: String) -> String,
|
||||
write: suspend D.(propertyName: String, value: String) -> Unit
|
||||
@ -203,7 +202,7 @@ public fun <D : Device> DeviceSpec<D>.mutableStringProperty(
|
||||
mutableProperty(MetaConverter.string, descriptorBuilder, name, read, write)
|
||||
|
||||
public fun <D : Device> DeviceSpec<D>.mutableMetaProperty(
|
||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||
descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {},
|
||||
name: String? = null,
|
||||
read: suspend D.(propertyName: String) -> Meta,
|
||||
write: suspend D.(propertyName: String, value: Meta) -> Unit
|
||||
|
@ -0,0 +1,822 @@
|
||||
package space.kscience.controls.spec
|
||||
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import space.kscience.dataforge.context.Context
|
||||
import space.kscience.dataforge.meta.Meta
|
||||
import space.kscience.dataforge.meta.double
|
||||
import space.kscience.dataforge.meta.int
|
||||
import space.kscience.dataforge.names.parseAsName
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertNotNull
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class CompositeControlTest {
|
||||
|
||||
// ---------------------- Device Specifications ----------------------------------
|
||||
|
||||
public object StepperMotorSpec : CompositeControlComponentSpec<StepperMotorDevice>() {
|
||||
public val position by intMutable(
|
||||
name = "position",
|
||||
read = { getPosition() },
|
||||
write = { _, value -> setPosition(value) }
|
||||
)
|
||||
|
||||
public val maxPosition by int(
|
||||
name = "maxPosition",
|
||||
read = { maxPosition }
|
||||
)
|
||||
}
|
||||
|
||||
public object ValveSpec : CompositeControlComponentSpec<ValveDevice>() {
|
||||
public val state by booleanMutable(
|
||||
read = { getState() },
|
||||
write = { _, value -> setState(value) }
|
||||
)
|
||||
}
|
||||
|
||||
public object PressureChamberSpec : CompositeControlComponentSpec<PressureChamberDevice>() {
|
||||
public val pressure by doubleMutable(
|
||||
read = { getPressure() },
|
||||
write = { _, value -> setPressure(value) }
|
||||
)
|
||||
}
|
||||
|
||||
public object SyringePumpSpec : CompositeControlComponentSpec<SyringePumpDevice>() {
|
||||
public val volume by doubleMutable(
|
||||
read = { getVolume() },
|
||||
write = { _, value -> setVolume(value) }
|
||||
)
|
||||
}
|
||||
|
||||
public object ReagentSensorSpec : CompositeControlComponentSpec<ReagentSensorDevice>() {
|
||||
public val isPresent by boolean(
|
||||
read = { checkReagent() }
|
||||
)
|
||||
}
|
||||
|
||||
public object NeedleSpec : CompositeControlComponentSpec<NeedleDevice>() {
|
||||
public val mode by enumMutable(
|
||||
enumValues = NeedleDevice.Mode.entries.toTypedArray(),
|
||||
read = { getMode() },
|
||||
write = { _, value -> setMode(value) }
|
||||
)
|
||||
|
||||
public val position by doubleMutable(
|
||||
read = { getPosition() },
|
||||
write = { _, value -> setPosition(value) }
|
||||
)
|
||||
}
|
||||
|
||||
public object ShakerSpec : CompositeControlComponentSpec<ShakerDevice>() {
|
||||
public val verticalMotor by childSpec<StepperMotorSpec, StepperMotorDevice>()
|
||||
public val horizontalMotor by childSpec<StepperMotorSpec, StepperMotorDevice>()
|
||||
}
|
||||
|
||||
public object TransportationSystemSpec : CompositeControlComponentSpec<TransportationSystem>() {
|
||||
public val slideMotor by childSpec<StepperMotorSpec, StepperMotorDevice>()
|
||||
|
||||
public val pushMotor by childSpec<StepperMotorSpec, StepperMotorDevice>()
|
||||
|
||||
public val receiveMotor by childSpec<StepperMotorSpec, StepperMotorDevice>()
|
||||
}
|
||||
|
||||
|
||||
public object AnalyzerSpec : CompositeControlComponentSpec<AnalyzerDevice>() {
|
||||
public val transportationSystem by childSpec<TransportationSystemSpec, TransportationSystemSpec>()
|
||||
public val shakerDevice by childSpec<ShakerSpec, ShakerDevice>()
|
||||
public val needleDevice by childSpec<NeedleSpec, NeedleDevice>()
|
||||
|
||||
|
||||
public val valveV20 by childSpec<ValveSpec, ValveDevice>()
|
||||
public val valveV17 by childSpec<ValveSpec, ValveDevice>()
|
||||
public val valveV18 by childSpec<ValveSpec, ValveDevice>()
|
||||
public val valveV35 by childSpec<ValveSpec, ValveDevice>()
|
||||
|
||||
|
||||
public val pressureChamberHigh by childSpec<PressureChamberSpec, PressureChamberDevice>()
|
||||
public val pressureChamberLow by childSpec<PressureChamberSpec, PressureChamberDevice>()
|
||||
|
||||
public val syringePumpMA100 by childSpec<SyringePumpSpec, SyringePumpDevice>()
|
||||
public val syringePumpMA25 by childSpec<SyringePumpSpec, SyringePumpDevice>()
|
||||
|
||||
public val reagentSensor1 by childSpec<ReagentSensorSpec, ReagentSensorDevice>()
|
||||
public val reagentSensor2 by childSpec<ReagentSensorSpec, ReagentSensorDevice>()
|
||||
public val reagentSensor3 by childSpec<ReagentSensorSpec, ReagentSensorDevice>()
|
||||
}
|
||||
|
||||
// ---------------------- Device Implementations ----------------------------------
|
||||
|
||||
// Implementation of Stepper Motor Device
|
||||
public class StepperMotorDevice(
|
||||
context: Context,
|
||||
meta: Meta = Meta.EMPTY
|
||||
) : ConfigurableCompositeControlComponent<StepperMotorDevice>(StepperMotorSpec, context, meta) {
|
||||
|
||||
private var _position: Int = 0
|
||||
public val maxPosition: Int = meta["maxPosition".parseAsName()].int ?: 100
|
||||
|
||||
/**
|
||||
* Get current position of the stepper motor.
|
||||
* @return Current position as Int
|
||||
*/
|
||||
public suspend fun getPosition(): Int = _position
|
||||
|
||||
/**
|
||||
* Set position of the stepper motor, if position is valid the move will occur.
|
||||
* @param value target position as Int
|
||||
*/
|
||||
public suspend fun setPosition(value: Int) {
|
||||
if (value in 0..maxPosition) {
|
||||
_position = value
|
||||
println("StepperMotorDevice: Moving to position $_position")
|
||||
delay(100)
|
||||
} else {
|
||||
println("StepperMotorDevice: Invalid position $value (max: $maxPosition)")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Implementation of Valve Device
|
||||
public class ValveDevice(
|
||||
context: Context,
|
||||
meta: Meta = Meta.EMPTY
|
||||
) : ConfigurableCompositeControlComponent<ValveDevice>(ValveSpec, context, meta) {
|
||||
|
||||
private var _state: Boolean = false
|
||||
|
||||
/**
|
||||
* Get current state of the valve
|
||||
* @return true if valve is open, false if closed
|
||||
*/
|
||||
public suspend fun getState(): Boolean = _state
|
||||
|
||||
/**
|
||||
* Set the current state of the valve and print the change.
|
||||
* @param value true if valve should be open, false if should be closed
|
||||
*/
|
||||
public suspend fun setState(value: Boolean) {
|
||||
_state = value
|
||||
val stateStr = if (_state) "open" else "closed"
|
||||
println("ValveDevice: Valve is now $stateStr")
|
||||
delay(50)
|
||||
}
|
||||
|
||||
/**
|
||||
* Simulates clicking the valve.
|
||||
*/
|
||||
public suspend fun click() {
|
||||
println("ValveDevice: Clicking valve...")
|
||||
setState(true)
|
||||
delay(50)
|
||||
setState(false)
|
||||
println("ValveDevice: Valve click completed")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Implementation of Pressure Chamber Device
|
||||
public class PressureChamberDevice(
|
||||
context: Context,
|
||||
meta: Meta = Meta.EMPTY
|
||||
) : ConfigurableCompositeControlComponent<PressureChamberDevice>(PressureChamberSpec, context, meta) {
|
||||
|
||||
private var _pressure: Double = 0.0
|
||||
|
||||
/**
|
||||
* Get the current pressure in the chamber.
|
||||
* @return current pressure as Double
|
||||
*/
|
||||
public suspend fun getPressure(): Double = _pressure
|
||||
|
||||
/**
|
||||
* Set the pressure in the chamber.
|
||||
* @param value target pressure as Double
|
||||
*/
|
||||
public suspend fun setPressure(value: Double) {
|
||||
_pressure = value
|
||||
println("PressureChamberDevice: Pressure is now $_pressure")
|
||||
delay(50)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Implementation of Syringe Pump Device
|
||||
public class SyringePumpDevice(
|
||||
context: Context,
|
||||
meta: Meta = Meta.EMPTY
|
||||
) : ConfigurableCompositeControlComponent<SyringePumpDevice>(SyringePumpSpec, context, meta) {
|
||||
|
||||
private var _volume: Double = 0.0
|
||||
public val maxVolume: Double = meta["maxVolume".parseAsName()].double ?: 5.0
|
||||
|
||||
/**
|
||||
* Get current volume in the syringe
|
||||
* @return volume as Double
|
||||
*/
|
||||
public suspend fun getVolume(): Double = _volume
|
||||
|
||||
/**
|
||||
* Set the current volume in the syringe.
|
||||
* @param value the target volume as Double
|
||||
*/
|
||||
public suspend fun setVolume(value: Double) {
|
||||
if (value in 0.0..maxVolume) {
|
||||
_volume = value
|
||||
println("SyringePumpDevice: Volume is now $_volume ml")
|
||||
delay(100)
|
||||
} else {
|
||||
println("SyringePumpDevice: Invalid volume $value (max: $maxVolume)")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Implementation of Reagent Sensor Device
|
||||
public class ReagentSensorDevice(
|
||||
context: Context,
|
||||
meta: Meta = Meta.EMPTY
|
||||
) : ConfigurableCompositeControlComponent<ReagentSensorDevice>(ReagentSensorSpec, context, meta) {
|
||||
|
||||
/**
|
||||
* Checks for reagent presence.
|
||||
* @return true if reagent is present.
|
||||
*/
|
||||
public suspend fun checkReagent(): Boolean {
|
||||
println("ReagentSensorDevice: Checking for reagent presence...")
|
||||
delay(100) // Simulate detection time
|
||||
val isPresent = true // Assume reagent is present
|
||||
println("ReagentSensorDevice: Reagent is ${if (isPresent) "present" else "not present"}")
|
||||
return isPresent
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Implementation of Needle Device
|
||||
public class NeedleDevice(
|
||||
context: Context,
|
||||
meta: Meta = Meta.EMPTY
|
||||
) : ConfigurableCompositeControlComponent<NeedleDevice>(NeedleSpec, context, meta) {
|
||||
|
||||
public enum class Mode { SAMPLING, WASHING }
|
||||
|
||||
private var _mode: Mode = Mode.WASHING
|
||||
private var _position: Double = 0.0
|
||||
|
||||
/**
|
||||
* Get the current mode of needle.
|
||||
* @return current mode of the needle.
|
||||
*/
|
||||
public suspend fun getMode(): Mode = _mode
|
||||
|
||||
/**
|
||||
* Set the mode of the needle
|
||||
* @param value the target mode
|
||||
*/
|
||||
public suspend fun setMode(value: Mode) {
|
||||
_mode = value
|
||||
println("NeedleDevice: Mode is now $_mode")
|
||||
delay(50)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current position of the needle
|
||||
* @return current position as Double
|
||||
*/
|
||||
public suspend fun getPosition(): Double = _position
|
||||
|
||||
/**
|
||||
* Set the needle position
|
||||
* @param value target position as Double
|
||||
*/
|
||||
public suspend fun setPosition(value: Double) {
|
||||
if (value in 0.0..100.0) {
|
||||
_position = value
|
||||
println("NeedleDevice: Moved to position $_position mm")
|
||||
delay(100)
|
||||
} else {
|
||||
println("NeedleDevice: Invalid position $value mm")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes washing process for given duration
|
||||
* @param duration time for washing in seconds
|
||||
*/
|
||||
public suspend fun performWashing(duration: Int) {
|
||||
println("NeedleDevice: Washing in progress for $duration seconds")
|
||||
delay(duration * 1000L) // Simulate washing (1 second = 1000 ms)
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute sampling procedure
|
||||
*/
|
||||
public suspend fun performSampling() {
|
||||
println("NeedleDevice: Performing sample intake at position $_position mm")
|
||||
delay(500) // Simulate sampling time
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Implementation of Shaker Device
|
||||
public class ShakerDevice(
|
||||
context: Context,
|
||||
meta: Meta = Meta.EMPTY
|
||||
) : ConfigurableCompositeControlComponent<ShakerDevice>(ShakerSpec, context, meta) {
|
||||
|
||||
/**
|
||||
* Get vertical stepper motor
|
||||
*/
|
||||
public val verticalMotor by childDevice<StepperMotorDevice>()
|
||||
|
||||
/**
|
||||
* Get horizontal stepper motor
|
||||
*/
|
||||
public val horizontalMotor by childDevice<StepperMotorDevice>()
|
||||
|
||||
/**
|
||||
* Shakes the device for given cycles.
|
||||
* @param cycles amount of cycles for shaking
|
||||
*/
|
||||
public suspend fun shake(cycles: Int) {
|
||||
println("ShakerDevice: Shaking started, cycles: $cycles")
|
||||
repeat(cycles) {
|
||||
verticalMotor.setPosition(3)
|
||||
verticalMotor.setPosition(1)
|
||||
println("ShakerDevice: cycle ${it+1} done")
|
||||
}
|
||||
println("ShakerDevice: Shaking completed")
|
||||
}
|
||||
}
|
||||
|
||||
// Implementation of Transportation System
|
||||
public class TransportationSystem(
|
||||
context: Context,
|
||||
meta: Meta = Meta.EMPTY
|
||||
) : ConfigurableCompositeControlComponent<TransportationSystem>(TransportationSystemSpec, context, meta) {
|
||||
|
||||
/**
|
||||
* Get slide stepper motor
|
||||
*/
|
||||
public val slideMotor by childDevice<StepperMotorDevice>()
|
||||
|
||||
|
||||
/**
|
||||
* Get push stepper motor
|
||||
*/
|
||||
public val pushMotor by childDevice<StepperMotorDevice>()
|
||||
|
||||
/**
|
||||
* Get receive stepper motor
|
||||
*/
|
||||
public val receiveMotor by childDevice<StepperMotorDevice>()
|
||||
|
||||
}
|
||||
|
||||
// Implementation of Analyzer Device
|
||||
public class AnalyzerDevice(
|
||||
context: Context,
|
||||
meta: Meta = Meta.EMPTY
|
||||
) : ConfigurableCompositeControlComponent<AnalyzerDevice>(AnalyzerSpec, context, meta) {
|
||||
|
||||
/**
|
||||
* Get transportation system
|
||||
*/
|
||||
public val transportationSystem by childDevice<TransportationSystem>()
|
||||
|
||||
/**
|
||||
* Get shaker device
|
||||
*/
|
||||
public val shakerDevice by childDevice<ShakerDevice>()
|
||||
|
||||
/**
|
||||
* Get needle device
|
||||
*/
|
||||
public val needleDevice by childDevice<NeedleDevice>()
|
||||
|
||||
/**
|
||||
* Get valve V20
|
||||
*/
|
||||
public val valveV20 by childDevice<ValveDevice>()
|
||||
|
||||
/**
|
||||
* Get valve V17
|
||||
*/
|
||||
public val valveV17 by childDevice<ValveDevice>()
|
||||
|
||||
/**
|
||||
* Get valve V18
|
||||
*/
|
||||
public val valveV18 by childDevice<ValveDevice>()
|
||||
|
||||
/**
|
||||
* Get valve V35
|
||||
*/
|
||||
public val valveV35 by childDevice<ValveDevice>()
|
||||
|
||||
/**
|
||||
* Get high pressure chamber
|
||||
*/
|
||||
public val pressureChamberHigh by childDevice<PressureChamberDevice>()
|
||||
|
||||
/**
|
||||
* Get low pressure chamber
|
||||
*/
|
||||
public val pressureChamberLow by childDevice<PressureChamberDevice>()
|
||||
|
||||
/**
|
||||
* Get syringe pump MA100
|
||||
*/
|
||||
public val syringePumpMA100 by childDevice<SyringePumpDevice>()
|
||||
|
||||
/**
|
||||
* Get syringe pump MA25
|
||||
*/
|
||||
public val syringePumpMA25 by childDevice<SyringePumpDevice>()
|
||||
|
||||
/**
|
||||
* Get reagent sensor 1
|
||||
*/
|
||||
public val reagentSensor1 by childDevice<ReagentSensorDevice>()
|
||||
|
||||
/**
|
||||
* Get reagent sensor 2
|
||||
*/
|
||||
public val reagentSensor2 by childDevice<ReagentSensorDevice>()
|
||||
|
||||
/**
|
||||
* Get reagent sensor 3
|
||||
*/
|
||||
public val reagentSensor3 by childDevice<ReagentSensorDevice>()
|
||||
|
||||
/**
|
||||
* Simulates a process of taking sample from tubes.
|
||||
*/
|
||||
public suspend fun processSample() {
|
||||
println("The beginning of the sampling process")
|
||||
|
||||
// Step 1: Open valve V20 and start taking a sample with a syringe pump MA 100 mcl
|
||||
valveV20.setState(true)
|
||||
syringePumpMA100.setVolume(0.1)
|
||||
delay(500) // Simulating waiting time for liquid collection
|
||||
valveV20.setState(false)
|
||||
|
||||
// Step 2: Open valve V17 and start delivering lysis buffer with syringe pump MA 2.5 ml
|
||||
valveV17.setState(true)
|
||||
syringePumpMA25.setVolume(2.5)
|
||||
delay(500) // Simulate lysis buffer delivery time
|
||||
valveV17.setState(false)
|
||||
|
||||
// Step 3: Cleaning system
|
||||
syringePumpMA100.setVolume(0.0)
|
||||
syringePumpMA25.setVolume(0.0)
|
||||
|
||||
println("The sampling process is completed")
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Simulates the analyzer calibration procedure.
|
||||
*/
|
||||
public suspend fun calibrate() {
|
||||
println("The beginning of calibration...")
|
||||
|
||||
// Step 1: Calibrate positions of all motors
|
||||
val motors = listOf(
|
||||
transportationSystem.slideMotor,
|
||||
transportationSystem.pushMotor,
|
||||
transportationSystem.receiveMotor,
|
||||
shakerDevice.verticalMotor,
|
||||
shakerDevice.horizontalMotor,
|
||||
|
||||
)
|
||||
|
||||
for (motor in motors) {
|
||||
for (position in 0..motor.maxPosition) {
|
||||
motor.setPosition(position)
|
||||
}
|
||||
motor.setPosition(0)
|
||||
}
|
||||
|
||||
// Step 2: Click all valves and set them to zero position
|
||||
val valves = listOf(valveV20, valveV17, valveV18, valveV35)
|
||||
for (valve in valves) {
|
||||
valve.click()
|
||||
valve.setState(false)
|
||||
}
|
||||
|
||||
// Step 3: Pump up pressure in high pressure chamber
|
||||
pressureChamberHigh.setPressure(2.0)
|
||||
// Step 4: Pump out pressure from low pressure chamber
|
||||
pressureChamberLow.setPressure(-1.0)
|
||||
|
||||
// Step 5: Fill the hydraulic system
|
||||
// 5.1 Check if reagents are present
|
||||
val sensors = listOf(reagentSensor1, reagentSensor2, reagentSensor3)
|
||||
for (sensor in sensors) {
|
||||
sensor.checkReagent()
|
||||
}
|
||||
|
||||
// 5.2 Perform 5 times full pump movement with all syringe pumps
|
||||
val pumps = listOf(syringePumpMA100, syringePumpMA25)
|
||||
for (pump in pumps) {
|
||||
repeat(5) {
|
||||
pump.setVolume(pump.maxVolume)
|
||||
pump.setVolume(0.0)
|
||||
}
|
||||
}
|
||||
|
||||
// 5.3 Wash needle at its washing position
|
||||
needleDevice.setPosition(0.0)
|
||||
needleDevice.setMode(NeedleDevice.Mode.WASHING)
|
||||
needleDevice.performWashing(5)
|
||||
|
||||
println("Calibration is completed")
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute recipe 1 - sample tube deliver
|
||||
*/
|
||||
public suspend fun executeRecipe1() {
|
||||
println("Executing recipe 1")
|
||||
|
||||
// Step 1: Move a slide to the next position
|
||||
val currentSlidePosition = transportationSystem.slideMotor.getPosition()
|
||||
transportationSystem.slideMotor.setPosition(currentSlidePosition + 1)
|
||||
println("Moved a slide to position ${currentSlidePosition+1}")
|
||||
|
||||
// Step 2: Capture a tube for mixing
|
||||
println("Capturing tube for mixing")
|
||||
|
||||
// 2.1 - 2.10: Control over a shaker and motors
|
||||
shakerDevice.verticalMotor.setPosition(1)
|
||||
shakerDevice.horizontalMotor.setPosition(1)
|
||||
println("Shaker: vertical - 1, horizontal - 1")
|
||||
|
||||
shakerDevice.horizontalMotor.setPosition(2)
|
||||
println("Shaker: horizontal - 2")
|
||||
|
||||
shakerDevice.verticalMotor.setPosition(2)
|
||||
println("Shaker: vertical - 2")
|
||||
|
||||
// Shake
|
||||
shakerDevice.shake(5)
|
||||
println("Shaker: movement done")
|
||||
|
||||
// Step 3: Sampling and measurement
|
||||
executeSampling()
|
||||
needleDevice.setPosition(0.0)
|
||||
println("Needle moved to its initial position")
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute recipe 2 - Automatic Measurement
|
||||
*/
|
||||
public suspend fun executeRecipe2() {
|
||||
println("Executing Recipe 2 - Automatic Measurement")
|
||||
|
||||
transportationSystem.receiveMotor.setPosition(transportationSystem.receiveMotor.getPosition() + 1)
|
||||
println("Pusher moved to position ${transportationSystem.receiveMotor.getPosition() + 1}")
|
||||
|
||||
//Check for a tray, if missing move again
|
||||
if (!checkTrayInPushSystem()) {
|
||||
println("Tray missing. Trying to move again")
|
||||
transportationSystem.receiveMotor.setPosition(transportationSystem.receiveMotor.getPosition() + 1)
|
||||
} else {
|
||||
executeSampling()
|
||||
}
|
||||
|
||||
// If last position, reset the plate
|
||||
if (transportationSystem.receiveMotor.getPosition() >= transportationSystem.receiveMotor.maxPosition) {
|
||||
println("Plate is complete. Resetting pusher to initial position")
|
||||
transportationSystem.receiveMotor.setPosition(0)
|
||||
}
|
||||
|
||||
println("Recipe 2 execution finished")
|
||||
needleDevice.setPosition(0.0)
|
||||
println("Needle moved to its initial position")
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute recipe 3 - Single Measurement
|
||||
*/
|
||||
public suspend fun executeRecipe3() {
|
||||
println("Executing Recipe 3 - Single measurement")
|
||||
executeSampling()
|
||||
println("Recipe 3 completed")
|
||||
needleDevice.setPosition(0.0)
|
||||
println("Needle moved to its initial position")
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Simulates tray presence check
|
||||
*/
|
||||
private suspend fun checkTrayInPushSystem(): Boolean {
|
||||
println("Checking for a tray in a pushing system")
|
||||
delay(200)
|
||||
return true // Simulate tray presence
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to execute sampling process with the needle
|
||||
*/
|
||||
private suspend fun executeSampling() {
|
||||
needleDevice.setMode(NeedleDevice.Mode.SAMPLING)
|
||||
needleDevice.performSampling()
|
||||
needleDevice.setMode(NeedleDevice.Mode.WASHING)
|
||||
needleDevice.performWashing(2)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createTestContext() = Context("test")
|
||||
|
||||
@Test
|
||||
fun `test StepperMotorDevice position setting`() = runTest {
|
||||
val context = createTestContext()
|
||||
val motor = StepperMotorDevice(context, Meta { "maxPosition" put 500 })
|
||||
|
||||
motor.setPosition(200)
|
||||
assertEquals(200, motor.getPosition(), "Position should be set correctly")
|
||||
|
||||
motor.setPosition(0)
|
||||
assertEquals(0, motor.getPosition(), "Position should be reset to 0")
|
||||
|
||||
motor.setPosition(500)
|
||||
assertEquals(500, motor.getPosition(), "Position should be set to max value")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test StepperMotorDevice invalid position`() = runTest {
|
||||
val context = createTestContext()
|
||||
val motor = StepperMotorDevice(context, Meta { "maxPosition" put 100 })
|
||||
|
||||
motor.setPosition(200) //Should be outside the range, so not changed
|
||||
assertEquals(0, motor.getPosition(), "Position should not be set for invalid value")
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun `test ValveDevice state toggling`() = runTest {
|
||||
val context = createTestContext()
|
||||
val valve = ValveDevice(context)
|
||||
|
||||
assertFalse(valve.getState(), "Initial state should be closed")
|
||||
|
||||
valve.setState(true)
|
||||
assertTrue(valve.getState(), "State should be set to open")
|
||||
|
||||
valve.setState(false)
|
||||
assertFalse(valve.getState(), "State should be set to closed")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test ValveDevice click operation`() = runTest {
|
||||
val context = createTestContext()
|
||||
val valve = ValveDevice(context)
|
||||
|
||||
assertFalse(valve.getState(), "Initial state should be closed")
|
||||
|
||||
valve.click()
|
||||
assertFalse(valve.getState(), "Valve should be closed after click")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test PressureChamberDevice pressure setting`() = runTest {
|
||||
val context = createTestContext()
|
||||
val chamber = PressureChamberDevice(context)
|
||||
|
||||
chamber.setPressure(1.5)
|
||||
assertEquals(1.5, chamber.getPressure(), "Pressure should be set correctly")
|
||||
|
||||
chamber.setPressure(0.0)
|
||||
assertEquals(0.0, chamber.getPressure(), "Pressure should be set to 0")
|
||||
|
||||
chamber.setPressure(-1.0)
|
||||
assertEquals(-1.0, chamber.getPressure(), "Pressure should be set to negative value")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test SyringePumpDevice volume setting`() = runTest {
|
||||
val context = createTestContext()
|
||||
val pump = SyringePumpDevice(context, Meta { "maxVolume" put 10.0 })
|
||||
|
||||
pump.setVolume(3.5)
|
||||
assertEquals(3.5, pump.getVolume(), "Volume should be set correctly")
|
||||
|
||||
pump.setVolume(0.0)
|
||||
assertEquals(0.0, pump.getVolume(), "Volume should be reset to 0")
|
||||
|
||||
pump.setVolume(10.0)
|
||||
assertEquals(10.0, pump.getVolume(), "Volume should be set to max value")
|
||||
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun `test SyringePumpDevice invalid volume`() = runTest {
|
||||
val context = createTestContext()
|
||||
val pump = SyringePumpDevice(context, Meta { "maxVolume" put 5.0 })
|
||||
|
||||
pump.setVolume(10.0)
|
||||
assertEquals(0.0, pump.getVolume(), "Pump volume should not be set for invalid value")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test ReagentSensorDevice checkReagent returns true`() = runTest {
|
||||
val context = createTestContext()
|
||||
val sensor = ReagentSensorDevice(context)
|
||||
|
||||
assertTrue(sensor.checkReagent(), "Reagent sensor should report presence by default")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test NeedleDevice position and mode setting`() = runTest {
|
||||
val context = createTestContext()
|
||||
val needle = NeedleDevice(context)
|
||||
|
||||
//Test setting mode
|
||||
needle.setMode(NeedleDevice.Mode.SAMPLING)
|
||||
assertEquals(NeedleDevice.Mode.SAMPLING, needle.getMode(), "Mode should be set to SAMPLING")
|
||||
|
||||
needle.setMode(NeedleDevice.Mode.WASHING)
|
||||
assertEquals(NeedleDevice.Mode.WASHING, needle.getMode(), "Mode should be set to WASHING")
|
||||
|
||||
//Test setting position
|
||||
needle.setPosition(50.0)
|
||||
assertEquals(50.0, needle.getPosition(), "Position should be set correctly")
|
||||
|
||||
needle.setPosition(0.0)
|
||||
assertEquals(0.0, needle.getPosition(), "Position should be set to 0")
|
||||
|
||||
needle.setPosition(100.0)
|
||||
assertEquals(100.0, needle.getPosition(), "Position should be set to max")
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun `test NeedleDevice invalid position`() = runTest {
|
||||
val context = createTestContext()
|
||||
val needle = NeedleDevice(context)
|
||||
|
||||
needle.setPosition(200.0)
|
||||
assertEquals(0.0, needle.getPosition(), "Needle position should not be set for invalid value")
|
||||
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun `test ShakerDevice shaking`() = runTest {
|
||||
val context = createTestContext()
|
||||
val shaker = ShakerDevice(context)
|
||||
|
||||
// Access properties to initialize motors and test shaking
|
||||
val verticalMotor = shaker.verticalMotor
|
||||
val horizontalMotor = shaker.horizontalMotor
|
||||
|
||||
shaker.shake(2)
|
||||
val verticalMotorPosition = verticalMotor.getPosition()
|
||||
val horizontalMotorPosition = horizontalMotor.getPosition()
|
||||
|
||||
assertEquals(2,verticalMotorPosition, "Vertical motor position should be set to 2 after shaking")
|
||||
assertEquals(1,horizontalMotorPosition, "Horizontal motor position should be set to 1 after shaking")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test TransportationSystem motors existence`() = runTest {
|
||||
val context = createTestContext()
|
||||
val transportationSystem = TransportationSystem(context)
|
||||
|
||||
// Access properties to initialize motors and test existence
|
||||
assertNotNull(transportationSystem.slideMotor, "slideMotor should exist")
|
||||
assertNotNull(transportationSystem.pushMotor, "pushMotor should exist")
|
||||
assertNotNull(transportationSystem.receiveMotor, "receiveMotor should exist")
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun `test AnalyzerDevice device access`() = runTest{
|
||||
val context = createTestContext()
|
||||
val analyzer = AnalyzerDevice(context)
|
||||
|
||||
// Access properties to initialize child devices and test existence
|
||||
assertNotNull(analyzer.transportationSystem, "Transportation system should exist")
|
||||
assertNotNull(analyzer.shakerDevice, "Shaker device should exist")
|
||||
assertNotNull(analyzer.needleDevice, "Needle device should exist")
|
||||
assertNotNull(analyzer.valveV20, "Valve V20 should exist")
|
||||
assertNotNull(analyzer.valveV17, "Valve V17 should exist")
|
||||
assertNotNull(analyzer.valveV18, "Valve V18 should exist")
|
||||
assertNotNull(analyzer.valveV35, "Valve V35 should exist")
|
||||
assertNotNull(analyzer.pressureChamberHigh, "High pressure chamber should exist")
|
||||
assertNotNull(analyzer.pressureChamberLow, "Low pressure chamber should exist")
|
||||
assertNotNull(analyzer.syringePumpMA100, "Syringe pump MA100 should exist")
|
||||
assertNotNull(analyzer.syringePumpMA25, "Syringe pump MA25 should exist")
|
||||
assertNotNull(analyzer.reagentSensor1, "Reagent sensor 1 should exist")
|
||||
assertNotNull(analyzer.reagentSensor2, "Reagent sensor 2 should exist")
|
||||
assertNotNull(analyzer.reagentSensor3, "Reagent sensor 3 should exist")
|
||||
}
|
||||
}
|
@ -1,9 +1,9 @@
|
||||
package space.kscience.controls.spec
|
||||
|
||||
import space.kscience.controls.api.ActionDescriptor
|
||||
import space.kscience.controls.api.PropertyDescriptor
|
||||
import space.kscience.controls.api.ActionDescriptorBuilder
|
||||
import space.kscience.controls.api.PropertyDescriptorBuilder
|
||||
import kotlin.reflect.KProperty
|
||||
|
||||
internal actual fun PropertyDescriptor.fromSpec(property: KProperty<*>){}
|
||||
internal actual fun PropertyDescriptorBuilder.fromSpec(property: KProperty<*>){}
|
||||
|
||||
internal actual fun ActionDescriptor.fromSpec(property: KProperty<*>){}
|
||||
internal actual fun ActionDescriptorBuilder.fromSpec(property: KProperty<*>){}
|
@ -1,18 +1,18 @@
|
||||
package space.kscience.controls.spec
|
||||
|
||||
import space.kscience.controls.api.ActionDescriptor
|
||||
import space.kscience.controls.api.PropertyDescriptor
|
||||
import space.kscience.controls.api.ActionDescriptorBuilder
|
||||
import space.kscience.controls.api.PropertyDescriptorBuilder
|
||||
import space.kscience.dataforge.descriptors.Description
|
||||
import kotlin.reflect.KProperty
|
||||
import kotlin.reflect.full.findAnnotation
|
||||
|
||||
internal actual fun PropertyDescriptor.fromSpec(property: KProperty<*>) {
|
||||
internal actual fun PropertyDescriptorBuilder.fromSpec(property: KProperty<*>) {
|
||||
property.findAnnotation<Description>()?.let {
|
||||
description = it.value
|
||||
}
|
||||
}
|
||||
|
||||
internal actual fun ActionDescriptor.fromSpec(property: KProperty<*>){
|
||||
internal actual fun ActionDescriptorBuilder.fromSpec(property: KProperty<*>){
|
||||
property.findAnnotation<Description>()?.let {
|
||||
description = it.value
|
||||
}
|
||||
|
@ -1,9 +1,9 @@
|
||||
package space.kscience.controls.spec
|
||||
|
||||
import space.kscience.controls.api.ActionDescriptor
|
||||
import space.kscience.controls.api.PropertyDescriptor
|
||||
import space.kscience.controls.api.ActionDescriptorBuilder
|
||||
import space.kscience.controls.api.PropertyDescriptorBuilder
|
||||
import kotlin.reflect.KProperty
|
||||
|
||||
internal actual fun PropertyDescriptor.fromSpec(property: KProperty<*>) {}
|
||||
internal actual fun PropertyDescriptorBuilder.fromSpec(property: KProperty<*>) {}
|
||||
|
||||
internal actual fun ActionDescriptor.fromSpec(property: KProperty<*>){}
|
||||
internal actual fun ActionDescriptorBuilder.fromSpec(property: KProperty<*>){}
|
@ -1,9 +1,9 @@
|
||||
package space.kscience.controls.spec
|
||||
|
||||
import space.kscience.controls.api.ActionDescriptor
|
||||
import space.kscience.controls.api.PropertyDescriptor
|
||||
import space.kscience.controls.api.ActionDescriptorBuilder
|
||||
import space.kscience.controls.api.PropertyDescriptorBuilder
|
||||
import kotlin.reflect.KProperty
|
||||
|
||||
internal actual fun PropertyDescriptor.fromSpec(property: KProperty<*>){}
|
||||
internal actual fun PropertyDescriptorBuilder.fromSpec(property: KProperty<*>){}
|
||||
|
||||
internal actual fun ActionDescriptor.fromSpec(property: KProperty<*>){}
|
||||
internal actual fun ActionDescriptorBuilder.fromSpec(property: KProperty<*>){}
|
@ -2,7 +2,6 @@ package space.kscience.controls.demo
|
||||
|
||||
import kotlinx.coroutines.launch
|
||||
import space.kscience.controls.api.Device
|
||||
import space.kscience.controls.api.metaDescriptor
|
||||
import space.kscience.controls.spec.*
|
||||
import space.kscience.dataforge.context.Context
|
||||
import space.kscience.dataforge.context.Factory
|
||||
|
@ -12,6 +12,7 @@ import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withTimeout
|
||||
import space.kscience.controls.api.DeviceHub
|
||||
import space.kscience.controls.api.PropertyDescriptor
|
||||
import space.kscience.controls.api.PropertyDescriptorBuilder
|
||||
import space.kscience.controls.misc.asMeta
|
||||
import space.kscience.controls.misc.duration
|
||||
import space.kscience.controls.ports.AsynchronousPort
|
||||
@ -235,7 +236,7 @@ class PiMotionMasterDevice(
|
||||
|
||||
private fun axisBooleanProperty(
|
||||
command: String,
|
||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||
descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {},
|
||||
) = mutableBooleanProperty(
|
||||
read = {
|
||||
readAxisBoolean("$command?")
|
||||
@ -248,7 +249,7 @@ class PiMotionMasterDevice(
|
||||
|
||||
private fun axisNumberProperty(
|
||||
command: String,
|
||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||
descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {},
|
||||
) = mutableDoubleProperty(
|
||||
read = {
|
||||
mm.requestAndParse("$command?", axisId)[axisId]?.toDoubleOrNull()
|
||||
|
Loading…
Reference in New Issue
Block a user