diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/ConstructorElement.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/ConstructorElement.kt index 8073689..4d2f476 100644 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/ConstructorElement.kt +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/ConstructorElement.kt @@ -32,7 +32,7 @@ public class StateConstructorElement( public val state: DeviceState, ) : ConstructorElement -public class ConnectionConstrucorElement( +public class ConnectionConstructorElement( public val reads: Collection>, public val writes: Collection>, ) : ConstructorElement @@ -58,7 +58,7 @@ public interface StateContainer : ContextAware, CoroutineScope { reads: Collection> = 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 DeviceState.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 StateContainer.combineState( * On resulting [Job] cancel the binding is unregistered */ public fun StateContainer.bind(sourceState: DeviceState, targetState: MutableDeviceState): 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 StateContainer.transformTo( targetState: MutableDeviceState, 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 StateContainer.combineTo( targetState: MutableDeviceState, 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 StateContainer.combineTo( targetState: MutableDeviceState, noinline transformation: suspend (Array) -> 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 diff --git a/controls-core/src/commonMain/kotlin/space/kscience/controls/api/descriptors.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/api/descriptors.kt index f3ffa93..bda25bb 100644 --- a/controls-core/src/commonMain/kotlin/space/kscience/controls/api/descriptors.kt +++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/api/descriptors.kt @@ -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() diff --git a/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/CompositeControlComponents.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/CompositeControlComponents.kt new file mode 100644 index 0000000..259238a --- /dev/null +++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/CompositeControlComponents.kt @@ -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 > getSpec(name: Name): CompositeControlComponentSpec? +} + +public class ComponentRegistryManager : AbstractPlugin(), ComponentRegistry { + private val specs = mutableMapOf>() + + override val tag: PluginTag = Companion.tag + + override fun > getSpec(name: Name): CompositeControlComponentSpec? { + try { + return specs[name] as? CompositeControlComponentSpec + } 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 { + 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() : CompositeDeviceSpec { + + private val _properties = hashMapOf>( + DeviceMetaPropertySpec.name to DeviceMetaPropertySpec + ) + + override val properties: Map> get() = _properties + + private val _actions = hashMapOf>() + + override val actions: Map> get() = _actions + + private val _childSpecs = mutableMapOf>() + + override val childSpecs: Map> get() = _childSpecs + + public fun , CD: ConfigurableCompositeControlComponent> childSpec( + deviceName: String? = null, + specName: Name? = null, + metaBuilder: (MutableMeta.() -> Unit)? = null, + configBuilder: DeviceLifecycleConfigBuilder.() -> Unit = {}, + ): PropertyDelegateProvider, ReadOnlyProperty, CompositeControlComponentSpec>> = + 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(childSpecName) ?: error("Spec with name '$specName' is not found") + val childComponentConfig = object : ChildComponentConfig{ + override val spec: CompositeControlComponentSpec = 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 > registerProperty(deviceProperty: P): P { + _properties[deviceProperty.name] = deviceProperty + return deviceProperty + } + + override fun registerAction(deviceAction: DeviceActionSpec): DeviceActionSpec { + _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 property( + converter: MetaConverter, + descriptorBuilder: PropertyDescriptorBuilder.() -> Unit, + name: String?, + read: suspend D.(propertyName: String) -> T?, + ): PropertyDelegateProvider, ReadOnlyProperty, DevicePropertySpec>> = + PropertyDelegateProvider { _: CompositeControlComponentSpec, property -> + val propertyName = name ?: property.name + val descriptor = createPropertyDescriptorInternal( + propertyName = propertyName, + converter = converter, + mutable = false, + property = property, + descriptorBuilder = descriptorBuilder + ) + val deviceProperty = registerProperty(object : DevicePropertySpec { + 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 mutableProperty( + converter: MetaConverter, + descriptorBuilder: PropertyDescriptorBuilder.() -> Unit, + name: String?, + read: suspend D.(propertyName: String) -> T?, + write: suspend D.(propertyName: String, value: T) -> Unit + ): PropertyDelegateProvider, ReadOnlyProperty, MutableDevicePropertySpec>> = + PropertyDelegateProvider { _: CompositeControlComponentSpec, property -> + val propertyName = name ?: property.name + val descriptor = createPropertyDescriptorInternal( + propertyName = propertyName, + converter = converter, + mutable = true, + property = property, + descriptorBuilder = descriptorBuilder + ) + val deviceProperty = registerProperty(object : MutableDevicePropertySpec { + 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 action( + inputConverter: MetaConverter, + outputConverter: MetaConverter, + descriptorBuilder: ActionDescriptorBuilder.() -> Unit, + name: String?, + execute: suspend D.(I) -> O, + ): PropertyDelegateProvider, ReadOnlyProperty, DeviceActionSpec>> = + PropertyDelegateProvider { _: CompositeControlComponentSpec, property -> + val actionName = name ?: property.name + val descriptor = createActionDescriptor( + actionName = actionName, + inputConverter = inputConverter, + outputConverter = outputConverter, + property = property, + descriptorBuilder = descriptorBuilder + ) + val deviceAction = registerAction(object : DeviceActionSpec { + 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 CompositeControlComponentSpec.unitAction( + descriptorBuilder: ActionDescriptorBuilder.() -> Unit = {}, + name: String? = null, + execute: suspend D.() -> Unit, +): PropertyDelegateProvider, ReadOnlyProperty, DeviceActionSpec>> = + action( + MetaConverter.unit, + MetaConverter.unit, + descriptorBuilder, + name + ) { + execute() + } + + +public fun CompositeControlComponentSpec.metaAction( + descriptorBuilder: ActionDescriptorBuilder.() -> Unit = {}, + name: String? = null, + execute: suspend D.(Meta) -> Meta, +): PropertyDelegateProvider, ReadOnlyProperty, DeviceActionSpec>> = + action( + MetaConverter.meta, + MetaConverter.meta, + descriptorBuilder, + name + ) { + execute(it) + } + +/** + * Basic interface for device description + */ +public interface CompositeDeviceSpec { + public val properties: Map> + + public val actions: Map> + + public val childSpecs: Map> + + /** + * Called on `start()` + */ + public suspend fun D.onOpen() + + /** + * Called on `stop()` + */ + public suspend fun D.onClose() + + /** + * Registers a property in the spec. + */ + public fun > registerProperty(deviceProperty: P): P + + /** + * Registers an action in the spec. + */ + public fun registerAction(deviceAction: DeviceActionSpec): DeviceActionSpec + + 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 property( + converter: MetaConverter, + descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {}, + name: String? = null, + read: suspend D.(propertyName: String) -> T?, + ): PropertyDelegateProvider, ReadOnlyProperty, DevicePropertySpec>> + + public fun mutableProperty( + converter: MetaConverter, + descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {}, + name: String? = null, + read: suspend D.(propertyName: String) -> T?, + write: suspend D.(propertyName: String, value: T) -> Unit, + ): PropertyDelegateProvider, ReadOnlyProperty, MutableDevicePropertySpec>> + + public fun action( + inputConverter: MetaConverter, + outputConverter: MetaConverter, + descriptorBuilder: ActionDescriptorBuilder.() -> Unit = {}, + name: String? = null, + execute: suspend D.(I) -> O, + ): PropertyDelegateProvider, ReadOnlyProperty, DeviceActionSpec>> +} + +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 = mutableMapOf() + public val devices: Map get() = childrenJobs.mapValues { it.value.device } + + internal data class ChildJob( + val device: Device, + val job: Job, + val lifecycleMode: LifecycleMode, + val messageBus: MutableSharedFlow, + val meta: Meta? = null + ) + + /** + * A centralized bus for messages + */ + public abstract val messageBus: MutableSharedFlow + + /** + * A centralized bus for device change events + */ + public abstract val deviceChanges: MutableSharedFlow + + + /** + * 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( + 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 clearReplayCache(mutableSharedFlow: MutableSharedFlow){ + 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? = 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 = MutableSharedFlow( + replay = 1000, + onBufferOverflow = BufferOverflow.DROP_OLDEST + ) + override val deviceChanges: MutableSharedFlow = 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 + + /** + * A map of child devices + */ + public val devices: Map +} + +/** + * A base class for devices created from specification, using AbstractDeviceHubManager for children management + */ +public open class ConfigurableCompositeControlComponent( + public open val spec: CompositeControlComponentSpec, + context: Context, + meta: Meta = Meta.EMPTY, + config: DeviceLifecycleConfig = DeviceLifecycleConfig(), + private val hubManager: AbstractDeviceHubManager = DeviceHubManagerImpl(context, config.dispatcher ?: Dispatchers.Default), +) : DeviceBase(context, meta), CompositeControlComponent { + + override val properties: Map> + get() = spec.properties + + override val actions: Map> + get() = spec.actions + + final override val messageBus: MutableSharedFlow + get() = hubManager.messageBus + + public val deviceChanges: MutableSharedFlow + get() = hubManager.deviceChanges + + public val aggregatedMessageFlow: SharedFlow + 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().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 + get() = hubManager.devices + + /** + * Get child device from this hub by name + */ + public fun > getChildDevice(name: Name): ConfigurableCompositeControlComponent = + hubManager.devices[name] as? ConfigurableCompositeControlComponent? ?: error("Device $name not found") + + public fun > childDevice(name: Name? = null): + PropertyDelegateProvider, ReadOnlyProperty, 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? = hubManager.getChildMessageBus(name) + + /** + * Get device, using delegate method + */ + public inline operator fun get(name: Name): D? = devices[name] as? D + + /** + * Get device, using delegate method + */ + public inline operator fun get(name: String): D? = devices[name.asName()] as? D + +} + +public suspend fun WithLifeCycle.stopWithTimeout(timeout: Duration = Duration.INFINITE) { + withTimeoutOrNull(timeout) { + stop() + } +} + +public interface ChildComponentConfig{ + public val spec: CompositeControlComponentSpec + public val config: DeviceLifecycleConfig + public val meta: Meta? + public val name: Name +} diff --git a/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/CompositeControlComponentsDelegates.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/CompositeControlComponentsDelegates.kt new file mode 100644 index 0000000..b042163 --- /dev/null +++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/CompositeControlComponentsDelegates.kt @@ -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 > createEnumConverter(enumValues: Array): MetaConverter = object : MetaConverter { + 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 CompositeControlComponentSpec.property( + converter: MetaConverter, + descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {}, + name: String? = null, + read: suspend D.(propertyName: String) -> T?, +): PropertyDelegateProvider, ReadOnlyProperty, DevicePropertySpec>> { + return property(converter, descriptorBuilder, name, read) +} + +/** + * Mutable property that delegates reading and writing to a device [KMutableProperty1] + */ +public fun CompositeControlComponentSpec.mutableProperty( + converter: MetaConverter, + descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {}, + name: String? = null, + read: suspend D.(propertyName: String) -> T?, + write: suspend D.(propertyName: String, value: T) -> Unit, +): PropertyDelegateProvider, ReadOnlyProperty, MutableDevicePropertySpec>> { + return mutableProperty(converter, descriptorBuilder, name, read, write) +} + +/** + * Register a mutable logical property (without a corresponding physical state) for a device + */ +public fun > CompositeControlComponentSpec.logical( + converter: MetaConverter, + descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {}, + name: String? = null, +): PropertyDelegateProvider, ReadOnlyProperty, MutableDevicePropertySpec>> = + 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 CompositeControlComponentSpec.boolean( + descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {}, + name: String? = null, + read: suspend D.(propertyName: String) -> Boolean?, +): PropertyDelegateProvider, ReadOnlyProperty, DevicePropertySpec>> = + property(MetaConverter.boolean, descriptorBuilder, name, read) + +/** + * Creates a mutable boolean property for a device. + */ +public fun CompositeControlComponentSpec.booleanMutable( + descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {}, + name: String? = null, + read: suspend D.(propertyName: String) -> Boolean?, + write: suspend D.(propertyName: String, value: Boolean) -> Unit +): PropertyDelegateProvider, ReadOnlyProperty, MutableDevicePropertySpec>> = + mutableProperty(MetaConverter.boolean, descriptorBuilder, name, read, write) + +/** + * Creates a read-only number property for a device. + */ +public fun CompositeControlComponentSpec.number( + descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {}, + name: String? = null, + read: suspend D.(propertyName: String) -> Number?, +): PropertyDelegateProvider, ReadOnlyProperty, DevicePropertySpec>> = + property(MetaConverter.number, descriptorBuilder, name, read) + +/** + * Creates a mutable number property for a device. + */ +public fun CompositeControlComponentSpec.numberMutable( + descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {}, + name: String? = null, + read: suspend D.(propertyName: String) -> Number?, + write: suspend D.(propertyName: String, value: Number) -> Unit +): PropertyDelegateProvider, ReadOnlyProperty, MutableDevicePropertySpec>> = + mutableProperty(MetaConverter.number, descriptorBuilder, name, read, write) + + +/** + * Creates a read-only double property for a device. + */ +public fun CompositeControlComponentSpec.double( + descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {}, + name: String? = null, + read: suspend D.(propertyName: String) -> Double?, +): PropertyDelegateProvider, ReadOnlyProperty, DevicePropertySpec>> = + property(MetaConverter.double, descriptorBuilder, name, read) + +/** + * Creates a mutable double property for a device. + */ +public fun CompositeControlComponentSpec.doubleMutable( + descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {}, + name: String? = null, + read: suspend D.(propertyName: String) -> Double?, + write: suspend D.(propertyName: String, value: Double) -> Unit +): PropertyDelegateProvider, ReadOnlyProperty, MutableDevicePropertySpec>> = + mutableProperty(MetaConverter.double, descriptorBuilder, name, read, write) + +/** + * Creates a read-only string property for a device. + */ +public fun CompositeControlComponentSpec.string( + descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {}, + name: String? = null, + read: suspend D.(propertyName: String) -> String? +): PropertyDelegateProvider, ReadOnlyProperty, DevicePropertySpec>> = + property(MetaConverter.string, descriptorBuilder, name, read) + +/** + * Creates a mutable string property for a device. + */ +public fun CompositeControlComponentSpec.stringMutableProperty( + descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {}, + name: String? = null, + read: suspend D.(propertyName: String) -> String?, + write: suspend D.(propertyName: String, value: String) -> Unit +): PropertyDelegateProvider, ReadOnlyProperty, MutableDevicePropertySpec>> = + mutableProperty(MetaConverter.string, descriptorBuilder, name, read, write) + +/** + * Creates a read-only meta property for a device. + */ +public fun CompositeControlComponentSpec.meta( + descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {}, + name: String? = null, + read: suspend D.(propertyName: String) -> Meta?, +): PropertyDelegateProvider, ReadOnlyProperty, DevicePropertySpec>> = + property(MetaConverter.meta, descriptorBuilder, name, read) + +/** + * Creates a mutable meta property for a device. + */ +public fun CompositeControlComponentSpec.metaMutable( + descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {}, + name: String? = null, + read: suspend D.(propertyName: String) -> Meta?, + write: suspend D.(propertyName: String, value: Meta) -> Unit +): PropertyDelegateProvider, ReadOnlyProperty, MutableDevicePropertySpec>> = + mutableProperty(MetaConverter.meta, descriptorBuilder, name, read, write) + +/** + * Creates a read-only enum property for a device. + */ +public fun , D : Device> CompositeControlComponentSpec.enum( + enumValues: Array, + descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {}, + name: String? = null, + read: suspend D.(propertyName: String) -> E?, +): PropertyDelegateProvider, ReadOnlyProperty, DevicePropertySpec>> { + val converter = createEnumConverter(enumValues) + return property(converter, descriptorBuilder, name, read) +} + +/** + * Creates a mutable enum property for a device. + */ +public fun , D : Device> CompositeControlComponentSpec.enumMutable( + enumValues: Array, + descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {}, + name: String? = null, + read: suspend D.(propertyName: String) -> E?, + write: suspend D.(propertyName: String, value: E) -> Unit +): PropertyDelegateProvider, ReadOnlyProperty, MutableDevicePropertySpec>> { + val converter = createEnumConverter(enumValues) + return mutableProperty(converter, descriptorBuilder, name, read, write) +} + +/** + * Creates a read-only float property for a device. + */ +public fun CompositeControlComponentSpec.float( + descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {}, + name: String? = null, + read: suspend D.(propertyName: String) -> Float?, +): PropertyDelegateProvider, ReadOnlyProperty, DevicePropertySpec>> = + property(MetaConverter.float, descriptorBuilder, name, read) + +/** + * Creates a mutable float property for a device. + */ +public fun CompositeControlComponentSpec.floatMutable( + descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {}, + name: String? = null, + read: suspend D.(propertyName: String) -> Float?, + write: suspend D.(propertyName: String, value: Float) -> Unit +): PropertyDelegateProvider, ReadOnlyProperty, MutableDevicePropertySpec>> = + mutableProperty(MetaConverter.float, descriptorBuilder, name, read, write) + +/** + * Creates a read-only long property for a device. + */ +public fun CompositeControlComponentSpec.long( + descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {}, + name: String? = null, + read: suspend D.(propertyName: String) -> Long?, +): PropertyDelegateProvider, ReadOnlyProperty, DevicePropertySpec>> = + property(MetaConverter.long, descriptorBuilder, name, read) + +/** + * Creates a mutable long property for a device. + */ +public fun CompositeControlComponentSpec.longMutable( + descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {}, + name: String? = null, + read: suspend D.(propertyName: String) -> Long?, + write: suspend D.(propertyName: String, value: Long) -> Unit +): PropertyDelegateProvider, ReadOnlyProperty, MutableDevicePropertySpec>> = + mutableProperty(MetaConverter.long, descriptorBuilder, name, read, write) + +/** + * Creates a read-only int property for a device. + */ +public fun CompositeControlComponentSpec.int( + descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {}, + name: String? = null, + read: suspend D.(propertyName: String) -> Int?, +): PropertyDelegateProvider, ReadOnlyProperty, DevicePropertySpec>> = + property(MetaConverter.int, descriptorBuilder, name, read) + +/** + * Creates a mutable int property for a device. + */ +public fun CompositeControlComponentSpec.intMutable( + descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {}, + name: String? = null, + read: suspend D.(propertyName: String) -> Int?, + write: suspend D.(propertyName: String, value: Int) -> Unit +): PropertyDelegateProvider, ReadOnlyProperty, MutableDevicePropertySpec>> = + mutableProperty(MetaConverter.int, descriptorBuilder, name, read, write) + +/** + * Creates a read-only list property for a device. + */ +public fun CompositeControlComponentSpec.list( + converter: MetaConverter>, + descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {}, + name: String? = null, + read: suspend D.(propertyName: String) -> List?, +): PropertyDelegateProvider, ReadOnlyProperty, DevicePropertySpec>>> = + property(converter, descriptorBuilder, name, read) + +/** + * Creates a mutable list property for a device. + */ +public fun CompositeControlComponentSpec.listMutable( + converter: MetaConverter>, + descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {}, + name: String? = null, + read: suspend D.(propertyName: String) -> List?, + write: suspend D.(propertyName: String, value: List) -> Unit +): PropertyDelegateProvider, ReadOnlyProperty, MutableDevicePropertySpec>>> = + mutableProperty(converter, descriptorBuilder, name, read, write) + + +public fun CompositeControlComponentSpec.asyncActionProperty( + inputConverter: MetaConverter, + outputConverter: MetaConverter, + descriptorBuilder: ActionDescriptorBuilder.() -> Unit = {}, + name: String? = null, + execute: suspend D.(I) -> Deferred, +): PropertyDelegateProvider, ReadOnlyProperty, DeviceActionSpec>> = + action(inputConverter, outputConverter, descriptorBuilder, name) { input -> + execute(input).await() + } + +public fun CompositeControlComponentSpec.metaProperty( + descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {}, + name: String? = null, + read: suspend D.(propertyName: String) -> Meta?, +): PropertyDelegateProvider, ReadOnlyProperty, DevicePropertySpec>> = + property(MetaConverter.meta, descriptorBuilder, name, read) + + +public fun CompositeControlComponentSpec.mutableMetaProperty( + descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {}, + name: String? = null, + read: suspend D.(propertyName: String) -> Meta?, + write: suspend D.(propertyName: String, value: Meta) -> Unit +): PropertyDelegateProvider, ReadOnlyProperty, MutableDevicePropertySpec>> = + mutableProperty(MetaConverter.meta, descriptorBuilder, name, read, write) + +/** + * An action that takes no parameters and returns no values + */ +public fun CompositeControlComponentSpec.unitAction( + descriptorBuilder: ActionDescriptorBuilder.() -> Unit = {}, + name: String? = null, + execute: suspend D.() -> Unit, +): PropertyDelegateProvider, ReadOnlyProperty, DeviceActionSpec>> = + action( + MetaConverter.unit, + MetaConverter.unit, + descriptorBuilder, + name + ) { + execute() + } + +public fun CompositeControlComponentSpec.asyncAction( + inputConverter: MetaConverter, + outputConverter: MetaConverter, + descriptorBuilder: ActionDescriptorBuilder.() -> Unit = {}, + name: String? = null, + execute: suspend D.(I) -> Deferred, +): PropertyDelegateProvider, ReadOnlyProperty, DeviceActionSpec>> = + action( + inputConverter, + outputConverter, + descriptorBuilder, + name + ) { + execute(it).await() + } + +/** + * An action that takes [Meta] and returns [Meta]. No conversions are done + */ +public fun CompositeControlComponentSpec.metaAction( + descriptorBuilder: ActionDescriptorBuilder.() -> Unit = {}, + name: String? = null, + execute: suspend D.(Meta) -> Meta, +): PropertyDelegateProvider, ReadOnlyProperty, DeviceActionSpec>> = + 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}" } + } +} \ No newline at end of file diff --git a/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/DeviceSpec.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/DeviceSpec.kt index c6f8b0a..945fe38 100644 --- a/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/DeviceSpec.kt +++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/DeviceSpec.kt @@ -44,24 +44,24 @@ public abstract class DeviceSpec { public fun property( converter: MetaConverter, - descriptorBuilder: PropertyDescriptor.() -> Unit = {}, + descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {}, name: String? = null, read: suspend D.(propertyName: String) -> T?, ): PropertyDelegateProvider, ReadOnlyProperty, DevicePropertySpec>> = PropertyDelegateProvider { _: DeviceSpec, property -> val propertyName = name ?: property.name - val deviceProperty = object : DevicePropertySpec { - - 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 { + override val descriptor = descriptor override val converter: MetaConverter = converter override suspend fun read(device: D): T? = @@ -75,26 +75,25 @@ public abstract class DeviceSpec { public fun mutableProperty( converter: MetaConverter, - descriptorBuilder: PropertyDescriptor.() -> Unit = {}, + descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {}, name: String? = null, read: suspend D.(propertyName: String) -> T?, write: suspend D.(propertyName: String, value: T) -> Unit, ): PropertyDelegateProvider, ReadOnlyProperty, MutableDevicePropertySpec>> = PropertyDelegateProvider { _: DeviceSpec, property: KProperty<*> -> val propertyName = name ?: property.name - val deviceProperty = object : MutableDevicePropertySpec { - 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 { + override val descriptor = descriptor override val converter: MetaConverter = converter override suspend fun read(device: D): T? = @@ -119,30 +118,28 @@ public abstract class DeviceSpec { public fun action( inputConverter: MetaConverter, outputConverter: MetaConverter, - descriptorBuilder: ActionDescriptor.() -> Unit = {}, + descriptorBuilder: ActionDescriptorBuilder.() -> Unit = {}, name: String? = null, execute: suspend D.(I) -> O, ): PropertyDelegateProvider, ReadOnlyProperty, DeviceActionSpec>> = PropertyDelegateProvider { _: DeviceSpec, property: KProperty<*> -> val actionName = name ?: property.name - val deviceAction = object : DeviceActionSpec { - 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 { + override val descriptor: ActionDescriptor = descriptor override val inputConverter: MetaConverter = inputConverter override val outputConverter: MetaConverter = outputConverter @@ -162,7 +159,7 @@ public abstract class DeviceSpec { * An action that takes no parameters and returns no values */ public fun DeviceSpec.unitAction( - descriptorBuilder: ActionDescriptor.() -> Unit = {}, + descriptorBuilder: ActionDescriptorBuilder.() -> Unit = {}, name: String? = null, execute: suspend D.() -> Unit, ): PropertyDelegateProvider, ReadOnlyProperty, DeviceActionSpec>> = @@ -179,7 +176,7 @@ public fun DeviceSpec.unitAction( * An action that takes [Meta] and returns [Meta]. No conversions are done */ public fun DeviceSpec.metaAction( - descriptorBuilder: ActionDescriptor.() -> Unit = {}, + descriptorBuilder: ActionDescriptorBuilder.() -> Unit = {}, name: String? = null, execute: suspend D.(Meta) -> Meta, ): PropertyDelegateProvider, ReadOnlyProperty, DeviceActionSpec>> = diff --git a/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/fromSpec.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/fromSpec.kt index ad5862b..57a9b4a 100644 --- a/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/fromSpec.kt +++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/fromSpec.kt @@ -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<*>) \ No newline at end of file +internal expect fun ActionDescriptorBuilder.fromSpec(property: KProperty<*>) \ No newline at end of file diff --git a/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/propertySpecDelegates.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/propertySpecDelegates.kt index efb66a7..34440fc 100644 --- a/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/propertySpecDelegates.kt +++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/propertySpecDelegates.kt @@ -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 DeviceSpec.property( converter: MetaConverter, readOnlyProperty: KProperty1, - descriptorBuilder: PropertyDescriptor.() -> Unit = {}, + descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {}, ): PropertyDelegateProvider, ReadOnlyProperty, DevicePropertySpec>> = property( converter, descriptorBuilder, @@ -31,7 +30,7 @@ public fun DeviceSpec.property( public fun DeviceSpec.mutableProperty( converter: MetaConverter, readWriteProperty: KMutableProperty1, - descriptorBuilder: PropertyDescriptor.() -> Unit = {}, + descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {}, ): PropertyDelegateProvider, ReadOnlyProperty, MutableDevicePropertySpec>> = mutableProperty( converter, @@ -49,7 +48,7 @@ public fun DeviceSpec.mutableProperty( */ public fun > DeviceSpec.property( converter: MetaConverter, - descriptorBuilder: PropertyDescriptor.() -> Unit = {}, + descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {}, name: String? = null, ): PropertyDelegateProvider, ReadOnlyProperty, DevicePropertySpec>> = property( @@ -60,7 +59,7 @@ public fun > DeviceSpec.property( ) public fun DeviceSpec.booleanProperty( - descriptorBuilder: PropertyDescriptor.() -> Unit = {}, + descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {}, name: String? = null, read: suspend D.(propertyName: String) -> Boolean? ): PropertyDelegateProvider, ReadOnlyProperty, DevicePropertySpec>> = property( @@ -76,8 +75,8 @@ public fun DeviceSpec.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 DeviceSpec.numberProperty( - descriptorBuilder: PropertyDescriptor.() -> Unit = {}, + descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {}, name: String? = null, read: suspend D.(propertyName: String) -> Number? ): PropertyDelegateProvider, ReadOnlyProperty, DevicePropertySpec>> = property( @@ -96,7 +95,7 @@ public fun DeviceSpec.numberProperty( ) public fun DeviceSpec.doubleProperty( - descriptorBuilder: PropertyDescriptor.() -> Unit = {}, + descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {}, name: String? = null, read: suspend D.(propertyName: String) -> Double? ): PropertyDelegateProvider, ReadOnlyProperty, DevicePropertySpec>> = property( @@ -107,7 +106,7 @@ public fun DeviceSpec.doubleProperty( ) public fun DeviceSpec.stringProperty( - descriptorBuilder: PropertyDescriptor.() -> Unit = {}, + descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {}, name: String? = null, read: suspend D.(propertyName: String) -> String? ): PropertyDelegateProvider, ReadOnlyProperty, DevicePropertySpec>> = property( @@ -123,7 +122,7 @@ public fun DeviceSpec.stringProperty( ) public fun DeviceSpec.metaProperty( - descriptorBuilder: PropertyDescriptor.() -> Unit = {}, + descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {}, name: String? = null, read: suspend D.(propertyName: String) -> Meta? ): PropertyDelegateProvider, ReadOnlyProperty, DevicePropertySpec>> = property( @@ -147,7 +146,7 @@ public fun DeviceSpec.metaProperty( */ public fun > DeviceSpec.mutableProperty( converter: MetaConverter, - descriptorBuilder: PropertyDescriptor.() -> Unit = {}, + descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {}, name: String? = null, ): PropertyDelegateProvider, ReadOnlyProperty, MutableDevicePropertySpec>> = mutableProperty( @@ -159,7 +158,7 @@ public fun > DeviceSpec.mutableProperty( ) public fun DeviceSpec.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 DeviceSpec.mutableBooleanProperty( public fun DeviceSpec.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 DeviceSpec.mutableNumberProperty( mutableProperty(MetaConverter.number, numberDescriptor(descriptorBuilder), name, read, write) public fun DeviceSpec.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 DeviceSpec.mutableDoubleProperty( mutableProperty(MetaConverter.double, numberDescriptor(descriptorBuilder), name, read, write) public fun DeviceSpec.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 DeviceSpec.mutableStringProperty( mutableProperty(MetaConverter.string, descriptorBuilder, name, read, write) public fun DeviceSpec.mutableMetaProperty( - descriptorBuilder: PropertyDescriptor.() -> Unit = {}, + descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {}, name: String? = null, read: suspend D.(propertyName: String) -> Meta, write: suspend D.(propertyName: String, value: Meta) -> Unit diff --git a/controls-core/src/commonTest/kotlin/space/kscience/controls/spec/CompositeControlTest.kt b/controls-core/src/commonTest/kotlin/space/kscience/controls/spec/CompositeControlTest.kt new file mode 100644 index 0000000..4966449 --- /dev/null +++ b/controls-core/src/commonTest/kotlin/space/kscience/controls/spec/CompositeControlTest.kt @@ -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() { + 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() { + public val state by booleanMutable( + read = { getState() }, + write = { _, value -> setState(value) } + ) + } + + public object PressureChamberSpec : CompositeControlComponentSpec() { + public val pressure by doubleMutable( + read = { getPressure() }, + write = { _, value -> setPressure(value) } + ) + } + + public object SyringePumpSpec : CompositeControlComponentSpec() { + public val volume by doubleMutable( + read = { getVolume() }, + write = { _, value -> setVolume(value) } + ) + } + + public object ReagentSensorSpec : CompositeControlComponentSpec() { + public val isPresent by boolean( + read = { checkReagent() } + ) + } + + public object NeedleSpec : CompositeControlComponentSpec() { + 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() { + public val verticalMotor by childSpec() + public val horizontalMotor by childSpec() + } + + public object TransportationSystemSpec : CompositeControlComponentSpec() { + public val slideMotor by childSpec() + + public val pushMotor by childSpec() + + public val receiveMotor by childSpec() + } + + + public object AnalyzerSpec : CompositeControlComponentSpec() { + public val transportationSystem by childSpec() + public val shakerDevice by childSpec() + public val needleDevice by childSpec() + + + public val valveV20 by childSpec() + public val valveV17 by childSpec() + public val valveV18 by childSpec() + public val valveV35 by childSpec() + + + public val pressureChamberHigh by childSpec() + public val pressureChamberLow by childSpec() + + public val syringePumpMA100 by childSpec() + public val syringePumpMA25 by childSpec() + + public val reagentSensor1 by childSpec() + public val reagentSensor2 by childSpec() + public val reagentSensor3 by childSpec() + } + +// ---------------------- Device Implementations ---------------------------------- + + // Implementation of Stepper Motor Device + public class StepperMotorDevice( + context: Context, + meta: Meta = Meta.EMPTY + ) : ConfigurableCompositeControlComponent(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(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(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(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(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(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(ShakerSpec, context, meta) { + + /** + * Get vertical stepper motor + */ + public val verticalMotor by childDevice() + + /** + * Get horizontal stepper motor + */ + public val horizontalMotor by childDevice() + + /** + * 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(TransportationSystemSpec, context, meta) { + + /** + * Get slide stepper motor + */ + public val slideMotor by childDevice() + + + /** + * Get push stepper motor + */ + public val pushMotor by childDevice() + + /** + * Get receive stepper motor + */ + public val receiveMotor by childDevice() + + } + + // Implementation of Analyzer Device + public class AnalyzerDevice( + context: Context, + meta: Meta = Meta.EMPTY + ) : ConfigurableCompositeControlComponent(AnalyzerSpec, context, meta) { + + /** + * Get transportation system + */ + public val transportationSystem by childDevice() + + /** + * Get shaker device + */ + public val shakerDevice by childDevice() + + /** + * Get needle device + */ + public val needleDevice by childDevice() + + /** + * Get valve V20 + */ + public val valveV20 by childDevice() + + /** + * Get valve V17 + */ + public val valveV17 by childDevice() + + /** + * Get valve V18 + */ + public val valveV18 by childDevice() + + /** + * Get valve V35 + */ + public val valveV35 by childDevice() + + /** + * Get high pressure chamber + */ + public val pressureChamberHigh by childDevice() + + /** + * Get low pressure chamber + */ + public val pressureChamberLow by childDevice() + + /** + * Get syringe pump MA100 + */ + public val syringePumpMA100 by childDevice() + + /** + * Get syringe pump MA25 + */ + public val syringePumpMA25 by childDevice() + + /** + * Get reagent sensor 1 + */ + public val reagentSensor1 by childDevice() + + /** + * Get reagent sensor 2 + */ + public val reagentSensor2 by childDevice() + + /** + * Get reagent sensor 3 + */ + public val reagentSensor3 by childDevice() + + /** + * 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") + } +} \ No newline at end of file diff --git a/controls-core/src/jsMain/kotlin/space/kscience/controls/spec/fromSpec.js.kt b/controls-core/src/jsMain/kotlin/space/kscience/controls/spec/fromSpec.js.kt index cd77248..bf44a4e 100644 --- a/controls-core/src/jsMain/kotlin/space/kscience/controls/spec/fromSpec.js.kt +++ b/controls-core/src/jsMain/kotlin/space/kscience/controls/spec/fromSpec.js.kt @@ -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<*>){} \ No newline at end of file +internal actual fun ActionDescriptorBuilder.fromSpec(property: KProperty<*>){} \ No newline at end of file diff --git a/controls-core/src/jvmMain/kotlin/space/kscience/controls/spec/fromSpec.jvm.kt b/controls-core/src/jvmMain/kotlin/space/kscience/controls/spec/fromSpec.jvm.kt index cb64fc3..91406f2 100644 --- a/controls-core/src/jvmMain/kotlin/space/kscience/controls/spec/fromSpec.jvm.kt +++ b/controls-core/src/jvmMain/kotlin/space/kscience/controls/spec/fromSpec.jvm.kt @@ -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()?.let { description = it.value } } -internal actual fun ActionDescriptor.fromSpec(property: KProperty<*>){ +internal actual fun ActionDescriptorBuilder.fromSpec(property: KProperty<*>){ property.findAnnotation()?.let { description = it.value } diff --git a/controls-core/src/nativeMain/kotlin/space/kscience/controls/spec/fromSpec.native.kt b/controls-core/src/nativeMain/kotlin/space/kscience/controls/spec/fromSpec.native.kt index 1d1ccc4..eeec224 100644 --- a/controls-core/src/nativeMain/kotlin/space/kscience/controls/spec/fromSpec.native.kt +++ b/controls-core/src/nativeMain/kotlin/space/kscience/controls/spec/fromSpec.native.kt @@ -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<*>){} \ No newline at end of file +internal actual fun ActionDescriptorBuilder.fromSpec(property: KProperty<*>){} \ No newline at end of file diff --git a/controls-core/src/wasmJsMain/kotlin/fromSpec.wasm.kt b/controls-core/src/wasmJsMain/kotlin/fromSpec.wasm.kt index cd77248..bf44a4e 100644 --- a/controls-core/src/wasmJsMain/kotlin/fromSpec.wasm.kt +++ b/controls-core/src/wasmJsMain/kotlin/fromSpec.wasm.kt @@ -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<*>){} \ No newline at end of file +internal actual fun ActionDescriptorBuilder.fromSpec(property: KProperty<*>){} \ No newline at end of file diff --git a/demo/all-things/src/main/kotlin/space/kscience/controls/demo/DemoDevice.kt b/demo/all-things/src/main/kotlin/space/kscience/controls/demo/DemoDevice.kt index 5cf1b28..1514eac 100644 --- a/demo/all-things/src/main/kotlin/space/kscience/controls/demo/DemoDevice.kt +++ b/demo/all-things/src/main/kotlin/space/kscience/controls/demo/DemoDevice.kt @@ -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 diff --git a/demo/motors/src/main/kotlin/ru/mipt/npm/devices/pimotionmaster/PiMotionMasterDevice.kt b/demo/motors/src/main/kotlin/ru/mipt/npm/devices/pimotionmaster/PiMotionMasterDevice.kt index 4324b9b..4108e7d 100644 --- a/demo/motors/src/main/kotlin/ru/mipt/npm/devices/pimotionmaster/PiMotionMasterDevice.kt +++ b/demo/motors/src/main/kotlin/ru/mipt/npm/devices/pimotionmaster/PiMotionMasterDevice.kt @@ -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()