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 { allprojects {
group = "space.kscience" group = "space.kscience"
version = "0.4.0-dev-3" version = "0.4.0-dev-4"
repositories{
maven("https://maven.pkg.jetbrains.space/spc/p/sci/dev")
}
} }
ksciencePublish { ksciencePublish {

View File

@ -1,8 +1,5 @@
package space.kscience.controls.constructor package space.kscience.controls.constructor
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import space.kscience.controls.api.Device import space.kscience.controls.api.Device
import space.kscience.controls.api.PropertyDescriptor import space.kscience.controls.api.PropertyDescriptor
import space.kscience.controls.spec.DevicePropertySpec import space.kscience.controls.spec.DevicePropertySpec
@ -25,29 +22,24 @@ public abstract class DeviceConstructor(
context: Context, context: Context,
meta: Meta = Meta.EMPTY, meta: Meta = Meta.EMPTY,
) : DeviceGroup(context, meta), StateContainer { ) : DeviceGroup(context, meta), StateContainer {
private val _stateDescriptors: MutableList<StateDescriptor> = mutableListOf() private val _stateDescriptors: MutableSet<StateDescriptor> = mutableSetOf()
override val stateDescriptors: List<StateDescriptor> get() = _stateDescriptors override val stateDescriptors: Set<StateDescriptor> get() = _stateDescriptors
override fun registerState(stateDescriptor: StateDescriptor) { override fun registerState(stateDescriptor: StateDescriptor) {
_stateDescriptors.add(stateDescriptor) _stateDescriptors.add(stateDescriptor)
} }
override fun registerProperty(descriptor: PropertyDescriptor, state: DeviceState<*>) { override fun unregisterState(stateDescriptor: StateDescriptor) {
super.registerProperty(descriptor, state) _stateDescriptors.remove(stateDescriptor)
registerState(PropertyStateDescriptor(this, descriptor.name, state))
} }
/** override fun <T> registerProperty(
* Bind an action to a [DeviceState]. [onChange] block is performed on each state change converter: MetaConverter<T>,
* descriptor: PropertyDescriptor,
* Optionally provide [writes] - a set of states that this change affects. state: DeviceState<T>,
*/ ) {
public fun <T> DeviceState<T>.onChange( super.registerProperty(converter, descriptor, state)
vararg writes: DeviceState<*>, registerState(StatePropertyDescriptor(this, descriptor.name, state))
reads: Collection<DeviceState<*>>,
onChange: suspend (T) -> Unit,
): Job = valueFlow.onEach(onChange).launchIn(this@DeviceConstructor).also {
registerState(ConnectionStateDescriptor(setOf(this, *reads.toTypedArray()), setOf(*writes)))
} }
} }
@ -84,6 +76,7 @@ public fun <D : Device> DeviceConstructor.device(
* Register a property and provide a direct reader for it * Register a property and provide a direct reader for it
*/ */
public fun <T, S : DeviceState<T>> DeviceConstructor.property( public fun <T, S : DeviceState<T>> DeviceConstructor.property(
converter: MetaConverter<T>,
state: S, state: S,
descriptorBuilder: PropertyDescriptor.() -> Unit = {}, descriptorBuilder: PropertyDescriptor.() -> Unit = {},
nameOverride: String? = null, nameOverride: String? = null,
@ -91,7 +84,7 @@ public fun <T, S : DeviceState<T>> DeviceConstructor.property(
PropertyDelegateProvider { _: DeviceConstructor, property -> PropertyDelegateProvider { _: DeviceConstructor, property ->
val name = nameOverride ?: property.name val name = nameOverride ?: property.name
val descriptor = PropertyDescriptor(name).apply(descriptorBuilder) val descriptor = PropertyDescriptor(name).apply(descriptorBuilder)
registerProperty(descriptor, state) registerProperty(converter, descriptor, state)
ReadOnlyProperty { _: DeviceConstructor, _ -> ReadOnlyProperty { _: DeviceConstructor, _ ->
state state
} }
@ -108,7 +101,8 @@ public fun <T : Any> DeviceConstructor.property(
descriptorBuilder: PropertyDescriptor.() -> Unit = {}, descriptorBuilder: PropertyDescriptor.() -> Unit = {},
nameOverride: String? = null, nameOverride: String? = null,
): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, DeviceState<T>>> = property( ): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, DeviceState<T>>> = property(
DeviceState.external(this, metaConverter, readInterval, initialState, reader), metaConverter,
DeviceState.external(this, readInterval, initialState, reader),
descriptorBuilder, descriptorBuilder,
nameOverride, nameOverride,
) )
@ -125,7 +119,8 @@ public fun <T : Any> DeviceConstructor.mutableProperty(
descriptorBuilder: PropertyDescriptor.() -> Unit = {}, descriptorBuilder: PropertyDescriptor.() -> Unit = {},
nameOverride: String? = null, nameOverride: String? = null,
): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, MutableDeviceState<T>>> = property( ): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, MutableDeviceState<T>>> = property(
DeviceState.external(this, metaConverter, readInterval, initialState, reader, writer), metaConverter,
DeviceState.external(this, readInterval, initialState, reader, writer),
descriptorBuilder, descriptorBuilder,
nameOverride, nameOverride,
) )
@ -140,7 +135,8 @@ public fun <T> DeviceConstructor.virtualProperty(
nameOverride: String? = null, nameOverride: String? = null,
callback: (T) -> Unit = {}, callback: (T) -> Unit = {},
): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, MutableDeviceState<T>>> = property( ): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, MutableDeviceState<T>>> = property(
DeviceState.internal(metaConverter, initialState, callback), metaConverter,
MutableDeviceState(initialState, callback),
descriptorBuilder, descriptorBuilder,
nameOverride, nameOverride,
) )
@ -153,7 +149,7 @@ public fun <T, D : Device> DeviceConstructor.deviceProperty(
property: DevicePropertySpec<D, T>, property: DevicePropertySpec<D, T>,
initialValue: T, initialValue: T,
): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, DeviceState<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 * Bind existing property provided by specification to this device
@ -163,4 +159,4 @@ public fun <T, D : Device> DeviceConstructor.deviceProperty(
property: MutableDevicePropertySpec<D, T>, property: MutableDevicePropertySpec<D, T>,
initialValue: T, initialValue: T,
): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, MutableDeviceState<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 package space.kscience.controls.constructor
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.*
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import space.kscience.controls.api.* import space.kscience.controls.api.*
import space.kscience.controls.api.DeviceLifecycleState.* import space.kscience.controls.api.DeviceLifecycleState.*
import space.kscience.controls.manager.DeviceManager import space.kscience.controls.manager.DeviceManager
import space.kscience.controls.manager.install import space.kscience.controls.manager.install
import space.kscience.controls.spec.DevicePropertySpec
import space.kscience.dataforge.context.* import space.kscience.dataforge.context.*
import space.kscience.dataforge.meta.Laminate import space.kscience.dataforge.meta.Laminate
import space.kscience.dataforge.meta.Meta import space.kscience.dataforge.meta.Meta
@ -30,12 +28,21 @@ public open class DeviceGroup(
override val meta: Meta, override val meta: Meta,
) : DeviceHub, CachingDevice { ) : DeviceHub, CachingDevice {
internal class Property( private class Property<T>(
val state: DeviceState<*>, val state: DeviceState<T>,
val converter: MetaConverter<T>,
val descriptor: PropertyDescriptor, 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 invoke: suspend (Meta?) -> Meta?,
val descriptor: ActionDescriptor, val descriptor: ActionDescriptor,
) )
@ -81,16 +88,20 @@ public open class DeviceGroup(
return device 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 * 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() val name = descriptor.name.parseAsName()
require(properties[name] == null) { "Can't add property with name $name. It already exists." } require(properties[name] == null) { "Can't add property with name $name. It already exists." }
properties[name] = Property(state, descriptor) properties[name] = Property(state, converter, descriptor)
state.metaFlow.onEach { state.valueFlow.map(converter::convert).onEach {
sharedMessageFlow.emit( sharedMessageFlow.emit(
PropertyChangedMessage( PropertyChangedMessage(
descriptor.name, descriptor.name,
@ -109,19 +120,18 @@ public open class DeviceGroup(
get() = actions.values.map { it.descriptor } get() = actions.values.map { it.descriptor }
override suspend fun readProperty(propertyName: String): Meta = override suspend fun readProperty(propertyName: String): Meta =
properties[propertyName.parseAsName()]?.state?.valueAsMeta properties[propertyName.parseAsName()]?.valueAsMeta
?: error("Property with name $propertyName not found") ?: 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) { override suspend fun invalidate(propertyName: String) {
//does nothing for this implementation //does nothing for this implementation
} }
override suspend fun writeProperty(propertyName: String, value: Meta) { override suspend fun writeProperty(propertyName: String, value: Meta) {
val property = (properties[propertyName.parseAsName()]?.state as? MutableDeviceState) val property = properties[propertyName.parseAsName()] ?: error("Property with name $propertyName not found")
?: error("Property with name $propertyName not found") property.setMeta(value)
property.valueAsMeta = 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( public fun DeviceManager.registerDeviceGroup(
name: String = "@group", name: String = "@group",
meta: Meta = Meta.EMPTY, meta: Meta = Meta.EMPTY,
@ -234,10 +248,12 @@ public fun DeviceGroup.registerDeviceGroup(name: String, block: DeviceGroup.() -
*/ */
public fun <T : Any> DeviceGroup.registerProperty( public fun <T : Any> DeviceGroup.registerProperty(
name: String, name: String,
converter: MetaConverter<T>,
state: DeviceState<T>, state: DeviceState<T>,
descriptorBuilder: PropertyDescriptor.() -> Unit = {}, descriptorBuilder: PropertyDescriptor.() -> Unit = {},
) { ) {
registerProperty( registerProperty(
converter,
PropertyDescriptor(name).apply(descriptorBuilder), PropertyDescriptor(name).apply(descriptorBuilder),
state state
) )
@ -248,10 +264,12 @@ public fun <T : Any> DeviceGroup.registerProperty(
*/ */
public fun <T : Any> DeviceGroup.registerMutableProperty( public fun <T : Any> DeviceGroup.registerMutableProperty(
name: String, name: String,
converter: MetaConverter<T>,
state: MutableDeviceState<T>, state: MutableDeviceState<T>,
descriptorBuilder: PropertyDescriptor.() -> Unit = {}, descriptorBuilder: PropertyDescriptor.() -> Unit = {},
) { ) {
registerProperty( registerProperty(
converter,
PropertyDescriptor(name).apply(descriptorBuilder), PropertyDescriptor(name).apply(descriptorBuilder),
state state
) )
@ -269,7 +287,7 @@ public fun <T : Any> DeviceGroup.registerVirtualProperty(
descriptorBuilder: PropertyDescriptor.() -> Unit = {}, descriptorBuilder: PropertyDescriptor.() -> Unit = {},
callback: (T) -> Unit = {}, callback: (T) -> Unit = {},
): MutableDeviceState<T> { ): MutableDeviceState<T> {
val state = DeviceState.internal<T>(converter, initialValue, callback) val state = MutableDeviceState<T>(initialValue, callback)
registerMutableProperty(name, state, descriptorBuilder) registerMutableProperty(name, converter, state, descriptorBuilder)
return state return state
} }

View File

@ -1,14 +1,32 @@
package space.kscience.controls.constructor package space.kscience.controls.constructor
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.newCoroutineContext
import space.kscience.dataforge.context.Context 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) { override fun registerState(stateDescriptor: StateDescriptor) {
_stateDescriptors.add(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.launchIn
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.MetaConverter
import kotlin.reflect.KProperty import kotlin.reflect.KProperty
/** /**
* An observable state of a device * An observable state of a device
*/ */
public interface DeviceState<T> { public interface DeviceState<T> {
public val converter: MetaConverter<T>
public val value: T public val value: T
public val valueFlow: Flow<T> public val valueFlow: Flow<T>
@ -24,9 +21,6 @@ public interface DeviceState<T> {
public companion object 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 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 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 * 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( public fun <T, R> DeviceState.Companion.map(
state: DeviceState<T>, state: DeviceState<T>,
converter: MetaConverter<R>, mapper: (T) -> R, mapper: (T) -> R,
): DeviceStateWithDependencies<R> = object : DeviceStateWithDependencies<R> { ): DeviceStateWithDependencies<R> = object : DeviceStateWithDependencies<R> {
override val dependencies = listOf(state) override val dependencies = listOf(state)
override val converter: MetaConverter<R> = converter
override val value: R get() = mapper(state.value) override val value: R get() = mapper(state.value)
override val valueFlow: Flow<R> = state.valueFlow.map(mapper) 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( public fun <T1, T2, R> DeviceState.Companion.combine(
state1: DeviceState<T1>, state1: DeviceState<T1>,
state2: DeviceState<T2>, state2: DeviceState<T2>,
converter: MetaConverter<R>,
mapper: (T1, T2) -> R, mapper: (T1, T2) -> R,
): DeviceStateWithDependencies<R> = object : DeviceStateWithDependencies<R> { ): DeviceStateWithDependencies<R> = object : DeviceStateWithDependencies<R> {
override val dependencies = listOf(state1, state2) override val dependencies = listOf(state1, state2)
override val converter: MetaConverter<R> = converter
override val value: R get() = mapper(state1.value, state2.value) 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 val valueFlow: Flow<R> = kotlinx.coroutines.flow.combine(state1.valueFlow, state2.valueFlow, mapper)

View File

@ -1,10 +1,14 @@
package space.kscience.controls.constructor 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.api.Device
import space.kscience.controls.manager.ClockManager import space.kscience.controls.manager.ClockManager
import space.kscience.dataforge.context.ContextAware import space.kscience.dataforge.context.ContextAware
import space.kscience.dataforge.context.request import space.kscience.dataforge.context.request
import space.kscience.dataforge.meta.MetaConverter
import kotlin.time.Duration import kotlin.time.Duration
/** /**
@ -15,7 +19,7 @@ public sealed interface StateDescriptor
/** /**
* A binding that exposes device property as read-only state * A binding that exposes device property as read-only state
*/ */
public class PropertyStateDescriptor<T>( public class StatePropertyDescriptor<T>(
public val device: Device, public val device: Device,
public val propertyName: String, public val propertyName: String,
public val state: DeviceState<T>, public val state: DeviceState<T>,
@ -24,51 +28,172 @@ public class PropertyStateDescriptor<T>(
/** /**
* A binding for independent state like a timer * A binding for independent state like a timer
*/ */
public class StateBinding<T>( public class StateNodeDescriptor<T>(
public val state: DeviceState<T>, public val state: DeviceState<T>,
) : StateDescriptor ) : StateDescriptor
public class ConnectionStateDescriptor( public class StateConnectionDescriptor(
public val reads: Collection<DeviceState<*>>, public val reads: Collection<DeviceState<*>>,
public val writes: Collection<DeviceState<*>>, public val writes: Collection<DeviceState<*>>,
) : StateDescriptor ) : StateDescriptor
public interface StateContainer : ContextAware { public interface StateContainer : ContextAware, CoroutineScope {
public val stateDescriptors: List<StateDescriptor> public val stateDescriptors: Set<StateDescriptor>
public fun registerState(stateDescriptor: 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] * 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 { public fun <T, D : DeviceState<T>> StateContainer.state(state: D): D {
registerState(StateBinding(state)) registerState(StateNodeDescriptor(state))
return state return state
} }
/** /**
* Create a register a [MutableDeviceState] with a given [converter] * Create a register a [MutableDeviceState] with a given [converter]
*/ */
public fun <T> StateContainer.state(converter: MetaConverter<T>, initialValue: T): MutableDeviceState<T> = state( public fun <T> StateContainer.mutableState(initialValue: T): MutableDeviceState<T> = state(
DeviceState.internal(converter, initialValue) MutableDeviceState(initialValue)
) )
/** public fun <T : DeviceModel> StateContainer.model(model: T): T {
* Create a register a mutable [Double] state model.stateDescriptors.forEach {
*/ registerState(it)
public fun StateContainer.doubleState(initialValue: Double): MutableDeviceState<Double> = state( }
MetaConverter.double, initialValue return model
) }
/**
* Create a register a mutable [String] state
*/
public fun StateContainer.stringState(initialValue: String): MutableDeviceState<String> = state(
MetaConverter.string, initialValue
)
/** /**
* Create and register a timer state. * Create and register a timer state.
*/ */
public fun StateContainer.timer(tick: Duration): TimerState = state(TimerState(context.request(ClockManager), tick)) 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.coroutines.launch
import kotlinx.datetime.Instant import kotlinx.datetime.Instant
import space.kscience.controls.manager.ClockManager import space.kscience.controls.manager.ClockManager
import space.kscience.controls.spec.instant
import space.kscience.dataforge.meta.MetaConverter
import kotlin.time.Duration import kotlin.time.Duration
/** /**
@ -23,7 +21,6 @@ public class TimerState(
public val clockManager: ClockManager, public val clockManager: ClockManager,
public val tick: Duration, public val tick: Duration,
) : DeviceState<Instant> { ) : DeviceState<Instant> {
override val converter: MetaConverter<Instant> get() = MetaConverter.instant
private val clock = MutableStateFlow(clockManager.clock.now()) 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 * A copy-free [DeviceState] bound to a device property
*/ */
private open class BoundDeviceState<T>( private open class BoundDeviceState<T>(
override val converter: MetaConverter<T>, val converter: MetaConverter<T>,
val device: Device, val device: Device,
val propertyName: String, val propertyName: String,
initialValue: T, initialValue: T,

View File

@ -2,7 +2,6 @@ package space.kscience.controls.constructor
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow 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" } require(initialValue in range) { "Initial value should be in range" }
} }
override val converter: MetaConverter<Double> = MetaConverter.double
private val _valueFlow = MutableStateFlow(initialValue) private val _valueFlow = MutableStateFlow(initialValue)
@ -32,18 +30,18 @@ public class DoubleInRangeState(
/** /**
* A state showing that the range is on its lower boundary * 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 it <= range.start
} }
/** /**
* A state showing that the range is on its higher boundary * 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 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, initialValue: Double,
range: ClosedFloatingPointRange<Double>, range: ClosedFloatingPointRange<Double>,
): DoubleInRangeState = DoubleInRangeState(initialValue, range).also { ): 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.delay
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import space.kscience.dataforge.meta.MetaConverter
import kotlin.time.Duration import kotlin.time.Duration
private open class ExternalState<T>( private open class ExternalState<T>(
val scope: CoroutineScope, val scope: CoroutineScope,
override val converter: MetaConverter<T>,
val readInterval: Duration, val readInterval: Duration,
initialValue: T, initialValue: T,
val reader: suspend () -> T, val reader: suspend () -> T,
@ -26,7 +24,7 @@ private open class ExternalState<T>(
override val value: T get() = flow.value override val value: T get() = flow.value
override val valueFlow: Flow<T> get() = flow 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( public fun <T> DeviceState.Companion.external(
scope: CoroutineScope, scope: CoroutineScope,
converter: MetaConverter<T>,
readInterval: Duration, readInterval: Duration,
initialValue: T, initialValue: T,
reader: suspend () -> T, reader: suspend () -> T,
): DeviceState<T> = ExternalState(scope, converter, readInterval, initialValue, reader) ): DeviceState<T> = ExternalState(scope, readInterval, initialValue, reader)
private class MutableExternalState<T>( private class MutableExternalState<T>(
scope: CoroutineScope, scope: CoroutineScope,
converter: MetaConverter<T>,
readInterval: Duration, readInterval: Duration,
initialValue: T, initialValue: T,
reader: suspend () -> T, reader: suspend () -> T,
val writer: suspend (T) -> Unit, 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 override var value: T
get() = super.value get() = super.value
set(value) { set(value) {
@ -62,9 +58,8 @@ private class MutableExternalState<T>(
*/ */
public fun <T> DeviceState.Companion.external( public fun <T> DeviceState.Companion.external(
scope: CoroutineScope, scope: CoroutineScope,
converter: MetaConverter<T>,
readInterval: Duration, readInterval: Duration,
initialValue: T, initialValue: T,
reader: suspend () -> T, reader: suspend () -> T,
writer: suspend (T) -> Unit, 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.Flow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import space.kscience.dataforge.meta.MetaConverter
private class StateFlowAsState<T>( private class StateFlowAsState<T>(
override val converter: MetaConverter<T>,
val flow: MutableStateFlow<T>, val flow: MutableStateFlow<T>,
) : MutableDeviceState<T> { ) : MutableDeviceState<T> {
override var value: T by flow::value override var value: T by flow::value
override val valueFlow: Flow<T> get() = flow 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]. * Create a read-only [DeviceState] that wraps [MutableStateFlow].
* No data copy is performed. * No data copy is performed.
*/ */
public fun <T> MutableStateFlow<T>.asDeviceState(converter: MetaConverter<T>): DeviceState<T> = public fun <T> MutableStateFlow<T>.asDeviceState(): MutableDeviceState<T> = StateFlowAsState(this)
StateFlowAsState(converter, this)

View File

@ -2,7 +2,6 @@ package space.kscience.controls.constructor
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import space.kscience.dataforge.meta.MetaConverter
/** /**
* A [MutableDeviceState] that does not correspond to a physical state * 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 * @param callback a synchronous callback that could be used without a scope
*/ */
private class VirtualDeviceState<T>( private class VirtualDeviceState<T>(
override val converter: MetaConverter<T>,
initialValue: T, initialValue: T,
private val callback: (T) -> Unit = {}, private val callback: (T) -> Unit = {},
) : MutableDeviceState<T> { ) : MutableDeviceState<T> {
@ -24,7 +22,7 @@ private class VirtualDeviceState<T>(
callback(value) 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 * @param callback a synchronous callback that could be used without a scope
*/ */
public fun <T> DeviceState.Companion.internal( public fun <T> MutableDeviceState(
converter: MetaConverter<T>,
initialValue: T, initialValue: T,
callback: (T) -> Unit = {}, 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 { } else {
emptyMap() emptyMap()
} }
//TODO send message on device change
public companion object public companion object
} }

View File

@ -1,4 +1,4 @@
package space.kscience.controls.spec package space.kscience.controls.misc
import kotlinx.datetime.Instant import kotlinx.datetime.Instant
import space.kscience.dataforge.meta.* import space.kscience.dataforge.meta.*
@ -39,3 +39,15 @@ private object InstantConverter : MetaConverter<Instant> {
} }
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 package space.kscience.controls.api
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import space.kscience.controls.spec.asMeta import space.kscience.controls.misc.asMeta
import kotlin.test.Test import kotlin.test.Test
import kotlin.test.assertEquals 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.meta.Meta
import space.kscience.dataforge.misc.DFExperimental import space.kscience.dataforge.misc.DFExperimental
import space.kscience.dataforge.names.Name 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.MagixEndpoint
import space.kscience.magix.api.send import space.kscience.magix.api.send
import space.kscience.magix.api.subscribe import space.kscience.magix.api.subscribe
@ -121,12 +119,18 @@ public suspend fun MagixEndpoint.remoteDevice(
deviceEndpoint: String, deviceEndpoint: String,
deviceName: Name, deviceName: Name,
): DeviceClient = coroutineScope { ): 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>() val deferredDescriptorMessage = CompletableDeferred<DescriptionMessage>()
launch { launch {
deferredDescriptorMessage.complete(subscription.filterIsInstance<DescriptionMessage>().first()) deferredDescriptorMessage.complete(
subscription.filterIsInstance<DescriptionMessage>().first()
)
} }
send( 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 * Create a dynamic [DeviceHub] from incoming messages
*/ */

View File

@ -1,6 +1,7 @@
package space.kscience.controls.client package space.kscience.controls.client
import com.benasher44.uuid.uuid4 import com.benasher44.uuid.uuid4
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
@ -56,7 +57,7 @@ public fun DeviceManager.launchMagixService(
) )
} }
}.catch { error -> }.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) }.launchIn(this)
hubMessageFlow().onEach { payload -> 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 { internal val controlsVisionSerializersModule = SerializersModule {
polymorphic(Vision::class) { 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( public fun <T> Plot.plotDeviceState(
context: Context, context: Context,
state: DeviceState<T>, state: DeviceState<T>,
extractValue: T.() -> Value = { state.converter.convert(this).value ?: Null }, extractValue: (T) -> Value = { Value.of(it) },
maxAge: Duration = defaultMaxAge, maxAge: Duration = defaultMaxAge,
maxPoints: Int = defaultMaxPoints, maxPoints: Int = defaultMaxPoints,
minPoints: Int = defaultMinPoints, minPoints: Int = defaultMinPoints,

View File

@ -10,7 +10,34 @@ import space.kscience.dataforge.names.asName
import space.kscience.visionforge.VisionPlugin import space.kscience.visionforge.VisionPlugin
import space.kscience.visionforge.html.ElementVisionRenderer 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) { override fun content(target: String): Map<Name, Any> = when (target) {
ElementVisionRenderer.TYPE -> mapOf( ElementVisionRenderer.TYPE -> mapOf(
"indicator".asName() to indicatorRenderer "indicator".asName() to indicatorRenderer,
"slider".asName() to sliderRenderer
) )
else -> super.content(target) else -> super.content(target)

View File

@ -64,7 +64,7 @@ class VirtualCarController : Controller(), ContextAware {
//mongoStorageJob = deviceManager.storeMessages(DefaultAsynchronousMongoClientFactory) //mongoStorageJob = deviceManager.storeMessages(DefaultAsynchronousMongoClientFactory)
//Launch device client and connect it to the server //Launch device client and connect it to the server
val deviceEndpoint = MagixEndpoint.rSocketWithTcp("localhost") 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.compose.desktop.application.dsl.TargetFormat
import org.jetbrains.kotlin.gradle.dsl.ExplicitApiMode import org.jetbrains.kotlin.gradle.dsl.ExplicitApiMode
@ -11,12 +12,15 @@ kscience {
withJava() withJava()
} }
useKtor() useKtor()
useSerialization()
useContextReceivers() useContextReceivers()
dependencies { commonMain {
api(projects.controlsVision) implementation(projects.controlsVision)
implementation(projects.controlsConstructor)
// implementation("io.github.koalaplot:koalaplot-core:0.6.0")
} }
jvmMain { jvmMain {
implementation("io.ktor:ktor-server-cio") // implementation("io.ktor:ktor-server-cio")
implementation(spclibs.logback.classic) implementation(spclibs.logback.classic)
} }
} }
@ -26,6 +30,8 @@ kotlin {
jvmMain { jvmMain {
dependencies { dependencies {
implementation(compose.desktop.currentOs) 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 kotlinx.coroutines.withTimeout
import space.kscience.controls.api.DeviceHub import space.kscience.controls.api.DeviceHub
import space.kscience.controls.api.PropertyDescriptor 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.AsynchronousPort
import space.kscience.controls.ports.KtorTcpPort import space.kscience.controls.ports.KtorTcpPort
import space.kscience.controls.ports.send 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-plotly = { module = "space.kscience:visionforge-plotly", version.ref = "visionforge" }
visionforge-markdown = { module = "space.kscience:visionforge-markdown", version.ref = "visionforge" } visionforge-markdown = { module = "space.kscience:visionforge-markdown", version.ref = "visionforge" }
visionforge-server = { module = "space.kscience:visionforge-server", 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 # Buildscript