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 {
|
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")
|
||||||
}
|
}
|
||||||
|
@ -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{
|
||||||
|
@ -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
|
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,
|
|
||||||
): 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(
|
|
||||||
device: D,
|
|
||||||
nameOverride: Name? = null,
|
|
||||||
): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, D>> =
|
|
||||||
PropertyDelegateProvider { _: DeviceConstructor, property: KProperty<*> ->
|
|
||||||
val name = nameOverride ?: property.name.asName()
|
|
||||||
install(name, device)
|
|
||||||
ReadOnlyProperty { _: DeviceConstructor, _ ->
|
|
||||||
device
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register a property and provide a direct reader for it
|
* Launch action that is performed on each [DeviceState] value change.
|
||||||
|
*
|
||||||
|
* Optionally provide [writes] - a set of states that this change affects.
|
||||||
*/
|
*/
|
||||||
public fun <T, S: DeviceState<T>> property(
|
public fun <T> DeviceState<T>.onChange(
|
||||||
state: S,
|
vararg writes: DeviceState<*>,
|
||||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
reads: Collection<DeviceState<*>>,
|
||||||
nameOverride: String? = null,
|
onChange: suspend (T) -> Unit,
|
||||||
): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, S>> =
|
): Job = valueFlow.onEach(onChange).launchIn(this@DeviceConstructor).also {
|
||||||
PropertyDelegateProvider { _: DeviceConstructor, property ->
|
registerBinding(ActionBinding(setOf(this, *reads.toTypedArray()), setOf(*writes)))
|
||||||
val name = nameOverride ?: property.name
|
}
|
||||||
val descriptor = PropertyDescriptor(name).apply(descriptorBuilder)
|
|
||||||
registerProperty(descriptor, state)
|
|
||||||
ReadOnlyProperty { _: DeviceConstructor, _ ->
|
|
||||||
state
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Register external state as a property
|
|
||||||
*/
|
|
||||||
public fun <T : Any> property(
|
|
||||||
metaConverter: MetaConverter<T>,
|
|
||||||
reader: suspend () -> T,
|
|
||||||
readInterval: Duration,
|
|
||||||
initialState: T,
|
|
||||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
|
||||||
nameOverride: String? = null,
|
|
||||||
): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, DeviceState<T>>> = property(
|
|
||||||
DeviceState.external(this, metaConverter, readInterval, initialState, reader),
|
|
||||||
descriptorBuilder,
|
|
||||||
nameOverride,
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Register a mutable external state as a property
|
|
||||||
*/
|
|
||||||
public fun <T : Any> mutableProperty(
|
|
||||||
metaConverter: MetaConverter<T>,
|
|
||||||
reader: suspend () -> T,
|
|
||||||
writer: suspend (T) -> Unit,
|
|
||||||
readInterval: Duration,
|
|
||||||
initialState: T,
|
|
||||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
|
||||||
nameOverride: String? = null,
|
|
||||||
): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, MutableDeviceState<T>>> = property(
|
|
||||||
DeviceState.external(this, metaConverter, readInterval, initialState, reader, writer),
|
|
||||||
descriptorBuilder,
|
|
||||||
nameOverride,
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create and register a virtual mutable property with optional [callback]
|
|
||||||
*/
|
|
||||||
public fun <T> 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),
|
|
||||||
descriptorBuilder,
|
|
||||||
nameOverride,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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> DeviceConstructor.device(
|
||||||
|
device: D,
|
||||||
|
nameOverride: Name? = null,
|
||||||
|
): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, D>> =
|
||||||
|
PropertyDelegateProvider { _: DeviceConstructor, property: KProperty<*> ->
|
||||||
|
val name = nameOverride ?: property.name.asName()
|
||||||
|
install(name, device)
|
||||||
|
ReadOnlyProperty { _: DeviceConstructor, _ ->
|
||||||
|
device
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register a property and provide a direct reader for it
|
||||||
|
*/
|
||||||
|
public fun <T, S : DeviceState<T>> DeviceConstructor.property(
|
||||||
|
state: S,
|
||||||
|
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||||
|
nameOverride: String? = null,
|
||||||
|
): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, S>> =
|
||||||
|
PropertyDelegateProvider { _: DeviceConstructor, property ->
|
||||||
|
val name = nameOverride ?: property.name
|
||||||
|
val descriptor = PropertyDescriptor(name).apply(descriptorBuilder)
|
||||||
|
registerProperty(descriptor, state)
|
||||||
|
ReadOnlyProperty { _: DeviceConstructor, _ ->
|
||||||
|
state
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register external state as a property
|
||||||
|
*/
|
||||||
|
public fun <T : Any> DeviceConstructor.property(
|
||||||
|
metaConverter: MetaConverter<T>,
|
||||||
|
reader: suspend () -> T,
|
||||||
|
readInterval: Duration,
|
||||||
|
initialState: T,
|
||||||
|
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||||
|
nameOverride: String? = null,
|
||||||
|
): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, DeviceState<T>>> = property(
|
||||||
|
DeviceState.external(this, metaConverter, readInterval, initialState, reader),
|
||||||
|
descriptorBuilder,
|
||||||
|
nameOverride,
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register a mutable external state as a property
|
||||||
|
*/
|
||||||
|
public fun <T : Any> DeviceConstructor.mutableProperty(
|
||||||
|
metaConverter: MetaConverter<T>,
|
||||||
|
reader: suspend () -> T,
|
||||||
|
writer: suspend (T) -> Unit,
|
||||||
|
readInterval: Duration,
|
||||||
|
initialState: T,
|
||||||
|
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||||
|
nameOverride: String? = null,
|
||||||
|
): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, MutableDeviceState<T>>> = property(
|
||||||
|
DeviceState.external(this, metaConverter, readInterval, initialState, reader, writer),
|
||||||
|
descriptorBuilder,
|
||||||
|
nameOverride,
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create and register a virtual mutable property with optional [callback]
|
||||||
|
*/
|
||||||
|
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.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.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
|
||||||
}
|
}
|
||||||
|
@ -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)
|
|
@ -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
|
* 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")
|
||||||
|
@ -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.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()
|
||||||
}
|
}
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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
|
@ -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.*
|
@ -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.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 {
|
||||||
|
@ -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()
|
||||||
|
)
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
@ -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].
|
||||||
*/
|
*/
|
||||||
@ -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.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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
|
@ -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
|
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()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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<*>)
|
||||||
|
|
||||||
|
@ -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.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
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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{
|
||||||
|
@ -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()
|
||||||
}
|
}
|
@ -63,7 +63,7 @@ public open class OpcUaDeviceBySpec<D : Device>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStop() {
|
override suspend fun onStop() {
|
||||||
client.disconnect()
|
client.disconnect()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
@ -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{
|
||||||
|
@ -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{
|
||||||
|
@ -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{
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
}
|
}
|
Loading…
Reference in New Issue
Block a user