New hierarchical structure based on CompositeDeviceSpec and ConfigurableCompositeDevice. Also added new builders for PropertyDescriptor and ActionDescriptor

This commit is contained in:
Максим Колпаков 2024-12-19 03:31:22 +03:00
parent e9a37f40fd
commit 7cde308114
12 changed files with 1365 additions and 102 deletions

View File

@ -32,7 +32,7 @@ public class StateConstructorElement<T>(
public val state: DeviceState<T>,
) : ConstructorElement
public class ConnectionConstrucorElement(
public class ConnectionConstructorElement(
public val reads: Collection<DeviceState<*>>,
public val writes: Collection<DeviceState<*>>,
) : ConstructorElement
@ -58,7 +58,7 @@ public interface StateContainer : ContextAware, CoroutineScope {
reads: Collection<DeviceState<*>> = emptySet(),
onChange: suspend (T) -> Unit,
): Job = valueFlow.onEach(onChange).launchIn(this@StateContainer).also {
registerElement(ConnectionConstrucorElement(reads + this, writes))
registerElement(ConnectionConstructorElement(reads + this, writes))
}
public fun <T> DeviceState<T>.onChange(
@ -72,7 +72,7 @@ public interface StateContainer : ContextAware, CoroutineScope {
onChange(pair.first, pair.second)
}
}.launchIn(this@StateContainer).also {
registerElement(ConnectionConstrucorElement(reads + this, writes))
registerElement(ConnectionConstructorElement(reads + this, writes))
}
}
@ -164,7 +164,7 @@ public fun <T1, T2, R> StateContainer.combineState(
* On resulting [Job] cancel the binding is unregistered
*/
public fun <T> StateContainer.bind(sourceState: DeviceState<T>, targetState: MutableDeviceState<T>): Job {
val descriptor = ConnectionConstrucorElement(setOf(sourceState), setOf(targetState))
val descriptor = ConnectionConstructorElement(setOf(sourceState), setOf(targetState))
registerElement(descriptor)
return sourceState.valueFlow.onEach {
targetState.value = it
@ -186,7 +186,7 @@ public fun <T, R> StateContainer.transformTo(
targetState: MutableDeviceState<R>,
transformation: suspend (T) -> R,
): Job {
val descriptor = ConnectionConstrucorElement(setOf(sourceState), setOf(targetState))
val descriptor = ConnectionConstructorElement(setOf(sourceState), setOf(targetState))
registerElement(descriptor)
return sourceState.valueFlow.onEach {
targetState.value = transformation(it)
@ -208,7 +208,7 @@ public fun <T1, T2, R> StateContainer.combineTo(
targetState: MutableDeviceState<R>,
transformation: suspend (T1, T2) -> R,
): Job {
val descriptor = ConnectionConstrucorElement(setOf(sourceState1, sourceState2), setOf(targetState))
val descriptor = ConnectionConstructorElement(setOf(sourceState1, sourceState2), setOf(targetState))
registerElement(descriptor)
return kotlinx.coroutines.flow.combine(sourceState1.valueFlow, sourceState2.valueFlow, transformation).onEach {
targetState.value = it
@ -229,7 +229,7 @@ public inline fun <reified T, R> StateContainer.combineTo(
targetState: MutableDeviceState<R>,
noinline transformation: suspend (Array<T>) -> R,
): Job {
val descriptor = ConnectionConstrucorElement(sourceStates, setOf(targetState))
val descriptor = ConnectionConstructorElement(sourceStates, setOf(targetState))
registerElement(descriptor)
return kotlinx.coroutines.flow.combine(sourceStates.map { it.valueFlow }, transformation).onEach {
targetState.value = it

View File

@ -4,34 +4,109 @@ import kotlinx.serialization.Serializable
import space.kscience.dataforge.meta.descriptors.MetaDescriptor
import space.kscience.dataforge.meta.descriptors.MetaDescriptorBuilder
//TODO add proper builders
//TODO check proper builders
/**
* A descriptor for property
* A descriptor for a property
*/
@Serializable
public class PropertyDescriptor(
public val name: String,
public var description: String? = null,
public var metaDescriptor: MetaDescriptor = MetaDescriptor(),
public var readable: Boolean = true,
public var mutable: Boolean = false,
public val description: String? = null,
public val metaDescriptor: MetaDescriptor = MetaDescriptor(),
public val readable: Boolean = true,
public val mutable: Boolean = false,
)
public fun PropertyDescriptor.metaDescriptor(block: MetaDescriptorBuilder.() -> Unit) {
metaDescriptor = MetaDescriptor {
from(metaDescriptor)
block()
/**
* A builder for PropertyDescriptor
*/
public class PropertyDescriptorBuilder(public val name: String) {
public var description: String? = null
public var metaDescriptor: MetaDescriptor = MetaDescriptor()
public var readable: Boolean = true
public var mutable: Boolean = false
/**
* Configure the metaDescriptor using a block
*/
public fun metaDescriptor(block: MetaDescriptorBuilder.() -> Unit) {
metaDescriptor = MetaDescriptor {
from(metaDescriptor)
block()
}
}
/**
* Build the PropertyDescriptor
*/
public fun build(): PropertyDescriptor = PropertyDescriptor(
name = name,
description = description,
metaDescriptor = metaDescriptor,
readable = readable,
mutable = mutable
)
}
/**
* A descriptor for property
* Create a PropertyDescriptor using a builder
*/
public fun propertyDescriptor(name: String, builder: PropertyDescriptorBuilder.() -> Unit = {}): PropertyDescriptor =
PropertyDescriptorBuilder(name).apply(builder).build()
/**
* A descriptor for an action
*/
@Serializable
public class ActionDescriptor(
public val name: String,
public var description: String? = null,
public var inputMetaDescriptor: MetaDescriptor = MetaDescriptor(),
public var outputMetaDescriptor: MetaDescriptor = MetaDescriptor()
public val description: String? = null,
public val inputMetaDescriptor: MetaDescriptor = MetaDescriptor(),
public val outputMetaDescriptor: MetaDescriptor = MetaDescriptor(),
)
/**
* A builder for ActionDescriptor
*/
public class ActionDescriptorBuilder(public val name: String) {
public var description: String? = null
public var inputMetaDescriptor: MetaDescriptor = MetaDescriptor()
public var outputMetaDescriptor: MetaDescriptor = MetaDescriptor()
/**
* Configure the inputMetaDescriptor using a block
*/
public fun inputMeta(block: MetaDescriptorBuilder.() -> Unit) {
inputMetaDescriptor = MetaDescriptor {
from(inputMetaDescriptor)
block()
}
}
/**
* Configure the outputMetaDescriptor using a block
*/
public fun outputMeta(block: MetaDescriptorBuilder.() -> Unit) {
outputMetaDescriptor = MetaDescriptor {
from(outputMetaDescriptor)
block()
}
}
/**
* Build the ActionDescriptor
*/
public fun build(): ActionDescriptor = ActionDescriptor(
name = name,
description = description,
inputMetaDescriptor = inputMetaDescriptor,
outputMetaDescriptor = outputMetaDescriptor
)
}
/**
* Create an ActionDescriptor using a builder
*/
public fun actionDescriptor(name: String, builder: ActionDescriptorBuilder.() -> Unit = {}): ActionDescriptor =
ActionDescriptorBuilder(name).apply(builder).build()

View File

@ -0,0 +1,809 @@
package space.kscience.controls.spec
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancelAndJoin
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeoutOrNull
import space.kscience.controls.api.*
import space.kscience.dataforge.context.Context
import space.kscience.dataforge.context.error
import space.kscience.dataforge.context.logger
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.MetaConverter
import space.kscience.dataforge.meta.MutableMeta
import space.kscience.dataforge.names.Name
import space.kscience.dataforge.names.asName
import space.kscience.dataforge.names.parseAsName
import space.kscience.dataforge.names.plus
import kotlin.properties.PropertyDelegateProvider
import kotlin.properties.ReadOnlyProperty
import kotlin.reflect.KProperty
import kotlin.time.Duration
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.launchIn
import space.kscience.dataforge.context.AbstractPlugin
import space.kscience.dataforge.context.ContextAware
import space.kscience.dataforge.context.ContextBuilder
import space.kscience.dataforge.context.PluginFactory
import space.kscience.dataforge.context.PluginTag
/**
* Defines how child device lifecycle should be managed.
*/
public enum class LifecycleMode {
/**
* Device starts and stops with parent
*/
LINKED,
/**
* Device is started and stopped independently
*/
INDEPENDENT,
/**
* Device is created but starts only when explicitly requested
*/
LAZY
}
public sealed class DeviceChangeEvent {
public abstract val deviceName: Name
public data class Added(override val deviceName: Name, val device: Device) : DeviceChangeEvent()
public data class Removed(override val deviceName: Name) : DeviceChangeEvent()
}
public interface ComponentRegistry : ContextAware {
public fun <D: ConfigurableCompositeControlComponent<*>> getSpec(name: Name): CompositeControlComponentSpec<D>?
}
public class ComponentRegistryManager : AbstractPlugin(), ComponentRegistry {
private val specs = mutableMapOf<Name, CompositeControlComponentSpec<*>>()
override val tag: PluginTag = Companion.tag
override fun <D : ConfigurableCompositeControlComponent<*>> getSpec(name: Name): CompositeControlComponentSpec<D>? {
try {
return specs[name] as? CompositeControlComponentSpec<D>
} catch (e: ClassCastException) {
logger.error(e) { "Failed to get spec $name" }
}
return null
}
public fun registerSpec(spec: CompositeControlComponentSpec<*>) {
specs[(spec as DevicePropertySpec<*, *>).name.asName()] = spec
}
public companion object : PluginFactory<ComponentRegistryManager> {
override val tag: PluginTag = PluginTag("controls.spechub", group = PluginTag.DATAFORGE_GROUP)
override fun build(context: Context, meta: Meta): ComponentRegistryManager = ComponentRegistryManager()
}
}
public val Context.componentRegistry: ComponentRegistry? get() = plugins[ComponentRegistryManager]
public fun ContextBuilder.withSpecHub() {
plugin(ComponentRegistryManager)
}
/**
* Builder for [DeviceLifecycleConfig].
*/
public class DeviceLifecycleConfigBuilder {
public var lifecycleMode: LifecycleMode = LifecycleMode.LINKED
public var messageBuffer: Int = 1000
public var startDelay: Duration = Duration.ZERO
public var startTimeout: Duration? = null
public var stopTimeout: Duration? = null
public var coroutineScope: CoroutineScope? = null
public var dispatcher: CoroutineDispatcher? = null
public var onError: ChildDeviceErrorHandler = ChildDeviceErrorHandler.RESTART
public fun build(): DeviceLifecycleConfig = DeviceLifecycleConfig(
lifecycleMode, messageBuffer, startDelay, startTimeout, stopTimeout, coroutineScope, dispatcher, onError
)
}
public fun DeviceLifecycleConfigBuilder.linked() {
lifecycleMode = LifecycleMode.LINKED
}
public fun DeviceLifecycleConfigBuilder.independent() {
lifecycleMode = LifecycleMode.INDEPENDENT
}
public fun DeviceLifecycleConfigBuilder.lazy() {
lifecycleMode = LifecycleMode.LAZY
}
public fun DeviceLifecycleConfigBuilder.restartOnError() {
onError = ChildDeviceErrorHandler.RESTART
}
public fun DeviceLifecycleConfigBuilder.propagateError() {
onError = ChildDeviceErrorHandler.PROPAGATE
}
public fun DeviceLifecycleConfigBuilder.withCustomTimeout(timeout: Duration) {
startTimeout = timeout
stopTimeout = timeout
}
@OptIn(InternalDeviceAPI::class)
public abstract class CompositeControlComponentSpec<D : Device>() : CompositeDeviceSpec<D> {
private val _properties = hashMapOf<String, DevicePropertySpec<D, *>>(
DeviceMetaPropertySpec.name to DeviceMetaPropertySpec
)
override val properties: Map<String, DevicePropertySpec<D, *>> get() = _properties
private val _actions = hashMapOf<String, DeviceActionSpec<D, *, *>>()
override val actions: Map<String, DeviceActionSpec<D, *, *>> get() = _actions
private val _childSpecs = mutableMapOf<String, ChildComponentConfig<*>>()
override val childSpecs: Map<String, ChildComponentConfig<*>> get() = _childSpecs
public fun <CDS: CompositeControlComponentSpec<CD>, CD: ConfigurableCompositeControlComponent<CD>> childSpec(
deviceName: String? = null,
specName: Name? = null,
metaBuilder: (MutableMeta.() -> Unit)? = null,
configBuilder: DeviceLifecycleConfigBuilder.() -> Unit = {},
): PropertyDelegateProvider<CompositeControlComponentSpec<D>, ReadOnlyProperty<CompositeControlComponentSpec<D>, CompositeControlComponentSpec<CD>>> =
PropertyDelegateProvider { thisRef, property ->
ReadOnlyProperty { _, _ ->
val childSpecName = specName ?: property.name.asName()
val nameForDevice = deviceName?.asName() ?: property.name.asName()
val config = DeviceLifecycleConfigBuilder().apply(configBuilder).build()
val meta = metaBuilder?.let { Meta(it) }
val spec = (thisRef as ConfigurableCompositeControlComponent<*>).context.componentRegistry?.getSpec<CD>(childSpecName) ?: error("Spec with name '$specName' is not found")
val childComponentConfig = object : ChildComponentConfig<CD>{
override val spec: CompositeControlComponentSpec<CD> = spec
override val config: DeviceLifecycleConfig = config
override val meta: Meta? = meta
override val name: Name = nameForDevice
}
_childSpecs[property.name] = childComponentConfig
childComponentConfig.spec
}
}
override fun validate(device: D) {
properties.map { it.value.descriptor }.forEach { specProperty ->
check(specProperty in device.propertyDescriptors) { "Property ${specProperty.name} not registered in ${device.id}" }
}
actions.map { it.value.descriptor }.forEach { specAction ->
check(specAction in device.actionDescriptors) { "Action ${specAction.name} not registered in ${device.id}" }
}
}
override fun <T, P : DevicePropertySpec<D, T>> registerProperty(deviceProperty: P): P {
_properties[deviceProperty.name] = deviceProperty
return deviceProperty
}
override fun <I, O> registerAction(deviceAction: DeviceActionSpec<D, I, O>): DeviceActionSpec<D, I, O> {
_actions[deviceAction.name] = deviceAction
return deviceAction
}
override fun createPropertyDescriptorInternal(
propertyName: String,
converter: MetaConverter<*>,
mutable: Boolean,
property: KProperty<*>,
descriptorBuilder: PropertyDescriptorBuilder.() -> Unit,
): PropertyDescriptor {
return propertyDescriptor(propertyName) {
this.mutable = mutable
converter.descriptor?.let { converterDescriptor ->
metaDescriptor {
from(converterDescriptor)
}
}
fromSpec(property)
descriptorBuilder()
}
}
override fun createActionDescriptor(
actionName: String,
inputConverter: MetaConverter<*>,
outputConverter: MetaConverter<*>,
property: KProperty<*>,
descriptorBuilder: ActionDescriptorBuilder.() -> Unit,
): ActionDescriptor {
return actionDescriptor(actionName) {
inputConverter.descriptor?.let { converterDescriptor ->
inputMeta {
from(converterDescriptor)
}
}
outputConverter.descriptor?.let { converterDescriptor ->
outputMeta {
from(converterDescriptor)
}
}
fromSpec(property)
descriptorBuilder()
}
}
override fun <T> property(
converter: MetaConverter<T>,
descriptorBuilder: PropertyDescriptorBuilder.() -> Unit,
name: String?,
read: suspend D.(propertyName: String) -> T?,
): PropertyDelegateProvider<CompositeControlComponentSpec<D>, ReadOnlyProperty<CompositeControlComponentSpec<D>, DevicePropertySpec<D, T>>> =
PropertyDelegateProvider { _: CompositeControlComponentSpec<D>, property ->
val propertyName = name ?: property.name
val descriptor = createPropertyDescriptorInternal(
propertyName = propertyName,
converter = converter,
mutable = false,
property = property,
descriptorBuilder = descriptorBuilder
)
val deviceProperty = registerProperty(object : DevicePropertySpec<D, T> {
override val descriptor = descriptor
override val converter = converter
override suspend fun read(device: D): T? =
withContext(device.coroutineContext) { device.read(propertyName) }
})
ReadOnlyProperty { _, _ ->
deviceProperty
}
}
override fun <T> mutableProperty(
converter: MetaConverter<T>,
descriptorBuilder: PropertyDescriptorBuilder.() -> Unit,
name: String?,
read: suspend D.(propertyName: String) -> T?,
write: suspend D.(propertyName: String, value: T) -> Unit
): PropertyDelegateProvider<CompositeControlComponentSpec<D>, ReadOnlyProperty<CompositeControlComponentSpec<D>, MutableDevicePropertySpec<D, T>>> =
PropertyDelegateProvider { _: CompositeControlComponentSpec<D>, property ->
val propertyName = name ?: property.name
val descriptor = createPropertyDescriptorInternal(
propertyName = propertyName,
converter = converter,
mutable = true,
property = property,
descriptorBuilder = descriptorBuilder
)
val deviceProperty = registerProperty(object : MutableDevicePropertySpec<D, T> {
override val descriptor = descriptor
override val converter = converter
override suspend fun read(device: D): T? =
withContext(device.coroutineContext) { device.read(propertyName) }
override suspend fun write(device: D, value: T): Unit = withContext(device.coroutineContext) {
device.write(propertyName, value)
}
})
ReadOnlyProperty { _, _ ->
deviceProperty
}
}
override fun <I, O> action(
inputConverter: MetaConverter<I>,
outputConverter: MetaConverter<O>,
descriptorBuilder: ActionDescriptorBuilder.() -> Unit,
name: String?,
execute: suspend D.(I) -> O,
): PropertyDelegateProvider<CompositeControlComponentSpec<D>, ReadOnlyProperty<CompositeControlComponentSpec<D>, DeviceActionSpec<D, I, O>>> =
PropertyDelegateProvider { _: CompositeControlComponentSpec<D>, property ->
val actionName = name ?: property.name
val descriptor = createActionDescriptor(
actionName = actionName,
inputConverter = inputConverter,
outputConverter = outputConverter,
property = property,
descriptorBuilder = descriptorBuilder
)
val deviceAction = registerAction(object : DeviceActionSpec<D, I, O> {
override val descriptor = descriptor
override val inputConverter = inputConverter
override val outputConverter = outputConverter
override suspend fun execute(device: D, input: I): O = withContext(device.coroutineContext) {
device.execute(input)
}
})
ReadOnlyProperty { _, _ ->
deviceAction
}
}
override suspend fun D.onOpen() {}
override suspend fun D.onClose() {}
}
public fun <D : Device> CompositeControlComponentSpec<D>.unitAction(
descriptorBuilder: ActionDescriptorBuilder.() -> Unit = {},
name: String? = null,
execute: suspend D.() -> Unit,
): PropertyDelegateProvider<CompositeControlComponentSpec<D>, ReadOnlyProperty<CompositeControlComponentSpec<D>, DeviceActionSpec<D, Unit, Unit>>> =
action(
MetaConverter.unit,
MetaConverter.unit,
descriptorBuilder,
name
) {
execute()
}
public fun <D : Device> CompositeControlComponentSpec<D>.metaAction(
descriptorBuilder: ActionDescriptorBuilder.() -> Unit = {},
name: String? = null,
execute: suspend D.(Meta) -> Meta,
): PropertyDelegateProvider<CompositeControlComponentSpec<D>, ReadOnlyProperty<CompositeControlComponentSpec<D>, DeviceActionSpec<D, Meta, Meta>>> =
action(
MetaConverter.meta,
MetaConverter.meta,
descriptorBuilder,
name
) {
execute(it)
}
/**
* Basic interface for device description
*/
public interface CompositeDeviceSpec<D : Device> {
public val properties: Map<String, DevicePropertySpec<D, *>>
public val actions: Map<String, DeviceActionSpec<D, *, *>>
public val childSpecs: Map<String, ChildComponentConfig<*>>
/**
* Called on `start()`
*/
public suspend fun D.onOpen()
/**
* Called on `stop()`
*/
public suspend fun D.onClose()
/**
* Registers a property in the spec.
*/
public fun <T, P : DevicePropertySpec<D, T>> registerProperty(deviceProperty: P): P
/**
* Registers an action in the spec.
*/
public fun <I, O> registerAction(deviceAction: DeviceActionSpec<D, I, O>): DeviceActionSpec<D, I, O>
public fun validate(device: D)
public fun createPropertyDescriptorInternal(
propertyName: String,
converter: MetaConverter<*>,
mutable: Boolean,
property: KProperty<*>,
descriptorBuilder: PropertyDescriptorBuilder.() -> Unit
): PropertyDescriptor
public fun createActionDescriptor(
actionName: String,
inputConverter: MetaConverter<*>,
outputConverter: MetaConverter<*>,
property: KProperty<*>,
descriptorBuilder: ActionDescriptorBuilder.() -> Unit
): ActionDescriptor
public fun <T> property(
converter: MetaConverter<T>,
descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {},
name: String? = null,
read: suspend D.(propertyName: String) -> T?,
): PropertyDelegateProvider<CompositeControlComponentSpec<D>, ReadOnlyProperty<CompositeControlComponentSpec<D>, DevicePropertySpec<D, T>>>
public fun <T> mutableProperty(
converter: MetaConverter<T>,
descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {},
name: String? = null,
read: suspend D.(propertyName: String) -> T?,
write: suspend D.(propertyName: String, value: T) -> Unit,
): PropertyDelegateProvider<CompositeControlComponentSpec<D>, ReadOnlyProperty<CompositeControlComponentSpec<D>, MutableDevicePropertySpec<D, T>>>
public fun <I, O> action(
inputConverter: MetaConverter<I>,
outputConverter: MetaConverter<O>,
descriptorBuilder: ActionDescriptorBuilder.() -> Unit = {},
name: String? = null,
execute: suspend D.(I) -> O,
): PropertyDelegateProvider<CompositeControlComponentSpec<D>, ReadOnlyProperty<CompositeControlComponentSpec<D>, DeviceActionSpec<D, I, O>>>
}
public data class DeviceLifecycleConfig(
val lifecycleMode: LifecycleMode = LifecycleMode.LINKED,
val messageBuffer: Int = 1000,
val startDelay: Duration = Duration.ZERO,
val startTimeout: Duration? = null,
val stopTimeout: Duration? = null,
val coroutineScope: CoroutineScope? = null,
val dispatcher: CoroutineDispatcher? = null,
val onError: ChildDeviceErrorHandler = ChildDeviceErrorHandler.RESTART
) {
init {
require(messageBuffer > 0) { "Message buffer size must be positive." }
startTimeout?.let { require(it.isPositive()) { "Start timeout must be positive." } }
stopTimeout?.let { require(it.isPositive()) { "Stop timeout must be positive." } }
}
}
public enum class ChildDeviceErrorHandler {
IGNORE,
RESTART,
STOP_PARENT,
PROPAGATE
}
/**
* Base class for managing child devices. Manages lifecycle and message flow.
*/
public abstract class AbstractDeviceHubManager(
public val context: Context,
private val dispatcher: CoroutineDispatcher = Dispatchers.Default,
) {
internal val childrenJobs: MutableMap<Name, ChildJob> = mutableMapOf()
public val devices: Map<Name, Device> get() = childrenJobs.mapValues { it.value.device }
internal data class ChildJob(
val device: Device,
val job: Job,
val lifecycleMode: LifecycleMode,
val messageBus: MutableSharedFlow<DeviceMessage>,
val meta: Meta? = null
)
/**
* A centralized bus for messages
*/
public abstract val messageBus: MutableSharedFlow<DeviceMessage>
/**
* A centralized bus for device change events
*/
public abstract val deviceChanges: MutableSharedFlow<DeviceChangeEvent>
/**
* Launches a child device with a specific lifecycle mode and error handling.
*/
@OptIn(ExperimentalCoroutinesApi::class)
internal fun launchChild(name: Name, device: Device, config: DeviceLifecycleConfig, meta: Meta? = null): ChildJob {
val childMessageBus = MutableSharedFlow<DeviceMessage>(
replay = config.messageBuffer,
onBufferOverflow = BufferOverflow.DROP_OLDEST
)
val childScope = config.coroutineScope ?: context
val job = childScope.launch(dispatcher + CoroutineName("Child device $name")) {
try {
if (config.lifecycleMode != LifecycleMode.INDEPENDENT) {
if (config.lifecycleMode == LifecycleMode.LINKED || device.lifecycleState == LifecycleState.STARTING){
delay(config.startDelay)
withTimeoutOrNull(config.startTimeout ?: Duration.INFINITE) {
device.start()
} ?: error("Timeout on start for $name")
}
}
device.messageFlow.collect { message ->
childMessageBus.emit(message.changeSource { name.plus(it) })
messageBus.emit(message.changeSource { name.plus(it) })
}
} catch (ex: Exception) {
val errorMessage = DeviceMessage.error(ex, name)
messageBus.emit(errorMessage)
when (config.onError) {
ChildDeviceErrorHandler.IGNORE -> context.logger.error(ex) { "Error in child device $name ignored" }
ChildDeviceErrorHandler.RESTART -> {
context.logger.error(ex) { "Error in child device $name, restarting" }
removeDevice(name)
childrenJobs[name] = launchChild(name, device, config, meta)
}
ChildDeviceErrorHandler.STOP_PARENT -> {
context.logger.error(ex) { "Error in child device $name, stopping parent" }
coroutineContext[Job]?.cancelAndJoin()
}
ChildDeviceErrorHandler.PROPAGATE -> {
context.logger.error(ex) { "Error in child device $name propagated to parent" }
throw ex
}
}
} finally {
childrenJobs.remove(name)
clearReplayCache(childMessageBus)
deviceChanges.emit(DeviceChangeEvent.Removed(name))
messageBus.emit(DeviceLogMessage("Device $name stopped", sourceDevice = name))
if (device is ConfigurableCompositeControlComponent<*>) {
device.onChildStop()
}
}
}
return ChildJob(device, job, config.lifecycleMode, childMessageBus, meta)
}
@OptIn(ExperimentalCoroutinesApi::class)
private fun <T> clearReplayCache(mutableSharedFlow: MutableSharedFlow<T>){
val cached = mutableSharedFlow.replayCache
mutableSharedFlow.resetReplayCache()
cached.forEach { mutableSharedFlow.tryEmit(it) }
}
/**
* Add a device to the hub and manage its lifecycle according to its spec
*/
public fun addDevice(name: Name, device: Device, config: DeviceLifecycleConfig, meta: Meta? = null) {
val existingDevice = devices[name]
if (existingDevice != null) {
if(existingDevice == device) {
error("Device with name $name is already installed")
}
context.launch(device.coroutineContext) { existingDevice.stopWithTimeout(config.stopTimeout ?: Duration.INFINITE) }
}
childrenJobs[name] = launchChild(name, device, config, meta)
context.launch {
deviceChanges.emit(DeviceChangeEvent.Added(name, device))
messageBus.emit(DeviceLogMessage("Device $name added", sourceDevice = name))
}
}
public fun removeDevice(name: Name) {
childrenJobs[name]?.let { childJob ->
context.launch(childJob.device.coroutineContext) {
val timeout = when (childJob.lifecycleMode) {
LifecycleMode.INDEPENDENT -> childJob.device.meta["stopTimeout".asName()]?.value?.let {
Duration.parse(it.toString())
} ?: Duration.INFINITE
else -> Duration.INFINITE
}
withTimeoutOrNull(timeout) {
childJob.job.cancelAndJoin()
childJob.device.stop()
} ?: error("Timeout on stop for $name")
}
childrenJobs.remove(name)
context.launch {
messageBus.emit(DeviceLogMessage("Device $name removed", sourceDevice = name))
deviceChanges.emit(DeviceChangeEvent.Removed(name))
}
}
}
/**
* Change lifecycle mode of a child device
*/
public fun changeLifecycleMode(name: Name, mode: LifecycleMode) {
val job = childrenJobs[name] ?: error("Device with name '$name' is not found")
val config = DeviceLifecycleConfig(lifecycleMode = mode, messageBuffer = job.messageBus.replayCache.size)
context.launch {
job.job.cancelAndJoin()
childrenJobs[name] = launchChild(name, job.device, config, job.meta)
}
}
/**
* Get local message bus for a child device
*/
public fun getChildMessageBus(name: Name) : MutableSharedFlow<DeviceMessage>? = childrenJobs[name]?.messageBus
/**
* Method for explicit call when child device is stopped.
*/
internal open fun onChildStop(){
}
}
private class DeviceHubManagerImpl(context: Context, dispatcher: CoroutineDispatcher = Dispatchers.Default) : AbstractDeviceHubManager(context, dispatcher){
override val messageBus: MutableSharedFlow<DeviceMessage> = MutableSharedFlow(
replay = 1000,
onBufferOverflow = BufferOverflow.DROP_OLDEST
)
override val deviceChanges: MutableSharedFlow<DeviceChangeEvent> = MutableSharedFlow(replay = 1)
}
/**
* An interface for a device that contains other devices as children.
*/
public interface CompositeControlComponent : Device {
/**
* A centralized flow of all device messages from this node and all its children
*/
public val messageBus: SharedFlow<DeviceMessage>
/**
* A map of child devices
*/
public val devices: Map<Name, Device>
}
/**
* A base class for devices created from specification, using AbstractDeviceHubManager for children management
*/
public open class ConfigurableCompositeControlComponent<D : Device>(
public open val spec: CompositeControlComponentSpec<D>,
context: Context,
meta: Meta = Meta.EMPTY,
config: DeviceLifecycleConfig = DeviceLifecycleConfig(),
private val hubManager: AbstractDeviceHubManager = DeviceHubManagerImpl(context, config.dispatcher ?: Dispatchers.Default),
) : DeviceBase<D>(context, meta), CompositeControlComponent {
override val properties: Map<String, DevicePropertySpec<D, *>>
get() = spec.properties
override val actions: Map<String, DeviceActionSpec<D, *, *>>
get() = spec.actions
final override val messageBus: MutableSharedFlow<DeviceMessage>
get() = hubManager.messageBus
public val deviceChanges: MutableSharedFlow<DeviceChangeEvent>
get() = hubManager.deviceChanges
public val aggregatedMessageFlow: SharedFlow<DeviceMessage>
get() = hubManager.messageBus
init {
spec.childSpecs.forEach{ (name, childSpec) ->
val childDevice = ConfigurableCompositeControlComponent(childSpec.spec, context, childSpec.meta ?: Meta.EMPTY, childSpec.config)
addDevice(childSpec.name, childDevice, childSpec.config, childSpec.meta)
}
spec.actions.values.forEach { actionSpec ->
launch {
val actionName = actionSpec.name
messageFlow.filterIsInstance<ActionExecuteMessage>().filter { it.action == actionName }.onEach {
val result = execute(actionName, it.argument)
messageBus.emit(
ActionResultMessage(
action = actionName,
result = result,
requestId = it.requestId,
sourceDevice = id.asName()
)
)
}.launchIn(this)
}
}
}
override suspend fun onStart() {
with(spec) {
self.onOpen()
validate(self)
}
hubManager.devices.values.filter {
it.lifecycleState != LifecycleState.STARTED && it.lifecycleState != LifecycleState.STARTING
}.forEach {
if (hubManager.childrenJobs[it.id.parseAsName()]?.lifecycleMode != LifecycleMode.LAZY) {
it.start()
}
}
}
private suspend fun getTimeout(device: Device): Duration {
return (hubManager.childrenJobs[device.id.parseAsName()]?.lifecycleMode ?: LifecycleMode.LINKED).let{
if(it == LifecycleMode.INDEPENDENT)
it.name.let{ meta["stopTimeout".parseAsName()]?.let { durationMeta ->
Duration.parse(durationMeta.value.toString())
} ?: Duration.INFINITE }
else Duration.INFINITE
}
}
override suspend fun onStop() {
hubManager.devices.values.forEach {
launch(it.coroutineContext){
withTimeoutOrNull(getTimeout(it)){
it.stop()
}
}
}
with(spec) {
self.onClose()
}
}
override fun toString(): String = "Device(spec=$spec)"
internal open fun onChildStop() {
}
/**
* Add existing device to this hub
*/
private fun addDevice(name: Name = device.id.asName(), device: Device, config: DeviceLifecycleConfig = DeviceLifecycleConfig(), meta: Meta? = null) {
hubManager.addDevice(name, device, config, meta)
}
/**
* Remove a child device from the hub by name.
*/
private fun removeDevice(name: Name) {
hubManager.removeDevice(name)
}
/**
* Get list of all children devices
*/
public override val devices: Map<Name, Device>
get() = hubManager.devices
/**
* Get child device from this hub by name
*/
public fun <CD : ConfigurableCompositeControlComponent<CD>> getChildDevice(name: Name): ConfigurableCompositeControlComponent<CD> =
hubManager.devices[name] as? ConfigurableCompositeControlComponent<CD>? ?: error("Device $name not found")
public fun <CD: ConfigurableCompositeControlComponent<CD>> childDevice(name: Name? = null):
PropertyDelegateProvider<ConfigurableCompositeControlComponent<D>, ReadOnlyProperty<ConfigurableCompositeControlComponent<D>, CD>>
= PropertyDelegateProvider{ thisRef, property ->
ReadOnlyProperty{ _, _ ->
val deviceName = name ?: property.name.asName()
thisRef.devices[deviceName] as? CD ?: error("Device $deviceName not found")
}
}
/**
* Get child device message bus by name
*/
public fun getChildMessageBus(name: Name): SharedFlow<DeviceMessage>? = hubManager.getChildMessageBus(name)
/**
* Get device, using delegate method
*/
public inline operator fun <reified D : Device> get(name: Name): D? = devices[name] as? D
/**
* Get device, using delegate method
*/
public inline operator fun <reified D : Device> get(name: String): D? = devices[name.asName()] as? D
}
public suspend fun WithLifeCycle.stopWithTimeout(timeout: Duration = Duration.INFINITE) {
withTimeoutOrNull(timeout) {
stop()
}
}
public interface ChildComponentConfig<CD: Device>{
public val spec: CompositeControlComponentSpec<CD>
public val config: DeviceLifecycleConfig
public val meta: Meta?
public val name: Name
}

View File

@ -0,0 +1,384 @@
package space.kscience.controls.spec
import kotlinx.coroutines.Deferred
import space.kscience.controls.api.ActionDescriptorBuilder
import space.kscience.controls.api.Device
import space.kscience.controls.api.PropertyDescriptorBuilder
import space.kscience.controls.api.id
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.MetaConverter
import space.kscience.dataforge.meta.ValueType
import space.kscience.dataforge.meta.descriptors.MetaDescriptor
import space.kscience.dataforge.meta.string
import kotlin.properties.PropertyDelegateProvider
import kotlin.properties.ReadOnlyProperty
import kotlin.reflect.KMutableProperty1
import kotlin.reflect.KProperty1
/**
* Create a [MetaConverter] for enum values
*/
public fun <E : Enum<E>> createEnumConverter(enumValues: Array<E>): MetaConverter<E> = object : MetaConverter<E> {
override val descriptor: MetaDescriptor = MetaDescriptor {
valueType(ValueType.STRING)
allowedValues(enumValues.map { it.name })
}
override fun readOrNull(source: Meta): E? {
val value = source.value ?: return null
return enumValues.firstOrNull { it.name == value.string }
}
override fun convert(obj: E): Meta = Meta(obj.name)
}
/**
* A read-only device property that delegates reading to a device [KProperty1]
*/
public fun <T, D : Device> CompositeControlComponentSpec<D>.property(
converter: MetaConverter<T>,
descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {},
name: String? = null,
read: suspend D.(propertyName: String) -> T?,
): PropertyDelegateProvider<CompositeControlComponentSpec<D>, ReadOnlyProperty<CompositeControlComponentSpec<D>, DevicePropertySpec<D, T>>> {
return property(converter, descriptorBuilder, name, read)
}
/**
* Mutable property that delegates reading and writing to a device [KMutableProperty1]
*/
public fun <T, D : Device> CompositeControlComponentSpec<D>.mutableProperty(
converter: MetaConverter<T>,
descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {},
name: String? = null,
read: suspend D.(propertyName: String) -> T?,
write: suspend D.(propertyName: String, value: T) -> Unit,
): PropertyDelegateProvider<CompositeControlComponentSpec<D>, ReadOnlyProperty<CompositeControlComponentSpec<D>, MutableDevicePropertySpec<D, T>>> {
return mutableProperty(converter, descriptorBuilder, name, read, write)
}
/**
* Register a mutable logical property (without a corresponding physical state) for a device
*/
public fun <T, D : DeviceBase<D>> CompositeControlComponentSpec<D>.logical(
converter: MetaConverter<T>,
descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {},
name: String? = null,
): PropertyDelegateProvider<CompositeControlComponentSpec<D>, ReadOnlyProperty<CompositeControlComponentSpec<D>, MutableDevicePropertySpec<D, T>>> =
mutableProperty(
converter,
descriptorBuilder,
name,
read = { propertyName -> getProperty(propertyName)?.let(converter::readOrNull) },
write = { propertyName, value -> writeProperty(propertyName, converter.convert(value)) }
)
/**
* Creates a boolean property for a device.
*/
public fun <D : Device> CompositeControlComponentSpec<D>.boolean(
descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {},
name: String? = null,
read: suspend D.(propertyName: String) -> Boolean?,
): PropertyDelegateProvider<CompositeControlComponentSpec<D>, ReadOnlyProperty<CompositeControlComponentSpec<D>, DevicePropertySpec<D, Boolean>>> =
property(MetaConverter.boolean, descriptorBuilder, name, read)
/**
* Creates a mutable boolean property for a device.
*/
public fun <D : Device> CompositeControlComponentSpec<D>.booleanMutable(
descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {},
name: String? = null,
read: suspend D.(propertyName: String) -> Boolean?,
write: suspend D.(propertyName: String, value: Boolean) -> Unit
): PropertyDelegateProvider<CompositeControlComponentSpec<D>, ReadOnlyProperty<CompositeControlComponentSpec<D>, MutableDevicePropertySpec<D, Boolean>>> =
mutableProperty(MetaConverter.boolean, descriptorBuilder, name, read, write)
/**
* Creates a read-only number property for a device.
*/
public fun <D : Device> CompositeControlComponentSpec<D>.number(
descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {},
name: String? = null,
read: suspend D.(propertyName: String) -> Number?,
): PropertyDelegateProvider<CompositeControlComponentSpec<D>, ReadOnlyProperty<CompositeControlComponentSpec<D>, DevicePropertySpec<D, Number>>> =
property(MetaConverter.number, descriptorBuilder, name, read)
/**
* Creates a mutable number property for a device.
*/
public fun <D : Device> CompositeControlComponentSpec<D>.numberMutable(
descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {},
name: String? = null,
read: suspend D.(propertyName: String) -> Number?,
write: suspend D.(propertyName: String, value: Number) -> Unit
): PropertyDelegateProvider<CompositeControlComponentSpec<D>, ReadOnlyProperty<CompositeControlComponentSpec<D>, MutableDevicePropertySpec<D, Number>>> =
mutableProperty(MetaConverter.number, descriptorBuilder, name, read, write)
/**
* Creates a read-only double property for a device.
*/
public fun <D : Device> CompositeControlComponentSpec<D>.double(
descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {},
name: String? = null,
read: suspend D.(propertyName: String) -> Double?,
): PropertyDelegateProvider<CompositeControlComponentSpec<D>, ReadOnlyProperty<CompositeControlComponentSpec<D>, DevicePropertySpec<D, Double>>> =
property(MetaConverter.double, descriptorBuilder, name, read)
/**
* Creates a mutable double property for a device.
*/
public fun <D : Device> CompositeControlComponentSpec<D>.doubleMutable(
descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {},
name: String? = null,
read: suspend D.(propertyName: String) -> Double?,
write: suspend D.(propertyName: String, value: Double) -> Unit
): PropertyDelegateProvider<CompositeControlComponentSpec<D>, ReadOnlyProperty<CompositeControlComponentSpec<D>, MutableDevicePropertySpec<D, Double>>> =
mutableProperty(MetaConverter.double, descriptorBuilder, name, read, write)
/**
* Creates a read-only string property for a device.
*/
public fun <D : Device> CompositeControlComponentSpec<D>.string(
descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {},
name: String? = null,
read: suspend D.(propertyName: String) -> String?
): PropertyDelegateProvider<CompositeControlComponentSpec<D>, ReadOnlyProperty<CompositeControlComponentSpec<D>, DevicePropertySpec<D, String>>> =
property(MetaConverter.string, descriptorBuilder, name, read)
/**
* Creates a mutable string property for a device.
*/
public fun <D : Device> CompositeControlComponentSpec<D>.stringMutableProperty(
descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {},
name: String? = null,
read: suspend D.(propertyName: String) -> String?,
write: suspend D.(propertyName: String, value: String) -> Unit
): PropertyDelegateProvider<CompositeControlComponentSpec<D>, ReadOnlyProperty<CompositeControlComponentSpec<D>, MutableDevicePropertySpec<D, String>>> =
mutableProperty(MetaConverter.string, descriptorBuilder, name, read, write)
/**
* Creates a read-only meta property for a device.
*/
public fun <D : Device> CompositeControlComponentSpec<D>.meta(
descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {},
name: String? = null,
read: suspend D.(propertyName: String) -> Meta?,
): PropertyDelegateProvider<CompositeControlComponentSpec<D>, ReadOnlyProperty<CompositeControlComponentSpec<D>, DevicePropertySpec<D, Meta>>> =
property(MetaConverter.meta, descriptorBuilder, name, read)
/**
* Creates a mutable meta property for a device.
*/
public fun <D : Device> CompositeControlComponentSpec<D>.metaMutable(
descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {},
name: String? = null,
read: suspend D.(propertyName: String) -> Meta?,
write: suspend D.(propertyName: String, value: Meta) -> Unit
): PropertyDelegateProvider<CompositeControlComponentSpec<D>, ReadOnlyProperty<CompositeControlComponentSpec<D>, MutableDevicePropertySpec<D, Meta>>> =
mutableProperty(MetaConverter.meta, descriptorBuilder, name, read, write)
/**
* Creates a read-only enum property for a device.
*/
public fun <E : Enum<E>, D : Device> CompositeControlComponentSpec<D>.enum(
enumValues: Array<E>,
descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {},
name: String? = null,
read: suspend D.(propertyName: String) -> E?,
): PropertyDelegateProvider<CompositeControlComponentSpec<D>, ReadOnlyProperty<CompositeControlComponentSpec<D>, DevicePropertySpec<D, E>>> {
val converter = createEnumConverter(enumValues)
return property(converter, descriptorBuilder, name, read)
}
/**
* Creates a mutable enum property for a device.
*/
public fun <E : Enum<E>, D : Device> CompositeControlComponentSpec<D>.enumMutable(
enumValues: Array<E>,
descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {},
name: String? = null,
read: suspend D.(propertyName: String) -> E?,
write: suspend D.(propertyName: String, value: E) -> Unit
): PropertyDelegateProvider<CompositeControlComponentSpec<D>, ReadOnlyProperty<CompositeControlComponentSpec<D>, MutableDevicePropertySpec<D, E>>> {
val converter = createEnumConverter(enumValues)
return mutableProperty(converter, descriptorBuilder, name, read, write)
}
/**
* Creates a read-only float property for a device.
*/
public fun <D : Device> CompositeControlComponentSpec<D>.float(
descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {},
name: String? = null,
read: suspend D.(propertyName: String) -> Float?,
): PropertyDelegateProvider<CompositeControlComponentSpec<D>, ReadOnlyProperty<CompositeControlComponentSpec<D>, DevicePropertySpec<D, Float>>> =
property(MetaConverter.float, descriptorBuilder, name, read)
/**
* Creates a mutable float property for a device.
*/
public fun <D : Device> CompositeControlComponentSpec<D>.floatMutable(
descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {},
name: String? = null,
read: suspend D.(propertyName: String) -> Float?,
write: suspend D.(propertyName: String, value: Float) -> Unit
): PropertyDelegateProvider<CompositeControlComponentSpec<D>, ReadOnlyProperty<CompositeControlComponentSpec<D>, MutableDevicePropertySpec<D, Float>>> =
mutableProperty(MetaConverter.float, descriptorBuilder, name, read, write)
/**
* Creates a read-only long property for a device.
*/
public fun <D : Device> CompositeControlComponentSpec<D>.long(
descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {},
name: String? = null,
read: suspend D.(propertyName: String) -> Long?,
): PropertyDelegateProvider<CompositeControlComponentSpec<D>, ReadOnlyProperty<CompositeControlComponentSpec<D>, DevicePropertySpec<D, Long>>> =
property(MetaConverter.long, descriptorBuilder, name, read)
/**
* Creates a mutable long property for a device.
*/
public fun <D : Device> CompositeControlComponentSpec<D>.longMutable(
descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {},
name: String? = null,
read: suspend D.(propertyName: String) -> Long?,
write: suspend D.(propertyName: String, value: Long) -> Unit
): PropertyDelegateProvider<CompositeControlComponentSpec<D>, ReadOnlyProperty<CompositeControlComponentSpec<D>, MutableDevicePropertySpec<D, Long>>> =
mutableProperty(MetaConverter.long, descriptorBuilder, name, read, write)
/**
* Creates a read-only int property for a device.
*/
public fun <D : Device> CompositeControlComponentSpec<D>.int(
descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {},
name: String? = null,
read: suspend D.(propertyName: String) -> Int?,
): PropertyDelegateProvider<CompositeControlComponentSpec<D>, ReadOnlyProperty<CompositeControlComponentSpec<D>, DevicePropertySpec<D, Int>>> =
property(MetaConverter.int, descriptorBuilder, name, read)
/**
* Creates a mutable int property for a device.
*/
public fun <D : Device> CompositeControlComponentSpec<D>.intMutable(
descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {},
name: String? = null,
read: suspend D.(propertyName: String) -> Int?,
write: suspend D.(propertyName: String, value: Int) -> Unit
): PropertyDelegateProvider<CompositeControlComponentSpec<D>, ReadOnlyProperty<CompositeControlComponentSpec<D>, MutableDevicePropertySpec<D, Int>>> =
mutableProperty(MetaConverter.int, descriptorBuilder, name, read, write)
/**
* Creates a read-only list property for a device.
*/
public fun <T, D : Device> CompositeControlComponentSpec<D>.list(
converter: MetaConverter<List<T>>,
descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {},
name: String? = null,
read: suspend D.(propertyName: String) -> List<T>?,
): PropertyDelegateProvider<CompositeControlComponentSpec<D>, ReadOnlyProperty<CompositeControlComponentSpec<D>, DevicePropertySpec<D, List<T>>>> =
property(converter, descriptorBuilder, name, read)
/**
* Creates a mutable list property for a device.
*/
public fun <T, D : Device> CompositeControlComponentSpec<D>.listMutable(
converter: MetaConverter<List<T>>,
descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {},
name: String? = null,
read: suspend D.(propertyName: String) -> List<T>?,
write: suspend D.(propertyName: String, value: List<T>) -> Unit
): PropertyDelegateProvider<CompositeControlComponentSpec<D>, ReadOnlyProperty<CompositeControlComponentSpec<D>, MutableDevicePropertySpec<D, List<T>>>> =
mutableProperty(converter, descriptorBuilder, name, read, write)
public fun <I, O, D : Device> CompositeControlComponentSpec<D>.asyncActionProperty(
inputConverter: MetaConverter<I>,
outputConverter: MetaConverter<O>,
descriptorBuilder: ActionDescriptorBuilder.() -> Unit = {},
name: String? = null,
execute: suspend D.(I) -> Deferred<O>,
): PropertyDelegateProvider<CompositeControlComponentSpec<D>, ReadOnlyProperty<CompositeControlComponentSpec<D>, DeviceActionSpec<D, I, O>>> =
action(inputConverter, outputConverter, descriptorBuilder, name) { input ->
execute(input).await()
}
public fun <T, D : Device> CompositeControlComponentSpec<D>.metaProperty(
descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {},
name: String? = null,
read: suspend D.(propertyName: String) -> Meta?,
): PropertyDelegateProvider<CompositeControlComponentSpec<D>, ReadOnlyProperty<CompositeControlComponentSpec<D>, DevicePropertySpec<D, Meta>>> =
property(MetaConverter.meta, descriptorBuilder, name, read)
public fun <T, D : Device> CompositeControlComponentSpec<D>.mutableMetaProperty(
descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {},
name: String? = null,
read: suspend D.(propertyName: String) -> Meta?,
write: suspend D.(propertyName: String, value: Meta) -> Unit
): PropertyDelegateProvider<CompositeControlComponentSpec<D>, ReadOnlyProperty<CompositeControlComponentSpec<D>, MutableDevicePropertySpec<D, Meta>>> =
mutableProperty(MetaConverter.meta, descriptorBuilder, name, read, write)
/**
* An action that takes no parameters and returns no values
*/
public fun <T, D : Device> CompositeControlComponentSpec<D>.unitAction(
descriptorBuilder: ActionDescriptorBuilder.() -> Unit = {},
name: String? = null,
execute: suspend D.() -> Unit,
): PropertyDelegateProvider<CompositeControlComponentSpec<D>, ReadOnlyProperty<CompositeControlComponentSpec<D>, DeviceActionSpec<D, Unit, Unit>>> =
action(
MetaConverter.unit,
MetaConverter.unit,
descriptorBuilder,
name
) {
execute()
}
public fun <I, O, D : Device> CompositeControlComponentSpec<D>.asyncAction(
inputConverter: MetaConverter<I>,
outputConverter: MetaConverter<O>,
descriptorBuilder: ActionDescriptorBuilder.() -> Unit = {},
name: String? = null,
execute: suspend D.(I) -> Deferred<O>,
): PropertyDelegateProvider<CompositeControlComponentSpec<D>, ReadOnlyProperty<CompositeControlComponentSpec<D>, DeviceActionSpec<D, I, O>>> =
action(
inputConverter,
outputConverter,
descriptorBuilder,
name
) {
execute(it).await()
}
/**
* An action that takes [Meta] and returns [Meta]. No conversions are done
*/
public fun <T, D : Device> CompositeControlComponentSpec<D>.metaAction(
descriptorBuilder: ActionDescriptorBuilder.() -> Unit = {},
name: String? = null,
execute: suspend D.(Meta) -> Meta,
): PropertyDelegateProvider<CompositeControlComponentSpec<D>, ReadOnlyProperty<CompositeControlComponentSpec<D>, DeviceActionSpec<D, Meta, Meta>>> =
action(
MetaConverter.meta,
MetaConverter.meta,
descriptorBuilder,
name
) {
execute(it)
}
/**
* Throw an exception if device does not have all properties and actions defined by this specification
*/
public fun CompositeControlComponentSpec<*>.validate(device: Device) {
properties.map { it.value.descriptor }.forEach { specProperty ->
check(specProperty in device.propertyDescriptors) { "Property ${specProperty.name} not registered in ${device.id}" }
}
actions.map { it.value.descriptor }.forEach { specAction ->
check(specAction in device.actionDescriptors) { "Action ${specAction.name} not registered in ${device.id}" }
}
}

View File

@ -44,24 +44,24 @@ public abstract class DeviceSpec<D : Device> {
public fun <T> property(
converter: MetaConverter<T>,
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {},
name: String? = null,
read: suspend D.(propertyName: String) -> T?,
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DevicePropertySpec<D, T>>> =
PropertyDelegateProvider { _: DeviceSpec<D>, property ->
val propertyName = name ?: property.name
val deviceProperty = object : DevicePropertySpec<D, T> {
override val descriptor: PropertyDescriptor = PropertyDescriptor(propertyName).apply {
converter.descriptor?.let { converterDescriptor ->
metaDescriptor {
from(converterDescriptor)
}
val descriptor = propertyDescriptor(propertyName) {
mutable = false
converter.descriptor?.let { converterDescriptor ->
metaDescriptor {
from(converterDescriptor)
}
fromSpec(property)
descriptorBuilder()
}
fromSpec(property)
descriptorBuilder()
}
val deviceProperty = object : DevicePropertySpec<D, T> {
override val descriptor = descriptor
override val converter: MetaConverter<T> = converter
override suspend fun read(device: D): T? =
@ -75,26 +75,25 @@ public abstract class DeviceSpec<D : Device> {
public fun <T> mutableProperty(
converter: MetaConverter<T>,
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {},
name: String? = null,
read: suspend D.(propertyName: String) -> T?,
write: suspend D.(propertyName: String, value: T) -> Unit,
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, MutableDevicePropertySpec<D, T>>> =
PropertyDelegateProvider { _: DeviceSpec<D>, property: KProperty<*> ->
val propertyName = name ?: property.name
val deviceProperty = object : MutableDevicePropertySpec<D, T> {
override val descriptor: PropertyDescriptor = PropertyDescriptor(
propertyName,
mutable = true
).apply {
converter.descriptor?.let { converterDescriptor ->
metaDescriptor {
from(converterDescriptor)
}
val descriptor = propertyDescriptor(propertyName) {
this.mutable = true
converter.descriptor?.let { converterDescriptor ->
metaDescriptor {
from(converterDescriptor)
}
fromSpec(property)
descriptorBuilder()
}
fromSpec(property)
descriptorBuilder()
}
val deviceProperty = object : MutableDevicePropertySpec<D, T> {
override val descriptor = descriptor
override val converter: MetaConverter<T> = converter
override suspend fun read(device: D): T? =
@ -119,30 +118,28 @@ public abstract class DeviceSpec<D : Device> {
public fun <I, O> action(
inputConverter: MetaConverter<I>,
outputConverter: MetaConverter<O>,
descriptorBuilder: ActionDescriptor.() -> Unit = {},
descriptorBuilder: ActionDescriptorBuilder.() -> Unit = {},
name: String? = null,
execute: suspend D.(I) -> O,
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DeviceActionSpec<D, I, O>>> =
PropertyDelegateProvider { _: DeviceSpec<D>, property: KProperty<*> ->
val actionName = name ?: property.name
val deviceAction = object : DeviceActionSpec<D, I, O> {
override val descriptor: ActionDescriptor = ActionDescriptor(actionName).apply {
inputConverter.descriptor?.let { converterDescriptor ->
inputMetaDescriptor = MetaDescriptor {
from(converterDescriptor)
from(inputMetaDescriptor)
}
val descriptor = actionDescriptor(actionName) {
inputConverter.descriptor?.let { converterDescriptor ->
inputMeta {
from(converterDescriptor)
}
outputConverter.descriptor?.let { converterDescriptor ->
outputMetaDescriptor = MetaDescriptor {
from(converterDescriptor)
from(outputMetaDescriptor)
}
}
fromSpec(property)
descriptorBuilder()
}
outputConverter.descriptor?.let { converterDescriptor ->
outputMeta {
from(converterDescriptor)
}
}
fromSpec(property)
descriptorBuilder()
}
val deviceAction = object : DeviceActionSpec<D, I, O> {
override val descriptor: ActionDescriptor = descriptor
override val inputConverter: MetaConverter<I> = inputConverter
override val outputConverter: MetaConverter<O> = outputConverter
@ -162,7 +159,7 @@ public abstract class DeviceSpec<D : Device> {
* An action that takes no parameters and returns no values
*/
public fun <D : Device> DeviceSpec<D>.unitAction(
descriptorBuilder: ActionDescriptor.() -> Unit = {},
descriptorBuilder: ActionDescriptorBuilder.() -> Unit = {},
name: String? = null,
execute: suspend D.() -> Unit,
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DeviceActionSpec<D, Unit, Unit>>> =
@ -179,7 +176,7 @@ public fun <D : Device> DeviceSpec<D>.unitAction(
* An action that takes [Meta] and returns [Meta]. No conversions are done
*/
public fun <D : Device> DeviceSpec<D>.metaAction(
descriptorBuilder: ActionDescriptor.() -> Unit = {},
descriptorBuilder: ActionDescriptorBuilder.() -> Unit = {},
name: String? = null,
execute: suspend D.(Meta) -> Meta,
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DeviceActionSpec<D, Meta, Meta>>> =

View File

@ -1,10 +1,10 @@
package space.kscience.controls.spec
import space.kscience.controls.api.ActionDescriptor
import space.kscience.controls.api.PropertyDescriptor
import space.kscience.controls.api.ActionDescriptorBuilder
import space.kscience.controls.api.PropertyDescriptorBuilder
import kotlin.reflect.KProperty
internal expect fun PropertyDescriptor.fromSpec(property: KProperty<*>)
internal expect fun PropertyDescriptorBuilder.fromSpec(property: KProperty<*>)
internal expect fun ActionDescriptor.fromSpec(property: KProperty<*>)
internal expect fun ActionDescriptorBuilder.fromSpec(property: KProperty<*>)

View File

@ -1,8 +1,7 @@
package space.kscience.controls.spec
import space.kscience.controls.api.Device
import space.kscience.controls.api.PropertyDescriptor
import space.kscience.controls.api.metaDescriptor
import space.kscience.controls.api.PropertyDescriptorBuilder
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.MetaConverter
import space.kscience.dataforge.meta.ValueType
@ -17,7 +16,7 @@ import kotlin.reflect.KProperty1
public fun <T, D : Device> DeviceSpec<D>.property(
converter: MetaConverter<T>,
readOnlyProperty: KProperty1<D, T>,
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {},
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DevicePropertySpec<D, T>>> = property(
converter,
descriptorBuilder,
@ -31,7 +30,7 @@ public fun <T, D : Device> DeviceSpec<D>.property(
public fun <T, D : Device> DeviceSpec<D>.mutableProperty(
converter: MetaConverter<T>,
readWriteProperty: KMutableProperty1<D, T>,
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {},
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, MutableDevicePropertySpec<D, T>>> =
mutableProperty(
converter,
@ -49,7 +48,7 @@ public fun <T, D : Device> DeviceSpec<D>.mutableProperty(
*/
public fun <T, D : DeviceBase<D>> DeviceSpec<D>.property(
converter: MetaConverter<T>,
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {},
name: String? = null,
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DevicePropertySpec<D, T>>> =
property(
@ -60,7 +59,7 @@ public fun <T, D : DeviceBase<D>> DeviceSpec<D>.property(
)
public fun <D : Device> DeviceSpec<D>.booleanProperty(
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {},
name: String? = null,
read: suspend D.(propertyName: String) -> Boolean?
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DevicePropertySpec<D, Boolean>>> = property(
@ -76,8 +75,8 @@ public fun <D : Device> DeviceSpec<D>.booleanProperty(
)
private inline fun numberDescriptor(
crossinline descriptorBuilder: PropertyDescriptor.() -> Unit = {}
): PropertyDescriptor.() -> Unit = {
crossinline descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {}
): PropertyDescriptorBuilder.() -> Unit = {
metaDescriptor {
valueType(ValueType.NUMBER)
}
@ -85,7 +84,7 @@ private inline fun numberDescriptor(
}
public fun <D : Device> DeviceSpec<D>.numberProperty(
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {},
name: String? = null,
read: suspend D.(propertyName: String) -> Number?
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DevicePropertySpec<D, Number>>> = property(
@ -96,7 +95,7 @@ public fun <D : Device> DeviceSpec<D>.numberProperty(
)
public fun <D : Device> DeviceSpec<D>.doubleProperty(
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {},
name: String? = null,
read: suspend D.(propertyName: String) -> Double?
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DevicePropertySpec<D, Double>>> = property(
@ -107,7 +106,7 @@ public fun <D : Device> DeviceSpec<D>.doubleProperty(
)
public fun <D : Device> DeviceSpec<D>.stringProperty(
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {},
name: String? = null,
read: suspend D.(propertyName: String) -> String?
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DevicePropertySpec<D, String>>> = property(
@ -123,7 +122,7 @@ public fun <D : Device> DeviceSpec<D>.stringProperty(
)
public fun <D : Device> DeviceSpec<D>.metaProperty(
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {},
name: String? = null,
read: suspend D.(propertyName: String) -> Meta?
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DevicePropertySpec<D, Meta>>> = property(
@ -147,7 +146,7 @@ public fun <D : Device> DeviceSpec<D>.metaProperty(
*/
public fun <T, D : DeviceBase<D>> DeviceSpec<D>.mutableProperty(
converter: MetaConverter<T>,
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {},
name: String? = null,
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, MutableDevicePropertySpec<D, T>>> =
mutableProperty(
@ -159,7 +158,7 @@ public fun <T, D : DeviceBase<D>> DeviceSpec<D>.mutableProperty(
)
public fun <D : Device> DeviceSpec<D>.mutableBooleanProperty(
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {},
name: String? = null,
read: suspend D.(propertyName: String) -> Boolean?,
write: suspend D.(propertyName: String, value: Boolean) -> Unit
@ -179,7 +178,7 @@ public fun <D : Device> DeviceSpec<D>.mutableBooleanProperty(
public fun <D : Device> DeviceSpec<D>.mutableNumberProperty(
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {},
name: String? = null,
read: suspend D.(propertyName: String) -> Number,
write: suspend D.(propertyName: String, value: Number) -> Unit
@ -187,7 +186,7 @@ public fun <D : Device> DeviceSpec<D>.mutableNumberProperty(
mutableProperty(MetaConverter.number, numberDescriptor(descriptorBuilder), name, read, write)
public fun <D : Device> DeviceSpec<D>.mutableDoubleProperty(
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {},
name: String? = null,
read: suspend D.(propertyName: String) -> Double,
write: suspend D.(propertyName: String, value: Double) -> Unit
@ -195,7 +194,7 @@ public fun <D : Device> DeviceSpec<D>.mutableDoubleProperty(
mutableProperty(MetaConverter.double, numberDescriptor(descriptorBuilder), name, read, write)
public fun <D : Device> DeviceSpec<D>.mutableStringProperty(
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {},
name: String? = null,
read: suspend D.(propertyName: String) -> String,
write: suspend D.(propertyName: String, value: String) -> Unit
@ -203,7 +202,7 @@ public fun <D : Device> DeviceSpec<D>.mutableStringProperty(
mutableProperty(MetaConverter.string, descriptorBuilder, name, read, write)
public fun <D : Device> DeviceSpec<D>.mutableMetaProperty(
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {},
name: String? = null,
read: suspend D.(propertyName: String) -> Meta,
write: suspend D.(propertyName: String, value: Meta) -> Unit

View File

@ -1,9 +1,9 @@
package space.kscience.controls.spec
import space.kscience.controls.api.ActionDescriptor
import space.kscience.controls.api.PropertyDescriptor
import space.kscience.controls.api.ActionDescriptorBuilder
import space.kscience.controls.api.PropertyDescriptorBuilder
import kotlin.reflect.KProperty
internal actual fun PropertyDescriptor.fromSpec(property: KProperty<*>){}
internal actual fun PropertyDescriptorBuilder.fromSpec(property: KProperty<*>){}
internal actual fun ActionDescriptor.fromSpec(property: KProperty<*>){}
internal actual fun ActionDescriptorBuilder.fromSpec(property: KProperty<*>){}

View File

@ -1,18 +1,18 @@
package space.kscience.controls.spec
import space.kscience.controls.api.ActionDescriptor
import space.kscience.controls.api.PropertyDescriptor
import space.kscience.controls.api.ActionDescriptorBuilder
import space.kscience.controls.api.PropertyDescriptorBuilder
import space.kscience.dataforge.descriptors.Description
import kotlin.reflect.KProperty
import kotlin.reflect.full.findAnnotation
internal actual fun PropertyDescriptor.fromSpec(property: KProperty<*>) {
internal actual fun PropertyDescriptorBuilder.fromSpec(property: KProperty<*>) {
property.findAnnotation<Description>()?.let {
description = it.value
}
}
internal actual fun ActionDescriptor.fromSpec(property: KProperty<*>){
internal actual fun ActionDescriptorBuilder.fromSpec(property: KProperty<*>){
property.findAnnotation<Description>()?.let {
description = it.value
}

View File

@ -1,9 +1,9 @@
package space.kscience.controls.spec
import space.kscience.controls.api.ActionDescriptor
import space.kscience.controls.api.PropertyDescriptor
import space.kscience.controls.api.ActionDescriptorBuilder
import space.kscience.controls.api.PropertyDescriptorBuilder
import kotlin.reflect.KProperty
internal actual fun PropertyDescriptor.fromSpec(property: KProperty<*>) {}
internal actual fun PropertyDescriptorBuilder.fromSpec(property: KProperty<*>) {}
internal actual fun ActionDescriptor.fromSpec(property: KProperty<*>){}
internal actual fun ActionDescriptorBuilder.fromSpec(property: KProperty<*>){}

View File

@ -1,9 +1,9 @@
package space.kscience.controls.spec
import space.kscience.controls.api.ActionDescriptor
import space.kscience.controls.api.PropertyDescriptor
import space.kscience.controls.api.ActionDescriptorBuilder
import space.kscience.controls.api.PropertyDescriptorBuilder
import kotlin.reflect.KProperty
internal actual fun PropertyDescriptor.fromSpec(property: KProperty<*>){}
internal actual fun PropertyDescriptorBuilder.fromSpec(property: KProperty<*>){}
internal actual fun ActionDescriptor.fromSpec(property: KProperty<*>){}
internal actual fun ActionDescriptorBuilder.fromSpec(property: KProperty<*>){}

View File

@ -2,7 +2,6 @@ package space.kscience.controls.demo
import kotlinx.coroutines.launch
import space.kscience.controls.api.Device
import space.kscience.controls.api.metaDescriptor
import space.kscience.controls.spec.*
import space.kscience.dataforge.context.Context
import space.kscience.dataforge.context.Factory