diff --git a/build.gradle.kts b/build.gradle.kts index 57cb978..aed37b5 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -8,7 +8,7 @@ plugins { allprojects { group = "space.kscience" - version = "0.3.1-dev-1" + version = "0.4.0-dev-1" repositories{ maven("https://maven.pkg.jetbrains.space/spc/p/sci/dev") } diff --git a/controls-constructor/build.gradle.kts b/controls-constructor/build.gradle.kts index e62dd8e..e69c4d4 100644 --- a/controls-constructor/build.gradle.kts +++ b/controls-constructor/build.gradle.kts @@ -10,9 +10,14 @@ description = """ kscience{ jvm() js() - dependencies { + useCoroutines() + commonMain { api(projects.controlsCore) } + + commonTest{ + implementation(spclibs.logback.classic) + } } readme{ diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/ConstructorBinding.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/ConstructorBinding.kt new file mode 100644 index 0000000..7ad370e --- /dev/null +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/ConstructorBinding.kt @@ -0,0 +1,26 @@ +package space.kscience.controls.constructor + +import space.kscience.controls.api.Device + +public sealed interface ConstructorBinding + +/** + * A binding that exposes device property as read-only state + */ +public class PropertyBinding( + public val device: Device, + public val propertyName: String, + public val state: DeviceState, +) : ConstructorBinding + +/** + * A binding for independent state like a timer + */ +public class StateBinding( + public val state: DeviceState +) : ConstructorBinding + +public class ActionBinding( + public val reads: Collection>, + public val writes: Collection> +): ConstructorBinding \ No newline at end of file diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceConstructor.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceConstructor.kt index 63b5303..7824e3c 100644 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceConstructor.kt +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceConstructor.kt @@ -1,9 +1,16 @@ package space.kscience.controls.constructor +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import space.kscience.controls.api.Device import space.kscience.controls.api.PropertyDescriptor +import space.kscience.controls.manager.ClockManager +import space.kscience.controls.spec.DevicePropertySpec +import space.kscience.controls.spec.MutableDevicePropertySpec import space.kscience.dataforge.context.Context import space.kscience.dataforge.context.Factory +import space.kscience.dataforge.context.request import space.kscience.dataforge.meta.Meta import space.kscience.dataforge.meta.MetaConverter import space.kscience.dataforge.names.Name @@ -14,105 +21,154 @@ import kotlin.reflect.KProperty import kotlin.time.Duration /** - * A base for strongly typed device constructor blocks. Has additional delegates for type-safe devices + * A base for strongly typed device constructor block. Has additional delegates for type-safe devices */ public abstract class DeviceConstructor( context: Context, - meta: Meta, + meta: Meta = Meta.EMPTY, ) : DeviceGroup(context, meta) { + private val _bindings: MutableList = mutableListOf() + public val bindings: List get() = _bindings + + public fun registerBinding(binding: ConstructorBinding) { + _bindings.add(binding) + } + + override fun registerProperty(descriptor: PropertyDescriptor, state: DeviceState<*>) { + super.registerProperty(descriptor, state) + registerBinding(PropertyBinding(this, descriptor.name, state)) + } /** - * Register a device, provided by a given [factory] and + * Create and register a timer. Timer is not counted as a device property. */ - public fun device( - factory: Factory, - meta: Meta? = null, - nameOverride: Name? = null, - metaLocation: Name? = null, - ): PropertyDelegateProvider> = - PropertyDelegateProvider { _: DeviceConstructor, property: KProperty<*> -> - val name = nameOverride ?: property.name.asName() - val device = install(name, factory, meta, metaLocation ?: name) - ReadOnlyProperty { _: DeviceConstructor, _ -> - device - } + public fun timer(tick: Duration): TimerState = TimerState(context.request(ClockManager), tick) + .also { registerBinding(StateBinding(it)) } + + /** + * Launch action that is performed on each [DeviceState] value change. + * + * Optionally provide [writes] - a set of states that this change affects. + */ + public fun DeviceState.onChange( + vararg writes: DeviceState<*>, + reads: Collection>, + onChange: suspend (T) -> Unit, + ): Job = valueFlow.onEach(onChange).launchIn(this@DeviceConstructor).also { + registerBinding(ActionBinding(setOf(this, *reads.toTypedArray()), setOf(*writes))) + } +} + +/** + * Register a device, provided by a given [factory] and + */ +public fun DeviceConstructor.device( + factory: Factory, + meta: Meta? = null, + nameOverride: Name? = null, + metaLocation: Name? = null, +): PropertyDelegateProvider> = + PropertyDelegateProvider { _: DeviceConstructor, property: KProperty<*> -> + val name = nameOverride ?: property.name.asName() + val device = install(name, factory, meta, metaLocation ?: name) + ReadOnlyProperty { _: DeviceConstructor, _ -> + device } + } - public fun device( - device: D, - nameOverride: Name? = null, - ): PropertyDelegateProvider> = - PropertyDelegateProvider { _: DeviceConstructor, property: KProperty<*> -> - val name = nameOverride ?: property.name.asName() - install(name, device) - ReadOnlyProperty { _: DeviceConstructor, _ -> - device - } +public fun DeviceConstructor.device( + device: D, + nameOverride: Name? = null, +): PropertyDelegateProvider> = + PropertyDelegateProvider { _: DeviceConstructor, property: KProperty<*> -> + val name = nameOverride ?: property.name.asName() + install(name, device) + ReadOnlyProperty { _: DeviceConstructor, _ -> + device } + } - - /** - * Register a property and provide a direct reader for it - */ - public fun > property( - state: S, - descriptorBuilder: PropertyDescriptor.() -> Unit = {}, - nameOverride: String? = null, - ): PropertyDelegateProvider> = - PropertyDelegateProvider { _: DeviceConstructor, property -> - val name = nameOverride ?: property.name - val descriptor = PropertyDescriptor(name).apply(descriptorBuilder) - registerProperty(descriptor, state) - ReadOnlyProperty { _: DeviceConstructor, _ -> - state - } +/** + * Register a property and provide a direct reader for it + */ +public fun > DeviceConstructor.property( + state: S, + descriptorBuilder: PropertyDescriptor.() -> Unit = {}, + nameOverride: String? = null, +): PropertyDelegateProvider> = + PropertyDelegateProvider { _: DeviceConstructor, property -> + val name = nameOverride ?: property.name + val descriptor = PropertyDescriptor(name).apply(descriptorBuilder) + registerProperty(descriptor, state) + ReadOnlyProperty { _: DeviceConstructor, _ -> + state } + } - /** - * Register external state as a property - */ - public fun property( - metaConverter: MetaConverter, - reader: suspend () -> T, - readInterval: Duration, - initialState: T, - descriptorBuilder: PropertyDescriptor.() -> Unit = {}, - nameOverride: String? = null, - ): PropertyDelegateProvider>> = property( - DeviceState.external(this, metaConverter, readInterval, initialState, reader), - descriptorBuilder, - nameOverride, - ) +/** + * Register external state as a property + */ +public fun DeviceConstructor.property( + metaConverter: MetaConverter, + reader: suspend () -> T, + readInterval: Duration, + initialState: T, + descriptorBuilder: PropertyDescriptor.() -> Unit = {}, + nameOverride: String? = null, +): PropertyDelegateProvider>> = property( + DeviceState.external(this, metaConverter, readInterval, initialState, reader), + descriptorBuilder, + nameOverride, +) - /** - * Register a mutable external state as a property - */ - public fun mutableProperty( - metaConverter: MetaConverter, - reader: suspend () -> T, - writer: suspend (T) -> Unit, - readInterval: Duration, - initialState: T, - descriptorBuilder: PropertyDescriptor.() -> Unit = {}, - nameOverride: String? = null, - ): PropertyDelegateProvider>> = property( - DeviceState.external(this, metaConverter, readInterval, initialState, reader, writer), - descriptorBuilder, - nameOverride, - ) +/** + * Register a mutable external state as a property + */ +public fun DeviceConstructor.mutableProperty( + metaConverter: MetaConverter, + reader: suspend () -> T, + writer: suspend (T) -> Unit, + readInterval: Duration, + initialState: T, + descriptorBuilder: PropertyDescriptor.() -> Unit = {}, + nameOverride: String? = null, +): PropertyDelegateProvider>> = property( + DeviceState.external(this, metaConverter, readInterval, initialState, reader, writer), + descriptorBuilder, + nameOverride, +) - /** - * Create and register a virtual mutable property with optional [callback] - */ - public fun virtualProperty( - metaConverter: MetaConverter, - initialState: T, - descriptorBuilder: PropertyDescriptor.() -> Unit = {}, - nameOverride: String? = null, - callback: (T) -> Unit = {}, - ): PropertyDelegateProvider>> = property( - DeviceState.virtual(metaConverter, initialState, callback), - descriptorBuilder, - nameOverride, - ) -} \ No newline at end of file +/** + * Create and register a virtual mutable property with optional [callback] + */ +public fun DeviceConstructor.virtualProperty( + metaConverter: MetaConverter, + initialState: T, + descriptorBuilder: PropertyDescriptor.() -> Unit = {}, + nameOverride: String? = null, + callback: (T) -> Unit = {}, +): PropertyDelegateProvider>> = property( + DeviceState.internal(metaConverter, initialState, callback), + descriptorBuilder, + nameOverride, +) + +/** + * Bind existing property provided by specification to this device + */ +public fun DeviceConstructor.deviceProperty( + device: D, + property: DevicePropertySpec, + initialValue: T, +): PropertyDelegateProvider>> = + property(device.propertyAsState(property, initialValue)) + +/** + * Bind existing property provided by specification to this device + */ +public fun DeviceConstructor.deviceProperty( + device: D, + property: MutableDevicePropertySpec, + initialValue: T, +): PropertyDelegateProvider>> = + property(device.mutablePropertyAsState(property, initialValue)) \ No newline at end of file diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceGroup.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceGroup.kt index ca05c27..6785b4b 100644 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceGroup.kt +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceGroup.kt @@ -9,9 +9,7 @@ import space.kscience.controls.api.* import space.kscience.controls.api.DeviceLifecycleState.* import space.kscience.controls.manager.DeviceManager import space.kscience.controls.manager.install -import space.kscience.dataforge.context.Context -import space.kscience.dataforge.context.Factory -import space.kscience.dataforge.context.request +import space.kscience.dataforge.context.* import space.kscience.dataforge.meta.* import space.kscience.dataforge.misc.DFExperimental import space.kscience.dataforge.names.* @@ -57,6 +55,7 @@ public open class DeviceGroup( ) ) } + logger.error(throwable) { "Exception in device $id" } } ) @@ -69,7 +68,7 @@ public open class DeviceGroup( * Register and initialize (synchronize child's lifecycle state with group state) a new device in this group */ @OptIn(DFExperimental::class) - public fun install(token: NameToken, device: D): D { + public open fun install(token: NameToken, device: D): D { require(_devices[token] == null) { "A child device with name $token already exists" } //start the child device if needed if (lifecycleState == STARTED || lifecycleState == STARTING) launch { device.start() } @@ -82,7 +81,7 @@ public open class DeviceGroup( /** * Register a new property based on [DeviceState]. Properties could be modified dynamically */ - public fun registerProperty(descriptor: PropertyDescriptor, state: DeviceState<*>) { + public open fun registerProperty(descriptor: PropertyDescriptor, state: DeviceState<*>) { val name = descriptor.name.parseAsName() require(properties[name] == null) { "Can't add property with name $name. It already exists." } properties[name] = Property(state, descriptor) @@ -126,37 +125,33 @@ public open class DeviceGroup( return action.invoke(argument) } - @DFExperimental - override var lifecycleState: DeviceLifecycleState = STOPPED - protected set(value) { - if (field != value) { - launch { - sharedMessageFlow.emit( - DeviceLifeCycleMessage(value) - ) - } - } - field = value - } + final override var lifecycleState: DeviceLifecycleState = DeviceLifecycleState.STOPPED + private set + + + private suspend fun setLifecycleState(lifecycleState: DeviceLifecycleState) { + this.lifecycleState = lifecycleState + sharedMessageFlow.emit( + DeviceLifeCycleMessage(lifecycleState) + ) + } - @OptIn(DFExperimental::class) override suspend fun start() { - lifecycleState = STARTING + setLifecycleState(STARTING) super.start() devices.values.forEach { it.start() } - lifecycleState = STARTED + setLifecycleState(STARTED) } - @OptIn(DFExperimental::class) - override fun stop() { + override suspend fun stop() { devices.values.forEach { it.stop() } + setLifecycleState(STOPPED) super.stop() - lifecycleState = STOPPED } public companion object { @@ -210,13 +205,9 @@ public fun DeviceGroup.install(name: Name, device: D): D { } } -public fun DeviceGroup.install(name: String, device: D): D = - install(name.parseAsName(), device) +public fun DeviceGroup.install(name: String, device: D): D = install(name.parseAsName(), device) -public fun DeviceGroup.install(device: D): D = - install(device.id, device) - -public fun Context.install(name: String, device: D): D = request(DeviceManager).install(name, device) +public fun DeviceGroup.install(device: D): D = install(device.id, device) /** * Add a device creating intermediate groups if necessary. If device with given [name] already exists, throws an error. @@ -292,7 +283,7 @@ public fun DeviceGroup.registerVirtualProperty( descriptorBuilder: PropertyDescriptor.() -> Unit = {}, callback: (T) -> Unit = {}, ): MutableDeviceState { - val state = DeviceState.virtual(converter, initialValue, callback) + val state = DeviceState.internal(converter, initialValue, callback) registerMutableProperty(name, state, descriptorBuilder) return state } diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceState.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceState.kt index faa7b2f..9a25f00 100644 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceState.kt +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/DeviceState.kt @@ -2,18 +2,13 @@ package space.kscience.controls.constructor import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.* -import kotlinx.coroutines.launch -import space.kscience.controls.api.Device -import space.kscience.controls.api.PropertyChangedMessage -import space.kscience.controls.spec.DevicePropertySpec -import space.kscience.controls.spec.MutableDevicePropertySpec -import space.kscience.controls.spec.name +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach import space.kscience.dataforge.meta.Meta import space.kscience.dataforge.meta.MetaConverter import kotlin.reflect.KProperty -import kotlin.time.Duration /** * An observable state of a device @@ -24,6 +19,8 @@ public interface DeviceState { public val valueFlow: Flow + override fun toString(): String + public companion object } @@ -36,7 +33,7 @@ public operator fun DeviceState.getValue(thisRef: Any?, property: KProper /** * Collect values in a given [scope] */ -public fun DeviceState.collectValuesIn(scope: CoroutineScope, block: suspend (T)->Unit): Job = +public fun DeviceState.collectValuesIn(scope: CoroutineScope, block: suspend (T) -> Unit): Job = valueFlow.onEach(block).launchIn(scope) /** @@ -57,186 +54,46 @@ public var MutableDeviceState.valueAsMeta: Meta } /** - * A [MutableDeviceState] that does not correspond to a physical state - * - * @param callback a synchronous callback that could be used without a scope + * Device state with a value that depends on other device states */ -private class VirtualDeviceState( - override val converter: MetaConverter, - initialValue: T, - private val callback: (T) -> Unit = {}, -) : MutableDeviceState { - private val flow = MutableStateFlow(initialValue) - override val valueFlow: Flow get() = flow - - override var value: T - get() = flow.value - set(value) { - flow.value = value - callback(value) - } -} - - -/** - * A [MutableDeviceState] that does not correspond to a physical state - * - * @param callback a synchronous callback that could be used without a scope - */ -public fun DeviceState.Companion.virtual( - converter: MetaConverter, - initialValue: T, - callback: (T) -> Unit = {}, -): MutableDeviceState = VirtualDeviceState(converter, initialValue, callback) - -private class StateFlowAsState( - override val converter: MetaConverter, - val flow: MutableStateFlow, -) : MutableDeviceState { - override var value: T by flow::value - override val valueFlow: Flow get() = flow -} - -public fun MutableStateFlow.asDeviceState(converter: MetaConverter): DeviceState = - StateFlowAsState(converter, this) - - -private open class BoundDeviceState( - override val converter: MetaConverter, - val device: Device, - val propertyName: String, - initialValue: T, -) : DeviceState { - - override val valueFlow: StateFlow = device.messageFlow.filterIsInstance().filter { - it.property == propertyName - }.mapNotNull { - converter.read(it.value) - }.stateIn(device.context, SharingStarted.Eagerly, initialValue) - - override val value: T get() = valueFlow.value +public interface DeviceStateWithDependencies : DeviceState { + public val dependencies: Collection> } /** - * Bind a read-only [DeviceState] to a [Device] property + * Create a new read-only [DeviceState] that mirrors receiver state by mapping the value with [mapper]. */ -public suspend fun Device.propertyAsState( - propertyName: String, - metaConverter: MetaConverter, -): DeviceState { - val initialValue = metaConverter.readOrNull(readProperty(propertyName)) ?: error("Conversion of property failed") - return BoundDeviceState(metaConverter, this, propertyName, initialValue) -} - -public suspend fun D.propertyAsState( - propertySpec: DevicePropertySpec, -): DeviceState = propertyAsState(propertySpec.name, propertySpec.converter) - public fun DeviceState.map( converter: MetaConverter, mapper: (T) -> R, -): DeviceState = object : DeviceState { +): DeviceStateWithDependencies = object : DeviceStateWithDependencies { + override val dependencies = listOf(this) + override val converter: MetaConverter = converter + override val value: R get() = mapper(this@map.value) override val valueFlow: Flow = this@map.valueFlow.map(mapper) -} -private class MutableBoundDeviceState( - converter: MetaConverter, - device: Device, - propertyName: String, - initialValue: T, -) : BoundDeviceState(converter, device, propertyName, initialValue), MutableDeviceState { - - override var value: T - get() = valueFlow.value - set(newValue) { - device.launch { - device.writeProperty(propertyName, converter.convert(newValue)) - } - } -} - -public fun Device.mutablePropertyAsState( - propertyName: String, - metaConverter: MetaConverter, - initialValue: T, -): MutableDeviceState = MutableBoundDeviceState(metaConverter, this, propertyName, initialValue) - -public suspend fun Device.mutablePropertyAsState( - propertyName: String, - metaConverter: MetaConverter, -): MutableDeviceState { - val initialValue = metaConverter.readOrNull(readProperty(propertyName)) ?: error("Conversion of property failed") - return mutablePropertyAsState(propertyName, metaConverter, initialValue) -} - -public suspend fun D.mutablePropertyAsState( - propertySpec: MutableDevicePropertySpec, -): MutableDeviceState = mutablePropertyAsState(propertySpec.name, propertySpec.converter) - -public fun D.mutablePropertyAsState( - propertySpec: MutableDevicePropertySpec, - initialValue: T, -): MutableDeviceState = mutablePropertyAsState(propertySpec.name, propertySpec.converter, initialValue) - - -private open class ExternalState( - val scope: CoroutineScope, - override val converter: MetaConverter, - val readInterval: Duration, - initialValue: T, - val reader: suspend () -> T, -) : DeviceState { - - protected val flow: StateFlow = flow { - while (true) { - delay(readInterval) - emit(reader()) - } - }.stateIn(scope, SharingStarted.Eagerly, initialValue) - - override val value: T get() = flow.value - override val valueFlow: Flow get() = flow + override fun toString(): String = "DeviceState.map(arg=${this@map}, converter=$converter)" } /** - * Create a [DeviceState] which is constructed by periodically reading external value + * Combine two device states into one read-only [DeviceState]. Only the latest value of each state is used. */ -public fun DeviceState.Companion.external( - scope: CoroutineScope, - converter: MetaConverter, - readInterval: Duration, - initialValue: T, - reader: suspend () -> T, -): DeviceState = ExternalState(scope, converter, readInterval, initialValue, reader) +public fun combine( + state1: DeviceState, + state2: DeviceState, + converter: MetaConverter, + mapper: (T1, T2) -> R, +): DeviceStateWithDependencies = object : DeviceStateWithDependencies { + override val dependencies = listOf(state1, state2) -private class MutableExternalState( - scope: CoroutineScope, - converter: MetaConverter, - readInterval: Duration, - initialValue: T, - reader: suspend () -> T, - val writer: suspend (T) -> Unit, -) : ExternalState(scope, converter, readInterval, initialValue, reader), MutableDeviceState { - override var value: T - get() = super.value - set(value) { - scope.launch { - writer(value) - } - } + override val converter: MetaConverter = converter + + override val value: R get() = mapper(state1.value, state2.value) + + override val valueFlow: Flow = kotlinx.coroutines.flow.combine(state1.valueFlow, state2.valueFlow, mapper) + + override fun toString(): String = "DeviceState.combine(state1=$state1, state2=$state2)" } - -/** - * Create a [DeviceState] that regularly reads and caches an external value - */ -public fun DeviceState.Companion.external( - scope: CoroutineScope, - converter: MetaConverter, - readInterval: Duration, - initialValue: T, - reader: suspend () -> T, - writer: suspend (T) -> Unit, -): MutableDeviceState = MutableExternalState(scope, converter, readInterval, initialValue, reader, writer) \ No newline at end of file diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/TimerState.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/TimerState.kt new file mode 100644 index 0000000..7b10cb9 --- /dev/null +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/TimerState.kt @@ -0,0 +1,41 @@ +package space.kscience.controls.constructor + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import kotlinx.datetime.Instant +import space.kscience.controls.manager.ClockManager +import space.kscience.controls.spec.instant +import space.kscience.dataforge.meta.MetaConverter +import kotlin.time.Duration + +/** + * A dedicated [DeviceState] that operates with time. + * The state changes with [tick] interval and always shows the time of the last update. + * + * Both [tick] and current time are computed by [clockManager] enabling time manipulation. + * + * The timer runs indefinitely until the parent context is closed + */ +public class TimerState( + public val clockManager: ClockManager, + public val tick: Duration, +) : DeviceState { + override val converter: MetaConverter get() = MetaConverter.instant + + private val clock = MutableStateFlow(clockManager.clock.now()) + + private val updateJob = clockManager.context.launch { + while (isActive) { + clockManager.delay(tick) + clock.value = clockManager.clock.now() + } + } + + override val valueFlow: Flow get() = clock + + override val value: Instant get() = clock.value + + override fun toString(): String = "TimerState(tick=$tick)" +} \ No newline at end of file diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/boundState.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/boundState.kt new file mode 100644 index 0000000..37be770 --- /dev/null +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/boundState.kt @@ -0,0 +1,103 @@ +package space.kscience.controls.constructor + +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch +import space.kscience.controls.api.Device +import space.kscience.controls.api.PropertyChangedMessage +import space.kscience.controls.api.id +import space.kscience.controls.spec.DevicePropertySpec +import space.kscience.controls.spec.MutableDevicePropertySpec +import space.kscience.controls.spec.name +import space.kscience.dataforge.meta.MetaConverter + + +/** + * A copy-free [DeviceState] bound to a device property + */ +private open class BoundDeviceState( + override val converter: MetaConverter, + val device: Device, + val propertyName: String, + initialValue: T, +) : DeviceState { + + override val valueFlow: StateFlow = device.messageFlow.filterIsInstance().filter { + it.property == propertyName + }.mapNotNull { + converter.read(it.value) + }.stateIn(device.context, SharingStarted.Eagerly, initialValue) + + override val value: T get() = valueFlow.value + override fun toString(): String = + "BoundDeviceState(converter=$converter, device=${device.id}, propertyName='$propertyName')" + + +} + +public fun Device.propertyAsState( + propertyName: String, + metaConverter: MetaConverter, + initialValue: T, +): DeviceState = BoundDeviceState(metaConverter, this, propertyName, initialValue) + +/** + * Bind a read-only [DeviceState] to a [Device] property + */ +public suspend fun Device.propertyAsState( + propertyName: String, + metaConverter: MetaConverter, +): DeviceState = propertyAsState( + propertyName, + metaConverter, + metaConverter.readOrNull(readProperty(propertyName)) ?: error("Conversion of property failed") +) + +public suspend fun D.propertyAsState( + propertySpec: DevicePropertySpec, +): DeviceState = propertyAsState(propertySpec.name, propertySpec.converter) + +public fun D.propertyAsState( + propertySpec: DevicePropertySpec, + initialValue: T, +): DeviceState = propertyAsState(propertySpec.name, propertySpec.converter, initialValue) + + +private class MutableBoundDeviceState( + converter: MetaConverter, + device: Device, + propertyName: String, + initialValue: T, +) : BoundDeviceState(converter, device, propertyName, initialValue), MutableDeviceState { + + override var value: T + get() = valueFlow.value + set(newValue) { + device.launch { + device.writeProperty(propertyName, converter.convert(newValue)) + } + } +} + +public fun Device.mutablePropertyAsState( + propertyName: String, + metaConverter: MetaConverter, + initialValue: T, +): MutableDeviceState = MutableBoundDeviceState(metaConverter, this, propertyName, initialValue) + +public suspend fun Device.mutablePropertyAsState( + propertyName: String, + metaConverter: MetaConverter, +): MutableDeviceState { + val initialValue = metaConverter.readOrNull(readProperty(propertyName)) ?: error("Conversion of property failed") + return mutablePropertyAsState(propertyName, metaConverter, initialValue) +} + +public suspend fun D.mutablePropertyAsState( + propertySpec: MutableDevicePropertySpec, +): MutableDeviceState = mutablePropertyAsState(propertySpec.name, propertySpec.converter) + +public fun D.mutablePropertyAsState( + propertySpec: MutableDevicePropertySpec, + initialValue: T, +): MutableDeviceState = mutablePropertyAsState(propertySpec.name, propertySpec.converter, initialValue) + diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/customState.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/customState.kt index 100f755..881bdea 100644 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/customState.kt +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/customState.kt @@ -38,6 +38,10 @@ public class DoubleRangeState( * A state showing that the range is on its higher boundary */ public val atEndState: DeviceState = map(MetaConverter.boolean) { it >= range.endInclusive } + + override fun toString(): String = "DoubleRangeState(range=$range, converter=$converter)" + + } @Suppress("UnusedReceiverParameter") diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/externalState.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/externalState.kt new file mode 100644 index 0000000..c670a23 --- /dev/null +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/externalState.kt @@ -0,0 +1,70 @@ +package space.kscience.controls.constructor + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch +import space.kscience.dataforge.meta.MetaConverter +import kotlin.time.Duration + + +private open class ExternalState( + val scope: CoroutineScope, + override val converter: MetaConverter, + val readInterval: Duration, + initialValue: T, + val reader: suspend () -> T, +) : DeviceState { + + protected val flow: StateFlow = flow { + while (true) { + delay(readInterval) + emit(reader()) + } + }.stateIn(scope, SharingStarted.Eagerly, initialValue) + + override val value: T get() = flow.value + override val valueFlow: Flow get() = flow + + override fun toString(): String = "ExternalState(converter=$converter)" +} + +/** + * Create a [DeviceState] which is constructed by regularly reading external value + */ +public fun DeviceState.Companion.external( + scope: CoroutineScope, + converter: MetaConverter, + readInterval: Duration, + initialValue: T, + reader: suspend () -> T, +): DeviceState = ExternalState(scope, converter, readInterval, initialValue, reader) + +private class MutableExternalState( + scope: CoroutineScope, + converter: MetaConverter, + readInterval: Duration, + initialValue: T, + reader: suspend () -> T, + val writer: suspend (T) -> Unit, +) : ExternalState(scope, converter, readInterval, initialValue, reader), MutableDeviceState { + override var value: T + get() = super.value + set(value) { + scope.launch { + writer(value) + } + } +} + +/** + * Create a [MutableDeviceState] which is constructed by regularly reading external value and allows writing + */ +public fun DeviceState.Companion.external( + scope: CoroutineScope, + converter: MetaConverter, + readInterval: Duration, + initialValue: T, + reader: suspend () -> T, + writer: suspend (T) -> Unit, +): MutableDeviceState = MutableExternalState(scope, converter, readInterval, initialValue, reader, writer) \ No newline at end of file diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/flowState.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/flowState.kt new file mode 100644 index 0000000..434c074 --- /dev/null +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/flowState.kt @@ -0,0 +1,23 @@ +package space.kscience.controls.constructor + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import space.kscience.dataforge.meta.MetaConverter + + +private class StateFlowAsState( + override val converter: MetaConverter, + val flow: MutableStateFlow, +) : MutableDeviceState { + override var value: T by flow::value + override val valueFlow: Flow get() = flow + + override fun toString(): String = "FlowAsState(converter=$converter)" +} + +/** + * Create a read-only [DeviceState] that wraps [MutableStateFlow]. + * No data copy is performed. + */ +public fun MutableStateFlow.asDeviceState(converter: MetaConverter): DeviceState = + StateFlowAsState(converter, this) \ No newline at end of file diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/internalState.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/internalState.kt new file mode 100644 index 0000000..6f9d086 --- /dev/null +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/internalState.kt @@ -0,0 +1,42 @@ +package space.kscience.controls.constructor + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import space.kscience.dataforge.meta.MetaConverter + +/** + * A [MutableDeviceState] that does not correspond to a physical state + * + * @param callback a synchronous callback that could be used without a scope + */ +private class VirtualDeviceState( + override val converter: MetaConverter, + initialValue: T, + private val callback: (T) -> Unit = {}, +) : MutableDeviceState { + private val flow = MutableStateFlow(initialValue) + override val valueFlow: Flow get() = flow + + override var value: T + get() = flow.value + set(value) { + flow.value = value + callback(value) + } + + override fun toString(): String = "VirtualDeviceState(converter=$converter)" + + +} + + +/** + * A [MutableDeviceState] that does not correspond to a physical state + * + * @param callback a synchronous callback that could be used without a scope + */ +public fun DeviceState.Companion.internal( + converter: MetaConverter, + initialValue: T, + callback: (T) -> Unit = {}, +): MutableDeviceState = VirtualDeviceState(converter, initialValue, callback) diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/Drive.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/library/Drive.kt similarity index 91% rename from controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/Drive.kt rename to controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/library/Drive.kt index 8301622..5678b34 100644 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/Drive.kt +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/library/Drive.kt @@ -1,10 +1,12 @@ -package space.kscience.controls.constructor +package space.kscience.controls.constructor.library import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import space.kscience.controls.api.Device +import space.kscience.controls.constructor.MutableDeviceState +import space.kscience.controls.constructor.mutablePropertyAsState import space.kscience.controls.manager.clock import space.kscience.controls.spec.* import space.kscience.dataforge.context.Context @@ -49,7 +51,7 @@ public class VirtualDrive( public val positionState: MutableDeviceState, ) : Drive, DeviceBySpec(Drive, context) { - private val dt = meta["time.step"].double?.milliseconds ?: 1.milliseconds + private val dt = meta["time.step"].double?.milliseconds ?: 5.milliseconds private val clock = context.clock override var force: Double = 0.0 @@ -82,7 +84,7 @@ public class VirtualDrive( } } - override fun onStop() { + override suspend fun onStop() { updateJob?.cancel() } diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/LimitSwitch.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/library/LimitSwitch.kt similarity index 83% rename from controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/LimitSwitch.kt rename to controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/library/LimitSwitch.kt index 977930d..895cee8 100644 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/LimitSwitch.kt +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/library/LimitSwitch.kt @@ -1,8 +1,9 @@ -package space.kscience.controls.constructor +package space.kscience.controls.constructor.library import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import space.kscience.controls.api.Device +import space.kscience.controls.constructor.DeviceState import space.kscience.controls.spec.DeviceBySpec import space.kscience.controls.spec.DevicePropertySpec import space.kscience.controls.spec.DeviceSpec @@ -20,7 +21,7 @@ public interface LimitSwitch : Device { public companion object : DeviceSpec() { public val locked: DevicePropertySpec by booleanProperty { locked } - public fun factory(lockedState: DeviceState): Factory = Factory { context, _ -> + public operator fun invoke(lockedState: DeviceState): Factory = Factory { context, _ -> VirtualLimitSwitch(context, lockedState) } } diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/PidRegulator.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/library/PidRegulator.kt similarity index 83% rename from controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/PidRegulator.kt rename to controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/library/PidRegulator.kt index d782e05..7ce2054 100644 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/PidRegulator.kt +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/library/PidRegulator.kt @@ -1,4 +1,4 @@ -package space.kscience.controls.constructor +package space.kscience.controls.constructor.library import kotlinx.coroutines.Job import kotlinx.coroutines.delay @@ -7,8 +7,11 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlinx.datetime.Instant +import space.kscience.controls.constructor.DeviceGroup +import space.kscience.controls.constructor.install import space.kscience.controls.manager.clock import space.kscience.controls.spec.DeviceBySpec +import space.kscience.controls.spec.write import kotlin.time.Duration import kotlin.time.Duration.Companion.milliseconds import kotlin.time.DurationUnit @@ -71,15 +74,17 @@ public class PidRegulator( lastTime = realTime lastPosition = drive.position - drive.force = pidParameters.kp * delta + pidParameters.ki * integral + pidParameters.kd * derivative + drive.write(Drive.force,pidParameters.kp * delta + pidParameters.ki * integral + pidParameters.kd * derivative) + //drive.force = pidParameters.kp * delta + pidParameters.ki * integral + pidParameters.kd * derivative propertyChanged(Regulator.position, drive.position) } } } } - override fun onStop() { + override suspend fun onStop() { updateJob?.cancel() + drive.stop() } override val position: Double get() = drive.position diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/Regulator.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/library/Regulator.kt similarity index 92% rename from controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/Regulator.kt rename to controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/library/Regulator.kt index cbe6967..adf319b 100644 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/Regulator.kt +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/library/Regulator.kt @@ -1,4 +1,4 @@ -package space.kscience.controls.constructor +package space.kscience.controls.constructor.library import space.kscience.controls.api.Device import space.kscience.controls.spec.* diff --git a/controls-constructor/src/commonTest/kotlin/space/kscience/controls/constructor/DeviceGroupTest.kt b/controls-constructor/src/commonTest/kotlin/space/kscience/controls/constructor/DeviceGroupTest.kt new file mode 100644 index 0000000..40f00dc --- /dev/null +++ b/controls-constructor/src/commonTest/kotlin/space/kscience/controls/constructor/DeviceGroupTest.kt @@ -0,0 +1,43 @@ +package space.kscience.controls.constructor + +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.test.runTest +import space.kscience.controls.api.Device +import space.kscience.controls.api.DeviceLifeCycleMessage +import space.kscience.controls.api.DeviceLifecycleState +import space.kscience.controls.manager.DeviceManager +import space.kscience.controls.manager.install +import space.kscience.controls.spec.doRecurring +import space.kscience.dataforge.context.Context +import space.kscience.dataforge.context.Factory +import space.kscience.dataforge.context.Global +import space.kscience.dataforge.context.request +import space.kscience.dataforge.meta.Meta +import kotlin.test.Test +import kotlin.time.Duration.Companion.milliseconds + +class DeviceGroupTest { + + class TestDevice(context: Context) : DeviceConstructor(context) { + + companion object : Factory { + override fun build(context: Context, meta: Meta): Device = TestDevice(context) + } + } + + @Test + fun testRecurringRead() = runTest { + var counter = 10 + val testDevice = Global.request(DeviceManager).install("test", TestDevice) + testDevice.doRecurring(1.milliseconds) { + counter-- + println(counter) + if (counter <= 0) { + testDevice.stop() + } + error("Error!") + } + testDevice.messageFlow.first { it is DeviceLifeCycleMessage && it.state == DeviceLifecycleState.STOPPED } + println("stopped") + } +} \ No newline at end of file diff --git a/controls-constructor/src/commonTest/kotlin/space/kscience/controls/constructor/TimerTest.kt b/controls-constructor/src/commonTest/kotlin/space/kscience/controls/constructor/TimerTest.kt new file mode 100644 index 0000000..323f8df --- /dev/null +++ b/controls-constructor/src/commonTest/kotlin/space/kscience/controls/constructor/TimerTest.kt @@ -0,0 +1,22 @@ +package space.kscience.controls.constructor + +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.take +import kotlinx.coroutines.test.runTest +import space.kscience.controls.manager.ClockManager +import space.kscience.dataforge.context.Global +import space.kscience.dataforge.context.request +import kotlin.test.Test +import kotlin.time.Duration.Companion.milliseconds + +class TimerTest { + + @Test + fun timer() = runTest { + val timer = TimerState(Global.request(ClockManager), 10.milliseconds) + timer.valueFlow.take(10).onEach { + println(it) + }.collect() + } +} \ No newline at end of file diff --git a/controls-core/src/commonMain/kotlin/space/kscience/controls/api/Device.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/api/Device.kt index 3226422..fc68bf6 100644 --- a/controls-core/src/commonMain/kotlin/space/kscience/controls/api/Device.kt +++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/api/Device.kt @@ -12,7 +12,6 @@ import space.kscience.dataforge.context.logger import space.kscience.dataforge.meta.Meta import space.kscience.dataforge.meta.get import space.kscience.dataforge.meta.string -import space.kscience.dataforge.misc.DFExperimental import space.kscience.dataforge.misc.DfType import space.kscience.dataforge.names.parseAsName @@ -100,12 +99,11 @@ public interface Device : ContextAware, CoroutineScope { /** * Close and terminate the device. This function does not wait for the device to be closed. */ - public fun stop() { + public suspend fun stop() { logger.info { "Device $this is closed" } cancel("The device is closed") } - @DFExperimental public val lifecycleState: DeviceLifecycleState public companion object { diff --git a/controls-core/src/commonMain/kotlin/space/kscience/controls/api/descriptors.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/api/descriptors.kt index 2adb89a..f3ffa93 100644 --- a/controls-core/src/commonMain/kotlin/space/kscience/controls/api/descriptors.kt +++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/api/descriptors.kt @@ -15,18 +15,23 @@ public class PropertyDescriptor( public var description: String? = null, public var metaDescriptor: MetaDescriptor = MetaDescriptor(), public var readable: Boolean = true, - public var mutable: Boolean = false + public var mutable: Boolean = false, ) -public fun PropertyDescriptor.metaDescriptor(block: MetaDescriptorBuilder.()->Unit){ - metaDescriptor = MetaDescriptor(block) +public fun PropertyDescriptor.metaDescriptor(block: MetaDescriptorBuilder.() -> Unit) { + metaDescriptor = MetaDescriptor { + from(metaDescriptor) + block() + } } /** * A descriptor for property */ @Serializable -public class ActionDescriptor(public val name: String) { - public var description: String? = null -} - +public class ActionDescriptor( + public val name: String, + public var description: String? = null, + public var inputMetaDescriptor: MetaDescriptor = MetaDescriptor(), + public var outputMetaDescriptor: MetaDescriptor = MetaDescriptor() +) diff --git a/controls-core/src/commonMain/kotlin/space/kscience/controls/manager/ClockManager.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/manager/ClockManager.kt index ca69b91..2f852e1 100644 --- a/controls-core/src/commonMain/kotlin/space/kscience/controls/manager/ClockManager.kt +++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/manager/ClockManager.kt @@ -6,15 +6,21 @@ import space.kscience.dataforge.context.Context import space.kscience.dataforge.context.PluginFactory import space.kscience.dataforge.context.PluginTag import space.kscience.dataforge.meta.Meta +import kotlin.time.Duration public class ClockManager : AbstractPlugin() { - override val tag: PluginTag get() = DeviceManager.tag + override val tag: PluginTag get() = Companion.tag public val clock: Clock by lazy { //TODO add clock customization Clock.System } + public suspend fun delay(duration: Duration) { + //TODO add time compression + kotlinx.coroutines.delay(duration) + } + public companion object : PluginFactory { override val tag: PluginTag = PluginTag("clock", group = PluginTag.DATAFORGE_GROUP) diff --git a/controls-core/src/commonMain/kotlin/space/kscience/controls/manager/DeviceManager.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/manager/DeviceManager.kt index c8c7ffc..93b0696 100644 --- a/controls-core/src/commonMain/kotlin/space/kscience/controls/manager/DeviceManager.kt +++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/manager/DeviceManager.kt @@ -49,6 +49,10 @@ public fun DeviceManager.install(name: String, device: D): D { public fun DeviceManager.install(device: D): D = install(device.id, device) +public fun Context.install(name: String, device: D): D = request(DeviceManager).install(name, device) + +public fun Context.install(device: D): D = request(DeviceManager).install(device.id, device) + /** * Register and start a device built by [factory] with current [Context] and [meta]. */ @@ -75,5 +79,4 @@ public inline fun DeviceManager.installing( current as D } } -} - +} \ No newline at end of file diff --git a/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/DeviceBase.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/DeviceBase.kt index e8b9210..941eda9 100644 --- a/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/DeviceBase.kt +++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/DeviceBase.kt @@ -9,11 +9,11 @@ import kotlinx.coroutines.sync.withLock import space.kscience.controls.api.* import space.kscience.dataforge.context.Context import space.kscience.dataforge.context.debug +import space.kscience.dataforge.context.error import space.kscience.dataforge.context.logger import space.kscience.dataforge.meta.Meta import space.kscience.dataforge.meta.get import space.kscience.dataforge.meta.int -import space.kscience.dataforge.misc.DFExperimental import kotlin.coroutines.CoroutineContext /** @@ -72,10 +72,10 @@ public abstract class DeviceBase( onBufferOverflow = BufferOverflow.DROP_OLDEST ) - @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class) + @OptIn(ExperimentalCoroutinesApi::class) override val coroutineContext: CoroutineContext = context.newCoroutineContext( SupervisorJob(context.coroutineContext[Job]) + - CoroutineName("Device $this") + + CoroutineName("Device $id") + CoroutineExceptionHandler { _, throwable -> launch { sharedMessageFlow.emit( @@ -86,6 +86,7 @@ public abstract class DeviceBase( ) ) } + logger.error(throwable) { "Exception in device $id" } } ) @@ -187,43 +188,39 @@ public abstract class DeviceBase( return spec.executeWithMeta(self, argument ?: Meta.EMPTY) } - @DFExperimental final override var lifecycleState: DeviceLifecycleState = DeviceLifecycleState.STOPPED - private set(value) { - if (field != value) { - launch { - sharedMessageFlow.emit( - DeviceLifeCycleMessage(value) - ) - } - } - field = value - } + private set + + + private suspend fun setLifecycleState(lifecycleState: DeviceLifecycleState) { + this.lifecycleState = lifecycleState + sharedMessageFlow.emit( + DeviceLifeCycleMessage(lifecycleState) + ) + } protected open suspend fun onStart() { } - @OptIn(DFExperimental::class) final override suspend fun start() { if (lifecycleState == DeviceLifecycleState.STOPPED) { super.start() - lifecycleState = DeviceLifecycleState.STARTING + setLifecycleState(DeviceLifecycleState.STARTING) onStart() - lifecycleState = DeviceLifecycleState.STARTED + setLifecycleState(DeviceLifecycleState.STARTED) } else { logger.debug { "Device $this is already started" } } } - protected open fun onStop() { + protected open suspend fun onStop() { } - @OptIn(DFExperimental::class) - final override fun stop() { + final override suspend fun stop() { onStop() - lifecycleState = DeviceLifecycleState.STOPPED + setLifecycleState(DeviceLifecycleState.STOPPED) super.stop() } diff --git a/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/DeviceBySpec.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/DeviceBySpec.kt index 31f4c09..f639fc7 100644 --- a/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/DeviceBySpec.kt +++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/DeviceBySpec.kt @@ -20,7 +20,7 @@ public open class DeviceBySpec( self.onOpen() } - override fun onStop(): Unit = with(spec){ + override suspend fun onStop(): Unit = with(spec){ self.onClose() } diff --git a/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/DeviceSpec.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/DeviceSpec.kt index 2f90cb8..07d831f 100644 --- a/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/DeviceSpec.kt +++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/DeviceSpec.kt @@ -4,8 +4,10 @@ import kotlinx.coroutines.withContext import space.kscience.controls.api.ActionDescriptor import space.kscience.controls.api.Device import space.kscience.controls.api.PropertyDescriptor +import space.kscience.controls.api.metaDescriptor import space.kscience.dataforge.meta.Meta import space.kscience.dataforge.meta.MetaConverter +import space.kscience.dataforge.meta.descriptors.MetaDescriptor import kotlin.properties.PropertyDelegateProvider import kotlin.properties.ReadOnlyProperty import kotlin.reflect.KProperty @@ -54,6 +56,11 @@ public abstract class DeviceSpec { val deviceProperty = object : DevicePropertySpec { override val descriptor: PropertyDescriptor = PropertyDescriptor(propertyName).apply { + converter.descriptor?.let { converterDescriptor -> + metaDescriptor { + from(converterDescriptor) + } + } fromSpec(property) descriptorBuilder() } @@ -83,6 +90,11 @@ public abstract class DeviceSpec { propertyName, mutable = true ).apply { + converter.descriptor?.let { converterDescriptor -> + metaDescriptor { + from(converterDescriptor) + } + } fromSpec(property) descriptorBuilder() } @@ -118,6 +130,19 @@ public abstract class DeviceSpec { val actionName = name ?: property.name val deviceAction = object : DeviceActionSpec { override val descriptor: ActionDescriptor = ActionDescriptor(actionName).apply { + inputConverter.descriptor?.let { converterDescriptor -> + inputMetaDescriptor = MetaDescriptor { + from(converterDescriptor) + from(inputMetaDescriptor) + } + } + outputConverter.descriptor?.let { converterDescriptor -> + outputMetaDescriptor = MetaDescriptor { + from(converterDescriptor) + from(outputMetaDescriptor) + } + } + fromSpec(property) descriptorBuilder() } diff --git a/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/converters.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/converters.kt new file mode 100644 index 0000000..89a28da --- /dev/null +++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/converters.kt @@ -0,0 +1,41 @@ +package space.kscience.controls.spec + +import kotlinx.datetime.Instant +import space.kscience.dataforge.meta.* +import kotlin.time.Duration +import kotlin.time.DurationUnit +import kotlin.time.toDuration + +public fun Double.asMeta(): Meta = Meta(asValue()) + +/** + * Generate a nullable [MetaConverter] from non-nullable one + */ +public fun MetaConverter.nullable(): MetaConverter = object : MetaConverter { + override fun convert(obj: T?): Meta = obj?.let { this@nullable.convert(it) }?: Meta(Null) + + override fun readOrNull(source: Meta): T? = if(source.value == Null) null else this@nullable.readOrNull(source) + +} + +//TODO to be moved to DF +private object DurationConverter : MetaConverter { + override fun readOrNull(source: Meta): Duration = source.value?.double?.toDuration(DurationUnit.SECONDS) + ?: run { + val unit: DurationUnit = source["unit"].enum() ?: DurationUnit.SECONDS + val value = source[Meta.VALUE_KEY].double ?: error("No value present for Duration") + return@run value.toDuration(unit) + } + + override fun convert(obj: Duration): Meta = obj.toDouble(DurationUnit.SECONDS).asMeta() +} + +public val MetaConverter.Companion.duration: MetaConverter get() = DurationConverter + + +private object InstantConverter : MetaConverter { + override fun readOrNull(source: Meta): Instant? = source.string?.let { Instant.parse(it) } + override fun convert(obj: Instant): Meta = Meta(obj.toString()) +} + +public val MetaConverter.Companion.instant: MetaConverter get() = InstantConverter \ No newline at end of file diff --git a/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/deviceExtensions.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/deviceExtensions.kt index fefb65e..ee47ea0 100644 --- a/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/deviceExtensions.kt +++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/deviceExtensions.kt @@ -1,14 +1,31 @@ package space.kscience.controls.spec -import kotlinx.coroutines.Job -import kotlinx.coroutines.delay +import kotlinx.coroutines.* import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.isActive -import kotlinx.coroutines.launch import space.kscience.controls.api.Device import kotlin.time.Duration +/** + * Do a recurring (with a fixed delay) task on a device. + */ +public fun D.doRecurring( + interval: Duration, + debugTaskName: String? = null, + task: suspend D.() -> Unit, +): Job { + val taskName = debugTaskName ?: "task[${task.hashCode().toString(16)}]" + return launch(CoroutineName(taskName)) { + while (isActive) { + delay(interval) + //launch in parent scope to properly evaluate exceptions + this@doRecurring.launch { + task() + } + } + } +} + /** * Perform a recurring asynchronous read action and return a flow of results. * The flow is lazy, so action is not performed unless flow is consumed. @@ -16,23 +33,12 @@ import kotlin.time.Duration * * The flow is canceled when the device scope is canceled */ -public fun D.readRecurring(interval: Duration, reader: suspend D.() -> R): Flow = flow { - while (isActive) { - delay(interval) - launch { - emit(reader()) - } - } -} - -/** - * Do a recurring (with a fixed delay) task on a device. - */ -public fun D.doRecurring(interval: Duration, task: suspend D.() -> Unit): Job = launch { - while (isActive) { - delay(interval) - launch { - task() - } +public fun D.readRecurring( + interval: Duration, + debugTaskName: String? = null, + reader: suspend D.() -> R, +): Flow = flow { + doRecurring(interval, debugTaskName) { + emit(reader()) } } \ No newline at end of file diff --git a/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/fromSpec.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/fromSpec.kt index 000458e..ad5862b 100644 --- a/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/fromSpec.kt +++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/fromSpec.kt @@ -4,8 +4,6 @@ import space.kscience.controls.api.ActionDescriptor import space.kscience.controls.api.PropertyDescriptor import kotlin.reflect.KProperty -@Target(AnnotationTarget.CLASS, AnnotationTarget.PROPERTY, AnnotationTarget.FIELD) -public annotation class Description(val content: String) internal expect fun PropertyDescriptor.fromSpec(property: KProperty<*>) diff --git a/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/misc.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/misc.kt deleted file mode 100644 index 1fc4649..0000000 --- a/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/misc.kt +++ /dev/null @@ -1,22 +0,0 @@ -package space.kscience.controls.spec - -import space.kscience.dataforge.meta.* -import kotlin.time.Duration -import kotlin.time.DurationUnit -import kotlin.time.toDuration - -public fun Double.asMeta(): Meta = Meta(asValue()) - -//TODO to be moved to DF -public object DurationConverter : MetaConverter { - override fun readOrNull(source: Meta): Duration = source.value?.double?.toDuration(DurationUnit.SECONDS) - ?: run { - val unit: DurationUnit = source["unit"].enum() ?: DurationUnit.SECONDS - val value = source[Meta.VALUE_KEY].double ?: error("No value present for Duration") - return@run value.toDuration(unit) - } - - override fun convert(obj: Duration): Meta = obj.toDouble(DurationUnit.SECONDS).asMeta() -} - -public val MetaConverter.Companion.duration: MetaConverter get() = DurationConverter \ No newline at end of file diff --git a/controls-core/src/jvmMain/kotlin/space/kscience/controls/spec/fromSpec.jvm.kt b/controls-core/src/jvmMain/kotlin/space/kscience/controls/spec/fromSpec.jvm.kt index 7ae572f..cb64fc3 100644 --- a/controls-core/src/jvmMain/kotlin/space/kscience/controls/spec/fromSpec.jvm.kt +++ b/controls-core/src/jvmMain/kotlin/space/kscience/controls/spec/fromSpec.jvm.kt @@ -2,17 +2,18 @@ package space.kscience.controls.spec import space.kscience.controls.api.ActionDescriptor import space.kscience.controls.api.PropertyDescriptor +import space.kscience.dataforge.descriptors.Description import kotlin.reflect.KProperty import kotlin.reflect.full.findAnnotation internal actual fun PropertyDescriptor.fromSpec(property: KProperty<*>) { property.findAnnotation()?.let { - description = it.content + description = it.value } } internal actual fun ActionDescriptor.fromSpec(property: KProperty<*>){ property.findAnnotation()?.let { - description = it.content + description = it.value } } \ No newline at end of file diff --git a/controls-modbus/build.gradle.kts b/controls-modbus/build.gradle.kts index 2f280b6..8ef9ca8 100644 --- a/controls-modbus/build.gradle.kts +++ b/controls-modbus/build.gradle.kts @@ -1,7 +1,7 @@ import space.kscience.gradle.Maturity plugins { - id("space.kscience.gradle.jvm") + id("space.kscience.gradle.mpp") `maven-publish` } @@ -9,10 +9,12 @@ description = """ A plugin for Controls-kt device server on top of modbus-rtu/modbus-tcp protocols """.trimIndent() - -dependencies { - api(projects.controlsCore) - api(libs.j2mod) +kscience { + jvm() + jvmMain { + api(projects.controlsCore) + api(libs.j2mod) + } } readme{ diff --git a/controls-modbus/src/main/kotlin/space/kscience/controls/modbus/DeviceProcessImage.kt b/controls-modbus/src/jvmMain/kotlin/space/kscience/controls/modbus/DeviceProcessImage.kt similarity index 100% rename from controls-modbus/src/main/kotlin/space/kscience/controls/modbus/DeviceProcessImage.kt rename to controls-modbus/src/jvmMain/kotlin/space/kscience/controls/modbus/DeviceProcessImage.kt diff --git a/controls-modbus/src/main/kotlin/space/kscience/controls/modbus/ModbusDevice.kt b/controls-modbus/src/jvmMain/kotlin/space/kscience/controls/modbus/ModbusDevice.kt similarity index 100% rename from controls-modbus/src/main/kotlin/space/kscience/controls/modbus/ModbusDevice.kt rename to controls-modbus/src/jvmMain/kotlin/space/kscience/controls/modbus/ModbusDevice.kt diff --git a/controls-modbus/src/main/kotlin/space/kscience/controls/modbus/ModbusDeviceBySpec.kt b/controls-modbus/src/jvmMain/kotlin/space/kscience/controls/modbus/ModbusDeviceBySpec.kt similarity index 97% rename from controls-modbus/src/main/kotlin/space/kscience/controls/modbus/ModbusDeviceBySpec.kt rename to controls-modbus/src/jvmMain/kotlin/space/kscience/controls/modbus/ModbusDeviceBySpec.kt index 995e0df..e0bd47a 100644 --- a/controls-modbus/src/main/kotlin/space/kscience/controls/modbus/ModbusDeviceBySpec.kt +++ b/controls-modbus/src/jvmMain/kotlin/space/kscience/controls/modbus/ModbusDeviceBySpec.kt @@ -24,7 +24,7 @@ public open class ModbusDeviceBySpec( master.connect() } - override fun onStop() { + override suspend fun onStop() { if(disposeMasterOnClose){ master.disconnect() } diff --git a/controls-modbus/src/main/kotlin/space/kscience/controls/modbus/ModbusRegistryMap.kt b/controls-modbus/src/jvmMain/kotlin/space/kscience/controls/modbus/ModbusRegistryMap.kt similarity index 100% rename from controls-modbus/src/main/kotlin/space/kscience/controls/modbus/ModbusRegistryMap.kt rename to controls-modbus/src/jvmMain/kotlin/space/kscience/controls/modbus/ModbusRegistryMap.kt diff --git a/controls-opcua/src/main/kotlin/space/kscience/controls/opcua/client/OpcUaDeviceBySpec.kt b/controls-opcua/src/main/kotlin/space/kscience/controls/opcua/client/OpcUaDeviceBySpec.kt index 7debc0e..ea85719 100644 --- a/controls-opcua/src/main/kotlin/space/kscience/controls/opcua/client/OpcUaDeviceBySpec.kt +++ b/controls-opcua/src/main/kotlin/space/kscience/controls/opcua/client/OpcUaDeviceBySpec.kt @@ -63,7 +63,7 @@ public open class OpcUaDeviceBySpec( } } - override fun onStop() { + override suspend fun onStop() { client.disconnect() } } diff --git a/controls-pi/build.gradle.kts b/controls-pi/build.gradle.kts index 997fcf2..850173d 100644 --- a/controls-pi/build.gradle.kts +++ b/controls-pi/build.gradle.kts @@ -1,5 +1,5 @@ plugins { - id("space.kscience.gradle.jvm") + id("space.kscience.gradle.mpp") `maven-publish` } @@ -7,10 +7,15 @@ description = """ Utils to work with controls-kt on Raspberry pi """.trimIndent() -dependencies{ - api(project(":controls-core")) - api(libs.pi4j.ktx) // Kotlin DSL - api(libs.pi4j.core) - api(libs.pi4j.plugin.raspberrypi) - api(libs.pi4j.plugin.pigpio) +kscience { + jvm() + + + jvmMain { + api(project(":controls-core")) + api(libs.pi4j.ktx) // Kotlin DSL + api(libs.pi4j.core) + api(libs.pi4j.plugin.raspberrypi) + api(libs.pi4j.plugin.pigpio) + } } \ No newline at end of file diff --git a/controls-pi/src/main/kotlin/space/kscience/controls/pi/AsynchronousPiPort.kt b/controls-pi/src/jvmMain/kotlin/space/kscience/controls/pi/AsynchronousPiPort.kt similarity index 100% rename from controls-pi/src/main/kotlin/space/kscience/controls/pi/AsynchronousPiPort.kt rename to controls-pi/src/jvmMain/kotlin/space/kscience/controls/pi/AsynchronousPiPort.kt diff --git a/controls-pi/src/main/kotlin/space/kscience/controls/pi/PiPlugin.kt b/controls-pi/src/jvmMain/kotlin/space/kscience/controls/pi/PiPlugin.kt similarity index 100% rename from controls-pi/src/main/kotlin/space/kscience/controls/pi/PiPlugin.kt rename to controls-pi/src/jvmMain/kotlin/space/kscience/controls/pi/PiPlugin.kt diff --git a/controls-pi/src/main/kotlin/space/kscience/controls/pi/SynchronousPiPort.kt b/controls-pi/src/jvmMain/kotlin/space/kscience/controls/pi/SynchronousPiPort.kt similarity index 100% rename from controls-pi/src/main/kotlin/space/kscience/controls/pi/SynchronousPiPort.kt rename to controls-pi/src/jvmMain/kotlin/space/kscience/controls/pi/SynchronousPiPort.kt diff --git a/controls-ports-ktor/build.gradle.kts b/controls-ports-ktor/build.gradle.kts index 1f1efda..c225097 100644 --- a/controls-ports-ktor/build.gradle.kts +++ b/controls-ports-ktor/build.gradle.kts @@ -1,7 +1,7 @@ import space.kscience.gradle.Maturity plugins { - id("space.kscience.gradle.jvm") + id("space.kscience.gradle.mpp") `maven-publish` } @@ -9,9 +9,12 @@ description = """ Implementation of byte ports on top os ktor-io asynchronous API """.trimIndent() -dependencies { - api(projects.controlsCore) - api(spclibs.ktor.network) +kscience { + jvm() + jvmMain { + api(projects.controlsCore) + api(spclibs.ktor.network) + } } readme{ diff --git a/controls-ports-ktor/src/main/kotlin/space/kscience/controls/ports/KtorPortsPlugin.kt b/controls-ports-ktor/src/jvmMain/kotlin/space/kscience/controls/ports/KtorPortsPlugin.kt similarity index 100% rename from controls-ports-ktor/src/main/kotlin/space/kscience/controls/ports/KtorPortsPlugin.kt rename to controls-ports-ktor/src/jvmMain/kotlin/space/kscience/controls/ports/KtorPortsPlugin.kt diff --git a/controls-ports-ktor/src/main/kotlin/space/kscience/controls/ports/KtorTcpPort.kt b/controls-ports-ktor/src/jvmMain/kotlin/space/kscience/controls/ports/KtorTcpPort.kt similarity index 100% rename from controls-ports-ktor/src/main/kotlin/space/kscience/controls/ports/KtorTcpPort.kt rename to controls-ports-ktor/src/jvmMain/kotlin/space/kscience/controls/ports/KtorTcpPort.kt diff --git a/controls-ports-ktor/src/main/kotlin/space/kscience/controls/ports/KtorUdpPort.kt b/controls-ports-ktor/src/jvmMain/kotlin/space/kscience/controls/ports/KtorUdpPort.kt similarity index 100% rename from controls-ports-ktor/src/main/kotlin/space/kscience/controls/ports/KtorUdpPort.kt rename to controls-ports-ktor/src/jvmMain/kotlin/space/kscience/controls/ports/KtorUdpPort.kt diff --git a/controls-serial/build.gradle.kts b/controls-serial/build.gradle.kts index d44a029..75bf017 100644 --- a/controls-serial/build.gradle.kts +++ b/controls-serial/build.gradle.kts @@ -1,15 +1,18 @@ import space.kscience.gradle.Maturity plugins { - id("space.kscience.gradle.jvm") + id("space.kscience.gradle.mpp") `maven-publish` } description = "Implementation of direct serial port communication with JSerialComm" -dependencies{ - api(project(":controls-core")) - implementation(libs.jSerialComm) +kscience { + jvm() + jvmMain { + api(project(":controls-core")) + implementation(libs.jSerialComm) + } } readme{ diff --git a/controls-serial/src/main/kotlin/space/kscience/controls/serial/AsynchronousSerialPort.kt b/controls-serial/src/jvmMain/kotlin/space/kscience/controls/serial/AsynchronousSerialPort.kt similarity index 100% rename from controls-serial/src/main/kotlin/space/kscience/controls/serial/AsynchronousSerialPort.kt rename to controls-serial/src/jvmMain/kotlin/space/kscience/controls/serial/AsynchronousSerialPort.kt diff --git a/controls-serial/src/main/kotlin/space/kscience/controls/serial/SerialPortPlugin.kt b/controls-serial/src/jvmMain/kotlin/space/kscience/controls/serial/SerialPortPlugin.kt similarity index 100% rename from controls-serial/src/main/kotlin/space/kscience/controls/serial/SerialPortPlugin.kt rename to controls-serial/src/jvmMain/kotlin/space/kscience/controls/serial/SerialPortPlugin.kt diff --git a/controls-serial/src/main/kotlin/space/kscience/controls/serial/SynchronousSerialPort.kt b/controls-serial/src/jvmMain/kotlin/space/kscience/controls/serial/SynchronousSerialPort.kt similarity index 100% rename from controls-serial/src/main/kotlin/space/kscience/controls/serial/SynchronousSerialPort.kt rename to controls-serial/src/jvmMain/kotlin/space/kscience/controls/serial/SynchronousSerialPort.kt diff --git a/controls-storage/controls-xodus/build.gradle.kts b/controls-storage/controls-xodus/build.gradle.kts index 8cebd37..04be46a 100644 --- a/controls-storage/controls-xodus/build.gradle.kts +++ b/controls-storage/controls-xodus/build.gradle.kts @@ -1,5 +1,5 @@ plugins { - id("space.kscience.gradle.jvm") + id("space.kscience.gradle.mpp") `maven-publish` } @@ -7,13 +7,18 @@ description = """ An implementation of controls-storage on top of JetBrains Xodus. """.trimIndent() -dependencies { - api(projects.controlsStorage) - implementation(libs.xodus.entity.store) +kscience { + jvm() + jvmMain { + api(projects.controlsStorage) + implementation(libs.xodus.entity.store) // implementation("org.jetbrains.xodus:xodus-environment:$xodusVersion") // implementation("org.jetbrains.xodus:xodus-vfs:$xodusVersion") - testImplementation(spclibs.kotlinx.coroutines.test) + } + jvmTest{ + implementation(spclibs.kotlinx.coroutines.test) + } } readme{ diff --git a/controls-storage/controls-xodus/src/main/kotlin/space/kscience/controls/xodus/XodusDeviceMessageStorage.kt b/controls-storage/controls-xodus/src/jvmMain/kotlin/space/kscience/controls/xodus/XodusDeviceMessageStorage.kt similarity index 100% rename from controls-storage/controls-xodus/src/main/kotlin/space/kscience/controls/xodus/XodusDeviceMessageStorage.kt rename to controls-storage/controls-xodus/src/jvmMain/kotlin/space/kscience/controls/xodus/XodusDeviceMessageStorage.kt diff --git a/controls-storage/controls-xodus/src/test/kotlin/PropertyHistoryTest.kt b/controls-storage/controls-xodus/src/jvmTest/kotlin/PropertyHistoryTest.kt similarity index 100% rename from controls-storage/controls-xodus/src/test/kotlin/PropertyHistoryTest.kt rename to controls-storage/controls-xodus/src/jvmTest/kotlin/PropertyHistoryTest.kt diff --git a/demo/all-things/src/main/kotlin/space/kscience/controls/demo/DemoControllerView.kt b/demo/all-things/src/main/kotlin/space/kscience/controls/demo/DemoControllerView.kt index 2d2086b..f341256 100644 --- a/demo/all-things/src/main/kotlin/space/kscience/controls/demo/DemoControllerView.kt +++ b/demo/all-things/src/main/kotlin/space/kscience/controls/demo/DemoControllerView.kt @@ -8,6 +8,7 @@ import javafx.stage.Stage import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking import kotlinx.serialization.json.Json import org.eclipse.milo.opcua.sdk.server.OpcUaServer import org.eclipse.milo.opcua.stack.core.types.builtin.LocalizedText @@ -91,7 +92,7 @@ class DemoController : Controller(), ContextAware { } } - fun shutdown() { + suspend fun shutdown() { logger.info { "Shutting down..." } opcUaServer.shutdown() logger.info { "OpcUa server stopped" } @@ -179,7 +180,9 @@ class DemoControllerApp : App(DemoControllerView::class) { } override fun stop() { - controller.shutdown() + runBlocking { + controller.shutdown() + } super.stop() } } diff --git a/demo/car/src/main/kotlin/space/kscience/controls/demo/car/VirtualCarController.kt b/demo/car/src/main/kotlin/space/kscience/controls/demo/car/VirtualCarController.kt index 3a63003..e76cfe0 100644 --- a/demo/car/src/main/kotlin/space/kscience/controls/demo/car/VirtualCarController.kt +++ b/demo/car/src/main/kotlin/space/kscience/controls/demo/car/VirtualCarController.kt @@ -8,6 +8,7 @@ import javafx.scene.layout.Priority import javafx.stage.Stage import kotlinx.coroutines.Job import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking import space.kscience.controls.client.launchMagixService import space.kscience.controls.demo.car.IVirtualCar.Companion.acceleration import space.kscience.controls.manager.DeviceManager @@ -67,7 +68,7 @@ class VirtualCarController : Controller(), ContextAware { } } - fun shutdown() { + suspend fun shutdown() { logger.info { "Shutting down..." } magixServer?.stop(1000, 5000) logger.info { "Magix server stopped" } @@ -137,7 +138,9 @@ class VirtualCarControllerApp : App(VirtualCarControllerView::class) { } override fun stop() { - controller.shutdown() + runBlocking { + controller.shutdown() + } super.stop() } } diff --git a/demo/constructor/src/jvmMain/kotlin/main.kt b/demo/constructor/src/jvmMain/kotlin/main.kt index 9b06d2e..e426e1b 100644 --- a/demo/constructor/src/jvmMain/kotlin/main.kt +++ b/demo/constructor/src/jvmMain/kotlin/main.kt @@ -16,9 +16,11 @@ import androidx.compose.ui.window.Window import androidx.compose.ui.window.application import kotlinx.coroutines.launch import space.kscience.controls.constructor.* +import space.kscience.controls.constructor.library.* import space.kscience.controls.manager.ClockManager import space.kscience.controls.manager.DeviceManager import space.kscience.controls.manager.clock +import space.kscience.controls.manager.install import space.kscience.controls.spec.doRecurring import space.kscience.controls.spec.name import space.kscience.controls.vision.plot @@ -48,13 +50,14 @@ class LinearDrive( val drive by device(VirtualDrive.factory(mass, state)) val pid by device(PidRegulator(drive, pidParameters)) - val start by device(LimitSwitch.factory(state.atStartState)) - val end by device(LimitSwitch.factory(state.atEndState)) + val start by device(LimitSwitch(state.atStartState)) + val end by device(LimitSwitch(state.atEndState)) val positionState: DoubleRangeState by property(state) - private val targetState: MutableDeviceState by property(pid.mutablePropertyAsState(Regulator.target, 0.0)) - var target by targetState + + private val targetState: MutableDeviceState by deviceProperty(pid, Regulator.target, 0.0) + var target: Double by targetState } @@ -73,7 +76,6 @@ private fun Context.launchPidDevice( val timeFromStart = clock.now() - clockStart val t = timeFromStart.toDouble(DurationUnit.SECONDS) val freq = 0.1 - target = 5 * sin(2.0 * PI * freq * t) + sin(2 * PI * 21 * freq * t + 0.02 * (timeFromStart / pidParameters.timeStep)) } @@ -150,7 +152,7 @@ fun main() = application { Row { Text("kp:", Modifier.align(Alignment.CenterVertically).width(50.dp).padding(5.dp)) TextField( - String.format("%.2f",pidParameters.kp), + String.format("%.2f", pidParameters.kp), { pidParameters.kp = it.toDouble() }, Modifier.width(100.dp), enabled = false @@ -165,7 +167,7 @@ fun main() = application { Row { Text("ki:", Modifier.align(Alignment.CenterVertically).width(50.dp).padding(5.dp)) TextField( - String.format("%.2f",pidParameters.ki), + String.format("%.2f", pidParameters.ki), { pidParameters.ki = it.toDouble() }, Modifier.width(100.dp), enabled = false @@ -181,7 +183,7 @@ fun main() = application { Row { Text("kd:", Modifier.align(Alignment.CenterVertically).width(50.dp).padding(5.dp)) TextField( - String.format("%.2f",pidParameters.kd), + String.format("%.2f", pidParameters.kd), { pidParameters.kd = it.toDouble() }, Modifier.width(100.dp), enabled = false diff --git a/magix/magix-server/build.gradle.kts b/magix/magix-server/build.gradle.kts index f00b81e..28fe492 100644 --- a/magix/magix-server/build.gradle.kts +++ b/magix/magix-server/build.gradle.kts @@ -1,36 +1,38 @@ import space.kscience.gradle.Maturity plugins { - id("space.kscience.gradle.jvm") + id("space.kscience.gradle.mpp") `maven-publish` - application } description = """ A magix event loop implementation in Kotlin. Includes HTTP/SSE and RSocket routes. """.trimIndent() -kscience { - useSerialization{ - json() - } -} - val dataforgeVersion: String by rootProject.extra val ktorVersion: String = space.kscience.gradle.KScienceVersions.ktorVersion -dependencies{ - api(projects.magix.magixApi) - api("io.ktor:ktor-server-cio:$ktorVersion") - api("io.ktor:ktor-server-websockets:$ktorVersion") - api("io.ktor:ktor-server-content-negotiation:$ktorVersion") - api("io.ktor:ktor-serialization-kotlinx-json:$ktorVersion") - api("io.ktor:ktor-server-html-builder:$ktorVersion") +kscience { + jvm() + useSerialization{ + json() + } + + jvmMain{ + api(projects.magix.magixApi) + api("io.ktor:ktor-server-cio:$ktorVersion") + api("io.ktor:ktor-server-websockets:$ktorVersion") + api("io.ktor:ktor-server-content-negotiation:$ktorVersion") + api("io.ktor:ktor-serialization-kotlinx-json:$ktorVersion") + api("io.ktor:ktor-server-html-builder:$ktorVersion") + + api(libs.rsocket.ktor.server) + api(libs.rsocket.transport.ktor.tcp) + } - api(libs.rsocket.ktor.server) - api(libs.rsocket.transport.ktor.tcp) } + readme{ maturity = Maturity.EXPERIMENTAL } \ No newline at end of file diff --git a/magix/magix-server/src/main/kotlin/space/kscience/magix/server/RSocketMagixFlowPlugin.kt b/magix/magix-server/src/jvmMain/kotlin/space/kscience/magix/server/RSocketMagixFlowPlugin.kt similarity index 100% rename from magix/magix-server/src/main/kotlin/space/kscience/magix/server/RSocketMagixFlowPlugin.kt rename to magix/magix-server/src/jvmMain/kotlin/space/kscience/magix/server/RSocketMagixFlowPlugin.kt diff --git a/magix/magix-server/src/main/kotlin/space/kscience/magix/server/magixModule.kt b/magix/magix-server/src/jvmMain/kotlin/space/kscience/magix/server/magixModule.kt similarity index 100% rename from magix/magix-server/src/main/kotlin/space/kscience/magix/server/magixModule.kt rename to magix/magix-server/src/jvmMain/kotlin/space/kscience/magix/server/magixModule.kt diff --git a/magix/magix-server/src/main/kotlin/space/kscience/magix/server/server.kt b/magix/magix-server/src/jvmMain/kotlin/space/kscience/magix/server/server.kt similarity index 100% rename from magix/magix-server/src/main/kotlin/space/kscience/magix/server/server.kt rename to magix/magix-server/src/jvmMain/kotlin/space/kscience/magix/server/server.kt diff --git a/magix/magix-server/src/main/kotlin/space/kscience/magix/server/sse.kt b/magix/magix-server/src/jvmMain/kotlin/space/kscience/magix/server/sse.kt similarity index 100% rename from magix/magix-server/src/main/kotlin/space/kscience/magix/server/sse.kt rename to magix/magix-server/src/jvmMain/kotlin/space/kscience/magix/server/sse.kt