make device stop suspended to properly await for lifecycle event.

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

View File

@ -8,7 +8,7 @@ plugins {
allprojects {
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")
}

View File

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

View File

@ -0,0 +1,26 @@
package space.kscience.controls.constructor
import space.kscience.controls.api.Device
public sealed interface ConstructorBinding
/**
* A binding that exposes device property as read-only state
*/
public class PropertyBinding<T>(
public val device: Device,
public val propertyName: String,
public val state: DeviceState<T>,
) : ConstructorBinding
/**
* A binding for independent state like a timer
*/
public class StateBinding<T>(
public val state: DeviceState<T>
) : ConstructorBinding
public class ActionBinding(
public val reads: Collection<DeviceState<*>>,
public val writes: Collection<DeviceState<*>>
): ConstructorBinding

View File

@ -1,9 +1,16 @@
package space.kscience.controls.constructor
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))

View File

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

View File

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

View File

@ -0,0 +1,41 @@
package space.kscience.controls.constructor
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.datetime.Instant
import space.kscience.controls.manager.ClockManager
import space.kscience.controls.spec.instant
import space.kscience.dataforge.meta.MetaConverter
import kotlin.time.Duration
/**
* A dedicated [DeviceState] that operates with time.
* The state changes with [tick] interval and always shows the time of the last update.
*
* Both [tick] and current time are computed by [clockManager] enabling time manipulation.
*
* The timer runs indefinitely until the parent context is closed
*/
public class TimerState(
public val clockManager: ClockManager,
public val tick: Duration,
) : DeviceState<Instant> {
override val converter: MetaConverter<Instant> get() = MetaConverter.instant
private val clock = MutableStateFlow(clockManager.clock.now())
private val updateJob = clockManager.context.launch {
while (isActive) {
clockManager.delay(tick)
clock.value = clockManager.clock.now()
}
}
override val valueFlow: Flow<Instant> get() = clock
override val value: Instant get() = clock.value
override fun toString(): String = "TimerState(tick=$tick)"
}

View File

@ -0,0 +1,103 @@
package space.kscience.controls.constructor
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import space.kscience.controls.api.Device
import space.kscience.controls.api.PropertyChangedMessage
import space.kscience.controls.api.id
import space.kscience.controls.spec.DevicePropertySpec
import space.kscience.controls.spec.MutableDevicePropertySpec
import space.kscience.controls.spec.name
import space.kscience.dataforge.meta.MetaConverter
/**
* A copy-free [DeviceState] bound to a device property
*/
private open class BoundDeviceState<T>(
override val converter: MetaConverter<T>,
val device: Device,
val propertyName: String,
initialValue: T,
) : DeviceState<T> {
override val valueFlow: StateFlow<T> = device.messageFlow.filterIsInstance<PropertyChangedMessage>().filter {
it.property == propertyName
}.mapNotNull {
converter.read(it.value)
}.stateIn(device.context, SharingStarted.Eagerly, initialValue)
override val value: T get() = valueFlow.value
override fun toString(): String =
"BoundDeviceState(converter=$converter, device=${device.id}, propertyName='$propertyName')"
}
public fun <T> Device.propertyAsState(
propertyName: String,
metaConverter: MetaConverter<T>,
initialValue: T,
): DeviceState<T> = BoundDeviceState(metaConverter, this, propertyName, initialValue)
/**
* Bind a read-only [DeviceState] to a [Device] property
*/
public suspend fun <T> Device.propertyAsState(
propertyName: String,
metaConverter: MetaConverter<T>,
): DeviceState<T> = propertyAsState(
propertyName,
metaConverter,
metaConverter.readOrNull(readProperty(propertyName)) ?: error("Conversion of property failed")
)
public suspend fun <D : Device, T> D.propertyAsState(
propertySpec: DevicePropertySpec<D, T>,
): DeviceState<T> = propertyAsState(propertySpec.name, propertySpec.converter)
public fun <D : Device, T> D.propertyAsState(
propertySpec: DevicePropertySpec<D, T>,
initialValue: T,
): DeviceState<T> = propertyAsState(propertySpec.name, propertySpec.converter, initialValue)
private class MutableBoundDeviceState<T>(
converter: MetaConverter<T>,
device: Device,
propertyName: String,
initialValue: T,
) : BoundDeviceState<T>(converter, device, propertyName, initialValue), MutableDeviceState<T> {
override var value: T
get() = valueFlow.value
set(newValue) {
device.launch {
device.writeProperty(propertyName, converter.convert(newValue))
}
}
}
public fun <T> Device.mutablePropertyAsState(
propertyName: String,
metaConverter: MetaConverter<T>,
initialValue: T,
): MutableDeviceState<T> = MutableBoundDeviceState(metaConverter, this, propertyName, initialValue)
public suspend fun <T> Device.mutablePropertyAsState(
propertyName: String,
metaConverter: MetaConverter<T>,
): MutableDeviceState<T> {
val initialValue = metaConverter.readOrNull(readProperty(propertyName)) ?: error("Conversion of property failed")
return mutablePropertyAsState(propertyName, metaConverter, initialValue)
}
public suspend fun <D : Device, T> D.mutablePropertyAsState(
propertySpec: MutableDevicePropertySpec<D, T>,
): MutableDeviceState<T> = mutablePropertyAsState(propertySpec.name, propertySpec.converter)
public fun <D : Device, T> D.mutablePropertyAsState(
propertySpec: MutableDevicePropertySpec<D, T>,
initialValue: T,
): MutableDeviceState<T> = mutablePropertyAsState(propertySpec.name, propertySpec.converter, initialValue)

View File

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

View File

@ -0,0 +1,70 @@
package space.kscience.controls.constructor
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import space.kscience.dataforge.meta.MetaConverter
import kotlin.time.Duration
private open class ExternalState<T>(
val scope: CoroutineScope,
override val converter: MetaConverter<T>,
val readInterval: Duration,
initialValue: T,
val reader: suspend () -> T,
) : DeviceState<T> {
protected val flow: StateFlow<T> = flow {
while (true) {
delay(readInterval)
emit(reader())
}
}.stateIn(scope, SharingStarted.Eagerly, initialValue)
override val value: T get() = flow.value
override val valueFlow: Flow<T> get() = flow
override fun toString(): String = "ExternalState(converter=$converter)"
}
/**
* Create a [DeviceState] which is constructed by regularly reading external value
*/
public fun <T> DeviceState.Companion.external(
scope: CoroutineScope,
converter: MetaConverter<T>,
readInterval: Duration,
initialValue: T,
reader: suspend () -> T,
): DeviceState<T> = ExternalState(scope, converter, readInterval, initialValue, reader)
private class MutableExternalState<T>(
scope: CoroutineScope,
converter: MetaConverter<T>,
readInterval: Duration,
initialValue: T,
reader: suspend () -> T,
val writer: suspend (T) -> Unit,
) : ExternalState<T>(scope, converter, readInterval, initialValue, reader), MutableDeviceState<T> {
override var value: T
get() = super.value
set(value) {
scope.launch {
writer(value)
}
}
}
/**
* Create a [MutableDeviceState] which is constructed by regularly reading external value and allows writing
*/
public fun <T> DeviceState.Companion.external(
scope: CoroutineScope,
converter: MetaConverter<T>,
readInterval: Duration,
initialValue: T,
reader: suspend () -> T,
writer: suspend (T) -> Unit,
): MutableDeviceState<T> = MutableExternalState(scope, converter, readInterval, initialValue, reader, writer)

View File

@ -0,0 +1,23 @@
package space.kscience.controls.constructor
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import space.kscience.dataforge.meta.MetaConverter
private class StateFlowAsState<T>(
override val converter: MetaConverter<T>,
val flow: MutableStateFlow<T>,
) : MutableDeviceState<T> {
override var value: T by flow::value
override val valueFlow: Flow<T> get() = flow
override fun toString(): String = "FlowAsState(converter=$converter)"
}
/**
* Create a read-only [DeviceState] that wraps [MutableStateFlow].
* No data copy is performed.
*/
public fun <T> MutableStateFlow<T>.asDeviceState(converter: MetaConverter<T>): DeviceState<T> =
StateFlowAsState(converter, this)

View File

@ -0,0 +1,42 @@
package space.kscience.controls.constructor
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import space.kscience.dataforge.meta.MetaConverter
/**
* A [MutableDeviceState] that does not correspond to a physical state
*
* @param callback a synchronous callback that could be used without a scope
*/
private class VirtualDeviceState<T>(
override val converter: MetaConverter<T>,
initialValue: T,
private val callback: (T) -> Unit = {},
) : MutableDeviceState<T> {
private val flow = MutableStateFlow(initialValue)
override val valueFlow: Flow<T> get() = flow
override var value: T
get() = flow.value
set(value) {
flow.value = value
callback(value)
}
override fun toString(): String = "VirtualDeviceState(converter=$converter)"
}
/**
* A [MutableDeviceState] that does not correspond to a physical state
*
* @param callback a synchronous callback that could be used without a scope
*/
public fun <T> DeviceState.Companion.internal(
converter: MetaConverter<T>,
initialValue: T,
callback: (T) -> Unit = {},
): MutableDeviceState<T> = VirtualDeviceState(converter, initialValue, callback)

View File

@ -1,10 +1,12 @@
package space.kscience.controls.constructor
package space.kscience.controls.constructor.library
import kotlinx.coroutines.Job
import kotlinx.coroutines.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()
}

View File

@ -1,8 +1,9 @@
package space.kscience.controls.constructor
package space.kscience.controls.constructor.library
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.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)
}
}

View File

@ -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

View File

@ -1,4 +1,4 @@
package space.kscience.controls.constructor
package space.kscience.controls.constructor.library
import space.kscience.controls.api.Device
import space.kscience.controls.spec.*

View File

@ -0,0 +1,43 @@
package space.kscience.controls.constructor
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.runTest
import space.kscience.controls.api.Device
import space.kscience.controls.api.DeviceLifeCycleMessage
import space.kscience.controls.api.DeviceLifecycleState
import space.kscience.controls.manager.DeviceManager
import space.kscience.controls.manager.install
import space.kscience.controls.spec.doRecurring
import space.kscience.dataforge.context.Context
import space.kscience.dataforge.context.Factory
import space.kscience.dataforge.context.Global
import space.kscience.dataforge.context.request
import space.kscience.dataforge.meta.Meta
import kotlin.test.Test
import kotlin.time.Duration.Companion.milliseconds
class DeviceGroupTest {
class TestDevice(context: Context) : DeviceConstructor(context) {
companion object : Factory<Device> {
override fun build(context: Context, meta: Meta): Device = TestDevice(context)
}
}
@Test
fun testRecurringRead() = runTest {
var counter = 10
val testDevice = Global.request(DeviceManager).install("test", TestDevice)
testDevice.doRecurring(1.milliseconds) {
counter--
println(counter)
if (counter <= 0) {
testDevice.stop()
}
error("Error!")
}
testDevice.messageFlow.first { it is DeviceLifeCycleMessage && it.state == DeviceLifecycleState.STOPPED }
println("stopped")
}
}

View File

@ -0,0 +1,22 @@
package space.kscience.controls.constructor
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.take
import kotlinx.coroutines.test.runTest
import space.kscience.controls.manager.ClockManager
import space.kscience.dataforge.context.Global
import space.kscience.dataforge.context.request
import kotlin.test.Test
import kotlin.time.Duration.Companion.milliseconds
class TimerTest {
@Test
fun timer() = runTest {
val timer = TimerState(Global.request(ClockManager), 10.milliseconds)
timer.valueFlow.take(10).onEach {
println(it)
}.collect()
}
}

View File

@ -12,7 +12,6 @@ import space.kscience.dataforge.context.logger
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.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 {

View File

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

View File

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

View File

@ -49,6 +49,10 @@ public fun <D : Device> DeviceManager.install(name: String, device: D): D {
public fun <D : Device> DeviceManager.install(device: D): D = install(device.id, device)
public fun <D : Device> 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(
}
}
}

View File

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

View File

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

View File

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

View File

@ -0,0 +1,41 @@
package space.kscience.controls.spec
import kotlinx.datetime.Instant
import space.kscience.dataforge.meta.*
import kotlin.time.Duration
import kotlin.time.DurationUnit
import kotlin.time.toDuration
public fun Double.asMeta(): Meta = Meta(asValue())
/**
* Generate a nullable [MetaConverter] from non-nullable one
*/
public fun <T : Any> MetaConverter<T>.nullable(): MetaConverter<T?> = object : MetaConverter<T?> {
override fun convert(obj: T?): Meta = obj?.let { this@nullable.convert(it) }?: Meta(Null)
override fun readOrNull(source: Meta): T? = if(source.value == Null) null else this@nullable.readOrNull(source)
}
//TODO to be moved to DF
private object DurationConverter : MetaConverter<Duration> {
override fun readOrNull(source: Meta): Duration = source.value?.double?.toDuration(DurationUnit.SECONDS)
?: run {
val unit: DurationUnit = source["unit"].enum<DurationUnit>() ?: DurationUnit.SECONDS
val value = source[Meta.VALUE_KEY].double ?: error("No value present for Duration")
return@run value.toDuration(unit)
}
override fun convert(obj: Duration): Meta = obj.toDouble(DurationUnit.SECONDS).asMeta()
}
public val MetaConverter.Companion.duration: MetaConverter<Duration> get() = DurationConverter
private object InstantConverter : MetaConverter<Instant> {
override fun readOrNull(source: Meta): Instant? = source.string?.let { Instant.parse(it) }
override fun convert(obj: Instant): Meta = Meta(obj.toString())
}
public val MetaConverter.Companion.instant: MetaConverter<Instant> get() = InstantConverter

View File

@ -1,14 +1,31 @@
package space.kscience.controls.spec
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()
}
}
}

View File

@ -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<*>)

View File

@ -1,22 +0,0 @@
package space.kscience.controls.spec
import space.kscience.dataforge.meta.*
import kotlin.time.Duration
import kotlin.time.DurationUnit
import kotlin.time.toDuration
public fun Double.asMeta(): Meta = Meta(asValue())
//TODO to be moved to DF
public object DurationConverter : MetaConverter<Duration> {
override fun readOrNull(source: Meta): Duration = source.value?.double?.toDuration(DurationUnit.SECONDS)
?: run {
val unit: DurationUnit = source["unit"].enum<DurationUnit>() ?: DurationUnit.SECONDS
val value = source[Meta.VALUE_KEY].double ?: error("No value present for Duration")
return@run value.toDuration(unit)
}
override fun convert(obj: Duration): Meta = obj.toDouble(DurationUnit.SECONDS).asMeta()
}
public val MetaConverter.Companion.duration: MetaConverter<Duration> get() = DurationConverter

View File

@ -2,17 +2,18 @@ package space.kscience.controls.spec
import space.kscience.controls.api.ActionDescriptor
import space.kscience.controls.api.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
}
}

View File

@ -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

View File

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

View File

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

View File

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

View File

@ -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

View File

@ -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

View File

@ -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{

View File

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

View File

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

View File

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

View File

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