make device stop suspended to properly await for lifecycle event.

Add capabilities to Constructor
This commit is contained in:
Alexander Nozik 2024-05-10 11:33:00 +03:00
parent 4b05f46fa7
commit 381da970bf
59 changed files with 818 additions and 441 deletions

View File

@ -8,7 +8,7 @@ plugins {
allprojects { allprojects {
group = "space.kscience" group = "space.kscience"
version = "0.3.1-dev-1" version = "0.4.0-dev-1"
repositories{ repositories{
maven("https://maven.pkg.jetbrains.space/spc/p/sci/dev") maven("https://maven.pkg.jetbrains.space/spc/p/sci/dev")
} }

View File

@ -10,9 +10,14 @@ description = """
kscience{ kscience{
jvm() jvm()
js() js()
dependencies { useCoroutines()
commonMain {
api(projects.controlsCore) api(projects.controlsCore)
} }
commonTest{
implementation(spclibs.logback.classic)
}
} }
readme{ readme{

View File

@ -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<T>(
public val device: Device,
public val propertyName: String,
public val state: DeviceState<T>,
) : ConstructorBinding
/**
* A binding for independent state like a timer
*/
public class StateBinding<T>(
public val state: DeviceState<T>
) : ConstructorBinding
public class ActionBinding(
public val reads: Collection<DeviceState<*>>,
public val writes: Collection<DeviceState<*>>
): ConstructorBinding

View File

@ -1,9 +1,16 @@
package space.kscience.controls.constructor 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.Device
import space.kscience.controls.api.PropertyDescriptor 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.Context
import space.kscience.dataforge.context.Factory import space.kscience.dataforge.context.Factory
import space.kscience.dataforge.context.request
import space.kscience.dataforge.meta.Meta import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.MetaConverter import space.kscience.dataforge.meta.MetaConverter
import space.kscience.dataforge.names.Name import space.kscience.dataforge.names.Name
@ -14,105 +21,154 @@ import kotlin.reflect.KProperty
import kotlin.time.Duration 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( public abstract class DeviceConstructor(
context: Context, context: Context,
meta: Meta, meta: Meta = Meta.EMPTY,
) : DeviceGroup(context, meta) { ) : DeviceGroup(context, meta) {
private val _bindings: MutableList<ConstructorBinding> = mutableListOf()
public val bindings: List<ConstructorBinding> 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 <D : Device> device( public fun timer(tick: Duration): TimerState = TimerState(context.request(ClockManager), tick)
factory: Factory<D>, .also { registerBinding(StateBinding(it)) }
meta: Meta? = null,
nameOverride: Name? = null, /**
metaLocation: Name? = null, * Launch action that is performed on each [DeviceState] value change.
): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, D>> = *
PropertyDelegateProvider { _: DeviceConstructor, property: KProperty<*> -> * Optionally provide [writes] - a set of states that this change affects.
val name = nameOverride ?: property.name.asName() */
val device = install(name, factory, meta, metaLocation ?: name) public fun <T> DeviceState<T>.onChange(
ReadOnlyProperty { _: DeviceConstructor, _ -> vararg writes: DeviceState<*>,
device reads: Collection<DeviceState<*>>,
} 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 <D : Device> DeviceConstructor.device(
factory: Factory<D>,
meta: Meta? = null,
nameOverride: Name? = null,
metaLocation: Name? = null,
): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, D>> =
PropertyDelegateProvider { _: DeviceConstructor, property: KProperty<*> ->
val name = nameOverride ?: property.name.asName()
val device = install(name, factory, meta, metaLocation ?: name)
ReadOnlyProperty { _: DeviceConstructor, _ ->
device
} }
}
public fun <D : Device> device( public fun <D : Device> DeviceConstructor.device(
device: D, device: D,
nameOverride: Name? = null, nameOverride: Name? = null,
): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, D>> = ): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, D>> =
PropertyDelegateProvider { _: DeviceConstructor, property: KProperty<*> -> PropertyDelegateProvider { _: DeviceConstructor, property: KProperty<*> ->
val name = nameOverride ?: property.name.asName() val name = nameOverride ?: property.name.asName()
install(name, device) install(name, device)
ReadOnlyProperty { _: DeviceConstructor, _ -> ReadOnlyProperty { _: DeviceConstructor, _ ->
device device
}
} }
}
/**
/** * Register a property and provide a direct reader for it
* Register a property and provide a direct reader for it */
*/ public fun <T, S : DeviceState<T>> DeviceConstructor.property(
public fun <T, S: DeviceState<T>> property( state: S,
state: S, descriptorBuilder: PropertyDescriptor.() -> Unit = {},
descriptorBuilder: PropertyDescriptor.() -> Unit = {}, nameOverride: String? = null,
nameOverride: String? = null, ): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, S>> =
): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, S>> = PropertyDelegateProvider { _: DeviceConstructor, property ->
PropertyDelegateProvider { _: DeviceConstructor, property -> val name = nameOverride ?: property.name
val name = nameOverride ?: property.name val descriptor = PropertyDescriptor(name).apply(descriptorBuilder)
val descriptor = PropertyDescriptor(name).apply(descriptorBuilder) registerProperty(descriptor, state)
registerProperty(descriptor, state) ReadOnlyProperty { _: DeviceConstructor, _ ->
ReadOnlyProperty { _: DeviceConstructor, _ -> state
state
}
} }
}
/** /**
* Register external state as a property * Register external state as a property
*/ */
public fun <T : Any> property( public fun <T : Any> DeviceConstructor.property(
metaConverter: MetaConverter<T>, metaConverter: MetaConverter<T>,
reader: suspend () -> T, reader: suspend () -> T,
readInterval: Duration, readInterval: Duration,
initialState: T, initialState: T,
descriptorBuilder: PropertyDescriptor.() -> Unit = {}, descriptorBuilder: PropertyDescriptor.() -> Unit = {},
nameOverride: String? = null, nameOverride: String? = null,
): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, DeviceState<T>>> = property( ): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, DeviceState<T>>> = property(
DeviceState.external(this, metaConverter, readInterval, initialState, reader), DeviceState.external(this, metaConverter, readInterval, initialState, reader),
descriptorBuilder, descriptorBuilder,
nameOverride, nameOverride,
) )
/** /**
* Register a mutable external state as a property * Register a mutable external state as a property
*/ */
public fun <T : Any> mutableProperty( public fun <T : Any> DeviceConstructor.mutableProperty(
metaConverter: MetaConverter<T>, metaConverter: MetaConverter<T>,
reader: suspend () -> T, reader: suspend () -> T,
writer: suspend (T) -> Unit, writer: suspend (T) -> Unit,
readInterval: Duration, readInterval: Duration,
initialState: T, initialState: T,
descriptorBuilder: PropertyDescriptor.() -> Unit = {}, descriptorBuilder: PropertyDescriptor.() -> Unit = {},
nameOverride: String? = null, nameOverride: String? = null,
): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, MutableDeviceState<T>>> = property( ): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, MutableDeviceState<T>>> = property(
DeviceState.external(this, metaConverter, readInterval, initialState, reader, writer), DeviceState.external(this, metaConverter, readInterval, initialState, reader, writer),
descriptorBuilder, descriptorBuilder,
nameOverride, nameOverride,
) )
/** /**
* Create and register a virtual mutable property with optional [callback] * Create and register a virtual mutable property with optional [callback]
*/ */
public fun <T> virtualProperty( public fun <T> DeviceConstructor.virtualProperty(
metaConverter: MetaConverter<T>, metaConverter: MetaConverter<T>,
initialState: T, initialState: T,
descriptorBuilder: PropertyDescriptor.() -> Unit = {}, descriptorBuilder: PropertyDescriptor.() -> Unit = {},
nameOverride: String? = null, nameOverride: String? = null,
callback: (T) -> Unit = {}, callback: (T) -> Unit = {},
): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, MutableDeviceState<T>>> = property( ): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, MutableDeviceState<T>>> = property(
DeviceState.virtual(metaConverter, initialState, callback), DeviceState.internal(metaConverter, initialState, callback),
descriptorBuilder, descriptorBuilder,
nameOverride, nameOverride,
) )
}
/**
* Bind existing property provided by specification to this device
*/
public fun <T, D : Device> DeviceConstructor.deviceProperty(
device: D,
property: DevicePropertySpec<D, T>,
initialValue: T,
): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, DeviceState<T>>> =
property(device.propertyAsState(property, initialValue))
/**
* Bind existing property provided by specification to this device
*/
public fun <T, D : Device> DeviceConstructor.deviceProperty(
device: D,
property: MutableDevicePropertySpec<D, T>,
initialValue: T,
): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, MutableDeviceState<T>>> =
property(device.mutablePropertyAsState(property, initialValue))

View File

@ -9,9 +9,7 @@ import space.kscience.controls.api.*
import space.kscience.controls.api.DeviceLifecycleState.* import space.kscience.controls.api.DeviceLifecycleState.*
import space.kscience.controls.manager.DeviceManager import space.kscience.controls.manager.DeviceManager
import space.kscience.controls.manager.install import space.kscience.controls.manager.install
import space.kscience.dataforge.context.Context import space.kscience.dataforge.context.*
import space.kscience.dataforge.context.Factory
import space.kscience.dataforge.context.request
import space.kscience.dataforge.meta.* import space.kscience.dataforge.meta.*
import space.kscience.dataforge.misc.DFExperimental import space.kscience.dataforge.misc.DFExperimental
import space.kscience.dataforge.names.* 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 * Register and initialize (synchronize child's lifecycle state with group state) a new device in this group
*/ */
@OptIn(DFExperimental::class) @OptIn(DFExperimental::class)
public fun <D : Device> install(token: NameToken, device: D): D { public open fun <D : Device> install(token: NameToken, device: D): D {
require(_devices[token] == null) { "A child device with name $token already exists" } require(_devices[token] == null) { "A child device with name $token already exists" }
//start the child device if needed //start the child device if needed
if (lifecycleState == STARTED || lifecycleState == STARTING) launch { device.start() } 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 * 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() val name = descriptor.name.parseAsName()
require(properties[name] == null) { "Can't add property with name $name. It already exists." } require(properties[name] == null) { "Can't add property with name $name. It already exists." }
properties[name] = Property(state, descriptor) properties[name] = Property(state, descriptor)
@ -126,37 +125,33 @@ public open class DeviceGroup(
return action.invoke(argument) return action.invoke(argument)
} }
@DFExperimental final override var lifecycleState: DeviceLifecycleState = DeviceLifecycleState.STOPPED
override var lifecycleState: DeviceLifecycleState = STOPPED private set
protected set(value) {
if (field != value) {
launch { private suspend fun setLifecycleState(lifecycleState: DeviceLifecycleState) {
sharedMessageFlow.emit( this.lifecycleState = lifecycleState
DeviceLifeCycleMessage(value) sharedMessageFlow.emit(
) DeviceLifeCycleMessage(lifecycleState)
} )
} }
field = value
}
@OptIn(DFExperimental::class)
override suspend fun start() { override suspend fun start() {
lifecycleState = STARTING setLifecycleState(STARTING)
super.start() super.start()
devices.values.forEach { devices.values.forEach {
it.start() it.start()
} }
lifecycleState = STARTED setLifecycleState(STARTED)
} }
@OptIn(DFExperimental::class) override suspend fun stop() {
override fun stop() {
devices.values.forEach { devices.values.forEach {
it.stop() it.stop()
} }
setLifecycleState(STOPPED)
super.stop() super.stop()
lifecycleState = STOPPED
} }
public companion object { public companion object {
@ -210,13 +205,9 @@ public fun <D : Device> DeviceGroup.install(name: Name, device: D): D {
} }
} }
public fun <D : Device> DeviceGroup.install(name: String, device: D): D = public fun <D : Device> DeviceGroup.install(name: String, device: D): D = install(name.parseAsName(), device)
install(name.parseAsName(), device)
public fun <D : Device> DeviceGroup.install(device: D): D = public fun <D : Device> DeviceGroup.install(device: D): D = install(device.id, device)
install(device.id, device)
public fun <D : Device> Context.install(name: String, device: D): D = request(DeviceManager).install(name, device)
/** /**
* Add a device creating intermediate groups if necessary. If device with given [name] already exists, throws an error. * Add a device creating intermediate groups if necessary. If device with given [name] already exists, throws an error.
@ -292,7 +283,7 @@ public fun <T : Any> DeviceGroup.registerVirtualProperty(
descriptorBuilder: PropertyDescriptor.() -> Unit = {}, descriptorBuilder: PropertyDescriptor.() -> Unit = {},
callback: (T) -> Unit = {}, callback: (T) -> Unit = {},
): MutableDeviceState<T> { ): MutableDeviceState<T> {
val state = DeviceState.virtual<T>(converter, initialValue, callback) val state = DeviceState.internal<T>(converter, initialValue, callback)
registerMutableProperty(name, state, descriptorBuilder) registerMutableProperty(name, state, descriptorBuilder)
return state return state
} }

View File

@ -2,18 +2,13 @@ package space.kscience.controls.constructor
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.launch import kotlinx.coroutines.flow.map
import space.kscience.controls.api.Device import kotlinx.coroutines.flow.onEach
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 space.kscience.dataforge.meta.Meta import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.MetaConverter import space.kscience.dataforge.meta.MetaConverter
import kotlin.reflect.KProperty import kotlin.reflect.KProperty
import kotlin.time.Duration
/** /**
* An observable state of a device * An observable state of a device
@ -24,6 +19,8 @@ public interface DeviceState<T> {
public val valueFlow: Flow<T> public val valueFlow: Flow<T>
override fun toString(): String
public companion object public companion object
} }
@ -36,7 +33,7 @@ public operator fun <T> DeviceState<T>.getValue(thisRef: Any?, property: KProper
/** /**
* Collect values in a given [scope] * Collect values in a given [scope]
*/ */
public fun <T> DeviceState<T>.collectValuesIn(scope: CoroutineScope, block: suspend (T)->Unit): Job = public fun <T> DeviceState<T>.collectValuesIn(scope: CoroutineScope, block: suspend (T) -> Unit): Job =
valueFlow.onEach(block).launchIn(scope) valueFlow.onEach(block).launchIn(scope)
/** /**
@ -57,186 +54,46 @@ public var <T> MutableDeviceState<T>.valueAsMeta: Meta
} }
/** /**
* A [MutableDeviceState] that does not correspond to a physical state * Device state with a value that depends on other device states
*
* @param callback a synchronous callback that could be used without a scope
*/ */
private class VirtualDeviceState<T>( public interface DeviceStateWithDependencies<T> : DeviceState<T> {
override val converter: MetaConverter<T>, public val dependencies: Collection<DeviceState<*>>
initialValue: T,
private val callback: (T) -> Unit = {},
) : MutableDeviceState<T> {
private val flow = MutableStateFlow(initialValue)
override val valueFlow: Flow<T> 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 <T> DeviceState.Companion.virtual(
converter: MetaConverter<T>,
initialValue: T,
callback: (T) -> Unit = {},
): MutableDeviceState<T> = VirtualDeviceState(converter, initialValue, callback)
private class StateFlowAsState<T>(
override val converter: MetaConverter<T>,
val flow: MutableStateFlow<T>,
) : MutableDeviceState<T> {
override var value: T by flow::value
override val valueFlow: Flow<T> get() = flow
}
public fun <T> MutableStateFlow<T>.asDeviceState(converter: MetaConverter<T>): DeviceState<T> =
StateFlowAsState(converter, this)
private open class BoundDeviceState<T>(
override val converter: MetaConverter<T>,
val device: Device,
val propertyName: String,
initialValue: T,
) : DeviceState<T> {
override val valueFlow: StateFlow<T> = device.messageFlow.filterIsInstance<PropertyChangedMessage>().filter {
it.property == propertyName
}.mapNotNull {
converter.read(it.value)
}.stateIn(device.context, SharingStarted.Eagerly, initialValue)
override val value: T get() = valueFlow.value
} }
/** /**
* 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 <T> Device.propertyAsState(
propertyName: String,
metaConverter: MetaConverter<T>,
): DeviceState<T> {
val initialValue = metaConverter.readOrNull(readProperty(propertyName)) ?: error("Conversion of property failed")
return BoundDeviceState(metaConverter, this, propertyName, initialValue)
}
public suspend fun <D : Device, T> D.propertyAsState(
propertySpec: DevicePropertySpec<D, T>,
): DeviceState<T> = propertyAsState(propertySpec.name, propertySpec.converter)
public fun <T, R> DeviceState<T>.map( public fun <T, R> DeviceState<T>.map(
converter: MetaConverter<R>, mapper: (T) -> R, converter: MetaConverter<R>, mapper: (T) -> R,
): DeviceState<R> = object : DeviceState<R> { ): DeviceStateWithDependencies<R> = object : DeviceStateWithDependencies<R> {
override val dependencies = listOf(this)
override val converter: MetaConverter<R> = converter override val converter: MetaConverter<R> = converter
override val value: R override val value: R
get() = mapper(this@map.value) get() = mapper(this@map.value)
override val valueFlow: Flow<R> = this@map.valueFlow.map(mapper) override val valueFlow: Flow<R> = this@map.valueFlow.map(mapper)
}
private class MutableBoundDeviceState<T>( override fun toString(): String = "DeviceState.map(arg=${this@map}, converter=$converter)"
converter: MetaConverter<T>,
device: Device,
propertyName: String,
initialValue: T,
) : BoundDeviceState<T>(converter, device, propertyName, initialValue), MutableDeviceState<T> {
override var value: T
get() = valueFlow.value
set(newValue) {
device.launch {
device.writeProperty(propertyName, converter.convert(newValue))
}
}
}
public fun <T> Device.mutablePropertyAsState(
propertyName: String,
metaConverter: MetaConverter<T>,
initialValue: T,
): MutableDeviceState<T> = MutableBoundDeviceState(metaConverter, this, propertyName, initialValue)
public suspend fun <T> Device.mutablePropertyAsState(
propertyName: String,
metaConverter: MetaConverter<T>,
): MutableDeviceState<T> {
val initialValue = metaConverter.readOrNull(readProperty(propertyName)) ?: error("Conversion of property failed")
return mutablePropertyAsState(propertyName, metaConverter, initialValue)
}
public suspend fun <D : Device, T> D.mutablePropertyAsState(
propertySpec: MutableDevicePropertySpec<D, T>,
): MutableDeviceState<T> = mutablePropertyAsState(propertySpec.name, propertySpec.converter)
public fun <D : Device, T> D.mutablePropertyAsState(
propertySpec: MutableDevicePropertySpec<D, T>,
initialValue: T,
): MutableDeviceState<T> = mutablePropertyAsState(propertySpec.name, propertySpec.converter, initialValue)
private open class ExternalState<T>(
val scope: CoroutineScope,
override val converter: MetaConverter<T>,
val readInterval: Duration,
initialValue: T,
val reader: suspend () -> T,
) : DeviceState<T> {
protected val flow: StateFlow<T> = flow {
while (true) {
delay(readInterval)
emit(reader())
}
}.stateIn(scope, SharingStarted.Eagerly, initialValue)
override val value: T get() = flow.value
override val valueFlow: Flow<T> get() = flow
} }
/** /**
* 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 <T> DeviceState.Companion.external( public fun <T1, T2, R> combine(
scope: CoroutineScope, state1: DeviceState<T1>,
converter: MetaConverter<T>, state2: DeviceState<T2>,
readInterval: Duration, converter: MetaConverter<R>,
initialValue: T, mapper: (T1, T2) -> R,
reader: suspend () -> T, ): DeviceStateWithDependencies<R> = object : DeviceStateWithDependencies<R> {
): DeviceState<T> = ExternalState(scope, converter, readInterval, initialValue, reader) override val dependencies = listOf(state1, state2)
private class MutableExternalState<T>( override val converter: MetaConverter<R> = converter
scope: CoroutineScope,
converter: MetaConverter<T>, override val value: R get() = mapper(state1.value, state2.value)
readInterval: Duration,
initialValue: T, override val valueFlow: Flow<R> = kotlinx.coroutines.flow.combine(state1.valueFlow, state2.valueFlow, mapper)
reader: suspend () -> T,
val writer: suspend (T) -> Unit, override fun toString(): String = "DeviceState.combine(state1=$state1, state2=$state2)"
) : ExternalState<T>(scope, converter, readInterval, initialValue, reader), MutableDeviceState<T> {
override var value: T
get() = super.value
set(value) {
scope.launch {
writer(value)
}
}
} }
/**
* Create a [DeviceState] that regularly reads and caches an external value
*/
public fun <T> DeviceState.Companion.external(
scope: CoroutineScope,
converter: MetaConverter<T>,
readInterval: Duration,
initialValue: T,
reader: suspend () -> T,
writer: suspend (T) -> Unit,
): MutableDeviceState<T> = MutableExternalState(scope, converter, readInterval, initialValue, reader, writer)

View File

@ -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<Instant> {
override val converter: MetaConverter<Instant> 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<Instant> get() = clock
override val value: Instant get() = clock.value
override fun toString(): String = "TimerState(tick=$tick)"
}

View File

@ -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<T>(
override val converter: MetaConverter<T>,
val device: Device,
val propertyName: String,
initialValue: T,
) : DeviceState<T> {
override val valueFlow: StateFlow<T> = device.messageFlow.filterIsInstance<PropertyChangedMessage>().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 <T> Device.propertyAsState(
propertyName: String,
metaConverter: MetaConverter<T>,
initialValue: T,
): DeviceState<T> = BoundDeviceState(metaConverter, this, propertyName, initialValue)
/**
* Bind a read-only [DeviceState] to a [Device] property
*/
public suspend fun <T> Device.propertyAsState(
propertyName: String,
metaConverter: MetaConverter<T>,
): DeviceState<T> = propertyAsState(
propertyName,
metaConverter,
metaConverter.readOrNull(readProperty(propertyName)) ?: error("Conversion of property failed")
)
public suspend fun <D : Device, T> D.propertyAsState(
propertySpec: DevicePropertySpec<D, T>,
): DeviceState<T> = propertyAsState(propertySpec.name, propertySpec.converter)
public fun <D : Device, T> D.propertyAsState(
propertySpec: DevicePropertySpec<D, T>,
initialValue: T,
): DeviceState<T> = propertyAsState(propertySpec.name, propertySpec.converter, initialValue)
private class MutableBoundDeviceState<T>(
converter: MetaConverter<T>,
device: Device,
propertyName: String,
initialValue: T,
) : BoundDeviceState<T>(converter, device, propertyName, initialValue), MutableDeviceState<T> {
override var value: T
get() = valueFlow.value
set(newValue) {
device.launch {
device.writeProperty(propertyName, converter.convert(newValue))
}
}
}
public fun <T> Device.mutablePropertyAsState(
propertyName: String,
metaConverter: MetaConverter<T>,
initialValue: T,
): MutableDeviceState<T> = MutableBoundDeviceState(metaConverter, this, propertyName, initialValue)
public suspend fun <T> Device.mutablePropertyAsState(
propertyName: String,
metaConverter: MetaConverter<T>,
): MutableDeviceState<T> {
val initialValue = metaConverter.readOrNull(readProperty(propertyName)) ?: error("Conversion of property failed")
return mutablePropertyAsState(propertyName, metaConverter, initialValue)
}
public suspend fun <D : Device, T> D.mutablePropertyAsState(
propertySpec: MutableDevicePropertySpec<D, T>,
): MutableDeviceState<T> = mutablePropertyAsState(propertySpec.name, propertySpec.converter)
public fun <D : Device, T> D.mutablePropertyAsState(
propertySpec: MutableDevicePropertySpec<D, T>,
initialValue: T,
): MutableDeviceState<T> = mutablePropertyAsState(propertySpec.name, propertySpec.converter, initialValue)

View File

@ -38,6 +38,10 @@ public class DoubleRangeState(
* A state showing that the range is on its higher boundary * A state showing that the range is on its higher boundary
*/ */
public val atEndState: DeviceState<Boolean> = map(MetaConverter.boolean) { it >= range.endInclusive } public val atEndState: DeviceState<Boolean> = map(MetaConverter.boolean) { it >= range.endInclusive }
override fun toString(): String = "DoubleRangeState(range=$range, converter=$converter)"
} }
@Suppress("UnusedReceiverParameter") @Suppress("UnusedReceiverParameter")

View File

@ -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<T>(
val scope: CoroutineScope,
override val converter: MetaConverter<T>,
val readInterval: Duration,
initialValue: T,
val reader: suspend () -> T,
) : DeviceState<T> {
protected val flow: StateFlow<T> = flow {
while (true) {
delay(readInterval)
emit(reader())
}
}.stateIn(scope, SharingStarted.Eagerly, initialValue)
override val value: T get() = flow.value
override val valueFlow: Flow<T> get() = flow
override fun toString(): String = "ExternalState(converter=$converter)"
}
/**
* Create a [DeviceState] which is constructed by regularly reading external value
*/
public fun <T> DeviceState.Companion.external(
scope: CoroutineScope,
converter: MetaConverter<T>,
readInterval: Duration,
initialValue: T,
reader: suspend () -> T,
): DeviceState<T> = ExternalState(scope, converter, readInterval, initialValue, reader)
private class MutableExternalState<T>(
scope: CoroutineScope,
converter: MetaConverter<T>,
readInterval: Duration,
initialValue: T,
reader: suspend () -> T,
val writer: suspend (T) -> Unit,
) : ExternalState<T>(scope, converter, readInterval, initialValue, reader), MutableDeviceState<T> {
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 <T> DeviceState.Companion.external(
scope: CoroutineScope,
converter: MetaConverter<T>,
readInterval: Duration,
initialValue: T,
reader: suspend () -> T,
writer: suspend (T) -> Unit,
): MutableDeviceState<T> = MutableExternalState(scope, converter, readInterval, initialValue, reader, writer)

View File

@ -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<T>(
override val converter: MetaConverter<T>,
val flow: MutableStateFlow<T>,
) : MutableDeviceState<T> {
override var value: T by flow::value
override val valueFlow: Flow<T> get() = flow
override fun toString(): String = "FlowAsState(converter=$converter)"
}
/**
* Create a read-only [DeviceState] that wraps [MutableStateFlow].
* No data copy is performed.
*/
public fun <T> MutableStateFlow<T>.asDeviceState(converter: MetaConverter<T>): DeviceState<T> =
StateFlowAsState(converter, this)

View File

@ -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<T>(
override val converter: MetaConverter<T>,
initialValue: T,
private val callback: (T) -> Unit = {},
) : MutableDeviceState<T> {
private val flow = MutableStateFlow(initialValue)
override val valueFlow: Flow<T> 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 <T> DeviceState.Companion.internal(
converter: MetaConverter<T>,
initialValue: T,
callback: (T) -> Unit = {},
): MutableDeviceState<T> = VirtualDeviceState(converter, initialValue, callback)

View File

@ -1,10 +1,12 @@
package space.kscience.controls.constructor package space.kscience.controls.constructor.library
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import space.kscience.controls.api.Device 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.manager.clock
import space.kscience.controls.spec.* import space.kscience.controls.spec.*
import space.kscience.dataforge.context.Context import space.kscience.dataforge.context.Context
@ -49,7 +51,7 @@ public class VirtualDrive(
public val positionState: MutableDeviceState<Double>, public val positionState: MutableDeviceState<Double>,
) : Drive, DeviceBySpec<Drive>(Drive, context) { ) : Drive, DeviceBySpec<Drive>(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 private val clock = context.clock
override var force: Double = 0.0 override var force: Double = 0.0
@ -82,7 +84,7 @@ public class VirtualDrive(
} }
} }
override fun onStop() { override suspend fun onStop() {
updateJob?.cancel() updateJob?.cancel()
} }

View File

@ -1,8 +1,9 @@
package space.kscience.controls.constructor package space.kscience.controls.constructor.library
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import space.kscience.controls.api.Device import space.kscience.controls.api.Device
import space.kscience.controls.constructor.DeviceState
import space.kscience.controls.spec.DeviceBySpec import space.kscience.controls.spec.DeviceBySpec
import space.kscience.controls.spec.DevicePropertySpec import space.kscience.controls.spec.DevicePropertySpec
import space.kscience.controls.spec.DeviceSpec import space.kscience.controls.spec.DeviceSpec
@ -20,7 +21,7 @@ public interface LimitSwitch : Device {
public companion object : DeviceSpec<LimitSwitch>() { public companion object : DeviceSpec<LimitSwitch>() {
public val locked: DevicePropertySpec<LimitSwitch, Boolean> by booleanProperty { locked } public val locked: DevicePropertySpec<LimitSwitch, Boolean> by booleanProperty { locked }
public fun factory(lockedState: DeviceState<Boolean>): Factory<LimitSwitch> = Factory { context, _ -> public operator fun invoke(lockedState: DeviceState<Boolean>): Factory<LimitSwitch> = Factory { context, _ ->
VirtualLimitSwitch(context, lockedState) VirtualLimitSwitch(context, lockedState)
} }
} }

View File

@ -1,4 +1,4 @@
package space.kscience.controls.constructor package space.kscience.controls.constructor.library
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
@ -7,8 +7,11 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
import kotlinx.datetime.Instant 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.manager.clock
import space.kscience.controls.spec.DeviceBySpec import space.kscience.controls.spec.DeviceBySpec
import space.kscience.controls.spec.write
import kotlin.time.Duration import kotlin.time.Duration
import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.DurationUnit import kotlin.time.DurationUnit
@ -71,15 +74,17 @@ public class PidRegulator(
lastTime = realTime lastTime = realTime
lastPosition = drive.position 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) propertyChanged(Regulator.position, drive.position)
} }
} }
} }
} }
override fun onStop() { override suspend fun onStop() {
updateJob?.cancel() updateJob?.cancel()
drive.stop()
} }
override val position: Double get() = drive.position override val position: Double get() = drive.position

View File

@ -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.api.Device
import space.kscience.controls.spec.* import space.kscience.controls.spec.*

View File

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

View File

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

View File

@ -12,7 +12,6 @@ import space.kscience.dataforge.context.logger
import space.kscience.dataforge.meta.Meta import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.get import space.kscience.dataforge.meta.get
import space.kscience.dataforge.meta.string import space.kscience.dataforge.meta.string
import space.kscience.dataforge.misc.DFExperimental
import space.kscience.dataforge.misc.DfType import space.kscience.dataforge.misc.DfType
import space.kscience.dataforge.names.parseAsName 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. * 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" } logger.info { "Device $this is closed" }
cancel("The device is closed") cancel("The device is closed")
} }
@DFExperimental
public val lifecycleState: DeviceLifecycleState public val lifecycleState: DeviceLifecycleState
public companion object { public companion object {

View File

@ -15,18 +15,23 @@ public class PropertyDescriptor(
public var description: String? = null, public var description: String? = null,
public var metaDescriptor: MetaDescriptor = MetaDescriptor(), public var metaDescriptor: MetaDescriptor = MetaDescriptor(),
public var readable: Boolean = true, public var readable: Boolean = true,
public var mutable: Boolean = false public var mutable: Boolean = false,
) )
public fun PropertyDescriptor.metaDescriptor(block: MetaDescriptorBuilder.()->Unit){ public fun PropertyDescriptor.metaDescriptor(block: MetaDescriptorBuilder.() -> Unit) {
metaDescriptor = MetaDescriptor(block) metaDescriptor = MetaDescriptor {
from(metaDescriptor)
block()
}
} }
/** /**
* A descriptor for property * A descriptor for property
*/ */
@Serializable @Serializable
public class ActionDescriptor(public val name: String) { public class ActionDescriptor(
public var description: String? = null public val name: String,
} public var description: String? = null,
public var inputMetaDescriptor: MetaDescriptor = MetaDescriptor(),
public var outputMetaDescriptor: MetaDescriptor = MetaDescriptor()
)

View File

@ -6,15 +6,21 @@ import space.kscience.dataforge.context.Context
import space.kscience.dataforge.context.PluginFactory import space.kscience.dataforge.context.PluginFactory
import space.kscience.dataforge.context.PluginTag import space.kscience.dataforge.context.PluginTag
import space.kscience.dataforge.meta.Meta import space.kscience.dataforge.meta.Meta
import kotlin.time.Duration
public class ClockManager : AbstractPlugin() { public class ClockManager : AbstractPlugin() {
override val tag: PluginTag get() = DeviceManager.tag override val tag: PluginTag get() = Companion.tag
public val clock: Clock by lazy { public val clock: Clock by lazy {
//TODO add clock customization //TODO add clock customization
Clock.System Clock.System
} }
public suspend fun delay(duration: Duration) {
//TODO add time compression
kotlinx.coroutines.delay(duration)
}
public companion object : PluginFactory<ClockManager> { public companion object : PluginFactory<ClockManager> {
override val tag: PluginTag = PluginTag("clock", group = PluginTag.DATAFORGE_GROUP) override val tag: PluginTag = PluginTag("clock", group = PluginTag.DATAFORGE_GROUP)

View File

@ -49,6 +49,10 @@ public fun <D : Device> DeviceManager.install(name: String, device: D): D {
public fun <D : Device> DeviceManager.install(device: D): D = install(device.id, device) public fun <D : Device> DeviceManager.install(device: D): D = install(device.id, device)
public fun <D : Device> Context.install(name: String, device: D): D = request(DeviceManager).install(name, device)
public fun <D : Device> Context.install(device: D): D = request(DeviceManager).install(device.id, device)
/** /**
* Register and start a device built by [factory] with current [Context] and [meta]. * Register and start a device built by [factory] with current [Context] and [meta].
*/ */
@ -75,5 +79,4 @@ public inline fun <D : Device> DeviceManager.installing(
current as D current as D
} }
} }
} }

View File

@ -9,11 +9,11 @@ import kotlinx.coroutines.sync.withLock
import space.kscience.controls.api.* import space.kscience.controls.api.*
import space.kscience.dataforge.context.Context import space.kscience.dataforge.context.Context
import space.kscience.dataforge.context.debug import space.kscience.dataforge.context.debug
import space.kscience.dataforge.context.error
import space.kscience.dataforge.context.logger import space.kscience.dataforge.context.logger
import space.kscience.dataforge.meta.Meta import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.get import space.kscience.dataforge.meta.get
import space.kscience.dataforge.meta.int import space.kscience.dataforge.meta.int
import space.kscience.dataforge.misc.DFExperimental
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
/** /**
@ -72,10 +72,10 @@ public abstract class DeviceBase<D : Device>(
onBufferOverflow = BufferOverflow.DROP_OLDEST onBufferOverflow = BufferOverflow.DROP_OLDEST
) )
@OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
override val coroutineContext: CoroutineContext = context.newCoroutineContext( override val coroutineContext: CoroutineContext = context.newCoroutineContext(
SupervisorJob(context.coroutineContext[Job]) + SupervisorJob(context.coroutineContext[Job]) +
CoroutineName("Device $this") + CoroutineName("Device $id") +
CoroutineExceptionHandler { _, throwable -> CoroutineExceptionHandler { _, throwable ->
launch { launch {
sharedMessageFlow.emit( sharedMessageFlow.emit(
@ -86,6 +86,7 @@ public abstract class DeviceBase<D : Device>(
) )
) )
} }
logger.error(throwable) { "Exception in device $id" }
} }
) )
@ -187,43 +188,39 @@ public abstract class DeviceBase<D : Device>(
return spec.executeWithMeta(self, argument ?: Meta.EMPTY) return spec.executeWithMeta(self, argument ?: Meta.EMPTY)
} }
@DFExperimental
final override var lifecycleState: DeviceLifecycleState = DeviceLifecycleState.STOPPED final override var lifecycleState: DeviceLifecycleState = DeviceLifecycleState.STOPPED
private set(value) { private set
if (field != value) {
launch {
sharedMessageFlow.emit( private suspend fun setLifecycleState(lifecycleState: DeviceLifecycleState) {
DeviceLifeCycleMessage(value) this.lifecycleState = lifecycleState
) sharedMessageFlow.emit(
} DeviceLifeCycleMessage(lifecycleState)
} )
field = value }
}
protected open suspend fun onStart() { protected open suspend fun onStart() {
} }
@OptIn(DFExperimental::class)
final override suspend fun start() { final override suspend fun start() {
if (lifecycleState == DeviceLifecycleState.STOPPED) { if (lifecycleState == DeviceLifecycleState.STOPPED) {
super.start() super.start()
lifecycleState = DeviceLifecycleState.STARTING setLifecycleState(DeviceLifecycleState.STARTING)
onStart() onStart()
lifecycleState = DeviceLifecycleState.STARTED setLifecycleState(DeviceLifecycleState.STARTED)
} else { } else {
logger.debug { "Device $this is already started" } logger.debug { "Device $this is already started" }
} }
} }
protected open fun onStop() { protected open suspend fun onStop() {
} }
@OptIn(DFExperimental::class) final override suspend fun stop() {
final override fun stop() {
onStop() onStop()
lifecycleState = DeviceLifecycleState.STOPPED setLifecycleState(DeviceLifecycleState.STOPPED)
super.stop() super.stop()
} }

View File

@ -20,7 +20,7 @@ public open class DeviceBySpec<D : Device>(
self.onOpen() self.onOpen()
} }
override fun onStop(): Unit = with(spec){ override suspend fun onStop(): Unit = with(spec){
self.onClose() self.onClose()
} }

View File

@ -4,8 +4,10 @@ import kotlinx.coroutines.withContext
import space.kscience.controls.api.ActionDescriptor import space.kscience.controls.api.ActionDescriptor
import space.kscience.controls.api.Device import space.kscience.controls.api.Device
import space.kscience.controls.api.PropertyDescriptor import space.kscience.controls.api.PropertyDescriptor
import space.kscience.controls.api.metaDescriptor
import space.kscience.dataforge.meta.Meta import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.MetaConverter import space.kscience.dataforge.meta.MetaConverter
import space.kscience.dataforge.meta.descriptors.MetaDescriptor
import kotlin.properties.PropertyDelegateProvider import kotlin.properties.PropertyDelegateProvider
import kotlin.properties.ReadOnlyProperty import kotlin.properties.ReadOnlyProperty
import kotlin.reflect.KProperty import kotlin.reflect.KProperty
@ -54,6 +56,11 @@ public abstract class DeviceSpec<D : Device> {
val deviceProperty = object : DevicePropertySpec<D, T> { val deviceProperty = object : DevicePropertySpec<D, T> {
override val descriptor: PropertyDescriptor = PropertyDescriptor(propertyName).apply { override val descriptor: PropertyDescriptor = PropertyDescriptor(propertyName).apply {
converter.descriptor?.let { converterDescriptor ->
metaDescriptor {
from(converterDescriptor)
}
}
fromSpec(property) fromSpec(property)
descriptorBuilder() descriptorBuilder()
} }
@ -83,6 +90,11 @@ public abstract class DeviceSpec<D : Device> {
propertyName, propertyName,
mutable = true mutable = true
).apply { ).apply {
converter.descriptor?.let { converterDescriptor ->
metaDescriptor {
from(converterDescriptor)
}
}
fromSpec(property) fromSpec(property)
descriptorBuilder() descriptorBuilder()
} }
@ -118,6 +130,19 @@ public abstract class DeviceSpec<D : Device> {
val actionName = name ?: property.name val actionName = name ?: property.name
val deviceAction = object : DeviceActionSpec<D, I, O> { val deviceAction = object : DeviceActionSpec<D, I, O> {
override val descriptor: ActionDescriptor = ActionDescriptor(actionName).apply { 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) fromSpec(property)
descriptorBuilder() descriptorBuilder()
} }

View File

@ -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 <T : Any> MetaConverter<T>.nullable(): MetaConverter<T?> = object : MetaConverter<T?> {
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<Duration> {
override fun readOrNull(source: Meta): Duration = source.value?.double?.toDuration(DurationUnit.SECONDS)
?: run {
val unit: DurationUnit = source["unit"].enum<DurationUnit>() ?: 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<Duration> get() = DurationConverter
private object InstantConverter : MetaConverter<Instant> {
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<Instant> get() = InstantConverter

View File

@ -1,14 +1,31 @@
package space.kscience.controls.spec package space.kscience.controls.spec
import kotlinx.coroutines.Job import kotlinx.coroutines.*
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
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 space.kscience.controls.api.Device
import kotlin.time.Duration import kotlin.time.Duration
/**
* Do a recurring (with a fixed delay) task on a device.
*/
public fun <D : Device> 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. * 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. * 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 * The flow is canceled when the device scope is canceled
*/ */
public fun <D : Device, R> D.readRecurring(interval: Duration, reader: suspend D.() -> R): Flow<R> = flow { public fun <D : Device, R> D.readRecurring(
while (isActive) { interval: Duration,
delay(interval) debugTaskName: String? = null,
launch { reader: suspend D.() -> R,
emit(reader()) ): Flow<R> = flow {
} doRecurring(interval, debugTaskName) {
} emit(reader())
}
/**
* Do a recurring (with a fixed delay) task on a device.
*/
public fun <D : Device> D.doRecurring(interval: Duration, task: suspend D.() -> Unit): Job = launch {
while (isActive) {
delay(interval)
launch {
task()
}
} }
} }

View File

@ -4,8 +4,6 @@ import space.kscience.controls.api.ActionDescriptor
import space.kscience.controls.api.PropertyDescriptor import space.kscience.controls.api.PropertyDescriptor
import kotlin.reflect.KProperty 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<*>) internal expect fun PropertyDescriptor.fromSpec(property: KProperty<*>)

View File

@ -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<Duration> {
override fun readOrNull(source: Meta): Duration = source.value?.double?.toDuration(DurationUnit.SECONDS)
?: run {
val unit: DurationUnit = source["unit"].enum<DurationUnit>() ?: 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<Duration> get() = DurationConverter

View File

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

View File

@ -1,7 +1,7 @@
import space.kscience.gradle.Maturity import space.kscience.gradle.Maturity
plugins { plugins {
id("space.kscience.gradle.jvm") id("space.kscience.gradle.mpp")
`maven-publish` `maven-publish`
} }
@ -9,10 +9,12 @@ description = """
A plugin for Controls-kt device server on top of modbus-rtu/modbus-tcp protocols A plugin for Controls-kt device server on top of modbus-rtu/modbus-tcp protocols
""".trimIndent() """.trimIndent()
kscience {
dependencies { jvm()
api(projects.controlsCore) jvmMain {
api(libs.j2mod) api(projects.controlsCore)
api(libs.j2mod)
}
} }
readme{ readme{

View File

@ -24,7 +24,7 @@ public open class ModbusDeviceBySpec<D: Device>(
master.connect() master.connect()
} }
override fun onStop() { override suspend fun onStop() {
if(disposeMasterOnClose){ if(disposeMasterOnClose){
master.disconnect() master.disconnect()
} }

View File

@ -63,7 +63,7 @@ public open class OpcUaDeviceBySpec<D : Device>(
} }
} }
override fun onStop() { override suspend fun onStop() {
client.disconnect() client.disconnect()
} }
} }

View File

@ -1,5 +1,5 @@
plugins { plugins {
id("space.kscience.gradle.jvm") id("space.kscience.gradle.mpp")
`maven-publish` `maven-publish`
} }
@ -7,10 +7,15 @@ description = """
Utils to work with controls-kt on Raspberry pi Utils to work with controls-kt on Raspberry pi
""".trimIndent() """.trimIndent()
dependencies{ kscience {
api(project(":controls-core")) jvm()
api(libs.pi4j.ktx) // Kotlin DSL
api(libs.pi4j.core)
api(libs.pi4j.plugin.raspberrypi) jvmMain {
api(libs.pi4j.plugin.pigpio) api(project(":controls-core"))
api(libs.pi4j.ktx) // Kotlin DSL
api(libs.pi4j.core)
api(libs.pi4j.plugin.raspberrypi)
api(libs.pi4j.plugin.pigpio)
}
} }

View File

@ -1,7 +1,7 @@
import space.kscience.gradle.Maturity import space.kscience.gradle.Maturity
plugins { plugins {
id("space.kscience.gradle.jvm") id("space.kscience.gradle.mpp")
`maven-publish` `maven-publish`
} }
@ -9,9 +9,12 @@ description = """
Implementation of byte ports on top os ktor-io asynchronous API Implementation of byte ports on top os ktor-io asynchronous API
""".trimIndent() """.trimIndent()
dependencies { kscience {
api(projects.controlsCore) jvm()
api(spclibs.ktor.network) jvmMain {
api(projects.controlsCore)
api(spclibs.ktor.network)
}
} }
readme{ readme{

View File

@ -1,15 +1,18 @@
import space.kscience.gradle.Maturity import space.kscience.gradle.Maturity
plugins { plugins {
id("space.kscience.gradle.jvm") id("space.kscience.gradle.mpp")
`maven-publish` `maven-publish`
} }
description = "Implementation of direct serial port communication with JSerialComm" description = "Implementation of direct serial port communication with JSerialComm"
dependencies{ kscience {
api(project(":controls-core")) jvm()
implementation(libs.jSerialComm) jvmMain {
api(project(":controls-core"))
implementation(libs.jSerialComm)
}
} }
readme{ readme{

View File

@ -1,5 +1,5 @@
plugins { plugins {
id("space.kscience.gradle.jvm") id("space.kscience.gradle.mpp")
`maven-publish` `maven-publish`
} }
@ -7,13 +7,18 @@ description = """
An implementation of controls-storage on top of JetBrains Xodus. An implementation of controls-storage on top of JetBrains Xodus.
""".trimIndent() """.trimIndent()
dependencies { kscience {
api(projects.controlsStorage) jvm()
implementation(libs.xodus.entity.store) jvmMain {
api(projects.controlsStorage)
implementation(libs.xodus.entity.store)
// implementation("org.jetbrains.xodus:xodus-environment:$xodusVersion") // implementation("org.jetbrains.xodus:xodus-environment:$xodusVersion")
// implementation("org.jetbrains.xodus:xodus-vfs:$xodusVersion") // implementation("org.jetbrains.xodus:xodus-vfs:$xodusVersion")
testImplementation(spclibs.kotlinx.coroutines.test) }
jvmTest{
implementation(spclibs.kotlinx.coroutines.test)
}
} }
readme{ readme{

View File

@ -8,6 +8,7 @@ import javafx.stage.Stage
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import org.eclipse.milo.opcua.sdk.server.OpcUaServer import org.eclipse.milo.opcua.sdk.server.OpcUaServer
import org.eclipse.milo.opcua.stack.core.types.builtin.LocalizedText 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..." } logger.info { "Shutting down..." }
opcUaServer.shutdown() opcUaServer.shutdown()
logger.info { "OpcUa server stopped" } logger.info { "OpcUa server stopped" }
@ -179,7 +180,9 @@ class DemoControllerApp : App(DemoControllerView::class) {
} }
override fun stop() { override fun stop() {
controller.shutdown() runBlocking {
controller.shutdown()
}
super.stop() super.stop()
} }
} }

View File

@ -8,6 +8,7 @@ import javafx.scene.layout.Priority
import javafx.stage.Stage import javafx.stage.Stage
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import space.kscience.controls.client.launchMagixService import space.kscience.controls.client.launchMagixService
import space.kscience.controls.demo.car.IVirtualCar.Companion.acceleration import space.kscience.controls.demo.car.IVirtualCar.Companion.acceleration
import space.kscience.controls.manager.DeviceManager import space.kscience.controls.manager.DeviceManager
@ -67,7 +68,7 @@ class VirtualCarController : Controller(), ContextAware {
} }
} }
fun shutdown() { suspend fun shutdown() {
logger.info { "Shutting down..." } logger.info { "Shutting down..." }
magixServer?.stop(1000, 5000) magixServer?.stop(1000, 5000)
logger.info { "Magix server stopped" } logger.info { "Magix server stopped" }
@ -137,7 +138,9 @@ class VirtualCarControllerApp : App(VirtualCarControllerView::class) {
} }
override fun stop() { override fun stop() {
controller.shutdown() runBlocking {
controller.shutdown()
}
super.stop() super.stop()
} }
} }

View File

@ -16,9 +16,11 @@ import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application import androidx.compose.ui.window.application
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import space.kscience.controls.constructor.* import space.kscience.controls.constructor.*
import space.kscience.controls.constructor.library.*
import space.kscience.controls.manager.ClockManager import space.kscience.controls.manager.ClockManager
import space.kscience.controls.manager.DeviceManager import space.kscience.controls.manager.DeviceManager
import space.kscience.controls.manager.clock import space.kscience.controls.manager.clock
import space.kscience.controls.manager.install
import space.kscience.controls.spec.doRecurring import space.kscience.controls.spec.doRecurring
import space.kscience.controls.spec.name import space.kscience.controls.spec.name
import space.kscience.controls.vision.plot import space.kscience.controls.vision.plot
@ -48,13 +50,14 @@ class LinearDrive(
val drive by device(VirtualDrive.factory(mass, state)) val drive by device(VirtualDrive.factory(mass, state))
val pid by device(PidRegulator(drive, pidParameters)) val pid by device(PidRegulator(drive, pidParameters))
val start by device(LimitSwitch.factory(state.atStartState)) val start by device(LimitSwitch(state.atStartState))
val end by device(LimitSwitch.factory(state.atEndState)) val end by device(LimitSwitch(state.atEndState))
val positionState: DoubleRangeState by property(state) val positionState: DoubleRangeState by property(state)
private val targetState: MutableDeviceState<Double> by property(pid.mutablePropertyAsState(Regulator.target, 0.0))
var target by targetState private val targetState: MutableDeviceState<Double> 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 timeFromStart = clock.now() - clockStart
val t = timeFromStart.toDouble(DurationUnit.SECONDS) val t = timeFromStart.toDouble(DurationUnit.SECONDS)
val freq = 0.1 val freq = 0.1
target = 5 * sin(2.0 * PI * freq * t) + target = 5 * sin(2.0 * PI * freq * t) +
sin(2 * PI * 21 * freq * t + 0.02 * (timeFromStart / pidParameters.timeStep)) sin(2 * PI * 21 * freq * t + 0.02 * (timeFromStart / pidParameters.timeStep))
} }
@ -150,7 +152,7 @@ fun main() = application {
Row { Row {
Text("kp:", Modifier.align(Alignment.CenterVertically).width(50.dp).padding(5.dp)) Text("kp:", Modifier.align(Alignment.CenterVertically).width(50.dp).padding(5.dp))
TextField( TextField(
String.format("%.2f",pidParameters.kp), String.format("%.2f", pidParameters.kp),
{ pidParameters.kp = it.toDouble() }, { pidParameters.kp = it.toDouble() },
Modifier.width(100.dp), Modifier.width(100.dp),
enabled = false enabled = false
@ -165,7 +167,7 @@ fun main() = application {
Row { Row {
Text("ki:", Modifier.align(Alignment.CenterVertically).width(50.dp).padding(5.dp)) Text("ki:", Modifier.align(Alignment.CenterVertically).width(50.dp).padding(5.dp))
TextField( TextField(
String.format("%.2f",pidParameters.ki), String.format("%.2f", pidParameters.ki),
{ pidParameters.ki = it.toDouble() }, { pidParameters.ki = it.toDouble() },
Modifier.width(100.dp), Modifier.width(100.dp),
enabled = false enabled = false
@ -181,7 +183,7 @@ fun main() = application {
Row { Row {
Text("kd:", Modifier.align(Alignment.CenterVertically).width(50.dp).padding(5.dp)) Text("kd:", Modifier.align(Alignment.CenterVertically).width(50.dp).padding(5.dp))
TextField( TextField(
String.format("%.2f",pidParameters.kd), String.format("%.2f", pidParameters.kd),
{ pidParameters.kd = it.toDouble() }, { pidParameters.kd = it.toDouble() },
Modifier.width(100.dp), Modifier.width(100.dp),
enabled = false enabled = false

View File

@ -1,36 +1,38 @@
import space.kscience.gradle.Maturity import space.kscience.gradle.Maturity
plugins { plugins {
id("space.kscience.gradle.jvm") id("space.kscience.gradle.mpp")
`maven-publish` `maven-publish`
application
} }
description = """ description = """
A magix event loop implementation in Kotlin. Includes HTTP/SSE and RSocket routes. A magix event loop implementation in Kotlin. Includes HTTP/SSE and RSocket routes.
""".trimIndent() """.trimIndent()
kscience {
useSerialization{
json()
}
}
val dataforgeVersion: String by rootProject.extra val dataforgeVersion: String by rootProject.extra
val ktorVersion: String = space.kscience.gradle.KScienceVersions.ktorVersion val ktorVersion: String = space.kscience.gradle.KScienceVersions.ktorVersion
dependencies{ kscience {
api(projects.magix.magixApi) jvm()
api("io.ktor:ktor-server-cio:$ktorVersion") useSerialization{
api("io.ktor:ktor-server-websockets:$ktorVersion") json()
api("io.ktor:ktor-server-content-negotiation:$ktorVersion") }
api("io.ktor:ktor-serialization-kotlinx-json:$ktorVersion")
api("io.ktor:ktor-server-html-builder:$ktorVersion") 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{ readme{
maturity = Maturity.EXPERIMENTAL maturity = Maturity.EXPERIMENTAL
} }