make device stop suspended to properly await for lifecycle event.
Add capabilities to Constructor
This commit is contained in:
parent
4b05f46fa7
commit
381da970bf
@ -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")
|
||||
}
|
||||
|
@ -10,9 +10,14 @@ description = """
|
||||
kscience{
|
||||
jvm()
|
||||
js()
|
||||
dependencies {
|
||||
useCoroutines()
|
||||
commonMain {
|
||||
api(projects.controlsCore)
|
||||
}
|
||||
|
||||
commonTest{
|
||||
implementation(spclibs.logback.classic)
|
||||
}
|
||||
}
|
||||
|
||||
readme{
|
||||
|
@ -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
|
@ -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,17 +21,48 @@ 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<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))
|
||||
}
|
||||
|
||||
/**
|
||||
* Create and register a timer. Timer is not counted as a device property.
|
||||
*/
|
||||
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 <T> DeviceState<T>.onChange(
|
||||
vararg writes: DeviceState<*>,
|
||||
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> device(
|
||||
public fun <D : Device> DeviceConstructor.device(
|
||||
factory: Factory<D>,
|
||||
meta: Meta? = null,
|
||||
nameOverride: Name? = null,
|
||||
@ -38,7 +76,7 @@ public abstract class DeviceConstructor(
|
||||
}
|
||||
}
|
||||
|
||||
public fun <D : Device> device(
|
||||
public fun <D : Device> DeviceConstructor.device(
|
||||
device: D,
|
||||
nameOverride: Name? = null,
|
||||
): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, D>> =
|
||||
@ -50,11 +88,10 @@ public abstract class DeviceConstructor(
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Register a property and provide a direct reader for it
|
||||
*/
|
||||
public fun <T, S: DeviceState<T>> property(
|
||||
public fun <T, S : DeviceState<T>> DeviceConstructor.property(
|
||||
state: S,
|
||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||
nameOverride: String? = null,
|
||||
@ -71,7 +108,7 @@ public abstract class DeviceConstructor(
|
||||
/**
|
||||
* Register external state as a property
|
||||
*/
|
||||
public fun <T : Any> property(
|
||||
public fun <T : Any> DeviceConstructor.property(
|
||||
metaConverter: MetaConverter<T>,
|
||||
reader: suspend () -> T,
|
||||
readInterval: Duration,
|
||||
@ -87,7 +124,7 @@ public abstract class DeviceConstructor(
|
||||
/**
|
||||
* Register a mutable external state as a property
|
||||
*/
|
||||
public fun <T : Any> mutableProperty(
|
||||
public fun <T : Any> DeviceConstructor.mutableProperty(
|
||||
metaConverter: MetaConverter<T>,
|
||||
reader: suspend () -> T,
|
||||
writer: suspend (T) -> Unit,
|
||||
@ -104,15 +141,34 @@ public abstract class DeviceConstructor(
|
||||
/**
|
||||
* Create and register a virtual mutable property with optional [callback]
|
||||
*/
|
||||
public fun <T> virtualProperty(
|
||||
public fun <T> DeviceConstructor.virtualProperty(
|
||||
metaConverter: MetaConverter<T>,
|
||||
initialState: T,
|
||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||
nameOverride: String? = null,
|
||||
callback: (T) -> Unit = {},
|
||||
): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, MutableDeviceState<T>>> = property(
|
||||
DeviceState.virtual(metaConverter, initialState, callback),
|
||||
DeviceState.internal(metaConverter, initialState, callback),
|
||||
descriptorBuilder,
|
||||
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))
|
@ -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 <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" }
|
||||
//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 {
|
||||
final override var lifecycleState: DeviceLifecycleState = DeviceLifecycleState.STOPPED
|
||||
private set
|
||||
|
||||
|
||||
private suspend fun setLifecycleState(lifecycleState: DeviceLifecycleState) {
|
||||
this.lifecycleState = lifecycleState
|
||||
sharedMessageFlow.emit(
|
||||
DeviceLifeCycleMessage(value)
|
||||
DeviceLifeCycleMessage(lifecycleState)
|
||||
)
|
||||
}
|
||||
}
|
||||
field = value
|
||||
}
|
||||
|
||||
|
||||
@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 <D : Device> DeviceGroup.install(name: Name, device: D): D {
|
||||
}
|
||||
}
|
||||
|
||||
public fun <D : Device> DeviceGroup.install(name: String, device: D): D =
|
||||
install(name.parseAsName(), device)
|
||||
public fun <D : Device> DeviceGroup.install(name: String, device: D): D = install(name.parseAsName(), device)
|
||||
|
||||
public fun <D : Device> DeviceGroup.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> 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 <T : Any> DeviceGroup.registerVirtualProperty(
|
||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||
callback: (T) -> Unit = {},
|
||||
): MutableDeviceState<T> {
|
||||
val state = DeviceState.virtual<T>(converter, initialValue, callback)
|
||||
val state = DeviceState.internal<T>(converter, initialValue, callback)
|
||||
registerMutableProperty(name, state, descriptorBuilder)
|
||||
return state
|
||||
}
|
||||
|
@ -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<T> {
|
||||
|
||||
public val valueFlow: Flow<T>
|
||||
|
||||
override fun toString(): String
|
||||
|
||||
public companion object
|
||||
}
|
||||
|
||||
@ -57,186 +54,46 @@ public var <T> MutableDeviceState<T>.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<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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 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
|
||||
public interface DeviceStateWithDependencies<T> : DeviceState<T> {
|
||||
public val dependencies: Collection<DeviceState<*>>
|
||||
}
|
||||
|
||||
/**
|
||||
* 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(
|
||||
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 value: R
|
||||
get() = mapper(this@map.value)
|
||||
|
||||
override val valueFlow: Flow<R> = this@map.valueFlow.map(mapper)
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
|
||||
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 = "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 <T> DeviceState.Companion.external(
|
||||
scope: CoroutineScope,
|
||||
converter: MetaConverter<T>,
|
||||
readInterval: Duration,
|
||||
initialValue: T,
|
||||
reader: suspend () -> T,
|
||||
): DeviceState<T> = ExternalState(scope, converter, readInterval, initialValue, reader)
|
||||
public fun <T1, T2, R> combine(
|
||||
state1: DeviceState<T1>,
|
||||
state2: DeviceState<T2>,
|
||||
converter: MetaConverter<R>,
|
||||
mapper: (T1, T2) -> R,
|
||||
): DeviceStateWithDependencies<R> = object : DeviceStateWithDependencies<R> {
|
||||
override val dependencies = listOf(state1, state2)
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
override val converter: MetaConverter<R> = converter
|
||||
|
||||
/**
|
||||
* 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)
|
||||
override val value: R get() = mapper(state1.value, state2.value)
|
||||
|
||||
override val valueFlow: Flow<R> = kotlinx.coroutines.flow.combine(state1.valueFlow, state2.valueFlow, mapper)
|
||||
|
||||
override fun toString(): String = "DeviceState.combine(state1=$state1, state2=$state2)"
|
||||
}
|
||||
|
@ -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)"
|
||||
}
|
@ -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)
|
||||
|
@ -38,6 +38,10 @@ public class DoubleRangeState(
|
||||
* A state showing that the range is on its higher boundary
|
||||
*/
|
||||
public val atEndState: DeviceState<Boolean> = map(MetaConverter.boolean) { it >= range.endInclusive }
|
||||
|
||||
override fun toString(): String = "DoubleRangeState(range=$range, converter=$converter)"
|
||||
|
||||
|
||||
}
|
||||
|
||||
@Suppress("UnusedReceiverParameter")
|
||||
|
@ -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)
|
@ -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)
|
@ -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)
|
@ -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<Double>,
|
||||
) : 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
|
||||
|
||||
override var force: Double = 0.0
|
||||
@ -82,7 +84,7 @@ public class VirtualDrive(
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
override suspend fun onStop() {
|
||||
updateJob?.cancel()
|
||||
}
|
||||
|
@ -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<LimitSwitch>() {
|
||||
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)
|
||||
}
|
||||
}
|
@ -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
|
@ -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.*
|
@ -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")
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
@ -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 {
|
||||
|
@ -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)
|
||||
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()
|
||||
)
|
||||
|
@ -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<ClockManager> {
|
||||
override val tag: PluginTag = PluginTag("clock", group = PluginTag.DATAFORGE_GROUP)
|
||||
|
||||
|
@ -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> 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].
|
||||
*/
|
||||
@ -76,4 +80,3 @@ public inline fun <D : Device> DeviceManager.installing(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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<D : Device>(
|
||||
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<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)
|
||||
}
|
||||
|
||||
@DFExperimental
|
||||
final override var lifecycleState: DeviceLifecycleState = DeviceLifecycleState.STOPPED
|
||||
private set(value) {
|
||||
if (field != value) {
|
||||
launch {
|
||||
private set
|
||||
|
||||
|
||||
private suspend fun setLifecycleState(lifecycleState: DeviceLifecycleState) {
|
||||
this.lifecycleState = lifecycleState
|
||||
sharedMessageFlow.emit(
|
||||
DeviceLifeCycleMessage(value)
|
||||
DeviceLifeCycleMessage(lifecycleState)
|
||||
)
|
||||
}
|
||||
}
|
||||
field = value
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
|
@ -20,7 +20,7 @@ public open class DeviceBySpec<D : Device>(
|
||||
self.onOpen()
|
||||
}
|
||||
|
||||
override fun onStop(): Unit = with(spec){
|
||||
override suspend fun onStop(): Unit = with(spec){
|
||||
self.onClose()
|
||||
}
|
||||
|
||||
|
@ -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<D : Device> {
|
||||
val deviceProperty = object : DevicePropertySpec<D, T> {
|
||||
|
||||
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<D : Device> {
|
||||
propertyName,
|
||||
mutable = true
|
||||
).apply {
|
||||
converter.descriptor?.let { converterDescriptor ->
|
||||
metaDescriptor {
|
||||
from(converterDescriptor)
|
||||
}
|
||||
}
|
||||
fromSpec(property)
|
||||
descriptorBuilder()
|
||||
}
|
||||
@ -118,6 +130,19 @@ public abstract class DeviceSpec<D : Device> {
|
||||
val actionName = name ?: property.name
|
||||
val deviceAction = object : DeviceActionSpec<D, I, O> {
|
||||
override val descriptor: ActionDescriptor = ActionDescriptor(actionName).apply {
|
||||
inputConverter.descriptor?.let { converterDescriptor ->
|
||||
inputMetaDescriptor = MetaDescriptor {
|
||||
from(converterDescriptor)
|
||||
from(inputMetaDescriptor)
|
||||
}
|
||||
}
|
||||
outputConverter.descriptor?.let { converterDescriptor ->
|
||||
outputMetaDescriptor = MetaDescriptor {
|
||||
from(converterDescriptor)
|
||||
from(outputMetaDescriptor)
|
||||
}
|
||||
}
|
||||
|
||||
fromSpec(property)
|
||||
descriptorBuilder()
|
||||
}
|
||||
|
@ -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
|
@ -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 : 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.
|
||||
* 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 : Device, R> D.readRecurring(interval: Duration, reader: suspend D.() -> R): Flow<R> = flow {
|
||||
while (isActive) {
|
||||
delay(interval)
|
||||
launch {
|
||||
public fun <D : Device, R> D.readRecurring(
|
||||
interval: Duration,
|
||||
debugTaskName: String? = null,
|
||||
reader: suspend D.() -> R,
|
||||
): 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()
|
||||
}
|
||||
}
|
||||
}
|
@ -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<*>)
|
||||
|
||||
|
@ -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
|
@ -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<Description>()?.let {
|
||||
description = it.content
|
||||
description = it.value
|
||||
}
|
||||
}
|
||||
|
||||
internal actual fun ActionDescriptor.fromSpec(property: KProperty<*>){
|
||||
property.findAnnotation<Description>()?.let {
|
||||
description = it.content
|
||||
description = it.value
|
||||
}
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
import space.kscience.gradle.Maturity
|
||||
|
||||
plugins {
|
||||
id("space.kscience.gradle.jvm")
|
||||
id("space.kscience.gradle.mpp")
|
||||
`maven-publish`
|
||||
}
|
||||
|
||||
@ -9,11 +9,13 @@ description = """
|
||||
A plugin for Controls-kt device server on top of modbus-rtu/modbus-tcp protocols
|
||||
""".trimIndent()
|
||||
|
||||
|
||||
dependencies {
|
||||
kscience {
|
||||
jvm()
|
||||
jvmMain {
|
||||
api(projects.controlsCore)
|
||||
api(libs.j2mod)
|
||||
}
|
||||
}
|
||||
|
||||
readme{
|
||||
maturity = Maturity.EXPERIMENTAL
|
||||
|
@ -24,7 +24,7 @@ public open class ModbusDeviceBySpec<D: Device>(
|
||||
master.connect()
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
override suspend fun onStop() {
|
||||
if(disposeMasterOnClose){
|
||||
master.disconnect()
|
||||
}
|
@ -63,7 +63,7 @@ public open class OpcUaDeviceBySpec<D : Device>(
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
override suspend fun onStop() {
|
||||
client.disconnect()
|
||||
}
|
||||
}
|
||||
|
@ -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{
|
||||
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)
|
||||
}
|
||||
}
|
@ -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,13 @@ description = """
|
||||
Implementation of byte ports on top os ktor-io asynchronous API
|
||||
""".trimIndent()
|
||||
|
||||
dependencies {
|
||||
kscience {
|
||||
jvm()
|
||||
jvmMain {
|
||||
api(projects.controlsCore)
|
||||
api(spclibs.ktor.network)
|
||||
}
|
||||
}
|
||||
|
||||
readme{
|
||||
maturity = Maturity.PROTOTYPE
|
||||
|
@ -1,16 +1,19 @@
|
||||
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{
|
||||
kscience {
|
||||
jvm()
|
||||
jvmMain {
|
||||
api(project(":controls-core"))
|
||||
implementation(libs.jSerialComm)
|
||||
}
|
||||
}
|
||||
|
||||
readme{
|
||||
maturity = Maturity.EXPERIMENTAL
|
||||
|
@ -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 {
|
||||
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{
|
||||
|
@ -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() {
|
||||
runBlocking {
|
||||
controller.shutdown()
|
||||
}
|
||||
super.stop()
|
||||
}
|
||||
}
|
||||
|
@ -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() {
|
||||
runBlocking {
|
||||
controller.shutdown()
|
||||
}
|
||||
super.stop()
|
||||
}
|
||||
}
|
||||
|
@ -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<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 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))
|
||||
}
|
||||
|
@ -1,25 +1,24 @@
|
||||
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{
|
||||
kscience {
|
||||
jvm()
|
||||
useSerialization{
|
||||
json()
|
||||
}
|
||||
|
||||
jvmMain{
|
||||
api(projects.magix.magixApi)
|
||||
api("io.ktor:ktor-server-cio:$ktorVersion")
|
||||
api("io.ktor:ktor-server-websockets:$ktorVersion")
|
||||
@ -31,6 +30,9 @@ dependencies{
|
||||
api(libs.rsocket.transport.ktor.tcp)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
readme{
|
||||
maturity = Maturity.EXPERIMENTAL
|
||||
}
|
Loading…
Reference in New Issue
Block a user