First draft of model binding

This commit is contained in:
Alexander Nozik 2024-05-24 13:56:54 +03:00
parent 55bcb08668
commit 05757aefdc
28 changed files with 537 additions and 172 deletions

View File

@ -1 +0,0 @@
./space/* "Project Admin"

View File

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

View File

@ -1,8 +1,5 @@
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.spec.DevicePropertySpec
@ -25,29 +22,24 @@ public abstract class DeviceConstructor(
context: Context,
meta: Meta = Meta.EMPTY,
) : DeviceGroup(context, meta), StateContainer {
private val _stateDescriptors: MutableList<StateDescriptor> = mutableListOf()
override val stateDescriptors: List<StateDescriptor> get() = _stateDescriptors
private val _stateDescriptors: MutableSet<StateDescriptor> = mutableSetOf()
override val stateDescriptors: Set<StateDescriptor> get() = _stateDescriptors
override fun registerState(stateDescriptor: StateDescriptor) {
_stateDescriptors.add(stateDescriptor)
}
override fun registerProperty(descriptor: PropertyDescriptor, state: DeviceState<*>) {
super.registerProperty(descriptor, state)
registerState(PropertyStateDescriptor(this, descriptor.name, state))
override fun unregisterState(stateDescriptor: StateDescriptor) {
_stateDescriptors.remove(stateDescriptor)
}
/**
* Bind an action to a [DeviceState]. [onChange] block is performed on each state 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 {
registerState(ConnectionStateDescriptor(setOf(this, *reads.toTypedArray()), setOf(*writes)))
override fun <T> registerProperty(
converter: MetaConverter<T>,
descriptor: PropertyDescriptor,
state: DeviceState<T>,
) {
super.registerProperty(converter, descriptor, state)
registerState(StatePropertyDescriptor(this, descriptor.name, state))
}
}
@ -84,6 +76,7 @@ public fun <D : Device> DeviceConstructor.device(
* Register a property and provide a direct reader for it
*/
public fun <T, S : DeviceState<T>> DeviceConstructor.property(
converter: MetaConverter<T>,
state: S,
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
nameOverride: String? = null,
@ -91,7 +84,7 @@ public fun <T, S : DeviceState<T>> DeviceConstructor.property(
PropertyDelegateProvider { _: DeviceConstructor, property ->
val name = nameOverride ?: property.name
val descriptor = PropertyDescriptor(name).apply(descriptorBuilder)
registerProperty(descriptor, state)
registerProperty(converter, descriptor, state)
ReadOnlyProperty { _: DeviceConstructor, _ ->
state
}
@ -108,7 +101,8 @@ public fun <T : Any> DeviceConstructor.property(
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
nameOverride: String? = null,
): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, DeviceState<T>>> = property(
DeviceState.external(this, metaConverter, readInterval, initialState, reader),
metaConverter,
DeviceState.external(this, readInterval, initialState, reader),
descriptorBuilder,
nameOverride,
)
@ -125,7 +119,8 @@ public fun <T : Any> DeviceConstructor.mutableProperty(
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
nameOverride: String? = null,
): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, MutableDeviceState<T>>> = property(
DeviceState.external(this, metaConverter, readInterval, initialState, reader, writer),
metaConverter,
DeviceState.external(this, readInterval, initialState, reader, writer),
descriptorBuilder,
nameOverride,
)
@ -140,7 +135,8 @@ public fun <T> DeviceConstructor.virtualProperty(
nameOverride: String? = null,
callback: (T) -> Unit = {},
): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, MutableDeviceState<T>>> = property(
DeviceState.internal(metaConverter, initialState, callback),
metaConverter,
MutableDeviceState(initialState, callback),
descriptorBuilder,
nameOverride,
)
@ -153,7 +149,7 @@ public fun <T, D : Device> DeviceConstructor.deviceProperty(
property: DevicePropertySpec<D, T>,
initialValue: T,
): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, DeviceState<T>>> =
property(device.propertyAsState(property, initialValue))
property(property.converter, device.propertyAsState(property, initialValue))
/**
* Bind existing property provided by specification to this device
@ -163,4 +159,4 @@ public fun <T, D : Device> DeviceConstructor.deviceProperty(
property: MutableDevicePropertySpec<D, T>,
initialValue: T,
): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, MutableDeviceState<T>>> =
property(device.mutablePropertyAsState(property, initialValue))
property(property.converter, device.mutablePropertyAsState(property, initialValue))

View File

@ -1,14 +1,12 @@
package space.kscience.controls.constructor
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.*
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.controls.spec.DevicePropertySpec
import space.kscience.dataforge.context.*
import space.kscience.dataforge.meta.Laminate
import space.kscience.dataforge.meta.Meta
@ -30,12 +28,21 @@ public open class DeviceGroup(
override val meta: Meta,
) : DeviceHub, CachingDevice {
internal class Property(
val state: DeviceState<*>,
private class Property<T>(
val state: DeviceState<T>,
val converter: MetaConverter<T>,
val descriptor: PropertyDescriptor,
)
) {
val valueAsMeta get() = converter.convert(state.value)
internal class Action(
fun setMeta(meta: Meta) {
check(state is MutableDeviceState) { "Can't write to read-only property" }
state.value = converter.read(meta)
}
}
private class Action(
val invoke: suspend (Meta?) -> Meta?,
val descriptor: ActionDescriptor,
)
@ -81,16 +88,20 @@ public open class DeviceGroup(
return device
}
private val properties: MutableMap<Name, Property> = hashMapOf()
private val properties: MutableMap<Name, Property<*>> = hashMapOf()
/**
* Register a new property based on [DeviceState]. Properties could be modified dynamically
*/
public open fun registerProperty(descriptor: PropertyDescriptor, state: DeviceState<*>) {
public open fun <T> registerProperty(
converter: MetaConverter<T>,
descriptor: PropertyDescriptor,
state: DeviceState<T>,
) {
val name = descriptor.name.parseAsName()
require(properties[name] == null) { "Can't add property with name $name. It already exists." }
properties[name] = Property(state, descriptor)
state.metaFlow.onEach {
properties[name] = Property(state, converter, descriptor)
state.valueFlow.map(converter::convert).onEach {
sharedMessageFlow.emit(
PropertyChangedMessage(
descriptor.name,
@ -109,19 +120,18 @@ public open class DeviceGroup(
get() = actions.values.map { it.descriptor }
override suspend fun readProperty(propertyName: String): Meta =
properties[propertyName.parseAsName()]?.state?.valueAsMeta
properties[propertyName.parseAsName()]?.valueAsMeta
?: error("Property with name $propertyName not found")
override fun getProperty(propertyName: String): Meta? = properties[propertyName.parseAsName()]?.state?.valueAsMeta
override fun getProperty(propertyName: String): Meta? = properties[propertyName.parseAsName()]?.valueAsMeta
override suspend fun invalidate(propertyName: String) {
//does nothing for this implementation
}
override suspend fun writeProperty(propertyName: String, value: Meta) {
val property = (properties[propertyName.parseAsName()]?.state as? MutableDeviceState)
?: error("Property with name $propertyName not found")
property.valueAsMeta = value
val property = properties[propertyName.parseAsName()] ?: error("Property with name $propertyName not found")
property.setMeta(value)
}
@ -164,6 +174,10 @@ public open class DeviceGroup(
}
}
public fun <T> DeviceGroup.registerProperty(propertySpec: DevicePropertySpec<*, T>, state: DeviceState<T>) {
registerProperty(propertySpec.converter, propertySpec.descriptor, state)
}
public fun DeviceManager.registerDeviceGroup(
name: String = "@group",
meta: Meta = Meta.EMPTY,
@ -234,10 +248,12 @@ public fun DeviceGroup.registerDeviceGroup(name: String, block: DeviceGroup.() -
*/
public fun <T : Any> DeviceGroup.registerProperty(
name: String,
converter: MetaConverter<T>,
state: DeviceState<T>,
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
) {
registerProperty(
converter,
PropertyDescriptor(name).apply(descriptorBuilder),
state
)
@ -248,10 +264,12 @@ public fun <T : Any> DeviceGroup.registerProperty(
*/
public fun <T : Any> DeviceGroup.registerMutableProperty(
name: String,
converter: MetaConverter<T>,
state: MutableDeviceState<T>,
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
) {
registerProperty(
converter,
PropertyDescriptor(name).apply(descriptorBuilder),
state
)
@ -269,7 +287,7 @@ public fun <T : Any> DeviceGroup.registerVirtualProperty(
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
callback: (T) -> Unit = {},
): MutableDeviceState<T> {
val state = DeviceState.internal<T>(converter, initialValue, callback)
registerMutableProperty(name, state, descriptorBuilder)
val state = MutableDeviceState<T>(initialValue, callback)
registerMutableProperty(name, converter, state, descriptorBuilder)
return state
}

View File

@ -1,14 +1,32 @@
package space.kscience.controls.constructor
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.newCoroutineContext
import space.kscience.dataforge.context.Context
import kotlin.coroutines.CoroutineContext
public abstract class DeviceModel(override val context: Context) : StateContainer {
public abstract class DeviceModel(
final override val context: Context,
vararg dependencies: DeviceState<*>,
) : StateContainer, CoroutineScope {
private val _stateDescriptors: MutableList<StateDescriptor> = mutableListOf()
override val coroutineContext: CoroutineContext = context.newCoroutineContext(SupervisorJob())
override val stateDescriptors: List<StateDescriptor> get() = _stateDescriptors
private val _stateDescriptors: MutableSet<StateDescriptor> = mutableSetOf<StateDescriptor>().apply {
dependencies.forEach {
add(StateNodeDescriptor(it))
}
}
override val stateDescriptors: Set<StateDescriptor> get() = _stateDescriptors
override fun registerState(stateDescriptor: StateDescriptor) {
_stateDescriptors.add(stateDescriptor)
}
override fun unregisterState(stateDescriptor: StateDescriptor) {
_stateDescriptors.remove(stateDescriptor)
}
}

View File

@ -6,15 +6,12 @@ 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
/**
* An observable state of a device
*/
public interface DeviceState<T> {
public val converter: MetaConverter<T>
public val value: T
public val valueFlow: Flow<T>
@ -24,9 +21,6 @@ public interface DeviceState<T> {
public companion object
}
public val <T> DeviceState<T>.metaFlow: Flow<Meta> get() = valueFlow.map(converter::convert)
public val <T> DeviceState<T>.valueAsMeta: Meta get() = converter.convert(value)
public operator fun <T> DeviceState<T>.getValue(thisRef: Any?, property: KProperty<*>): T = value
@ -47,12 +41,6 @@ public operator fun <T> MutableDeviceState<T>.setValue(thisRef: Any?, property:
this.value = value
}
public var <T> MutableDeviceState<T>.valueAsMeta: Meta
get() = converter.convert(value)
set(arg) {
value = converter.read(arg)
}
/**
* Device state with a value that depends on other device states
*/
@ -65,17 +53,15 @@ public interface DeviceStateWithDependencies<T> : DeviceState<T> {
*/
public fun <T, R> DeviceState.Companion.map(
state: DeviceState<T>,
converter: MetaConverter<R>, mapper: (T) -> R,
mapper: (T) -> R,
): DeviceStateWithDependencies<R> = object : DeviceStateWithDependencies<R> {
override val dependencies = listOf(state)
override val converter: MetaConverter<R> = converter
override val value: R get() = mapper(state.value)
override val valueFlow: Flow<R> = state.valueFlow.map(mapper)
override fun toString(): String = "DeviceState.map(arg=${state}, converter=$converter)"
override fun toString(): String = "DeviceState.map(arg=${state})"
}
/**
@ -84,13 +70,10 @@ public fun <T, R> DeviceState.Companion.map(
public fun <T1, T2, R> DeviceState.Companion.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)
override val converter: MetaConverter<R> = converter
override val value: R get() = mapper(state1.value, state2.value)
override val valueFlow: Flow<R> = kotlinx.coroutines.flow.combine(state1.valueFlow, state2.valueFlow, mapper)

View File

@ -1,10 +1,14 @@
package space.kscience.controls.constructor
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.runningFold
import space.kscience.controls.api.Device
import space.kscience.controls.manager.ClockManager
import space.kscience.dataforge.context.ContextAware
import space.kscience.dataforge.context.request
import space.kscience.dataforge.meta.MetaConverter
import kotlin.time.Duration
/**
@ -15,7 +19,7 @@ public sealed interface StateDescriptor
/**
* A binding that exposes device property as read-only state
*/
public class PropertyStateDescriptor<T>(
public class StatePropertyDescriptor<T>(
public val device: Device,
public val propertyName: String,
public val state: DeviceState<T>,
@ -24,51 +28,172 @@ public class PropertyStateDescriptor<T>(
/**
* A binding for independent state like a timer
*/
public class StateBinding<T>(
public class StateNodeDescriptor<T>(
public val state: DeviceState<T>,
) : StateDescriptor
public class ConnectionStateDescriptor(
public class StateConnectionDescriptor(
public val reads: Collection<DeviceState<*>>,
public val writes: Collection<DeviceState<*>>,
) : StateDescriptor
public interface StateContainer : ContextAware {
public val stateDescriptors: List<StateDescriptor>
public interface StateContainer : ContextAware, CoroutineScope {
public val stateDescriptors: Set<StateDescriptor>
public fun registerState(stateDescriptor: StateDescriptor)
public fun unregisterState(stateDescriptor: StateDescriptor)
/**
* Bind an action to a [DeviceState]. [onChange] block is performed on each state change
*
* Optionally provide [writes] - a set of states that this change affects.
*/
public fun <T> DeviceState<T>.onNext(
vararg writes: DeviceState<*>,
alsoReads: Collection<DeviceState<*>> = emptySet(),
onChange: suspend (T) -> Unit,
): Job = valueFlow.onEach(onChange).launchIn(this@StateContainer).also {
registerState(StateConnectionDescriptor(setOf(this, *alsoReads.toTypedArray()), setOf(*writes)))
}
public fun <T> DeviceState<T>.onChange(
vararg writes: DeviceState<*>,
alsoReads: Collection<DeviceState<*>> = emptySet(),
onChange: suspend (prev: T, next: T) -> Unit,
): Job = valueFlow.runningFold(Pair(value, value)) { pair, next ->
Pair(pair.second, next)
}.onEach { pair ->
if (pair.first != pair.second) {
onChange(pair.first, pair.second)
}
}.launchIn(this@StateContainer).also {
registerState(StateConnectionDescriptor(setOf(this, *alsoReads.toTypedArray()), setOf(*writes)))
}
}
/**
* Register a [state] in this container. The state is not registered as a device property if [this] is a [DeviceConstructor]
*/
public fun <T, D : DeviceState<T>> StateContainer.state(state: D): D {
registerState(StateBinding(state))
registerState(StateNodeDescriptor(state))
return state
}
/**
* Create a register a [MutableDeviceState] with a given [converter]
*/
public fun <T> StateContainer.state(converter: MetaConverter<T>, initialValue: T): MutableDeviceState<T> = state(
DeviceState.internal(converter, initialValue)
public fun <T> StateContainer.mutableState(initialValue: T): MutableDeviceState<T> = state(
MutableDeviceState(initialValue)
)
/**
* Create a register a mutable [Double] state
*/
public fun StateContainer.doubleState(initialValue: Double): MutableDeviceState<Double> = state(
MetaConverter.double, initialValue
)
/**
* Create a register a mutable [String] state
*/
public fun StateContainer.stringState(initialValue: String): MutableDeviceState<String> = state(
MetaConverter.string, initialValue
)
public fun <T : DeviceModel> StateContainer.model(model: T): T {
model.stateDescriptors.forEach {
registerState(it)
}
return model
}
/**
* Create and register a timer state.
*/
public fun StateContainer.timer(tick: Duration): TimerState = state(TimerState(context.request(ClockManager), tick))
public fun <T, R> StateContainer.mapState(
state: DeviceState<T>,
transformation: (T) -> R,
): DeviceStateWithDependencies<R> = state(DeviceState.map(state, transformation))
/**
* Create a new state by combining two existing ones
*/
public fun <T1, T2, R> StateContainer.combineState(
first: DeviceState<T1>,
second: DeviceState<T2>,
transformation: (T1, T2) -> R,
): DeviceState<R> = state(DeviceState.combine(first, second, transformation))
/**
* Create and start binding between [sourceState] and [targetState]. Changes made to [sourceState] are automatically
* transferred onto [targetState], but not vise versa.
*
* On resulting [Job] cancel the binding is unregistered
*/
public fun <T> StateContainer.bindTo(sourceState: DeviceState<T>, targetState: MutableDeviceState<T>): Job {
val descriptor = StateConnectionDescriptor(setOf(sourceState), setOf(targetState))
registerState(descriptor)
return sourceState.valueFlow.onEach {
targetState.value = it
}.launchIn(this).apply {
invokeOnCompletion {
unregisterState(descriptor)
}
}
}
/**
* Create and start binding between [sourceState] and [targetState]. Changes made to [sourceState] are automatically
* transferred onto [targetState] via [transformation], but not vise versa.
*
* On resulting [Job] cancel the binding is unregistered
*/
public fun <T, R> StateContainer.transformTo(
sourceState: DeviceState<T>,
targetState: MutableDeviceState<R>,
transformation: suspend (T) -> R,
): Job {
val descriptor = StateConnectionDescriptor(setOf(sourceState), setOf(targetState))
registerState(descriptor)
return sourceState.valueFlow.onEach {
targetState.value = transformation(it)
}.launchIn(this).apply {
invokeOnCompletion {
unregisterState(descriptor)
}
}
}
/**
* Register [StateDescriptor] that combines values from [sourceState1] and [sourceState2] using [transformation].
*
* On resulting [Job] cancel the binding is unregistered
*/
public fun <T1, T2, R> StateContainer.combineTo(
sourceState1: DeviceState<T1>,
sourceState2: DeviceState<T2>,
targetState: MutableDeviceState<R>,
transformation: suspend (T1, T2) -> R,
): Job {
val descriptor = StateConnectionDescriptor(setOf(sourceState1, sourceState2), setOf(targetState))
registerState(descriptor)
return kotlinx.coroutines.flow.combine(sourceState1.valueFlow, sourceState2.valueFlow, transformation).onEach {
targetState.value = it
}.launchIn(this).apply {
invokeOnCompletion {
unregisterState(descriptor)
}
}
}
/**
* Register [StateDescriptor] that combines values from [sourceStates] using [transformation].
*
* On resulting [Job] cancel the binding is unregistered
*/
public inline fun <reified T, R> StateContainer.combineTo(
sourceStates: Collection<DeviceState<T>>,
targetState: MutableDeviceState<R>,
noinline transformation: suspend (Array<T>) -> R,
): Job {
val descriptor = StateConnectionDescriptor(sourceStates, setOf(targetState))
registerState(descriptor)
return kotlinx.coroutines.flow.combine(sourceStates.map { it.valueFlow }, transformation).onEach {
targetState.value = it
}.launchIn(this).apply {
invokeOnCompletion {
unregisterState(descriptor)
}
}
}

View File

@ -7,8 +7,6 @@ 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
/**
@ -23,7 +21,6 @@ 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())

View File

@ -15,7 +15,7 @@ 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 converter: MetaConverter<T>,
val device: Device,
val propertyName: String,
initialValue: T,

View File

@ -2,7 +2,6 @@ package space.kscience.controls.constructor
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import space.kscience.dataforge.meta.MetaConverter
/**
@ -17,7 +16,6 @@ public class DoubleInRangeState(
require(initialValue in range) { "Initial value should be in range" }
}
override val converter: MetaConverter<Double> = MetaConverter.double
private val _valueFlow = MutableStateFlow(initialValue)
@ -32,18 +30,18 @@ public class DoubleInRangeState(
/**
* A state showing that the range is on its lower boundary
*/
public val atStart: DeviceState<Boolean> = DeviceState.map(this, MetaConverter.boolean) {
public val atStart: DeviceState<Boolean> = DeviceState.map(this) {
it <= range.start
}
/**
* A state showing that the range is on its higher boundary
*/
public val atEnd: DeviceState<Boolean> = DeviceState.map(this, MetaConverter.boolean) {
public val atEnd: DeviceState<Boolean> = DeviceState.map(this) {
it >= range.endInclusive
}
override fun toString(): String = "DoubleRangeState(range=$range, converter=$converter)"
override fun toString(): String = "DoubleRangeState(range=$range)"
}
@ -55,5 +53,5 @@ public fun StateContainer.doubleInRangeState(
initialValue: Double,
range: ClosedFloatingPointRange<Double>,
): DoubleInRangeState = DoubleInRangeState(initialValue, range).also {
registerState(StateBinding(it))
registerState(StateNodeDescriptor(it))
}

View File

@ -4,13 +4,11 @@ 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,
@ -26,7 +24,7 @@ private open class ExternalState<T>(
override val value: T get() = flow.value
override val valueFlow: Flow<T> get() = flow
override fun toString(): String = "ExternalState(converter=$converter)"
override fun toString(): String = "ExternalState()"
}
/**
@ -34,20 +32,18 @@ private open class ExternalState<T>(
*/
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)
): DeviceState<T> = ExternalState(scope, 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> {
) : ExternalState<T>(scope, readInterval, initialValue, reader), MutableDeviceState<T> {
override var value: T
get() = super.value
set(value) {
@ -62,9 +58,8 @@ private class MutableExternalState<T>(
*/
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)
): MutableDeviceState<T> = MutableExternalState(scope, readInterval, initialValue, reader, writer)

View File

@ -2,22 +2,19 @@ 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)"
override fun toString(): String = "FlowAsState()"
}
/**
* 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)
public fun <T> MutableStateFlow<T>.asDeviceState(): MutableDeviceState<T> = StateFlowAsState(this)

View File

@ -2,7 +2,6 @@ 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
@ -10,7 +9,6 @@ import space.kscience.dataforge.meta.MetaConverter
* @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> {
@ -24,7 +22,7 @@ private class VirtualDeviceState<T>(
callback(value)
}
override fun toString(): String = "VirtualDeviceState(converter=$converter)"
override fun toString(): String = "VirtualDeviceState()"
}
@ -33,8 +31,7 @@ private class VirtualDeviceState<T>(
*
* @param callback a synchronous callback that could be used without a scope
*/
public fun <T> DeviceState.Companion.internal(
converter: MetaConverter<T>,
public fun <T> MutableDeviceState(
initialValue: T,
callback: (T) -> Unit = {},
): MutableDeviceState<T> = VirtualDeviceState(converter, initialValue, callback)
): MutableDeviceState<T> = VirtualDeviceState(initialValue, callback)

View File

@ -22,6 +22,7 @@ public interface DeviceHub : Provider {
} else {
emptyMap()
}
//TODO send message on device change
public companion object
}

View File

@ -1,4 +1,4 @@
package space.kscience.controls.spec
package space.kscience.controls.misc
import kotlinx.datetime.Instant
import space.kscience.dataforge.meta.*
@ -12,9 +12,9 @@ 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 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)
override fun readOrNull(source: Meta): T? = if (source.value == Null) null else this@nullable.readOrNull(source)
}
@ -38,4 +38,16 @@ private object InstantConverter : MetaConverter<Instant> {
override fun convert(obj: Instant): Meta = Meta(obj.toString())
}
public val MetaConverter.Companion.instant: MetaConverter<Instant> get() = InstantConverter
public val MetaConverter.Companion.instant: MetaConverter<Instant> get() = InstantConverter
private object DoubleRangeConverter : MetaConverter<ClosedFloatingPointRange<Double>> {
override fun readOrNull(source: Meta): ClosedFloatingPointRange<Double>? = source.value?.doubleArray?.let { (start, end)->
start..end
}
override fun convert(
obj: ClosedFloatingPointRange<Double>,
): Meta = Meta(doubleArrayOf(obj.start, obj.endInclusive).asValue())
}
public val MetaConverter.Companion.doubleRange: MetaConverter<ClosedFloatingPointRange<Double>> get() = DoubleRangeConverter

View File

@ -1,9 +1,8 @@
package space.kscience.controls.api
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import space.kscience.controls.spec.asMeta
import space.kscience.controls.misc.asMeta
import kotlin.test.Test
import kotlin.test.assertEquals

View File

@ -16,8 +16,6 @@ import space.kscience.dataforge.context.Context
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.misc.DFExperimental
import space.kscience.dataforge.names.Name
import space.kscience.dataforge.names.length
import space.kscience.dataforge.names.startsWith
import space.kscience.magix.api.MagixEndpoint
import space.kscience.magix.api.send
import space.kscience.magix.api.subscribe
@ -121,12 +119,18 @@ public suspend fun MagixEndpoint.remoteDevice(
deviceEndpoint: String,
deviceName: Name,
): DeviceClient = coroutineScope {
val subscription = subscribe(DeviceManager.magixFormat, originFilter = listOf(deviceEndpoint)).map { it.second }
val subscription = subscribe(DeviceManager.magixFormat, originFilter = listOf(deviceEndpoint))
.map { it.second }
.filter {
it.sourceDevice == null || it.sourceDevice == deviceName
}
val deferredDescriptorMessage = CompletableDeferred<DescriptionMessage>()
launch {
deferredDescriptorMessage.complete(subscription.filterIsInstance<DescriptionMessage>().first())
deferredDescriptorMessage.complete(
subscription.filterIsInstance<DescriptionMessage>().first()
)
}
send(
@ -157,12 +161,6 @@ public suspend fun MagixEndpoint.remoteDevice(
}
}
private class MapBasedDeviceHub(val deviceMap: Map<Name, Device>, val prefix: Name) : DeviceHub {
override val devices: Map<Name, Device>
get() = deviceMap.filterKeys { name: Name -> name == prefix || (name.startsWith(prefix) && name.length == prefix.length + 1) }
}
/**
* Create a dynamic [DeviceHub] from incoming messages
*/

View File

@ -1,6 +1,7 @@
package space.kscience.controls.client
import com.benasher44.uuid.uuid4
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.launchIn
@ -56,7 +57,7 @@ public fun DeviceManager.launchMagixService(
)
}
}.catch { error ->
logger.error(error) { "Error while responding to message: ${error.message}" }
if (error !is CancellationException) logger.error(error) { "Error while responding to message: ${error.message}" }
}.launchIn(this)
hubMessageFlow().onEach { payload ->

View File

@ -1,24 +0,0 @@
package space.kscience.controls.vision
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import space.kscience.dataforge.meta.boolean
import space.kscience.visionforge.AbstractVision
import space.kscience.visionforge.Vision
import space.kscience.visionforge.html.VisionOfHtml
/**
* A [Vision] that shows an indicator
*/
@Serializable
@SerialName("controls.indicator")
public class BooleanIndicatorVision : AbstractVision(), VisionOfHtml {
public val isOn: Boolean by properties.boolean(false)
}
///**
// * A [Vision] that allows both showing the value and changing it
// */
//public interface RegulatorVision: IndicatorVision{
//
//}

View File

@ -13,6 +13,7 @@ public expect class ControlVisionPlugin: VisionPlugin{
internal val controlsVisionSerializersModule = SerializersModule {
polymorphic(Vision::class) {
subclass(BooleanIndicatorVision.serializer())
subclass(IndicatorVision.serializer())
subclass(SliderVision.serializer())
}
}

View File

@ -0,0 +1,35 @@
package space.kscience.controls.vision
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import space.kscience.controls.misc.doubleRange
import space.kscience.dataforge.meta.MetaConverter
import space.kscience.dataforge.meta.convertable
import space.kscience.dataforge.meta.double
import space.kscience.dataforge.meta.string
import space.kscience.visionforge.AbstractControlVision
import space.kscience.visionforge.AbstractVision
import space.kscience.visionforge.Vision
/**
* A [Vision] that shows a colored indicator
*/
@Serializable
@SerialName("controls.indicator")
public class IndicatorVision : AbstractVision() {
public val color: String? by properties.string()
}
@Serializable
@SerialName("controls.slider")
public class SliderVision : AbstractControlVision() {
public var position: Double? by properties.double()
public var range: ClosedFloatingPointRange<Double>? by properties.convertable(MetaConverter.doubleRange)
}
///**
// * A [Vision] that allows both showing the value and changing it
// */
//public interface RegulatorVision: IndicatorVision{
//
//}

View File

@ -145,7 +145,7 @@ private fun <T> Trace.updateFromState(
public fun <T> Plot.plotDeviceState(
context: Context,
state: DeviceState<T>,
extractValue: T.() -> Value = { state.converter.convert(this).value ?: Null },
extractValue: (T) -> Value = { Value.of(it) },
maxAge: Duration = defaultMaxAge,
maxPoints: Int = defaultMaxPoints,
minPoints: Int = defaultMinPoints,

View File

@ -10,7 +10,34 @@ import space.kscience.dataforge.names.asName
import space.kscience.visionforge.VisionPlugin
import space.kscience.visionforge.html.ElementVisionRenderer
private val indicatorRenderer = ElementVisionRenderer<BooleanIndicatorVision> { name, vision: BooleanIndicatorVision, meta ->
private val indicatorRenderer = ElementVisionRenderer<IndicatorVision> { name, vision: IndicatorVision, meta ->
// val ledSize = vision.properties["size"].int ?: 15
// val color = vision.color ?: "LightGray"
// div("controls-indicator") {
// style = """
//
// @keyframes blink {
// 0% { box-shadow: 0 0 10px; }
// 50% { box-shadow: 0 0 30px; }
// 100% { box-shadow: 0 0 10px; }
// }
//
// display: inline-block;
// margin: ${ledSize}px;
// width: ${ledSize}px;
// height: ${ledSize}px;
// border-radius: 50%;
//
// background: $color;
// border: 1px solid darken($color,5%);
// color: $color;
// animation: blink 3s infinite;
// """.trimIndent()
// }
}
private val sliderRenderer = ElementVisionRenderer<SliderVision> { name, vision: SliderVision, meta ->
}
@ -22,7 +49,8 @@ public actual class ControlVisionPlugin : VisionPlugin() {
override fun content(target: String): Map<Name, Any> = when (target) {
ElementVisionRenderer.TYPE -> mapOf(
"indicator".asName() to indicatorRenderer
"indicator".asName() to indicatorRenderer,
"slider".asName() to sliderRenderer
)
else -> super.content(target)

View File

@ -64,7 +64,7 @@ class VirtualCarController : Controller(), ContextAware {
//mongoStorageJob = deviceManager.storeMessages(DefaultAsynchronousMongoClientFactory)
//Launch device client and connect it to the server
val deviceEndpoint = MagixEndpoint.rSocketWithTcp("localhost")
deviceManager.launchMagixService(deviceEndpoint)
deviceManager.launchMagixService(deviceEndpoint, "car")
}
}

View File

@ -1,3 +1,4 @@
import org.jetbrains.compose.ExperimentalComposeLibrary
import org.jetbrains.compose.desktop.application.dsl.TargetFormat
import org.jetbrains.kotlin.gradle.dsl.ExplicitApiMode
@ -11,12 +12,15 @@ kscience {
withJava()
}
useKtor()
useSerialization()
useContextReceivers()
dependencies {
api(projects.controlsVision)
commonMain {
implementation(projects.controlsVision)
implementation(projects.controlsConstructor)
// implementation("io.github.koalaplot:koalaplot-core:0.6.0")
}
jvmMain {
implementation("io.ktor:ktor-server-cio")
// implementation("io.ktor:ktor-server-cio")
implementation(spclibs.logback.classic)
}
}
@ -26,6 +30,8 @@ kotlin {
jvmMain {
dependencies {
implementation(compose.desktop.currentOs)
@OptIn(ExperimentalComposeLibrary::class)
implementation(compose.desktop.components.splitPane)
}
}
}

View File

@ -0,0 +1,185 @@
package space.kscience.controls.demo.constructor
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.size
import androidx.compose.material.MaterialTheme
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application
import kotlinx.serialization.Serializable
import space.kscience.controls.constructor.*
import space.kscience.dataforge.context.Context
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
import kotlin.math.pow
import kotlin.math.sqrt
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.DurationUnit
@Serializable
data class XY(val x: Double, val y: Double) {
companion object {
val ZERO = XY(0.0, 0.0)
}
}
operator fun XY.plus(other: XY): XY = XY(x + other.x, y + other.y)
operator fun XY.times(c: Double): XY = XY(x * c, y * c)
operator fun XY.div(c: Double): XY = XY(x / c, y / c)
//
//class XYPosition(context: Context, x0: Double, y0: Double) : DeviceModel(context) {
// val x: MutableDeviceState<Double> = mutableState(x0)
// val y: MutableDeviceState<Double> = mutableState(y0)
//
// val xy = combineState(x, y) { x, y -> XY(x, y) }
//}
class Spring(
context: Context,
val k: Double,
val l0: Double,
val begin: DeviceState<XY>,
val end: DeviceState<XY>,
) : DeviceConstructor(context) {
val length = combineState(begin, end) { begin, end ->
sqrt((end.y - begin.y).pow(2) + (end.x - begin.x).pow(2))
}
val tension: DeviceState<Double> = mapState(length) { l ->
val delta = l - l0
k * delta
}
/**
* direction from start to end
*/
val direction = combineState(begin, end) { begin, end ->
val dx = end.x - begin.x
val dy = end.y - begin.y
val l = sqrt((end.y - begin.y).pow(2) + (end.x - begin.x).pow(2))
XY(dx / l, dy / l)
}
val beginForce = combineState(direction, tension) { direction: XY, tension: Double ->
direction * (tension)
}
val endForce = combineState(direction, tension) { direction: XY, tension: Double ->
direction * (-tension)
}
}
class MaterialPoint(
context: Context,
val mass: Double,
val force: DeviceState<XY>,
val position: MutableDeviceState<XY>,
val velocity: MutableDeviceState<XY> = MutableDeviceState(XY.ZERO),
) : DeviceModel(context, force) {
private val timer: TimerState = timer(2.milliseconds)
private val movement = timer.onChange(
position, velocity,
alsoReads = setOf(force, velocity, position)
) { prev, next ->
val dt = (next - prev).toDouble(DurationUnit.SECONDS)
val a = force.value / mass
position.value += a * (dt * dt / 2) + velocity.value * dt
velocity.value += a * dt
}
}
class BodyOnSprings(
context: Context,
mass: Double,
k: Double,
startPosition: XY,
l0: Double = 1.0,
val xLeft: Double = 0.0,
val xRight: Double = 2.0,
val yBottom: Double = 0.0,
val yTop: Double = 2.0,
) : DeviceConstructor(context) {
val width = xRight - xLeft
val height = yTop - yBottom
val position = mutableState(startPosition)
private val leftAnchor = mutableState(XY(xLeft, yTop + yBottom / 2))
val leftSpring by device(
Spring(context, k, l0, leftAnchor, position)
)
private val rightAnchor = mutableState(XY(xRight, yTop + yBottom / 2))
val rightSpring by device(
Spring(context, k, l0, rightAnchor, position)
)
val force: DeviceState<XY> = combineState(leftSpring.endForce, rightSpring.endForce) { left, rignt ->
left + rignt
}
val body = model(
MaterialPoint(
context = context,
mass = mass,
force = force,
position = position
)
)
}
@Composable
fun <T> DeviceState<T>.collect(
coroutineContext: CoroutineContext = EmptyCoroutineContext,
): State<T> = valueFlow.collectAsState(value, coroutineContext)
fun main() = application {
val initialState = XY(1.1, 1.1)
Window(title = "Ball on springs", onCloseRequest = ::exitApplication) {
MaterialTheme {
val context = remember {
Context("simulation")
}
val model = remember {
BodyOnSprings(context, 100.0, 1000.0, initialState)
}
val position: XY by model.body.position.collect()
Box(Modifier.size(400.dp)) {
Canvas(modifier = Modifier.fillMaxSize()) {
fun XY.toOffset() = Offset(
(x / model.width * size.width).toFloat(),
(y / model.height * size.height).toFloat()
)
drawCircle(
Color.Red, 10f, center = position.toOffset()
)
drawLine(Color.Blue, model.leftSpring.begin.value.toOffset(), model.leftSpring.end.value.toOffset())
drawLine(
Color.Blue,
model.rightSpring.begin.value.toOffset(),
model.rightSpring.end.value.toOffset()
)
}
}
}
}
}

View File

@ -12,6 +12,8 @@ import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withTimeout
import space.kscience.controls.api.DeviceHub
import space.kscience.controls.api.PropertyDescriptor
import space.kscience.controls.misc.asMeta
import space.kscience.controls.misc.duration
import space.kscience.controls.ports.AsynchronousPort
import space.kscience.controls.ports.KtorTcpPort
import space.kscience.controls.ports.send

View File

@ -79,6 +79,7 @@ visionforge-jupiter = { module = "space.kscience:visionforge-jupyter", version.r
visionforge-plotly = { module = "space.kscience:visionforge-plotly", version.ref = "visionforge" }
visionforge-markdown = { module = "space.kscience:visionforge-markdown", version.ref = "visionforge" }
visionforge-server = { module = "space.kscience:visionforge-server", version.ref = "visionforge" }
visionforge-compose-html = { module = "space.kscience:visionforge-compose-html", version.ref = "visionforge" }
# Buildscript