WIP: Composition of specifications and devices #13

Draft
kolpakov.mm wants to merge 3 commits from dev-maxim into dev
14 changed files with 2190 additions and 104 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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<*>){}

View File

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

View File

@ -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<*>){}

View File

@ -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<*>){}

View File

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

View File

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