From 7e286ca1111f6eae68cf2eae118d8bf707c04cb3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9A=D0=BE=D0=BB=D0=BF=D0=B0=D0=BA=D0=BE=D0=B2=20=D0=9C?= =?UTF-8?q?=D0=B0=D0=BA=D1=81=D0=B8=D0=BC?= Date: Wed, 25 Dec 2024 18:11:01 +0300 Subject: [PATCH] Implemented external configuration support through ExternalConfigurationProvider and ExternalConfigApplier, and expanded error handling in AbstractDeviceHubManager with CUSTOM strategy support. Improved DeviceLifecycleConfig, added HealthChecker support for device health checks. Implemented hot-swappable device functionality. --- .../kscience/controls/api/WithLifeCycle.kt | 12 + .../spec/CompositeControlComponents.kt | 1336 ++++++++++------- .../CompositeControlComponentsDelegates.kt | 492 +++--- .../controls/spec/CompositeControlTest.kt | 75 +- 4 files changed, 1011 insertions(+), 904 deletions(-) diff --git a/controls-core/src/commonMain/kotlin/space/kscience/controls/api/WithLifeCycle.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/api/WithLifeCycle.kt index 631a66d..e3835ad 100644 --- a/controls-core/src/commonMain/kotlin/space/kscience/controls/api/WithLifeCycle.kt +++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/api/WithLifeCycle.kt @@ -8,6 +8,11 @@ import kotlinx.serialization.Serializable @Serializable public enum class LifecycleState { + /** + * The device is newly created and has not started yet. + */ + INITIAL, + /** * Device is initializing */ @@ -18,6 +23,11 @@ public enum class LifecycleState { */ STARTED, + /** + * The Device is stopping + */ + STOPPING, + /** * The Device is closed */ @@ -50,8 +60,10 @@ public interface WithLifeCycle { public fun WithLifeCycle.bindToDeviceLifecycle(device: Device){ device.onLifecycleEvent { when(it){ + LifecycleState.INITIAL -> {/*ignore*/} LifecycleState.STARTING -> start() LifecycleState.STARTED -> {/*ignore*/} + LifecycleState.STOPPING -> stop() LifecycleState.STOPPED -> stop() LifecycleState.ERROR -> stop() } 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 index 259238a..96842a0 100644 --- a/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/CompositeControlComponents.kt +++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/CompositeControlComponents.kt @@ -1,104 +1,117 @@ 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.* 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 kotlinx.coroutines.flow.* +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock 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 space.kscience.dataforge.context.* +import space.kscience.dataforge.meta.* +import space.kscience.dataforge.names.* 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 + * The device starts and stops together with the parent. */ LINKED, /** - * Device is started and stopped independently + * The device is started and stopped independently from the parent. */ INDEPENDENT, /** - * Device is created but starts only when explicitly requested + * The device is created but starts only upon an explicit request. */ LAZY } -public sealed class DeviceChangeEvent { +/** + * An interface for external configuration sources. + */ +public interface ExternalConfigurationProvider { + /** + * Load custom configuration from an external system. Return a [Meta] if any. + */ + public suspend fun loadExternalConfig(name: Name): Meta? +} + +/** + * A basic interface for device health checking. + */ +public fun interface HealthChecker { + /** + * Return true if the given [device] is healthy, false otherwise. + */ + public suspend fun isHealthy(device: Device): Boolean +} + +/** + * Defines child device error handling policy, extended with a CUSTOM option. + */ +public enum class ChildDeviceErrorHandler { + IGNORE, + RESTART, + STOP_PARENT, + PROPAGATE, + /** + * A custom user-defined strategy can be handled in [AbstractDeviceHubManager.onCustomError]. + */ + CUSTOM, +} + +/** + * Represents different possible device state changes or events. + */ +public sealed class DeviceStateEvent { 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 data class DeviceStarted(override val deviceName: Name) : DeviceStateEvent() + public data class DeviceStopped(override val deviceName: Name) : DeviceStateEvent() + public data class DeviceRemoved(override val deviceName: Name) : DeviceStateEvent() + public data class DeviceFailed(override val deviceName: Name, val error: Throwable) : DeviceStateEvent() + public data class DeviceDetached(override val deviceName: Name) : DeviceStateEvent() } -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() +/** + * A simple separate event describing child addition (optional usage). + */ +public data class DeviceAddedEvent(val deviceName: Name, val device: Device) +/** + * Holds lifecycle-related configuration for devices, with optional fields. + */ +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, + val healthChecker: HealthChecker? = null, +) { + 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 val Context.componentRegistry: ComponentRegistry? get() = plugins[ComponentRegistryManager] -public fun ContextBuilder.withSpecHub() { - plugin(ComponentRegistryManager) +/** + * An interface to attach external config to a [DeviceLifecycleConfigBuilder]. + */ +public fun interface ExternalConfigApplier { + public suspend fun applyConfig(builder: DeviceLifecycleConfigBuilder, deviceName: Name) } /** @@ -113,295 +126,102 @@ public class DeviceLifecycleConfigBuilder { public var coroutineScope: CoroutineScope? = null public var dispatcher: CoroutineDispatcher? = null public var onError: ChildDeviceErrorHandler = ChildDeviceErrorHandler.RESTART + public var healthChecker: HealthChecker? = null + + public suspend fun applyExternalConfig(deviceName: Name, externalApplier: ExternalConfigApplier) { + externalApplier.applyConfig(this, deviceName) + } public fun build(): DeviceLifecycleConfig = DeviceLifecycleConfig( - lifecycleMode, messageBuffer, startDelay, startTimeout, stopTimeout, coroutineScope, dispatcher, onError + lifecycleMode = lifecycleMode, + messageBuffer = messageBuffer, + startDelay = startDelay, + startTimeout = startTimeout, + stopTimeout = stopTimeout, + coroutineScope = coroutineScope, + dispatcher = dispatcher, + onError = onError, + healthChecker = healthChecker, ) } -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.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() {} - +/** + * Provides access to a registry of specifications. + */ +public interface ComponentRegistry : ContextAware { + public fun > getSpec(name: Name): CompositeControlComponentSpec? } -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() +/** + * A default plugin implementation of [ComponentRegistry]. + */ +public class ComponentRegistryManager : AbstractPlugin(), ComponentRegistry { + private val specs = mutableMapOf>() + + override val tag: PluginTag = Companion.tag + + @Suppress("UNCHECKED_CAST") + override fun > getSpec(name: Name): CompositeControlComponentSpec? { + return try { + specs[name] as? CompositeControlComponentSpec + } catch (e: ClassCastException) { + logger.error(e) { "Failed to get spec $name" } + null + } } - -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) + public fun registerSpec(spec: CompositeControlComponentSpec<*>, name: Name) { + specs[name] = 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) +} + /** - * Basic interface for device description + * Aggregates configuration for a child device. */ -public interface CompositeDeviceSpec { +public interface ChildComponentConfig> { + public val spec: CompositeControlComponentSpec + public val config: DeviceLifecycleConfig + public val meta: Meta? + public val name: Name +} + +/** + * Base specification for a composite device. + */ +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 > registerProperty(deviceProperty: P): P + public fun registerAction(deviceAction: DeviceActionSpec): DeviceActionSpec + public fun createPropertyDescriptorInternal( propertyName: String, converter: MetaConverter<*>, @@ -423,7 +243,7 @@ public interface CompositeDeviceSpec { descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {}, name: String? = null, read: suspend D.(propertyName: String) -> T?, - ): PropertyDelegateProvider, ReadOnlyProperty, DevicePropertySpec>> + ): PropertyDelegateProvider, ReadOnlyProperty, DevicePropertySpec>> public fun mutableProperty( converter: MetaConverter, @@ -431,7 +251,7 @@ public interface CompositeDeviceSpec { name: String? = null, read: suspend D.(propertyName: String) -> T?, write: suspend D.(propertyName: String, value: T) -> Unit, - ): PropertyDelegateProvider, ReadOnlyProperty, MutableDevicePropertySpec>> + ): PropertyDelegateProvider, ReadOnlyProperty, MutableDevicePropertySpec>> public fun action( inputConverter: MetaConverter, @@ -439,229 +259,623 @@ public interface CompositeDeviceSpec { 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 + ): PropertyDelegateProvider, ReadOnlyProperty, DeviceActionSpec>> } /** - * Base class for managing child devices. Manages lifecycle and message flow. + * Default implementation of [CompositeDeviceSpec]. + */ +@OptIn(InternalDeviceAPI::class) +public open class CompositeControlComponentSpec>( + public val registry: ComponentRegistry? = null +) : CompositeDeviceSpec { + private val propertyMap = hashMapOf>( + DeviceMetaPropertySpec.name to DeviceMetaPropertySpec + ) + private val actionMap = hashMapOf>() + private val childSpecMap = mutableMapOf>() + + override val properties: Map> + get() = propertyMap + + override val actions: Map> + get() = actionMap + + override val childSpecs: Map> + get() = childSpecMap + + override suspend fun D.onOpen() {} + override suspend fun D.onClose() {} + + override fun validate(device: D) { + properties.values.forEach { prop -> + check(prop.descriptor in device.propertyDescriptors) { + "Property ${prop.descriptor.name} not registered in ${device.id}" + } + } + actions.values.forEach { act -> + check(act.descriptor in device.actionDescriptors) { + "Action ${act.descriptor.name} not registered in ${device.id}" + } + } + } + + override fun > registerProperty(deviceProperty: P): P { + propertyMap[deviceProperty.name] = deviceProperty + return deviceProperty + } + + override fun registerAction(deviceAction: DeviceActionSpec): DeviceActionSpec { + actionMap[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 { conv -> metaDescriptor { from(conv) } } + fromSpec(property) + descriptorBuilder() + } + } + + override fun createActionDescriptor( + actionName: String, + inputConverter: MetaConverter<*>, + outputConverter: MetaConverter<*>, + property: KProperty<*>, + descriptorBuilder: ActionDescriptorBuilder.() -> Unit + ): ActionDescriptor { + return actionDescriptor(actionName) { + inputConverter.descriptor?.let { convIn -> inputMeta { from(convIn) } } + outputConverter.descriptor?.let { convOut -> outputMeta { from(convOut) } } + fromSpec(property) + descriptorBuilder() + } + } + + override fun property( + converter: MetaConverter, + descriptorBuilder: PropertyDescriptorBuilder.() -> Unit, + name: String?, + read: suspend D.(propertyName: String) -> T? + ): PropertyDelegateProvider, ReadOnlyProperty, DevicePropertySpec>> = + PropertyDelegateProvider { _, prop -> + val propertyName = name ?: prop.name + val descriptor = createPropertyDescriptorInternal( + propertyName, converter, mutable = false, property = prop, descriptorBuilder = descriptorBuilder + ) + val devProp = registerProperty(object : DevicePropertySpec { + override val descriptor: PropertyDescriptor = descriptor + override val converter: MetaConverter = converter + override suspend fun read(device: D): T? = + withContext(device.coroutineContext) { device.read(propertyName) } + }) + ReadOnlyProperty { _, _ -> devProp } + } + + 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 { _, prop -> + val propertyName = name ?: prop.name + val descriptor = createPropertyDescriptorInternal( + propertyName, converter, mutable = true, property = prop, descriptorBuilder = descriptorBuilder + ) + val devProp = registerProperty(object : MutableDevicePropertySpec { + override val descriptor: PropertyDescriptor = descriptor + override val converter: MetaConverter = converter + override suspend fun read(device: D): T? = + withContext(device.coroutineContext) { device.read(propertyName) } + + override suspend fun write(device: D, value: T) = + withContext(device.coroutineContext) { device.write(propertyName, value) } + }) + ReadOnlyProperty { _, _ -> devProp } + } + + public fun , CD : ConfigurableCompositeControlComponent> childSpec( + fallbackSpec: CDS, + specKeyInRegistry: Name? = null, + childDeviceName: Name? = null, + metaBuilder: (MutableMeta.() -> Unit)? = null, + configBuilder: DeviceLifecycleConfigBuilder.() -> Unit = {}, + ): PropertyDelegateProvider, ReadOnlyProperty, CompositeControlComponentSpec>> = + PropertyDelegateProvider { _, property -> + ReadOnlyProperty { _, _ -> + val registryKey = specKeyInRegistry ?: property.name.asName() + val childName = childDeviceName ?: property.name.asName() + val config = DeviceLifecycleConfigBuilder().apply(configBuilder).build() + val meta = metaBuilder?.let { Meta(it) } + val fromRegistry: CompositeControlComponentSpec? = + registry?.getSpec(registryKey) + + val foundSpec: CompositeControlComponentSpec = fromRegistry ?: fallbackSpec + + val mapKey = childName.toString() + check(childSpecMap[mapKey] == null) { + "Child spec with name '$mapKey' is already registered in $this." + } + + val childConfig = object : ChildComponentConfig { + override val spec: CompositeControlComponentSpec = foundSpec + override val config: DeviceLifecycleConfig = config + override val meta: Meta? = meta + override val name: Name = childName + } + childSpecMap[mapKey] = childConfig + childConfig.spec + } + } + + override fun action( + inputConverter: MetaConverter, + outputConverter: MetaConverter, + descriptorBuilder: ActionDescriptorBuilder.() -> Unit, + name: String?, + execute: suspend D.(I) -> O + ): PropertyDelegateProvider, ReadOnlyProperty, DeviceActionSpec>> = + PropertyDelegateProvider { _, prop -> + val actionName = name ?: prop.name + val descriptor = createActionDescriptor(actionName, inputConverter, outputConverter, prop, descriptorBuilder) + val devAction = registerAction(object : DeviceActionSpec { + override val descriptor: ActionDescriptor = descriptor + override val inputConverter: MetaConverter = inputConverter + override val outputConverter: MetaConverter = outputConverter + override suspend fun execute(device: D, input: I): O = + withContext(device.coroutineContext) { device.execute(input) } + }) + ReadOnlyProperty { _, _ -> devAction } + } +} + +/** + * Defines an action with Unit input and Unit output. + */ +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() + } + +/** + * Defines an action with Meta input and Meta output. + */ +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) + } + +/** + * An extended device manager that supports advanced lifecycle, errors, transactions, etc. */ 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 } + /** + * An optional parentJob to differentiate the parent's context from the child's context. + */ + protected val parentJob: Job = SupervisorJob() + private val childLock = Mutex() + + internal val childrenJobs: MutableMap = mutableMapOf() + + public val devices: Map + get() = childrenJobs.mapValues { it.value.device } + + public abstract val messageBus: MutableSharedFlow + public abstract val deviceChanges: MutableSharedFlow + + /** + * A data class describing a child device, its job, config, and dedicated messageBus. + */ internal data class ChildJob( val device: Device, val job: Job, - val lifecycleMode: LifecycleMode, + val config: DeviceLifecycleConfig, val messageBus: MutableSharedFlow, - val meta: Meta? = null - ) + val meta: Meta? = null, + /** + * If true, we keep the old messageBus on hotSwap. + */ + val reuseBus: Boolean = false + ) { + val lifecycleMode: LifecycleMode get() = config.lifecycleMode + } /** - * A centralized bus for messages + * Called when a child device error occurs. */ - public abstract val messageBus: MutableSharedFlow + protected open suspend fun onChildErrorCaught(ex: Throwable, childName: Name, config: DeviceLifecycleConfig) { + context.logger.error(ex) { "Error in child device $childName with policy ${config.onError}" } + } /** - * A centralized bus for device change events + * Called when STOP_PARENT policy is triggered. + * We cancel the [parentJob] instead of the current context to avoid self-cancellation in child coroutines. */ - public abstract val deviceChanges: MutableSharedFlow - + protected open suspend fun onParentStopRequested(ex: Throwable, childName: Name) { + context.logger.error(ex) { "Stopping parent due to error in child $childName" } + parentJob.cancelAndJoin() + } /** - * Launches a child device with a specific lifecycle mode and error handling. + * Called when a CUSTOM error policy is triggered. */ + protected open suspend fun onCustomError(ex: Throwable, childName: Name, config: DeviceLifecycleConfig) { + context.logger.error(ex) { "Custom error strategy for device $childName: override onCustomError." } + } + + /** + * Called if the device's start operation times out. + */ + protected open suspend fun onStartTimeout(deviceName: Name, config: DeviceLifecycleConfig) { + context.logger.error { "Timeout while starting $deviceName." } + throw RuntimeException("Timeout on start for $deviceName") + } + + /** + * Called if the device's stop operation times out. + */ + internal open suspend fun onStopTimeout(deviceName: Name, config: DeviceLifecycleConfig) { + context.logger.warn { "Timeout while stopping $deviceName. Consider overriding onStopTimeout." } + } + + /** + * Perform a health check if configured. + */ + internal open suspend fun checkHealth(child: ChildJob) { + val hc = child.config.healthChecker ?: return + if (!hc.isHealthy(child.device)) { + val ex = RuntimeException("Health check failed for device ${child.device.id}") + deviceChanges.emit(DeviceStateEvent.DeviceFailed(child.device.id.parseAsName(), ex)) + } + } + @OptIn(ExperimentalCoroutinesApi::class) - internal fun launchChild(name: Name, device: Device, config: DeviceLifecycleConfig, meta: Meta? = null): ChildJob { - val childMessageBus = MutableSharedFlow( + internal fun launchChild( + name: Name, + device: Device, + config: DeviceLifecycleConfig, + meta: Meta?, + /** + * If true, we reuse the old messageBus from previous device on hotSwap. + * Otherwise, we create a new bus. + */ + reuseBus: MutableSharedFlow? = null + ): ChildJob { + val childMessageBus = reuseBus ?: MutableSharedFlow( replay = config.messageBuffer, onBufferOverflow = BufferOverflow.DROP_OLDEST ) - val childScope = config.coroutineScope ?: context - val job = childScope.launch(dispatcher + CoroutineName("Child device $name")) { + val childScope = config.coroutineScope ?: CoroutineScope(parentJob + dispatcher) + + val childJob = childScope.launch(CoroutineName("Child device $name")) { try { + // Attempt to auto-start if not independent. if (config.lifecycleMode != LifecycleMode.INDEPENDENT) { - if (config.lifecycleMode == LifecycleMode.LINKED || device.lifecycleState == LifecycleState.STARTING){ + if (config.lifecycleMode == LifecycleMode.LINKED || + device.lifecycleState == LifecycleState.STARTING || + device.lifecycleState == LifecycleState.INITIAL + ) { delay(config.startDelay) - withTimeoutOrNull(config.startTimeout ?: Duration.INFINITE) { + val started = withTimeoutOrNull(config.startTimeout ?: Duration.INFINITE) { device.start() - } ?: error("Timeout on start for $name") + } + if (started == null) { + onStartTimeout(name, config) + } else { + deviceChanges.emit(DeviceStateEvent.DeviceStarted(name)) + checkHealth( + ChildJob(device, this.coroutineContext[Job]!!, config, childMessageBus, meta, reuseBus != null) + ) + } } } - - device.messageFlow.collect { message -> - childMessageBus.emit(message.changeSource { name.plus(it) }) - messageBus.emit(message.changeSource { name.plus(it) }) + // Collect all device messages + device.messageFlow.collect { msg -> + val wrapped = msg.changeSource { name.plus(it) } + childMessageBus.emit(wrapped) + messageBus.emit(wrapped) } } catch (ex: Exception) { - val errorMessage = DeviceMessage.error(ex, name) - messageBus.emit(errorMessage) + onChildErrorCaught(ex, name, config) + deviceChanges.emit(DeviceStateEvent.DeviceFailed(name, ex)) + messageBus.emit(DeviceMessage.error(ex, name)) when (config.onError) { - ChildDeviceErrorHandler.IGNORE -> context.logger.error(ex) { "Error in child device $name ignored" } + ChildDeviceErrorHandler.IGNORE -> {} 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 + // schedule restart + CoroutineScope(parentJob + dispatcher).launch { + restartDevice(name) + } } + ChildDeviceErrorHandler.STOP_PARENT -> onParentStopRequested(ex, name) + ChildDeviceErrorHandler.PROPAGATE -> throw ex + ChildDeviceErrorHandler.CUSTOM -> onCustomError(ex, name, config) } } 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() + // If ended, device is considered stopped. + if (!isActive) { + removeJobFromRegistry(name, device, childMessageBus) + deviceChanges.emit(DeviceStateEvent.DeviceStopped(name)) } } } - return ChildJob(device, job, config.lifecycleMode, childMessageBus, meta) + return ChildJob(device, childJob, config, childMessageBus, meta, reuseBus != null) } + /** + * Removes the child device from registry. + */ @OptIn(ExperimentalCoroutinesApi::class) - private fun clearReplayCache(mutableSharedFlow: MutableSharedFlow){ - val cached = mutableSharedFlow.replayCache - mutableSharedFlow.resetReplayCache() - cached.forEach { mutableSharedFlow.tryEmit(it) } + private suspend fun removeJobFromRegistry( + name: Name, + device: Device, + bus: MutableSharedFlow + ) { + childLock.withLock { + val current = childrenJobs[name] + if (current?.device == device) { + childrenJobs.remove(name) + } + } + bus.resetReplayCache() + deviceChanges.emit(DeviceStateEvent.DeviceDetached(name)) + messageBus.emit(DeviceLogMessage("Device $name physically removed.", sourceDevice = name)) + if (device is ConfigurableCompositeControlComponent<*>) { + device.onChildStop() + } } /** - * Add a device to the hub and manage its lifecycle according to its spec + * Adds a device asynchronously (does not wait for full start). */ - 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") + public suspend fun addDevice( + name: Name, + device: Device, + config: DeviceLifecycleConfig, + meta: Meta? = null + ) { + childLock.withLock { + val existing = childrenJobs[name] + if (existing != null && existing.device != device) { + removeDeviceUnlocked(name, waitStop = false) } - context.launch(device.coroutineContext) { existingDevice.stopWithTimeout(config.stopTimeout ?: Duration.INFINITE) } + val newChild = launchChild(name, device, config, meta) + childrenJobs[name] = newChild } - childrenJobs[name] = launchChild(name, device, config, meta) + deviceChanges.emit(DeviceStateEvent.DeviceStarted(name)) + messageBus.emit(DeviceLogMessage("Device $name added", sourceDevice = name)) + } - context.launch { - deviceChanges.emit(DeviceChangeEvent.Added(name, device)) - messageBus.emit(DeviceLogMessage("Device $name added", sourceDevice = name)) + /** + * Adds a device and joins the child job if it completes. + */ + public suspend fun addDeviceSync( + name: Name, + device: Device, + config: DeviceLifecycleConfig, + meta: Meta? = null + ) { + addDevice(name, device, config, meta) + childLock.withLock { + childrenJobs[name]?.job?.join() } } - 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 + /** + * Removes a device asynchronously (not waiting for full stop). + */ + public suspend fun removeDevice(name: Name) { + childLock.withLock { + removeDeviceUnlocked(name, waitStop = false) + } + } - else -> Duration.INFINITE + /** + * Restarts a device, preserving the same config & meta. + */ + public suspend fun restartDevice(name: Name) { + childLock.withLock { + val old = childrenJobs[name] ?: return + removeDeviceUnlocked(name, waitStop = true) + val newChild = launchChild(name, old.device, old.config, old.meta, if (old.reuseBus) old.messageBus else null) + childrenJobs[name] = newChild + messageBus.emit(DeviceLogMessage("Device $name restarted", sourceDevice = name)) + } + } + + /** + * Changes the child's lifecycle mode. + */ + public suspend fun changeLifecycleMode(name: Name, newMode: LifecycleMode) { + childLock.withLock { + val old = childrenJobs[name] ?: error("Device $name not found") + val newConfig = old.config.copy(lifecycleMode = newMode) + removeDeviceUnlocked(name, waitStop = true) + val newChild = launchChild(name, old.device, newConfig, old.meta, if (old.reuseBus) old.messageBus else null) + childrenJobs[name] = newChild + messageBus.emit(DeviceLogMessage("Device $name lifecycle changed to $newMode", sourceDevice = name)) + } + } + + /** + * A "hot swap" approach, optionally reusing the old bus to keep subscribers. + */ + public suspend fun hotSwapDevice( + name: Name, + newDevice: Device, + config: DeviceLifecycleConfig, + meta: Meta? = null, + reuseMessageBus: Boolean = false + ) { + childLock.withLock { + val old = childrenJobs[name] + val oldBus = if (reuseMessageBus) old?.messageBus else null + removeDeviceUnlocked(name, waitStop = true) + val newChild = launchChild(name, newDevice, config, meta, oldBus) + childrenJobs[name] = newChild + messageBus.emit(DeviceLogMessage("Device $name hot-swapped", sourceDevice = name)) + } + } + + /** + * Remove device, either waiting or not. Will attempt to stop within [stopTimeout]. + */ + private suspend fun removeDeviceUnlocked(name: Name, waitStop: Boolean) { + val child = childrenJobs[name] ?: return + childrenJobs.remove(name) + val timeout = child.config.stopTimeout ?: Duration.INFINITE + if (waitStop) { + child.job.cancelAndJoin() + val stopped = withTimeoutOrNull(timeout) { + child.device.stop() + } + if (stopped == null) onStopTimeout(name, child.config) + deviceChanges.emit(DeviceStateEvent.DeviceRemoved(name)) + messageBus.emit(DeviceLogMessage("Device $name removed (waitStop=true)", sourceDevice = name)) + } else { + // do not wait, just launch + CoroutineScope(parentJob + dispatcher).launch { + val stopped = withTimeoutOrNull(timeout) { + child.job.cancelAndJoin() + child.device.stop() } - withTimeoutOrNull(timeout) { - childJob.job.cancelAndJoin() - childJob.device.stop() - } ?: error("Timeout on stop for $name") + if (stopped == null) onStopTimeout(name, child.config) } - childrenJobs.remove(name) - context.launch { - messageBus.emit(DeviceLogMessage("Device $name removed", sourceDevice = name)) - deviceChanges.emit(DeviceChangeEvent.Removed(name)) + deviceChanges.emit(DeviceStateEvent.DeviceRemoved(name)) + messageBus.emit(DeviceLogMessage("Device $name removed (async)", sourceDevice = name)) + } + } + + public fun getChildMessageBus(name: Name): MutableSharedFlow? = childrenJobs[name]?.messageBus + + internal open fun onChildStop() {} + + /** + * Starts multiple devices in a transactional manner. + * If any fails, we stop all that started successfully, logging any rollback errors. + */ + public suspend fun startDevicesBatch(deviceNames: List): Boolean { + val startedSuccessfully = mutableListOf() + try { + for (dn in deviceNames) { + val job = childrenJobs[dn] ?: continue + if (job.device.lifecycleState == LifecycleState.INITIAL) { + job.device.start() + deviceChanges.emit(DeviceStateEvent.DeviceStarted(dn)) + startedSuccessfully += dn + } } + return true + } catch (_: Exception) { + // rollback + for (dn in startedSuccessfully) { + val job = childrenJobs[dn] ?: continue + try { + job.device.stop() + deviceChanges.emit(DeviceStateEvent.DeviceStopped(dn)) + } catch (rollbackEx: Exception) { + context.logger.error(rollbackEx) { "Failed to rollback stop for device $dn" } + } + } + return false } } /** - * Change lifecycle mode of a child device + * Stops multiple devices in a transactional manner. + * If any fails to stop, we attempt to restart those already stopped, logging any errors. */ - 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) + public suspend fun stopDevicesBatch(deviceNames: List): Boolean { + val stoppedSuccessfully = mutableListOf() + try { + for (dn in deviceNames) { + val job = childrenJobs[dn] ?: continue + if (job.device.lifecycleState == LifecycleState.STARTED) { + job.device.stop() + deviceChanges.emit(DeviceStateEvent.DeviceStopped(dn)) + stoppedSuccessfully += dn + } + } + return true + } catch (_: Exception) { + for (dn in stoppedSuccessfully) { + val job = childrenJobs[dn] ?: continue + try { + job.device.start() + deviceChanges.emit(DeviceStateEvent.DeviceStarted(dn)) + } catch (rollbackEx: Exception) { + context.logger.error(rollbackEx) { "Failed to rollback start for device $dn" } + } + } + return false } } /** - * Get local message bus for a child device + * A placeholder method to install distributed transport. */ - public fun getChildMessageBus(name: Name) : MutableSharedFlow? = childrenJobs[name]?.messageBus - - /** - * Method for explicit call when child device is stopped. - */ - internal open fun onChildStop(){ - + public open fun installDistributedTransport() { + context.logger.info { "installDistributedTransport: implement broker here." } } } - -private class DeviceHubManagerImpl(context: Context, dispatcher: CoroutineDispatcher = Dispatchers.Default) : AbstractDeviceHubManager(context, dispatcher){ +/** + * A default implementation of the device hub manager with extended event types. + * We store a [parentJob] for controlling all child coroutines from the parent side. + */ +private class DeviceHubManagerImpl(context: Context, dispatcher: CoroutineDispatcher) : AbstractDeviceHubManager(context, dispatcher) { override val messageBus: MutableSharedFlow = MutableSharedFlow( replay = 1000, onBufferOverflow = BufferOverflow.DROP_OLDEST ) - override val deviceChanges: MutableSharedFlow = MutableSharedFlow(replay = 1) + override val deviceChanges: MutableSharedFlow = MutableSharedFlow(replay = 1) } /** - * An interface for a device that contains other devices as children. + * A device that can host child devices. */ 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 + * A base device created from [spec] with an optional [registry]. */ -public open class ConfigurableCompositeControlComponent( +public open class ConfigurableCompositeControlComponent>( public open val spec: CompositeControlComponentSpec, context: Context, meta: Meta = Meta.EMPTY, config: DeviceLifecycleConfig = DeviceLifecycleConfig(), + registry: ComponentRegistry? = null, private val hubManager: AbstractDeviceHubManager = DeviceHubManagerImpl(context, config.dispatcher ?: Dispatchers.Default), ) : DeviceBase(context, meta), CompositeControlComponent { + protected val effectiveRegistry: ComponentRegistry? = registry ?: context.componentRegistry + override val properties: Map> get() = spec.properties @@ -671,65 +885,106 @@ public open class ConfigurableCompositeControlComponent( final override val messageBus: MutableSharedFlow get() = hubManager.messageBus - public val deviceChanges: MutableSharedFlow - get() = hubManager.deviceChanges + override fun toString(): String = "Device(id=$id, spec=$spec)" - public val aggregatedMessageFlow: SharedFlow - get() = hubManager.messageBus + override val devices: Map + get() = hubManager.devices + + private val childConfigs: List> = spec.childSpecs.values.toList() 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) - } - + // Register action execution logic for local device 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() + messageFlow + .filterIsInstance() + .filter { it.action == actionName } + .onEach { msg -> + val result = execute(actionName, msg.argument) + messageBus.emit( + ActionResultMessage( + action = actionName, + result = result, + requestId = msg.requestId, + sourceDevice = id.asName() + ) ) - ) - }.launchIn(this) + } + .launchIn(this) } } } + /** + * Creates all child devices and waits for them if needed. + */ + public suspend fun initChildren() { + for (childCfg in childConfigs) { + val childDevice = ConfigurableCompositeControlComponent( + spec = childCfg.spec, + registry = effectiveRegistry, + context = context, + meta = childCfg.meta ?: Meta.EMPTY, + config = childCfg.config + ) + hubManager.addDeviceSync(childCfg.name, childDevice, childCfg.config, childCfg.meta) + } + } + + /** + * A factory method that creates a device and immediately calls [initChildren]. + */ + public companion object { + public suspend fun > create( + spec: CompositeControlComponentSpec, + context: Context, + meta: Meta = Meta.EMPTY, + config: DeviceLifecycleConfig = DeviceLifecycleConfig(), + registry: ComponentRegistry? = null, + ): D { + @Suppress("UNCHECKED_CAST") + val device = ConfigurableCompositeControlComponent( + spec = spec, + context = context, + meta = meta, + config = config, + registry = registry + ) as D + device.initChildren() + return device + } + } + 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() + // Autostart children if not LAZY + hubManager.devices.values + .filter { it.lifecycleState != LifecycleState.STARTED && it.lifecycleState != LifecycleState.STARTING } + .forEach { child -> + val mode = hubManager.childrenJobs[child.id.parseAsName()]?.lifecycleMode + if (mode != LifecycleMode.LAZY) { + child.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() + // Stop children, respecting each child's stopTimeout + hubManager.devices.values.forEach { child -> + launch(child.coroutineContext) { + val stopResult = withTimeoutOrNull(getChildStopTimeout(child)) { + child.stop() + } + if (stopResult == null) { + // calls onStopTimeout if child config has it + val job = hubManager.childrenJobs[child.id.parseAsName()] + if (job != null) { + hubManager.onStopTimeout(child.id.parseAsName(), job.config) + } } } } @@ -738,72 +993,47 @@ public open class ConfigurableCompositeControlComponent( } } - override fun toString(): String = "Device(spec=$spec)" - - internal open fun onChildStop() { + private fun getChildStopTimeout(device: Device): Duration { + val job = hubManager.childrenJobs[device.id.parseAsName()] + return job?.config?.stopTimeout ?: Duration.INFINITE } /** - * Add existing device to this hub + * Called when a child device is stopped in [AbstractDeviceHubManager]. */ - private fun addDevice(name: Name = device.id.asName(), device: Device, config: DeviceLifecycleConfig = DeviceLifecycleConfig(), meta: Meta? = null) { - hubManager.addDevice(name, device, config, meta) + internal open fun onChildStop() {} + + @Suppress("UNCHECKED_CAST") + public fun > getChildDevice(name: Name): CD { + return (hubManager.devices[name] as? CD) + ?: error("Child device $name not found or type mismatch") } - /** - * Remove a child device from the hub by name. - */ - private fun removeDevice(name: Name) { - hubManager.removeDevice(name) - } + public fun getChildMessageBus(name: Name): SharedFlow? = + hubManager.getChildMessageBus(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") + public fun > childDevice(name: Name? = null): + PropertyDelegateProvider, ReadOnlyProperty, CD>> = + PropertyDelegateProvider { _, property -> + ReadOnlyProperty { _, _ -> + val devName = name ?: property.name.asName() + getChildDevice(devName) + } } - } - - /** - * 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 inline operator fun get(name: Name): Dev? = devices[name] as? Dev + public inline operator fun get(name: String): Dev? = this[name.asName()] } +/** + * Stops the device with a given [timeout]. + * Logs a warning if the timeout is reached. + */ public suspend fun WithLifeCycle.stopWithTimeout(timeout: Duration = Duration.INFINITE) { - withTimeoutOrNull(timeout) { + val result = withTimeoutOrNull(timeout) { stop() } -} - -public interface ChildComponentConfig{ - public val spec: CompositeControlComponentSpec - public val config: DeviceLifecycleConfig - public val meta: Meta? - public val name: Name + if (result == null) { + (this as? DeviceBase<*>)?.logger?.warn { "Timeout on stop for device ${this.id}" } + } } 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 index b042163..ada76ed 100644 --- a/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/CompositeControlComponentsDelegates.kt +++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/CompositeControlComponentsDelegates.kt @@ -1,384 +1,248 @@ 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.controls.api.* +import space.kscience.dataforge.meta.* 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 + * Create a [MetaConverter] for enum values using [reified] type with an option to ignore case. */ -public fun > createEnumConverter(enumValues: Array): MetaConverter = object : MetaConverter { - override val descriptor: MetaDescriptor = MetaDescriptor { - valueType(ValueType.STRING) - allowedValues(enumValues.map { it.name }) - } +public inline fun > createEnumConverter(ignoreCase: Boolean = false): MetaConverter { + val allValues = enumValues() + return object : MetaConverter { + override val descriptor: MetaDescriptor = MetaDescriptor { + valueType(ValueType.STRING) + allowedValues(allValues.map { it.name }) + } - override fun readOrNull(source: Meta): E? { - val value = source.value ?: return null - return enumValues.firstOrNull { it.name == value.string } - } + override fun readOrNull(source: Meta): E? { + val stringVal = source.value?.string ?: return null + return allValues.firstOrNull { it.name.equals(stringVal, ignoreCase) } + } - override fun convert(obj: E): Meta = Meta(obj.name) + override fun convert(obj: E): Meta = Meta(obj.name) + } } /** - * A read-only device property that delegates reading to a device [KProperty1] + * Unified function: if [write] == null -> read-only property, else -> mutable property. */ -public fun CompositeControlComponentSpec.property( +public fun > CompositeControlComponentSpec.typedProperty( converter: MetaConverter, - descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {}, name: String? = null, - read: suspend D.(propertyName: String) -> T?, + descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {}, + read: suspend D.(String) -> T?, + write: (suspend D.(String, T) -> Unit)? = null, ): PropertyDelegateProvider, ReadOnlyProperty, DevicePropertySpec>> { - return property(converter, descriptorBuilder, name, read) + return if (write == null) { + property(converter, descriptorBuilder, name, read) + } else { + mutableProperty(converter, descriptorBuilder, name, read, write) + } } /** - * Mutable property that delegates reading and writing to a device [KMutableProperty1] + * Boolean property: read-only or mutable (if [write] is not null). */ -public fun CompositeControlComponentSpec.mutableProperty( - converter: MetaConverter, - descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {}, +public fun > CompositeControlComponentSpec.booleanProperty( 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) -} + descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {}, + read: suspend D.(String) -> Boolean?, + write: (suspend D.(String, Boolean) -> Unit)? = null, +) = typedProperty(MetaConverter.boolean, name, descriptorBuilder, read, write) /** - * Register a mutable logical property (without a corresponding physical state) for a device + * Int property: read-only or mutable. */ -public fun > CompositeControlComponentSpec.logical( - converter: MetaConverter, - descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {}, +public fun > CompositeControlComponentSpec.intProperty( 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)) } + descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {}, + read: suspend D.(String) -> Int?, + write: (suspend D.(String, Int) -> Unit)? = null, +) = typedProperty(MetaConverter.int, name, descriptorBuilder, read, write) + +/** + * Double property: read-only or mutable. + */ +public fun > CompositeControlComponentSpec.doubleProperty( + name: String? = null, + descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {}, + read: suspend D.(String) -> Double?, + write: (suspend D.(String, Double) -> Unit)? = null, +) = typedProperty(MetaConverter.double, name, descriptorBuilder, read, write) + +/** + * Long property: read-only or mutable. + */ +public fun > CompositeControlComponentSpec.longProperty( + name: String? = null, + descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {}, + read: suspend D.(String) -> Long?, + write: (suspend D.(String, Long) -> Unit)? = null, +) = typedProperty(MetaConverter.long, name, descriptorBuilder, read, write) + +/** + * Float property: read-only or mutable. + */ +public fun > CompositeControlComponentSpec.floatProperty( + name: String? = null, + descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {}, + read: suspend D.(String) -> Float?, + write: (suspend D.(String, Float) -> Unit)? = null, +) = typedProperty(MetaConverter.float, name, descriptorBuilder, read, write) + +/** + * Number property: read-only or mutable. + */ +public fun > CompositeControlComponentSpec.numberProperty( + name: String? = null, + descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {}, + read: suspend D.(String) -> Number?, + write: (suspend D.(String, Number) -> Unit)? = null, +) = typedProperty(MetaConverter.number, name, descriptorBuilder, read, write) + +/** + * String property: read-only or mutable. + */ +public fun > CompositeControlComponentSpec.stringProperty( + name: String? = null, + descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {}, + read: suspend D.(String) -> String?, + write: (suspend D.(String, String) -> Unit)? = null, +) = typedProperty(MetaConverter.string, name, descriptorBuilder, read, write) + +/** + * Meta property: read-only or mutable. + */ +public fun > CompositeControlComponentSpec.metaProperty( + name: String? = null, + descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {}, + read: suspend D.(String) -> Meta?, + write: (suspend D.(String, Meta) -> Unit)? = null, +) = typedProperty(MetaConverter.meta, name, descriptorBuilder, read, write) + + +/** + * Enum property (read-only or mutable). + * [ignoreCase] controls case sensitivity when reading the enum value from Meta. + * If [write] is null, the property is read-only; otherwise it's read-write. + */ +public inline fun , D : ConfigurableCompositeControlComponent> + CompositeControlComponentSpec.enumProperty( + name: String? = null, + ignoreCase: Boolean = false, + noinline descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {}, + noinline read: suspend D.(String) -> E?, + noinline write: (suspend D.(String, E) -> Unit)? = null, +): PropertyDelegateProvider, ReadOnlyProperty, DevicePropertySpec>> = + typedProperty( + converter = createEnumConverter(ignoreCase), + name = name, + descriptorBuilder = descriptorBuilder, + read = read, + write = write ) /** - * Creates a boolean property for a device. + * List property: read-only or mutable. */ -public fun CompositeControlComponentSpec.boolean( - descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {}, +public fun > CompositeControlComponentSpec.listProperty( + listConverter: MetaConverter>, name: String? = null, - read: suspend D.(propertyName: String) -> Boolean?, -): PropertyDelegateProvider, ReadOnlyProperty, DevicePropertySpec>> = - property(MetaConverter.boolean, descriptorBuilder, name, read) + descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {}, + read: suspend D.(String) -> List?, + write: (suspend D.(String, List) -> Unit)? = null, +) = typedProperty(listConverter, name, descriptorBuilder, read, write) /** - * Creates a mutable boolean property for a device. + * Logical property (no real hardware I/O). */ -public fun CompositeControlComponentSpec.booleanMutable( - descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {}, +public fun > CompositeControlComponentSpec.logicalProperty( + converter: MetaConverter, 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) + descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {}, +) = typedProperty( + converter = converter, + name = name, + descriptorBuilder = descriptorBuilder, + read = { propertyName -> getProperty(propertyName)?.let(converter::readOrNull) }, + write = { propertyName, value -> writeProperty(propertyName, converter.convert(value)) } +) /** - * Creates a read-only number property for a device. + * Creates an action with optional input/output converters. */ -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( +public fun > CompositeControlComponentSpec.typedAction( inputConverter: MetaConverter, outputConverter: MetaConverter, + name: String? = null, descriptorBuilder: ActionDescriptorBuilder.() -> Unit = {}, - name: String? = null, - execute: suspend D.(I) -> Deferred, + execute: suspend D.(I) -> O, ): 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) + action( + inputConverter = inputConverter, + outputConverter = outputConverter, + descriptorBuilder = descriptorBuilder, + name = name, + execute = execute + ) /** - * An action that takes no parameters and returns no values + * Action with no parameters and no return values. */ -public fun CompositeControlComponentSpec.unitAction( - descriptorBuilder: ActionDescriptorBuilder.() -> Unit = {}, +public fun > CompositeControlComponentSpec.unitAction( name: String? = null, + descriptorBuilder: ActionDescriptorBuilder.() -> Unit = {}, execute: suspend D.() -> Unit, ): PropertyDelegateProvider, ReadOnlyProperty, DeviceActionSpec>> = - action( - MetaConverter.unit, - MetaConverter.unit, - descriptorBuilder, - name + typedAction( + inputConverter = MetaConverter.unit, + outputConverter = MetaConverter.unit, + name = name, + descriptorBuilder = descriptorBuilder, ) { execute() } -public fun CompositeControlComponentSpec.asyncAction( +/** + * Action with async result. The result is awaited. + */ +public fun > CompositeControlComponentSpec.asyncAction( inputConverter: MetaConverter, outputConverter: MetaConverter, - descriptorBuilder: ActionDescriptorBuilder.() -> Unit = {}, name: String? = null, + descriptorBuilder: ActionDescriptorBuilder.() -> Unit = {}, execute: suspend D.(I) -> Deferred, ): PropertyDelegateProvider, ReadOnlyProperty, DeviceActionSpec>> = - action( - inputConverter, - outputConverter, - descriptorBuilder, - name - ) { - execute(it).await() + typedAction(inputConverter, outputConverter, name, descriptorBuilder) { input -> + execute(input).await() } /** - * An action that takes [Meta] and returns [Meta]. No conversions are done + * Action that takes and returns [Meta]. */ -public fun CompositeControlComponentSpec.metaAction( - descriptorBuilder: ActionDescriptorBuilder.() -> Unit = {}, +public fun > CompositeControlComponentSpec.metaAction( name: String? = null, + descriptorBuilder: ActionDescriptorBuilder.() -> Unit = {}, execute: suspend D.(Meta) -> Meta, ): PropertyDelegateProvider, ReadOnlyProperty, DeviceActionSpec>> = - action( - MetaConverter.meta, - MetaConverter.meta, - descriptorBuilder, - name - ) { - execute(it) - } + typedAction(MetaConverter.meta, MetaConverter.meta, name, descriptorBuilder, execute) /** - * Throw an exception if device does not have all properties and actions defined by this specification + * Validates that [device] has all properties and actions defined by this spec. */ -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}" } +public fun CompositeControlComponentSpec<*>.validateSpec(device: Device) { + properties.values.forEach { propSpec -> + check(propSpec.descriptor in device.propertyDescriptors) { + "Property ${propSpec.descriptor.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}" } + actions.values.forEach { actSpec -> + check(actSpec.descriptor in device.actionDescriptors) { + "Action ${actSpec.descriptor.name} not registered in ${device.id}" + } } -} \ No newline at end of file +} 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 index 4966449..74773a0 100644 --- a/controls-core/src/commonTest/kotlin/space/kscience/controls/spec/CompositeControlTest.kt +++ b/controls-core/src/commonTest/kotlin/space/kscience/controls/spec/CompositeControlTest.kt @@ -18,98 +18,92 @@ class CompositeControlTest { // ---------------------- Device Specifications ---------------------------------- public object StepperMotorSpec : CompositeControlComponentSpec() { - public val position by intMutable( + public val position by intProperty( name = "position", read = { getPosition() }, write = { _, value -> setPosition(value) } ) - public val maxPosition by int( + public val maxPosition by intProperty( name = "maxPosition", read = { maxPosition } ) } public object ValveSpec : CompositeControlComponentSpec() { - public val state by booleanMutable( + public val state by booleanProperty( read = { getState() }, write = { _, value -> setState(value) } ) } public object PressureChamberSpec : CompositeControlComponentSpec() { - public val pressure by doubleMutable( + public val pressure by doubleProperty( read = { getPressure() }, write = { _, value -> setPressure(value) } ) } public object SyringePumpSpec : CompositeControlComponentSpec() { - public val volume by doubleMutable( + public val volume by doubleProperty( read = { getVolume() }, write = { _, value -> setVolume(value) } ) } public object ReagentSensorSpec : CompositeControlComponentSpec() { - public val isPresent by boolean( + public val isPresent by booleanProperty( read = { checkReagent() } ) } public object NeedleSpec : CompositeControlComponentSpec() { - public val mode by enumMutable( - enumValues = NeedleDevice.Mode.entries.toTypedArray(), + public val mode by enumProperty( read = { getMode() }, write = { _, value -> setMode(value) } ) - public val position by doubleMutable( + public val position by doubleProperty( read = { getPosition() }, write = { _, value -> setPosition(value) } ) } public object ShakerSpec : CompositeControlComponentSpec() { - public val verticalMotor by childSpec() - public val horizontalMotor by childSpec() + public val verticalMotor by childSpec(StepperMotorSpec) + public val horizontalMotor by childSpec(StepperMotorSpec) } public object TransportationSystemSpec : CompositeControlComponentSpec() { - public val slideMotor by childSpec() - - public val pushMotor by childSpec() - - public val receiveMotor by childSpec() + public val slideMotor by childSpec(StepperMotorSpec) + public val pushMotor by childSpec(StepperMotorSpec) + public val receiveMotor by childSpec(StepperMotorSpec) } public object AnalyzerSpec : CompositeControlComponentSpec() { - public val transportationSystem by childSpec() - public val shakerDevice by childSpec() - public val needleDevice by childSpec() + public val transportationSystem by childSpec(TransportationSystemSpec) + public val shakerDevice by childSpec(ShakerSpec) + public val needleDevice by childSpec(NeedleSpec) + public val valveV20 by childSpec(ValveSpec) + public val valveV17 by childSpec(ValveSpec) + public val valveV18 by childSpec(ValveSpec) + public val valveV35 by childSpec(ValveSpec) - 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(PressureChamberSpec) + public val pressureChamberLow by childSpec(PressureChamberSpec) + public val syringePumpMA100 by childSpec(SyringePumpSpec) + public val syringePumpMA25 by childSpec(SyringePumpSpec) - 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() + public val reagentSensor1 by childSpec(ReagentSensorSpec) + public val reagentSensor2 by childSpec(ReagentSensorSpec) + public val reagentSensor3 by childSpec(ReagentSensorSpec) } -// ---------------------- Device Implementations ---------------------------------- + // ---------------------- Device Implementations ---------------------------------- - // Implementation of Stepper Motor Device public class StepperMotorDevice( context: Context, meta: Meta = Meta.EMPTY @@ -774,6 +768,9 @@ class CompositeControlTest { val context = createTestContext() val shaker = ShakerDevice(context) + shaker.initChildren() + shaker.start()// Start the device + // Access properties to initialize motors and test shaking val verticalMotor = shaker.verticalMotor val horizontalMotor = shaker.horizontalMotor @@ -791,19 +788,23 @@ class CompositeControlTest { val context = createTestContext() val transportationSystem = TransportationSystem(context) + transportationSystem.initChildren() + transportationSystem.start()// Start the device + // 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{ + fun `test AnalyzerDevice device access`() = runTest { val context = createTestContext() val analyzer = AnalyzerDevice(context) + analyzer.initChildren() + analyzer.start()// Start the device - // Access properties to initialize child devices and test existence + // Access properties to initialize child devices assertNotNull(analyzer.transportationSystem, "Transportation system should exist") assertNotNull(analyzer.shakerDevice, "Shaker device should exist") assertNotNull(analyzer.needleDevice, "Needle device should exist")