Compare commits
No commits in common. "05757aefdc1afbe674c67be6f0602ebf0ee37125" and "673a7c89a6e6e06a089da89c60492f559284c745" have entirely different histories.
05757aefdc
...
673a7c89a6
1
.space/CODEOWNERS
Normal file
1
.space/CODEOWNERS
Normal file
@ -0,0 +1 @@
|
|||||||
|
./space/* "Project Admin"
|
@ -7,7 +7,10 @@ plugins {
|
|||||||
|
|
||||||
allprojects {
|
allprojects {
|
||||||
group = "space.kscience"
|
group = "space.kscience"
|
||||||
version = "0.4.0-dev-4"
|
version = "0.4.0-dev-3"
|
||||||
|
repositories{
|
||||||
|
maven("https://maven.pkg.jetbrains.space/spc/p/sci/dev")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ksciencePublish {
|
ksciencePublish {
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
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
|
||||||
@ -22,24 +25,29 @@ 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: MutableSet<StateDescriptor> = mutableSetOf()
|
private val _stateDescriptors: MutableList<StateDescriptor> = mutableListOf()
|
||||||
override val stateDescriptors: Set<StateDescriptor> get() = _stateDescriptors
|
override val stateDescriptors: List<StateDescriptor> get() = _stateDescriptors
|
||||||
|
|
||||||
override fun registerState(stateDescriptor: StateDescriptor) {
|
override fun registerState(stateDescriptor: StateDescriptor) {
|
||||||
_stateDescriptors.add(stateDescriptor)
|
_stateDescriptors.add(stateDescriptor)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun unregisterState(stateDescriptor: StateDescriptor) {
|
override fun registerProperty(descriptor: PropertyDescriptor, state: DeviceState<*>) {
|
||||||
_stateDescriptors.remove(stateDescriptor)
|
super.registerProperty(descriptor, state)
|
||||||
|
registerState(PropertyStateDescriptor(this, descriptor.name, state))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun <T> registerProperty(
|
/**
|
||||||
converter: MetaConverter<T>,
|
* Bind an action to a [DeviceState]. [onChange] block is performed on each state change
|
||||||
descriptor: PropertyDescriptor,
|
*
|
||||||
state: DeviceState<T>,
|
* Optionally provide [writes] - a set of states that this change affects.
|
||||||
) {
|
*/
|
||||||
super.registerProperty(converter, descriptor, state)
|
public fun <T> DeviceState<T>.onChange(
|
||||||
registerState(StatePropertyDescriptor(this, descriptor.name, state))
|
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)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -76,7 +84,6 @@ 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,
|
||||||
@ -84,7 +91,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(converter, descriptor, state)
|
registerProperty(descriptor, state)
|
||||||
ReadOnlyProperty { _: DeviceConstructor, _ ->
|
ReadOnlyProperty { _: DeviceConstructor, _ ->
|
||||||
state
|
state
|
||||||
}
|
}
|
||||||
@ -101,8 +108,7 @@ 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(
|
||||||
metaConverter,
|
DeviceState.external(this, metaConverter, readInterval, initialState, reader),
|
||||||
DeviceState.external(this, readInterval, initialState, reader),
|
|
||||||
descriptorBuilder,
|
descriptorBuilder,
|
||||||
nameOverride,
|
nameOverride,
|
||||||
)
|
)
|
||||||
@ -119,8 +125,7 @@ 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(
|
||||||
metaConverter,
|
DeviceState.external(this, metaConverter, readInterval, initialState, reader, writer),
|
||||||
DeviceState.external(this, readInterval, initialState, reader, writer),
|
|
||||||
descriptorBuilder,
|
descriptorBuilder,
|
||||||
nameOverride,
|
nameOverride,
|
||||||
)
|
)
|
||||||
@ -135,8 +140,7 @@ 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(
|
||||||
metaConverter,
|
DeviceState.internal(metaConverter, initialState, callback),
|
||||||
MutableDeviceState(initialState, callback),
|
|
||||||
descriptorBuilder,
|
descriptorBuilder,
|
||||||
nameOverride,
|
nameOverride,
|
||||||
)
|
)
|
||||||
@ -149,7 +153,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(property.converter, device.propertyAsState(property, initialValue))
|
property(device.propertyAsState(property, initialValue))
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Bind existing property provided by specification to this device
|
* Bind existing property provided by specification to this device
|
||||||
@ -159,4 +163,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(property.converter, device.mutablePropertyAsState(property, initialValue))
|
property(device.mutablePropertyAsState(property, initialValue))
|
||||||
|
@ -1,12 +1,14 @@
|
|||||||
package space.kscience.controls.constructor
|
package space.kscience.controls.constructor
|
||||||
|
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.flow.*
|
import kotlinx.coroutines.flow.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
|
||||||
@ -28,21 +30,12 @@ public open class DeviceGroup(
|
|||||||
override val meta: Meta,
|
override val meta: Meta,
|
||||||
) : DeviceHub, CachingDevice {
|
) : DeviceHub, CachingDevice {
|
||||||
|
|
||||||
private class Property<T>(
|
internal class Property(
|
||||||
val state: DeviceState<T>,
|
val state: DeviceState<*>,
|
||||||
val converter: MetaConverter<T>,
|
|
||||||
val descriptor: PropertyDescriptor,
|
val descriptor: PropertyDescriptor,
|
||||||
) {
|
)
|
||||||
val valueAsMeta get() = converter.convert(state.value)
|
|
||||||
|
|
||||||
fun setMeta(meta: Meta) {
|
internal class Action(
|
||||||
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,
|
||||||
)
|
)
|
||||||
@ -88,20 +81,16 @@ 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 <T> registerProperty(
|
public open fun registerProperty(descriptor: PropertyDescriptor, state: DeviceState<*>) {
|
||||||
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, converter, descriptor)
|
properties[name] = Property(state, descriptor)
|
||||||
state.valueFlow.map(converter::convert).onEach {
|
state.metaFlow.onEach {
|
||||||
sharedMessageFlow.emit(
|
sharedMessageFlow.emit(
|
||||||
PropertyChangedMessage(
|
PropertyChangedMessage(
|
||||||
descriptor.name,
|
descriptor.name,
|
||||||
@ -120,18 +109,19 @@ 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()]?.valueAsMeta
|
properties[propertyName.parseAsName()]?.state?.valueAsMeta
|
||||||
?: error("Property with name $propertyName not found")
|
?: error("Property with name $propertyName not found")
|
||||||
|
|
||||||
override fun getProperty(propertyName: String): Meta? = properties[propertyName.parseAsName()]?.valueAsMeta
|
override fun getProperty(propertyName: String): Meta? = properties[propertyName.parseAsName()]?.state?.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()] ?: error("Property with name $propertyName not found")
|
val property = (properties[propertyName.parseAsName()]?.state as? MutableDeviceState)
|
||||||
property.setMeta(value)
|
?: error("Property with name $propertyName not found")
|
||||||
|
property.valueAsMeta = value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -174,10 +164,6 @@ 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,
|
||||||
@ -248,12 +234,10 @@ 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
|
||||||
)
|
)
|
||||||
@ -264,12 +248,10 @@ 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
|
||||||
)
|
)
|
||||||
@ -287,7 +269,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 = MutableDeviceState<T>(initialValue, callback)
|
val state = DeviceState.internal<T>(converter, initialValue, callback)
|
||||||
registerMutableProperty(name, converter, state, descriptorBuilder)
|
registerMutableProperty(name, state, descriptorBuilder)
|
||||||
return state
|
return state
|
||||||
}
|
}
|
||||||
|
@ -1,32 +1,14 @@
|
|||||||
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(
|
public abstract class DeviceModel(override val context: Context) : StateContainer {
|
||||||
final override val context: Context,
|
|
||||||
vararg dependencies: DeviceState<*>,
|
|
||||||
) : StateContainer, CoroutineScope {
|
|
||||||
|
|
||||||
override val coroutineContext: CoroutineContext = context.newCoroutineContext(SupervisorJob())
|
private val _stateDescriptors: MutableList<StateDescriptor> = mutableListOf()
|
||||||
|
|
||||||
|
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
@ -6,12 +6,15 @@ 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>
|
||||||
@ -21,6 +24,9 @@ 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
|
||||||
|
|
||||||
@ -41,6 +47,12 @@ 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
|
||||||
*/
|
*/
|
||||||
@ -53,15 +65,17 @@ 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>,
|
||||||
mapper: (T) -> R,
|
converter: MetaConverter<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})"
|
override fun toString(): String = "DeviceState.map(arg=${state}, converter=$converter)"
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -70,10 +84,13 @@ 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)
|
||||||
|
@ -1,14 +1,10 @@
|
|||||||
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
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -19,7 +15,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 StatePropertyDescriptor<T>(
|
public class PropertyStateDescriptor<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>,
|
||||||
@ -28,172 +24,51 @@ public class StatePropertyDescriptor<T>(
|
|||||||
/**
|
/**
|
||||||
* A binding for independent state like a timer
|
* A binding for independent state like a timer
|
||||||
*/
|
*/
|
||||||
public class StateNodeDescriptor<T>(
|
public class StateBinding<T>(
|
||||||
public val state: DeviceState<T>,
|
public val state: DeviceState<T>,
|
||||||
) : StateDescriptor
|
) : StateDescriptor
|
||||||
|
|
||||||
public class StateConnectionDescriptor(
|
public class ConnectionStateDescriptor(
|
||||||
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, CoroutineScope {
|
public interface StateContainer : ContextAware {
|
||||||
public val stateDescriptors: Set<StateDescriptor>
|
public val stateDescriptors: List<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(StateNodeDescriptor(state))
|
registerState(StateBinding(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.mutableState(initialValue: T): MutableDeviceState<T> = state(
|
public fun <T> StateContainer.state(converter: MetaConverter<T>, initialValue: T): MutableDeviceState<T> = state(
|
||||||
MutableDeviceState(initialValue)
|
DeviceState.internal(converter, initialValue)
|
||||||
)
|
)
|
||||||
|
|
||||||
public fun <T : DeviceModel> StateContainer.model(model: T): T {
|
/**
|
||||||
model.stateDescriptors.forEach {
|
* Create a register a mutable [Double] state
|
||||||
registerState(it)
|
*/
|
||||||
}
|
public fun StateContainer.doubleState(initialValue: Double): MutableDeviceState<Double> = state(
|
||||||
return model
|
MetaConverter.double, initialValue
|
||||||
}
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -7,6 +7,8 @@ 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
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -21,6 +23,7 @@ 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())
|
||||||
|
|
||||||
|
@ -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>(
|
||||||
val converter: MetaConverter<T>,
|
override val converter: MetaConverter<T>,
|
||||||
val device: Device,
|
val device: Device,
|
||||||
val propertyName: String,
|
val propertyName: String,
|
||||||
initialValue: T,
|
initialValue: T,
|
||||||
|
@ -2,6 +2,7 @@ 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
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -16,6 +17,7 @@ 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)
|
||||||
|
|
||||||
@ -30,18 +32,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) {
|
public val atStart: DeviceState<Boolean> = DeviceState.map(this, MetaConverter.boolean) {
|
||||||
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) {
|
public val atEnd: DeviceState<Boolean> = DeviceState.map(this, MetaConverter.boolean) {
|
||||||
it >= range.endInclusive
|
it >= range.endInclusive
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun toString(): String = "DoubleRangeState(range=$range)"
|
override fun toString(): String = "DoubleRangeState(range=$range, converter=$converter)"
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -53,5 +55,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(StateNodeDescriptor(it))
|
registerState(StateBinding(it))
|
||||||
}
|
}
|
@ -4,11 +4,13 @@ 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,
|
||||||
@ -24,7 +26,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()"
|
override fun toString(): String = "ExternalState(converter=$converter)"
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -32,18 +34,20 @@ 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, readInterval, initialValue, reader)
|
): DeviceState<T> = ExternalState(scope, converter, 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, readInterval, initialValue, reader), MutableDeviceState<T> {
|
) : ExternalState<T>(scope, converter, readInterval, initialValue, reader), MutableDeviceState<T> {
|
||||||
override var value: T
|
override var value: T
|
||||||
get() = super.value
|
get() = super.value
|
||||||
set(value) {
|
set(value) {
|
||||||
@ -58,8 +62,9 @@ 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, readInterval, initialValue, reader, writer)
|
): MutableDeviceState<T> = MutableExternalState(scope, converter, readInterval, initialValue, reader, writer)
|
@ -2,19 +2,22 @@ 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()"
|
override fun toString(): String = "FlowAsState(converter=$converter)"
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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(): MutableDeviceState<T> = StateFlowAsState(this)
|
public fun <T> MutableStateFlow<T>.asDeviceState(converter: MetaConverter<T>): DeviceState<T> =
|
||||||
|
StateFlowAsState(converter, this)
|
@ -2,6 +2,7 @@ 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
|
||||||
@ -9,6 +10,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
|||||||
* @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> {
|
||||||
@ -22,7 +24,7 @@ private class VirtualDeviceState<T>(
|
|||||||
callback(value)
|
callback(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun toString(): String = "VirtualDeviceState()"
|
override fun toString(): String = "VirtualDeviceState(converter=$converter)"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -31,7 +33,8 @@ 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> MutableDeviceState(
|
public fun <T> DeviceState.Companion.internal(
|
||||||
|
converter: MetaConverter<T>,
|
||||||
initialValue: T,
|
initialValue: T,
|
||||||
callback: (T) -> Unit = {},
|
callback: (T) -> Unit = {},
|
||||||
): MutableDeviceState<T> = VirtualDeviceState(initialValue, callback)
|
): MutableDeviceState<T> = VirtualDeviceState(converter, initialValue, callback)
|
||||||
|
@ -22,7 +22,6 @@ public interface DeviceHub : Provider {
|
|||||||
} else {
|
} else {
|
||||||
emptyMap()
|
emptyMap()
|
||||||
}
|
}
|
||||||
//TODO send message on device change
|
|
||||||
|
|
||||||
public companion object
|
public companion object
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
package space.kscience.controls.misc
|
package space.kscience.controls.spec
|
||||||
|
|
||||||
import kotlinx.datetime.Instant
|
import kotlinx.datetime.Instant
|
||||||
import space.kscience.dataforge.meta.*
|
import space.kscience.dataforge.meta.*
|
||||||
@ -12,9 +12,9 @@ public fun Double.asMeta(): Meta = Meta(asValue())
|
|||||||
* Generate a nullable [MetaConverter] from non-nullable one
|
* Generate a nullable [MetaConverter] from non-nullable one
|
||||||
*/
|
*/
|
||||||
public fun <T : Any> MetaConverter<T>.nullable(): MetaConverter<T?> = object : MetaConverter<T?> {
|
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)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -39,15 +39,3 @@ 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
|
|
@ -1,8 +1,9 @@
|
|||||||
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.misc.asMeta
|
import space.kscience.controls.spec.asMeta
|
||||||
import kotlin.test.Test
|
import kotlin.test.Test
|
||||||
import kotlin.test.assertEquals
|
import kotlin.test.assertEquals
|
||||||
|
|
||||||
|
@ -16,6 +16,8 @@ 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
|
||||||
@ -119,18 +121,12 @@ 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))
|
val subscription = subscribe(DeviceManager.magixFormat, originFilter = listOf(deviceEndpoint)).map { it.second }
|
||||||
.map { it.second }
|
|
||||||
.filter {
|
|
||||||
it.sourceDevice == null || it.sourceDevice == deviceName
|
|
||||||
}
|
|
||||||
|
|
||||||
val deferredDescriptorMessage = CompletableDeferred<DescriptionMessage>()
|
val deferredDescriptorMessage = CompletableDeferred<DescriptionMessage>()
|
||||||
|
|
||||||
launch {
|
launch {
|
||||||
deferredDescriptorMessage.complete(
|
deferredDescriptorMessage.complete(subscription.filterIsInstance<DescriptionMessage>().first())
|
||||||
subscription.filterIsInstance<DescriptionMessage>().first()
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
send(
|
send(
|
||||||
@ -161,6 +157,12 @@ 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
|
||||||
*/
|
*/
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
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
|
||||||
@ -41,7 +40,7 @@ internal fun generateId(request: MagixMessage): String = if (request.id != null)
|
|||||||
*/
|
*/
|
||||||
public fun DeviceManager.launchMagixService(
|
public fun DeviceManager.launchMagixService(
|
||||||
endpoint: MagixEndpoint,
|
endpoint: MagixEndpoint,
|
||||||
endpointID: String,
|
endpointID: String = controlsMagixFormat.defaultFormat,
|
||||||
coroutineContext: CoroutineContext = EmptyCoroutineContext,
|
coroutineContext: CoroutineContext = EmptyCoroutineContext,
|
||||||
): Job = context.launch(coroutineContext) {
|
): Job = context.launch(coroutineContext) {
|
||||||
endpoint.subscribe(controlsMagixFormat, targetFilter = listOf(endpointID, null)).onEach { (request, payload) ->
|
endpoint.subscribe(controlsMagixFormat, targetFilter = listOf(endpointID, null)).onEach { (request, payload) ->
|
||||||
@ -57,7 +56,7 @@ public fun DeviceManager.launchMagixService(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}.catch { error ->
|
}.catch { error ->
|
||||||
if (error !is CancellationException) logger.error(error) { "Error while responding to message: ${error.message}" }
|
logger.error(error) { "Error while responding to message: ${error.message}" }
|
||||||
}.launchIn(this)
|
}.launchIn(this)
|
||||||
|
|
||||||
hubMessageFlow().onEach { payload ->
|
hubMessageFlow().onEach { payload ->
|
||||||
|
@ -0,0 +1,24 @@
|
|||||||
|
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{
|
||||||
|
//
|
||||||
|
//}
|
@ -13,7 +13,6 @@ public expect class ControlVisionPlugin: VisionPlugin{
|
|||||||
|
|
||||||
internal val controlsVisionSerializersModule = SerializersModule {
|
internal val controlsVisionSerializersModule = SerializersModule {
|
||||||
polymorphic(Vision::class) {
|
polymorphic(Vision::class) {
|
||||||
subclass(IndicatorVision.serializer())
|
subclass(BooleanIndicatorVision.serializer())
|
||||||
subclass(SliderVision.serializer())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,35 +0,0 @@
|
|||||||
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{
|
|
||||||
//
|
|
||||||
//}
|
|
@ -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 = { Value.of(it) },
|
extractValue: T.() -> Value = { state.converter.convert(this).value ?: Null },
|
||||||
maxAge: Duration = defaultMaxAge,
|
maxAge: Duration = defaultMaxAge,
|
||||||
maxPoints: Int = defaultMaxPoints,
|
maxPoints: Int = defaultMaxPoints,
|
||||||
minPoints: Int = defaultMinPoints,
|
minPoints: Int = defaultMinPoints,
|
||||||
|
@ -10,34 +10,7 @@ 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 ->
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -49,8 +22,7 @@ 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)
|
||||||
|
@ -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, "car")
|
deviceManager.launchMagixService(deviceEndpoint)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
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
|
||||||
|
|
||||||
@ -12,15 +11,12 @@ kscience {
|
|||||||
withJava()
|
withJava()
|
||||||
}
|
}
|
||||||
useKtor()
|
useKtor()
|
||||||
useSerialization()
|
|
||||||
useContextReceivers()
|
useContextReceivers()
|
||||||
commonMain {
|
dependencies {
|
||||||
implementation(projects.controlsVision)
|
api(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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -30,8 +26,6 @@ kotlin {
|
|||||||
jvmMain {
|
jvmMain {
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(compose.desktop.currentOs)
|
implementation(compose.desktop.currentOs)
|
||||||
@OptIn(ExperimentalComposeLibrary::class)
|
|
||||||
implementation(compose.desktop.components.splitPane)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,185 +0,0 @@
|
|||||||
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()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -12,8 +12,6 @@ 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
|
||||||
|
@ -79,7 +79,6 @@ 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
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user