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 {
|
||||
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 {
|
||||
|
@ -1,5 +1,8 @@
|
||||
package space.kscience.controls.constructor
|
||||
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import space.kscience.controls.api.Device
|
||||
import space.kscience.controls.api.PropertyDescriptor
|
||||
import space.kscience.controls.spec.DevicePropertySpec
|
||||
@ -22,24 +25,29 @@ public abstract class DeviceConstructor(
|
||||
context: Context,
|
||||
meta: Meta = Meta.EMPTY,
|
||||
) : DeviceGroup(context, meta), StateContainer {
|
||||
private val _stateDescriptors: MutableSet<StateDescriptor> = mutableSetOf()
|
||||
override val stateDescriptors: Set<StateDescriptor> get() = _stateDescriptors
|
||||
private val _stateDescriptors: MutableList<StateDescriptor> = mutableListOf()
|
||||
override val stateDescriptors: List<StateDescriptor> get() = _stateDescriptors
|
||||
|
||||
override fun registerState(stateDescriptor: StateDescriptor) {
|
||||
_stateDescriptors.add(stateDescriptor)
|
||||
}
|
||||
|
||||
override fun unregisterState(stateDescriptor: StateDescriptor) {
|
||||
_stateDescriptors.remove(stateDescriptor)
|
||||
override fun registerProperty(descriptor: PropertyDescriptor, state: DeviceState<*>) {
|
||||
super.registerProperty(descriptor, state)
|
||||
registerState(PropertyStateDescriptor(this, descriptor.name, state))
|
||||
}
|
||||
|
||||
override fun <T> registerProperty(
|
||||
converter: MetaConverter<T>,
|
||||
descriptor: PropertyDescriptor,
|
||||
state: DeviceState<T>,
|
||||
) {
|
||||
super.registerProperty(converter, descriptor, state)
|
||||
registerState(StatePropertyDescriptor(this, descriptor.name, state))
|
||||
/**
|
||||
* Bind an action to a [DeviceState]. [onChange] block is performed on each state change
|
||||
*
|
||||
* Optionally provide [writes] - a set of states that this change affects.
|
||||
*/
|
||||
public fun <T> DeviceState<T>.onChange(
|
||||
vararg writes: DeviceState<*>,
|
||||
reads: Collection<DeviceState<*>>,
|
||||
onChange: suspend (T) -> Unit,
|
||||
): Job = valueFlow.onEach(onChange).launchIn(this@DeviceConstructor).also {
|
||||
registerState(ConnectionStateDescriptor(setOf(this, *reads.toTypedArray()), setOf(*writes)))
|
||||
}
|
||||
}
|
||||
|
||||
@ -76,7 +84,6 @@ public fun <D : Device> DeviceConstructor.device(
|
||||
* Register a property and provide a direct reader for it
|
||||
*/
|
||||
public fun <T, S : DeviceState<T>> DeviceConstructor.property(
|
||||
converter: MetaConverter<T>,
|
||||
state: S,
|
||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||
nameOverride: String? = null,
|
||||
@ -84,7 +91,7 @@ public fun <T, S : DeviceState<T>> DeviceConstructor.property(
|
||||
PropertyDelegateProvider { _: DeviceConstructor, property ->
|
||||
val name = nameOverride ?: property.name
|
||||
val descriptor = PropertyDescriptor(name).apply(descriptorBuilder)
|
||||
registerProperty(converter, descriptor, state)
|
||||
registerProperty(descriptor, state)
|
||||
ReadOnlyProperty { _: DeviceConstructor, _ ->
|
||||
state
|
||||
}
|
||||
@ -101,8 +108,7 @@ public fun <T : Any> DeviceConstructor.property(
|
||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||
nameOverride: String? = null,
|
||||
): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, DeviceState<T>>> = property(
|
||||
metaConverter,
|
||||
DeviceState.external(this, readInterval, initialState, reader),
|
||||
DeviceState.external(this, metaConverter, readInterval, initialState, reader),
|
||||
descriptorBuilder,
|
||||
nameOverride,
|
||||
)
|
||||
@ -119,8 +125,7 @@ public fun <T : Any> DeviceConstructor.mutableProperty(
|
||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||
nameOverride: String? = null,
|
||||
): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, MutableDeviceState<T>>> = property(
|
||||
metaConverter,
|
||||
DeviceState.external(this, readInterval, initialState, reader, writer),
|
||||
DeviceState.external(this, metaConverter, readInterval, initialState, reader, writer),
|
||||
descriptorBuilder,
|
||||
nameOverride,
|
||||
)
|
||||
@ -135,8 +140,7 @@ public fun <T> DeviceConstructor.virtualProperty(
|
||||
nameOverride: String? = null,
|
||||
callback: (T) -> Unit = {},
|
||||
): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, MutableDeviceState<T>>> = property(
|
||||
metaConverter,
|
||||
MutableDeviceState(initialState, callback),
|
||||
DeviceState.internal(metaConverter, initialState, callback),
|
||||
descriptorBuilder,
|
||||
nameOverride,
|
||||
)
|
||||
@ -149,7 +153,7 @@ public fun <T, D : Device> DeviceConstructor.deviceProperty(
|
||||
property: DevicePropertySpec<D, T>,
|
||||
initialValue: 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
|
||||
@ -159,4 +163,4 @@ public fun <T, D : Device> DeviceConstructor.deviceProperty(
|
||||
property: MutableDevicePropertySpec<D, T>,
|
||||
initialValue: 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
|
||||
|
||||
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.DeviceLifecycleState.*
|
||||
import space.kscience.controls.manager.DeviceManager
|
||||
import space.kscience.controls.manager.install
|
||||
import space.kscience.controls.spec.DevicePropertySpec
|
||||
import space.kscience.dataforge.context.*
|
||||
import space.kscience.dataforge.meta.Laminate
|
||||
import space.kscience.dataforge.meta.Meta
|
||||
@ -28,21 +30,12 @@ public open class DeviceGroup(
|
||||
override val meta: Meta,
|
||||
) : DeviceHub, CachingDevice {
|
||||
|
||||
private class Property<T>(
|
||||
val state: DeviceState<T>,
|
||||
val converter: MetaConverter<T>,
|
||||
internal class Property(
|
||||
val state: DeviceState<*>,
|
||||
val descriptor: PropertyDescriptor,
|
||||
) {
|
||||
val valueAsMeta get() = converter.convert(state.value)
|
||||
)
|
||||
|
||||
fun setMeta(meta: Meta) {
|
||||
check(state is MutableDeviceState) { "Can't write to read-only property" }
|
||||
|
||||
state.value = converter.read(meta)
|
||||
}
|
||||
}
|
||||
|
||||
private class Action(
|
||||
internal class Action(
|
||||
val invoke: suspend (Meta?) -> Meta?,
|
||||
val descriptor: ActionDescriptor,
|
||||
)
|
||||
@ -88,20 +81,16 @@ public open class DeviceGroup(
|
||||
return device
|
||||
}
|
||||
|
||||
private val properties: MutableMap<Name, Property<*>> = hashMapOf()
|
||||
private val properties: MutableMap<Name, Property> = hashMapOf()
|
||||
|
||||
/**
|
||||
* Register a new property based on [DeviceState]. Properties could be modified dynamically
|
||||
*/
|
||||
public open fun <T> registerProperty(
|
||||
converter: MetaConverter<T>,
|
||||
descriptor: PropertyDescriptor,
|
||||
state: DeviceState<T>,
|
||||
) {
|
||||
public open fun registerProperty(descriptor: PropertyDescriptor, state: DeviceState<*>) {
|
||||
val name = descriptor.name.parseAsName()
|
||||
require(properties[name] == null) { "Can't add property with name $name. It already exists." }
|
||||
properties[name] = Property(state, converter, descriptor)
|
||||
state.valueFlow.map(converter::convert).onEach {
|
||||
properties[name] = Property(state, descriptor)
|
||||
state.metaFlow.onEach {
|
||||
sharedMessageFlow.emit(
|
||||
PropertyChangedMessage(
|
||||
descriptor.name,
|
||||
@ -120,18 +109,19 @@ public open class DeviceGroup(
|
||||
get() = actions.values.map { it.descriptor }
|
||||
|
||||
override suspend fun readProperty(propertyName: String): Meta =
|
||||
properties[propertyName.parseAsName()]?.valueAsMeta
|
||||
properties[propertyName.parseAsName()]?.state?.valueAsMeta
|
||||
?: 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) {
|
||||
//does nothing for this implementation
|
||||
}
|
||||
|
||||
override suspend fun writeProperty(propertyName: String, value: Meta) {
|
||||
val property = properties[propertyName.parseAsName()] ?: error("Property with name $propertyName not found")
|
||||
property.setMeta(value)
|
||||
val property = (properties[propertyName.parseAsName()]?.state as? MutableDeviceState)
|
||||
?: 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(
|
||||
name: String = "@group",
|
||||
meta: Meta = Meta.EMPTY,
|
||||
@ -248,12 +234,10 @@ public fun DeviceGroup.registerDeviceGroup(name: String, block: DeviceGroup.() -
|
||||
*/
|
||||
public fun <T : Any> DeviceGroup.registerProperty(
|
||||
name: String,
|
||||
converter: MetaConverter<T>,
|
||||
state: DeviceState<T>,
|
||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||
) {
|
||||
registerProperty(
|
||||
converter,
|
||||
PropertyDescriptor(name).apply(descriptorBuilder),
|
||||
state
|
||||
)
|
||||
@ -264,12 +248,10 @@ public fun <T : Any> DeviceGroup.registerProperty(
|
||||
*/
|
||||
public fun <T : Any> DeviceGroup.registerMutableProperty(
|
||||
name: String,
|
||||
converter: MetaConverter<T>,
|
||||
state: MutableDeviceState<T>,
|
||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||
) {
|
||||
registerProperty(
|
||||
converter,
|
||||
PropertyDescriptor(name).apply(descriptorBuilder),
|
||||
state
|
||||
)
|
||||
@ -287,7 +269,7 @@ public fun <T : Any> DeviceGroup.registerVirtualProperty(
|
||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||
callback: (T) -> Unit = {},
|
||||
): MutableDeviceState<T> {
|
||||
val state = MutableDeviceState<T>(initialValue, callback)
|
||||
registerMutableProperty(name, converter, state, descriptorBuilder)
|
||||
val state = DeviceState.internal<T>(converter, initialValue, callback)
|
||||
registerMutableProperty(name, state, descriptorBuilder)
|
||||
return state
|
||||
}
|
||||
|
@ -1,32 +1,14 @@
|
||||
package space.kscience.controls.constructor
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.newCoroutineContext
|
||||
import space.kscience.dataforge.context.Context
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
public abstract class DeviceModel(
|
||||
final override val context: Context,
|
||||
vararg dependencies: DeviceState<*>,
|
||||
) : StateContainer, CoroutineScope {
|
||||
public abstract class DeviceModel(override val context: Context) : StateContainer {
|
||||
|
||||
override val coroutineContext: CoroutineContext = context.newCoroutineContext(SupervisorJob())
|
||||
private val _stateDescriptors: MutableList<StateDescriptor> = mutableListOf()
|
||||
|
||||
|
||||
private val _stateDescriptors: MutableSet<StateDescriptor> = mutableSetOf<StateDescriptor>().apply {
|
||||
dependencies.forEach {
|
||||
add(StateNodeDescriptor(it))
|
||||
}
|
||||
}
|
||||
|
||||
override val stateDescriptors: Set<StateDescriptor> get() = _stateDescriptors
|
||||
override val stateDescriptors: List<StateDescriptor> get() = _stateDescriptors
|
||||
|
||||
override fun registerState(stateDescriptor: 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.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import space.kscience.dataforge.meta.Meta
|
||||
import space.kscience.dataforge.meta.MetaConverter
|
||||
import kotlin.reflect.KProperty
|
||||
|
||||
/**
|
||||
* An observable state of a device
|
||||
*/
|
||||
public interface DeviceState<T> {
|
||||
public val converter: MetaConverter<T>
|
||||
public val value: T
|
||||
|
||||
public val valueFlow: Flow<T>
|
||||
@ -21,6 +24,9 @@ public interface DeviceState<T> {
|
||||
public companion object
|
||||
}
|
||||
|
||||
public val <T> DeviceState<T>.metaFlow: Flow<Meta> get() = valueFlow.map(converter::convert)
|
||||
|
||||
public val <T> DeviceState<T>.valueAsMeta: Meta get() = converter.convert(value)
|
||||
|
||||
public operator fun <T> DeviceState<T>.getValue(thisRef: Any?, property: KProperty<*>): T = value
|
||||
|
||||
@ -41,6 +47,12 @@ public operator fun <T> MutableDeviceState<T>.setValue(thisRef: Any?, property:
|
||||
this.value = value
|
||||
}
|
||||
|
||||
public var <T> MutableDeviceState<T>.valueAsMeta: Meta
|
||||
get() = converter.convert(value)
|
||||
set(arg) {
|
||||
value = converter.read(arg)
|
||||
}
|
||||
|
||||
/**
|
||||
* Device state with a value that depends on other device states
|
||||
*/
|
||||
@ -53,15 +65,17 @@ public interface DeviceStateWithDependencies<T> : DeviceState<T> {
|
||||
*/
|
||||
public fun <T, R> DeviceState.Companion.map(
|
||||
state: DeviceState<T>,
|
||||
mapper: (T) -> R,
|
||||
converter: MetaConverter<R>, mapper: (T) -> R,
|
||||
): DeviceStateWithDependencies<R> = object : DeviceStateWithDependencies<R> {
|
||||
override val dependencies = listOf(state)
|
||||
|
||||
override val converter: MetaConverter<R> = converter
|
||||
|
||||
override val value: R get() = mapper(state.value)
|
||||
|
||||
override val valueFlow: Flow<R> = state.valueFlow.map(mapper)
|
||||
|
||||
override fun toString(): String = "DeviceState.map(arg=${state})"
|
||||
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(
|
||||
state1: DeviceState<T1>,
|
||||
state2: DeviceState<T2>,
|
||||
converter: MetaConverter<R>,
|
||||
mapper: (T1, T2) -> R,
|
||||
): DeviceStateWithDependencies<R> = object : DeviceStateWithDependencies<R> {
|
||||
override val dependencies = listOf(state1, state2)
|
||||
|
||||
override val converter: MetaConverter<R> = converter
|
||||
|
||||
override val value: R get() = mapper(state1.value, state2.value)
|
||||
|
||||
override val valueFlow: Flow<R> = kotlinx.coroutines.flow.combine(state1.valueFlow, state2.valueFlow, mapper)
|
||||
|
@ -1,14 +1,10 @@
|
||||
package space.kscience.controls.constructor
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.runningFold
|
||||
import space.kscience.controls.api.Device
|
||||
import space.kscience.controls.manager.ClockManager
|
||||
import space.kscience.dataforge.context.ContextAware
|
||||
import space.kscience.dataforge.context.request
|
||||
import space.kscience.dataforge.meta.MetaConverter
|
||||
import kotlin.time.Duration
|
||||
|
||||
/**
|
||||
@ -19,7 +15,7 @@ public sealed interface StateDescriptor
|
||||
/**
|
||||
* A binding that exposes device property as read-only state
|
||||
*/
|
||||
public class StatePropertyDescriptor<T>(
|
||||
public class PropertyStateDescriptor<T>(
|
||||
public val device: Device,
|
||||
public val propertyName: String,
|
||||
public val state: DeviceState<T>,
|
||||
@ -28,172 +24,51 @@ public class StatePropertyDescriptor<T>(
|
||||
/**
|
||||
* A binding for independent state like a timer
|
||||
*/
|
||||
public class StateNodeDescriptor<T>(
|
||||
public class StateBinding<T>(
|
||||
public val state: DeviceState<T>,
|
||||
) : StateDescriptor
|
||||
|
||||
public class StateConnectionDescriptor(
|
||||
public class ConnectionStateDescriptor(
|
||||
public val reads: Collection<DeviceState<*>>,
|
||||
public val writes: Collection<DeviceState<*>>,
|
||||
) : StateDescriptor
|
||||
|
||||
|
||||
public interface StateContainer : ContextAware, CoroutineScope {
|
||||
public val stateDescriptors: Set<StateDescriptor>
|
||||
public interface StateContainer : ContextAware {
|
||||
public val stateDescriptors: List<StateDescriptor>
|
||||
public fun registerState(stateDescriptor: StateDescriptor)
|
||||
public fun unregisterState(stateDescriptor: StateDescriptor)
|
||||
|
||||
|
||||
/**
|
||||
* Bind an action to a [DeviceState]. [onChange] block is performed on each state change
|
||||
*
|
||||
* Optionally provide [writes] - a set of states that this change affects.
|
||||
*/
|
||||
public fun <T> DeviceState<T>.onNext(
|
||||
vararg writes: DeviceState<*>,
|
||||
alsoReads: Collection<DeviceState<*>> = emptySet(),
|
||||
onChange: suspend (T) -> Unit,
|
||||
): Job = valueFlow.onEach(onChange).launchIn(this@StateContainer).also {
|
||||
registerState(StateConnectionDescriptor(setOf(this, *alsoReads.toTypedArray()), setOf(*writes)))
|
||||
}
|
||||
|
||||
public fun <T> DeviceState<T>.onChange(
|
||||
vararg writes: DeviceState<*>,
|
||||
alsoReads: Collection<DeviceState<*>> = emptySet(),
|
||||
onChange: suspend (prev: T, next: T) -> Unit,
|
||||
): Job = valueFlow.runningFold(Pair(value, value)) { pair, next ->
|
||||
Pair(pair.second, next)
|
||||
}.onEach { pair ->
|
||||
if (pair.first != pair.second) {
|
||||
onChange(pair.first, pair.second)
|
||||
}
|
||||
}.launchIn(this@StateContainer).also {
|
||||
registerState(StateConnectionDescriptor(setOf(this, *alsoReads.toTypedArray()), setOf(*writes)))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a [state] in this container. The state is not registered as a device property if [this] is a [DeviceConstructor]
|
||||
*/
|
||||
public fun <T, D : DeviceState<T>> StateContainer.state(state: D): D {
|
||||
registerState(StateNodeDescriptor(state))
|
||||
registerState(StateBinding(state))
|
||||
return state
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a register a [MutableDeviceState] with a given [converter]
|
||||
*/
|
||||
public fun <T> StateContainer.mutableState(initialValue: T): MutableDeviceState<T> = state(
|
||||
MutableDeviceState(initialValue)
|
||||
public fun <T> StateContainer.state(converter: MetaConverter<T>, initialValue: T): MutableDeviceState<T> = state(
|
||||
DeviceState.internal(converter, initialValue)
|
||||
)
|
||||
|
||||
public fun <T : DeviceModel> StateContainer.model(model: T): T {
|
||||
model.stateDescriptors.forEach {
|
||||
registerState(it)
|
||||
}
|
||||
return model
|
||||
}
|
||||
/**
|
||||
* Create a register a mutable [Double] state
|
||||
*/
|
||||
public fun StateContainer.doubleState(initialValue: Double): MutableDeviceState<Double> = state(
|
||||
MetaConverter.double, initialValue
|
||||
)
|
||||
|
||||
/**
|
||||
* Create a register a mutable [String] state
|
||||
*/
|
||||
public fun StateContainer.stringState(initialValue: String): MutableDeviceState<String> = state(
|
||||
MetaConverter.string, initialValue
|
||||
)
|
||||
|
||||
/**
|
||||
* Create and register a timer state.
|
||||
*/
|
||||
public fun StateContainer.timer(tick: Duration): TimerState = state(TimerState(context.request(ClockManager), tick))
|
||||
|
||||
|
||||
public fun <T, R> StateContainer.mapState(
|
||||
state: DeviceState<T>,
|
||||
transformation: (T) -> R,
|
||||
): DeviceStateWithDependencies<R> = state(DeviceState.map(state, transformation))
|
||||
|
||||
/**
|
||||
* Create a new state by combining two existing ones
|
||||
*/
|
||||
public fun <T1, T2, R> StateContainer.combineState(
|
||||
first: DeviceState<T1>,
|
||||
second: DeviceState<T2>,
|
||||
transformation: (T1, T2) -> R,
|
||||
): DeviceState<R> = state(DeviceState.combine(first, second, transformation))
|
||||
|
||||
|
||||
/**
|
||||
* Create and start binding between [sourceState] and [targetState]. Changes made to [sourceState] are automatically
|
||||
* transferred onto [targetState], but not vise versa.
|
||||
*
|
||||
* On resulting [Job] cancel the binding is unregistered
|
||||
*/
|
||||
public fun <T> StateContainer.bindTo(sourceState: DeviceState<T>, targetState: MutableDeviceState<T>): Job {
|
||||
val descriptor = StateConnectionDescriptor(setOf(sourceState), setOf(targetState))
|
||||
registerState(descriptor)
|
||||
return sourceState.valueFlow.onEach {
|
||||
targetState.value = it
|
||||
}.launchIn(this).apply {
|
||||
invokeOnCompletion {
|
||||
unregisterState(descriptor)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create and start binding between [sourceState] and [targetState]. Changes made to [sourceState] are automatically
|
||||
* transferred onto [targetState] via [transformation], but not vise versa.
|
||||
*
|
||||
* On resulting [Job] cancel the binding is unregistered
|
||||
*/
|
||||
public fun <T, R> StateContainer.transformTo(
|
||||
sourceState: DeviceState<T>,
|
||||
targetState: MutableDeviceState<R>,
|
||||
transformation: suspend (T) -> R,
|
||||
): Job {
|
||||
val descriptor = StateConnectionDescriptor(setOf(sourceState), setOf(targetState))
|
||||
registerState(descriptor)
|
||||
return sourceState.valueFlow.onEach {
|
||||
targetState.value = transformation(it)
|
||||
}.launchIn(this).apply {
|
||||
invokeOnCompletion {
|
||||
unregisterState(descriptor)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register [StateDescriptor] that combines values from [sourceState1] and [sourceState2] using [transformation].
|
||||
*
|
||||
* On resulting [Job] cancel the binding is unregistered
|
||||
*/
|
||||
public fun <T1, T2, R> StateContainer.combineTo(
|
||||
sourceState1: DeviceState<T1>,
|
||||
sourceState2: DeviceState<T2>,
|
||||
targetState: MutableDeviceState<R>,
|
||||
transformation: suspend (T1, T2) -> R,
|
||||
): Job {
|
||||
val descriptor = StateConnectionDescriptor(setOf(sourceState1, sourceState2), setOf(targetState))
|
||||
registerState(descriptor)
|
||||
return kotlinx.coroutines.flow.combine(sourceState1.valueFlow, sourceState2.valueFlow, transformation).onEach {
|
||||
targetState.value = it
|
||||
}.launchIn(this).apply {
|
||||
invokeOnCompletion {
|
||||
unregisterState(descriptor)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register [StateDescriptor] that combines values from [sourceStates] using [transformation].
|
||||
*
|
||||
* On resulting [Job] cancel the binding is unregistered
|
||||
*/
|
||||
public inline fun <reified T, R> StateContainer.combineTo(
|
||||
sourceStates: Collection<DeviceState<T>>,
|
||||
targetState: MutableDeviceState<R>,
|
||||
noinline transformation: suspend (Array<T>) -> R,
|
||||
): Job {
|
||||
val descriptor = StateConnectionDescriptor(sourceStates, setOf(targetState))
|
||||
registerState(descriptor)
|
||||
return kotlinx.coroutines.flow.combine(sourceStates.map { it.valueFlow }, transformation).onEach {
|
||||
targetState.value = it
|
||||
}.launchIn(this).apply {
|
||||
invokeOnCompletion {
|
||||
unregisterState(descriptor)
|
||||
}
|
||||
}
|
||||
}
|
@ -7,6 +7,8 @@ import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.datetime.Instant
|
||||
import space.kscience.controls.manager.ClockManager
|
||||
import space.kscience.controls.spec.instant
|
||||
import space.kscience.dataforge.meta.MetaConverter
|
||||
import kotlin.time.Duration
|
||||
|
||||
/**
|
||||
@ -21,6 +23,7 @@ public class TimerState(
|
||||
public val clockManager: ClockManager,
|
||||
public val tick: Duration,
|
||||
) : DeviceState<Instant> {
|
||||
override val converter: MetaConverter<Instant> get() = MetaConverter.instant
|
||||
|
||||
private val clock = MutableStateFlow(clockManager.clock.now())
|
||||
|
||||
|
@ -15,7 +15,7 @@ import space.kscience.dataforge.meta.MetaConverter
|
||||
* A copy-free [DeviceState] bound to a device property
|
||||
*/
|
||||
private open class BoundDeviceState<T>(
|
||||
val converter: MetaConverter<T>,
|
||||
override val converter: MetaConverter<T>,
|
||||
val device: Device,
|
||||
val propertyName: String,
|
||||
initialValue: T,
|
||||
|
@ -2,6 +2,7 @@ package space.kscience.controls.constructor
|
||||
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
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" }
|
||||
}
|
||||
|
||||
override val converter: MetaConverter<Double> = MetaConverter.double
|
||||
|
||||
private val _valueFlow = MutableStateFlow(initialValue)
|
||||
|
||||
@ -30,18 +32,18 @@ public class DoubleInRangeState(
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
|
||||
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,
|
||||
range: ClosedFloatingPointRange<Double>,
|
||||
): 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.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import space.kscience.dataforge.meta.MetaConverter
|
||||
import kotlin.time.Duration
|
||||
|
||||
|
||||
private open class ExternalState<T>(
|
||||
val scope: CoroutineScope,
|
||||
override val converter: MetaConverter<T>,
|
||||
val readInterval: Duration,
|
||||
initialValue: T,
|
||||
val reader: suspend () -> T,
|
||||
@ -24,7 +26,7 @@ private open class ExternalState<T>(
|
||||
override val value: T get() = flow.value
|
||||
override val valueFlow: Flow<T> get() = flow
|
||||
|
||||
override fun toString(): String = "ExternalState()"
|
||||
override fun toString(): String = "ExternalState(converter=$converter)"
|
||||
}
|
||||
|
||||
/**
|
||||
@ -32,18 +34,20 @@ private open class ExternalState<T>(
|
||||
*/
|
||||
public fun <T> DeviceState.Companion.external(
|
||||
scope: CoroutineScope,
|
||||
converter: MetaConverter<T>,
|
||||
readInterval: Duration,
|
||||
initialValue: T,
|
||||
reader: suspend () -> T,
|
||||
): DeviceState<T> = ExternalState(scope, readInterval, initialValue, reader)
|
||||
): DeviceState<T> = ExternalState(scope, converter, readInterval, initialValue, reader)
|
||||
|
||||
private class MutableExternalState<T>(
|
||||
scope: CoroutineScope,
|
||||
converter: MetaConverter<T>,
|
||||
readInterval: Duration,
|
||||
initialValue: T,
|
||||
reader: suspend () -> T,
|
||||
val writer: suspend (T) -> Unit,
|
||||
) : ExternalState<T>(scope, readInterval, initialValue, reader), MutableDeviceState<T> {
|
||||
) : ExternalState<T>(scope, converter, readInterval, initialValue, reader), MutableDeviceState<T> {
|
||||
override var value: T
|
||||
get() = super.value
|
||||
set(value) {
|
||||
@ -58,8 +62,9 @@ private class MutableExternalState<T>(
|
||||
*/
|
||||
public fun <T> DeviceState.Companion.external(
|
||||
scope: CoroutineScope,
|
||||
converter: MetaConverter<T>,
|
||||
readInterval: Duration,
|
||||
initialValue: T,
|
||||
reader: suspend () -> T,
|
||||
writer: suspend (T) -> Unit,
|
||||
): MutableDeviceState<T> = MutableExternalState(scope, 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.MutableStateFlow
|
||||
import space.kscience.dataforge.meta.MetaConverter
|
||||
|
||||
|
||||
private class StateFlowAsState<T>(
|
||||
override val converter: MetaConverter<T>,
|
||||
val flow: MutableStateFlow<T>,
|
||||
) : MutableDeviceState<T> {
|
||||
override var value: T by flow::value
|
||||
override val valueFlow: Flow<T> get() = flow
|
||||
|
||||
override fun toString(): String = "FlowAsState()"
|
||||
override fun toString(): String = "FlowAsState(converter=$converter)"
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a read-only [DeviceState] that wraps [MutableStateFlow].
|
||||
* No data copy is performed.
|
||||
*/
|
||||
public fun <T> MutableStateFlow<T>.asDeviceState(): 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.MutableStateFlow
|
||||
import space.kscience.dataforge.meta.MetaConverter
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
private class VirtualDeviceState<T>(
|
||||
override val converter: MetaConverter<T>,
|
||||
initialValue: T,
|
||||
private val callback: (T) -> Unit = {},
|
||||
) : MutableDeviceState<T> {
|
||||
@ -22,7 +24,7 @@ private class VirtualDeviceState<T>(
|
||||
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
|
||||
*/
|
||||
public fun <T> MutableDeviceState(
|
||||
public fun <T> DeviceState.Companion.internal(
|
||||
converter: MetaConverter<T>,
|
||||
initialValue: T,
|
||||
callback: (T) -> Unit = {},
|
||||
): MutableDeviceState<T> = VirtualDeviceState(initialValue, callback)
|
||||
): MutableDeviceState<T> = VirtualDeviceState(converter, initialValue, callback)
|
||||
|
@ -22,7 +22,6 @@ public interface DeviceHub : Provider {
|
||||
} else {
|
||||
emptyMap()
|
||||
}
|
||||
//TODO send message on device change
|
||||
|
||||
public companion object
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
package space.kscience.controls.misc
|
||||
package space.kscience.controls.spec
|
||||
|
||||
import kotlinx.datetime.Instant
|
||||
import space.kscience.dataforge.meta.*
|
||||
@ -12,9 +12,9 @@ public fun Double.asMeta(): Meta = Meta(asValue())
|
||||
* Generate a nullable [MetaConverter] from non-nullable one
|
||||
*/
|
||||
public fun <T : Any> MetaConverter<T>.nullable(): MetaConverter<T?> = object : MetaConverter<T?> {
|
||||
override fun convert(obj: T?): Meta = obj?.let { this@nullable.convert(it) } ?: Meta(Null)
|
||||
override fun convert(obj: T?): Meta = obj?.let { this@nullable.convert(it) }?: Meta(Null)
|
||||
|
||||
override fun readOrNull(source: Meta): T? = if (source.value == Null) null else this@nullable.readOrNull(source)
|
||||
override fun readOrNull(source: Meta): T? = if(source.value == Null) null else this@nullable.readOrNull(source)
|
||||
|
||||
}
|
||||
|
||||
@ -38,16 +38,4 @@ private object InstantConverter : MetaConverter<Instant> {
|
||||
override fun convert(obj: Instant): Meta = Meta(obj.toString())
|
||||
}
|
||||
|
||||
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
|
||||
public val MetaConverter.Companion.instant: MetaConverter<Instant> get() = InstantConverter
|
@ -1,8 +1,9 @@
|
||||
package space.kscience.controls.api
|
||||
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import space.kscience.controls.misc.asMeta
|
||||
import space.kscience.controls.spec.asMeta
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
|
@ -16,6 +16,8 @@ import space.kscience.dataforge.context.Context
|
||||
import space.kscience.dataforge.meta.Meta
|
||||
import space.kscience.dataforge.misc.DFExperimental
|
||||
import space.kscience.dataforge.names.Name
|
||||
import space.kscience.dataforge.names.length
|
||||
import space.kscience.dataforge.names.startsWith
|
||||
import space.kscience.magix.api.MagixEndpoint
|
||||
import space.kscience.magix.api.send
|
||||
import space.kscience.magix.api.subscribe
|
||||
@ -119,18 +121,12 @@ public suspend fun MagixEndpoint.remoteDevice(
|
||||
deviceEndpoint: String,
|
||||
deviceName: Name,
|
||||
): DeviceClient = coroutineScope {
|
||||
val subscription = subscribe(DeviceManager.magixFormat, originFilter = listOf(deviceEndpoint))
|
||||
.map { it.second }
|
||||
.filter {
|
||||
it.sourceDevice == null || it.sourceDevice == deviceName
|
||||
}
|
||||
val subscription = subscribe(DeviceManager.magixFormat, originFilter = listOf(deviceEndpoint)).map { it.second }
|
||||
|
||||
val deferredDescriptorMessage = CompletableDeferred<DescriptionMessage>()
|
||||
|
||||
launch {
|
||||
deferredDescriptorMessage.complete(
|
||||
subscription.filterIsInstance<DescriptionMessage>().first()
|
||||
)
|
||||
deferredDescriptorMessage.complete(subscription.filterIsInstance<DescriptionMessage>().first())
|
||||
}
|
||||
|
||||
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
|
||||
*/
|
||||
|
@ -1,7 +1,6 @@
|
||||
package space.kscience.controls.client
|
||||
|
||||
import com.benasher44.uuid.uuid4
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
@ -41,7 +40,7 @@ internal fun generateId(request: MagixMessage): String = if (request.id != null)
|
||||
*/
|
||||
public fun DeviceManager.launchMagixService(
|
||||
endpoint: MagixEndpoint,
|
||||
endpointID: String,
|
||||
endpointID: String = controlsMagixFormat.defaultFormat,
|
||||
coroutineContext: CoroutineContext = EmptyCoroutineContext,
|
||||
): Job = context.launch(coroutineContext) {
|
||||
endpoint.subscribe(controlsMagixFormat, targetFilter = listOf(endpointID, null)).onEach { (request, payload) ->
|
||||
@ -57,7 +56,7 @@ public fun DeviceManager.launchMagixService(
|
||||
)
|
||||
}
|
||||
}.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)
|
||||
|
||||
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 {
|
||||
polymorphic(Vision::class) {
|
||||
subclass(IndicatorVision.serializer())
|
||||
subclass(SliderVision.serializer())
|
||||
subclass(BooleanIndicatorVision.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(
|
||||
context: Context,
|
||||
state: DeviceState<T>,
|
||||
extractValue: (T) -> Value = { Value.of(it) },
|
||||
extractValue: T.() -> Value = { state.converter.convert(this).value ?: Null },
|
||||
maxAge: Duration = defaultMaxAge,
|
||||
maxPoints: Int = defaultMaxPoints,
|
||||
minPoints: Int = defaultMinPoints,
|
||||
|
@ -10,34 +10,7 @@ import space.kscience.dataforge.names.asName
|
||||
import space.kscience.visionforge.VisionPlugin
|
||||
import space.kscience.visionforge.html.ElementVisionRenderer
|
||||
|
||||
|
||||
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 ->
|
||||
private val indicatorRenderer = ElementVisionRenderer<BooleanIndicatorVision> { name, vision: BooleanIndicatorVision, meta ->
|
||||
|
||||
}
|
||||
|
||||
@ -49,8 +22,7 @@ public actual class ControlVisionPlugin : VisionPlugin() {
|
||||
|
||||
override fun content(target: String): Map<Name, Any> = when (target) {
|
||||
ElementVisionRenderer.TYPE -> mapOf(
|
||||
"indicator".asName() to indicatorRenderer,
|
||||
"slider".asName() to sliderRenderer
|
||||
"indicator".asName() to indicatorRenderer
|
||||
)
|
||||
|
||||
else -> super.content(target)
|
||||
|
@ -64,7 +64,7 @@ class VirtualCarController : Controller(), ContextAware {
|
||||
//mongoStorageJob = deviceManager.storeMessages(DefaultAsynchronousMongoClientFactory)
|
||||
//Launch device client and connect it to the server
|
||||
val deviceEndpoint = MagixEndpoint.rSocketWithTcp("localhost")
|
||||
deviceManager.launchMagixService(deviceEndpoint, "car")
|
||||
deviceManager.launchMagixService(deviceEndpoint)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,3 @@
|
||||
import org.jetbrains.compose.ExperimentalComposeLibrary
|
||||
import org.jetbrains.compose.desktop.application.dsl.TargetFormat
|
||||
import org.jetbrains.kotlin.gradle.dsl.ExplicitApiMode
|
||||
|
||||
@ -12,15 +11,12 @@ kscience {
|
||||
withJava()
|
||||
}
|
||||
useKtor()
|
||||
useSerialization()
|
||||
useContextReceivers()
|
||||
commonMain {
|
||||
implementation(projects.controlsVision)
|
||||
implementation(projects.controlsConstructor)
|
||||
// implementation("io.github.koalaplot:koalaplot-core:0.6.0")
|
||||
dependencies {
|
||||
api(projects.controlsVision)
|
||||
}
|
||||
jvmMain {
|
||||
// implementation("io.ktor:ktor-server-cio")
|
||||
implementation("io.ktor:ktor-server-cio")
|
||||
implementation(spclibs.logback.classic)
|
||||
}
|
||||
}
|
||||
@ -30,8 +26,6 @@ kotlin {
|
||||
jvmMain {
|
||||
dependencies {
|
||||
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 space.kscience.controls.api.DeviceHub
|
||||
import space.kscience.controls.api.PropertyDescriptor
|
||||
import space.kscience.controls.misc.asMeta
|
||||
import space.kscience.controls.misc.duration
|
||||
import space.kscience.controls.ports.AsynchronousPort
|
||||
import space.kscience.controls.ports.KtorTcpPort
|
||||
import space.kscience.controls.ports.send
|
||||
|
@ -79,7 +79,6 @@ visionforge-jupiter = { module = "space.kscience:visionforge-jupyter", version.r
|
||||
visionforge-plotly = { module = "space.kscience:visionforge-plotly", version.ref = "visionforge" }
|
||||
visionforge-markdown = { module = "space.kscience:visionforge-markdown", version.ref = "visionforge" }
|
||||
visionforge-server = { module = "space.kscience:visionforge-server", version.ref = "visionforge" }
|
||||
visionforge-compose-html = { module = "space.kscience:visionforge-compose-html", version.ref = "visionforge" }
|
||||
|
||||
# Buildscript
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user