Major constructor refactoring
This commit is contained in:
parent
f72d7aa3fa
commit
9edde7bdbd
@ -8,6 +8,9 @@ plugins {
|
|||||||
allprojects {
|
allprojects {
|
||||||
group = "space.kscience"
|
group = "space.kscience"
|
||||||
version = "0.4.0-dev-4"
|
version = "0.4.0-dev-4"
|
||||||
|
repositories{
|
||||||
|
google()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ksciencePublish {
|
ksciencePublish {
|
||||||
|
@ -11,6 +11,7 @@ kscience{
|
|||||||
jvm()
|
jvm()
|
||||||
js()
|
js()
|
||||||
useCoroutines()
|
useCoroutines()
|
||||||
|
useSerialization()
|
||||||
commonMain {
|
commonMain {
|
||||||
api(projects.controlsCore)
|
api(projects.controlsCore)
|
||||||
}
|
}
|
||||||
|
@ -2,9 +2,7 @@ package space.kscience.controls.constructor
|
|||||||
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.*
|
||||||
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
|
||||||
@ -14,34 +12,38 @@ import kotlin.time.Duration
|
|||||||
/**
|
/**
|
||||||
* A binding that is used to describe device functionality
|
* A binding that is used to describe device functionality
|
||||||
*/
|
*/
|
||||||
public sealed interface StateDescriptor
|
public sealed interface ConstructorElement
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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 PropertyConstructorElement<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>,
|
||||||
) : StateDescriptor
|
) : ConstructorElement
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A binding for independent state like a timer
|
* A binding for independent state like a timer
|
||||||
*/
|
*/
|
||||||
public class StateNodeDescriptor<T>(
|
public class StateConstructorElement<T>(
|
||||||
public val state: DeviceState<T>,
|
public val state: DeviceState<T>,
|
||||||
) : StateDescriptor
|
) : ConstructorElement
|
||||||
|
|
||||||
public class StateConnectionDescriptor(
|
public class ConnectionConstrucorElement(
|
||||||
public val reads: Collection<DeviceState<*>>,
|
public val reads: Collection<DeviceState<*>>,
|
||||||
public val writes: Collection<DeviceState<*>>,
|
public val writes: Collection<DeviceState<*>>,
|
||||||
) : StateDescriptor
|
) : ConstructorElement
|
||||||
|
|
||||||
|
public class ModelConstructorElement(
|
||||||
|
public val model: ConstructorModel
|
||||||
|
) : ConstructorElement
|
||||||
|
|
||||||
|
|
||||||
public interface StateContainer : ContextAware, CoroutineScope {
|
public interface StateContainer : ContextAware, CoroutineScope {
|
||||||
public val stateDescriptors: Set<StateDescriptor>
|
public val constructorElements: Set<ConstructorElement>
|
||||||
public fun registerState(stateDescriptor: StateDescriptor)
|
public fun registerElement(constructorElement: ConstructorElement)
|
||||||
public fun unregisterState(stateDescriptor: StateDescriptor)
|
public fun unregisterElement(constructorElement: ConstructorElement)
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -50,16 +52,16 @@ public interface StateContainer : ContextAware, CoroutineScope {
|
|||||||
* Optionally provide [writes] - a set of states that this change affects.
|
* Optionally provide [writes] - a set of states that this change affects.
|
||||||
*/
|
*/
|
||||||
public fun <T> DeviceState<T>.onNext(
|
public fun <T> DeviceState<T>.onNext(
|
||||||
vararg writes: DeviceState<*>,
|
writes: Collection<DeviceState<*>> = emptySet(),
|
||||||
alsoReads: Collection<DeviceState<*>> = emptySet(),
|
reads: Collection<DeviceState<*>> = emptySet(),
|
||||||
onChange: suspend (T) -> Unit,
|
onChange: suspend (T) -> Unit,
|
||||||
): Job = valueFlow.onEach(onChange).launchIn(this@StateContainer).also {
|
): Job = valueFlow.onEach(onChange).launchIn(this@StateContainer).also {
|
||||||
registerState(StateConnectionDescriptor(setOf(this, *alsoReads.toTypedArray()), setOf(*writes)))
|
registerElement(ConnectionConstrucorElement(reads + this, writes))
|
||||||
}
|
}
|
||||||
|
|
||||||
public fun <T> DeviceState<T>.onChange(
|
public fun <T> DeviceState<T>.onChange(
|
||||||
vararg writes: DeviceState<*>,
|
writes: Collection<DeviceState<*>> = emptySet(),
|
||||||
alsoReads: Collection<DeviceState<*>> = emptySet(),
|
reads: Collection<DeviceState<*>> = emptySet(),
|
||||||
onChange: suspend (prev: T, next: T) -> Unit,
|
onChange: suspend (prev: T, next: T) -> Unit,
|
||||||
): Job = valueFlow.runningFold(Pair(value, value)) { pair, next ->
|
): Job = valueFlow.runningFold(Pair(value, value)) { pair, next ->
|
||||||
Pair(pair.second, next)
|
Pair(pair.second, next)
|
||||||
@ -68,7 +70,7 @@ public interface StateContainer : ContextAware, CoroutineScope {
|
|||||||
onChange(pair.first, pair.second)
|
onChange(pair.first, pair.second)
|
||||||
}
|
}
|
||||||
}.launchIn(this@StateContainer).also {
|
}.launchIn(this@StateContainer).also {
|
||||||
registerState(StateConnectionDescriptor(setOf(this, *alsoReads.toTypedArray()), setOf(*writes)))
|
registerElement(ConnectionConstrucorElement(reads + this, writes))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -76,21 +78,19 @@ public interface StateContainer : ContextAware, CoroutineScope {
|
|||||||
* 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))
|
registerElement(StateConstructorElement(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.stateOf(initialValue: T): MutableDeviceState<T> = state(
|
||||||
MutableDeviceState(initialValue)
|
MutableDeviceState(initialValue)
|
||||||
)
|
)
|
||||||
|
|
||||||
public fun <T : DeviceModel> StateContainer.model(model: T): T {
|
public fun <T : ConstructorModel> StateContainer.model(model: T): T {
|
||||||
model.stateDescriptors.forEach {
|
registerElement(ModelConstructorElement(model))
|
||||||
registerState(it)
|
|
||||||
}
|
|
||||||
return model
|
return model
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -101,9 +101,20 @@ public fun StateContainer.timer(tick: Duration): TimerState = state(TimerState(c
|
|||||||
|
|
||||||
|
|
||||||
public fun <T, R> StateContainer.mapState(
|
public fun <T, R> StateContainer.mapState(
|
||||||
state: DeviceState<T>,
|
origin: DeviceState<T>,
|
||||||
transformation: (T) -> R,
|
transformation: (T) -> R,
|
||||||
): DeviceStateWithDependencies<R> = state(DeviceState.map(state, transformation))
|
): DeviceStateWithDependencies<R> = state(DeviceState.map(origin, transformation))
|
||||||
|
|
||||||
|
|
||||||
|
public fun <T, R> StateContainer.flowState(
|
||||||
|
origin: DeviceState<T>,
|
||||||
|
initialValue: R,
|
||||||
|
transformation: suspend FlowCollector<R>.(T) -> Unit
|
||||||
|
): DeviceStateWithDependencies<R> {
|
||||||
|
val state = MutableDeviceState(initialValue)
|
||||||
|
origin.valueFlow.transform(transformation).onEach { state.value = it }.launchIn(this)
|
||||||
|
return state(state.withDependencies(setOf(origin)))
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new state by combining two existing ones
|
* Create a new state by combining two existing ones
|
||||||
@ -122,13 +133,13 @@ public fun <T1, T2, R> StateContainer.combineState(
|
|||||||
* On resulting [Job] cancel the binding is unregistered
|
* On resulting [Job] cancel the binding is unregistered
|
||||||
*/
|
*/
|
||||||
public fun <T> StateContainer.bindTo(sourceState: DeviceState<T>, targetState: MutableDeviceState<T>): Job {
|
public fun <T> StateContainer.bindTo(sourceState: DeviceState<T>, targetState: MutableDeviceState<T>): Job {
|
||||||
val descriptor = StateConnectionDescriptor(setOf(sourceState), setOf(targetState))
|
val descriptor = ConnectionConstrucorElement(setOf(sourceState), setOf(targetState))
|
||||||
registerState(descriptor)
|
registerElement(descriptor)
|
||||||
return sourceState.valueFlow.onEach {
|
return sourceState.valueFlow.onEach {
|
||||||
targetState.value = it
|
targetState.value = it
|
||||||
}.launchIn(this).apply {
|
}.launchIn(this).apply {
|
||||||
invokeOnCompletion {
|
invokeOnCompletion {
|
||||||
unregisterState(descriptor)
|
unregisterElement(descriptor)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -144,19 +155,19 @@ public fun <T, R> StateContainer.transformTo(
|
|||||||
targetState: MutableDeviceState<R>,
|
targetState: MutableDeviceState<R>,
|
||||||
transformation: suspend (T) -> R,
|
transformation: suspend (T) -> R,
|
||||||
): Job {
|
): Job {
|
||||||
val descriptor = StateConnectionDescriptor(setOf(sourceState), setOf(targetState))
|
val descriptor = ConnectionConstrucorElement(setOf(sourceState), setOf(targetState))
|
||||||
registerState(descriptor)
|
registerElement(descriptor)
|
||||||
return sourceState.valueFlow.onEach {
|
return sourceState.valueFlow.onEach {
|
||||||
targetState.value = transformation(it)
|
targetState.value = transformation(it)
|
||||||
}.launchIn(this).apply {
|
}.launchIn(this).apply {
|
||||||
invokeOnCompletion {
|
invokeOnCompletion {
|
||||||
unregisterState(descriptor)
|
unregisterElement(descriptor)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register [StateDescriptor] that combines values from [sourceState1] and [sourceState2] using [transformation].
|
* Register [ConstructorElement] that combines values from [sourceState1] and [sourceState2] using [transformation].
|
||||||
*
|
*
|
||||||
* On resulting [Job] cancel the binding is unregistered
|
* On resulting [Job] cancel the binding is unregistered
|
||||||
*/
|
*/
|
||||||
@ -166,19 +177,19 @@ public fun <T1, T2, R> StateContainer.combineTo(
|
|||||||
targetState: MutableDeviceState<R>,
|
targetState: MutableDeviceState<R>,
|
||||||
transformation: suspend (T1, T2) -> R,
|
transformation: suspend (T1, T2) -> R,
|
||||||
): Job {
|
): Job {
|
||||||
val descriptor = StateConnectionDescriptor(setOf(sourceState1, sourceState2), setOf(targetState))
|
val descriptor = ConnectionConstrucorElement(setOf(sourceState1, sourceState2), setOf(targetState))
|
||||||
registerState(descriptor)
|
registerElement(descriptor)
|
||||||
return kotlinx.coroutines.flow.combine(sourceState1.valueFlow, sourceState2.valueFlow, transformation).onEach {
|
return kotlinx.coroutines.flow.combine(sourceState1.valueFlow, sourceState2.valueFlow, transformation).onEach {
|
||||||
targetState.value = it
|
targetState.value = it
|
||||||
}.launchIn(this).apply {
|
}.launchIn(this).apply {
|
||||||
invokeOnCompletion {
|
invokeOnCompletion {
|
||||||
unregisterState(descriptor)
|
unregisterElement(descriptor)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register [StateDescriptor] that combines values from [sourceStates] using [transformation].
|
* Register [ConstructorElement] that combines values from [sourceStates] using [transformation].
|
||||||
*
|
*
|
||||||
* On resulting [Job] cancel the binding is unregistered
|
* On resulting [Job] cancel the binding is unregistered
|
||||||
*/
|
*/
|
||||||
@ -187,13 +198,13 @@ public inline fun <reified T, R> StateContainer.combineTo(
|
|||||||
targetState: MutableDeviceState<R>,
|
targetState: MutableDeviceState<R>,
|
||||||
noinline transformation: suspend (Array<T>) -> R,
|
noinline transformation: suspend (Array<T>) -> R,
|
||||||
): Job {
|
): Job {
|
||||||
val descriptor = StateConnectionDescriptor(sourceStates, setOf(targetState))
|
val descriptor = ConnectionConstrucorElement(sourceStates, setOf(targetState))
|
||||||
registerState(descriptor)
|
registerElement(descriptor)
|
||||||
return kotlinx.coroutines.flow.combine(sourceStates.map { it.valueFlow }, transformation).onEach {
|
return kotlinx.coroutines.flow.combine(sourceStates.map { it.valueFlow }, transformation).onEach {
|
||||||
targetState.value = it
|
targetState.value = it
|
||||||
}.launchIn(this).apply {
|
}.launchIn(this).apply {
|
||||||
invokeOnCompletion {
|
invokeOnCompletion {
|
||||||
unregisterState(descriptor)
|
unregisterElement(descriptor)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -0,0 +1,33 @@
|
|||||||
|
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 ConstructorModel(
|
||||||
|
final override val context: Context,
|
||||||
|
vararg dependencies: DeviceState<*>,
|
||||||
|
) : StateContainer, CoroutineScope {
|
||||||
|
|
||||||
|
@OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
|
||||||
|
override val coroutineContext: CoroutineContext = context.newCoroutineContext(SupervisorJob())
|
||||||
|
|
||||||
|
|
||||||
|
private val _constructorElements: MutableSet<ConstructorElement> = mutableSetOf<ConstructorElement>().apply {
|
||||||
|
dependencies.forEach {
|
||||||
|
add(StateConstructorElement(it))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override val constructorElements: Set<ConstructorElement> get() = _constructorElements
|
||||||
|
|
||||||
|
override fun registerElement(constructorElement: ConstructorElement) {
|
||||||
|
_constructorElements.add(constructorElement)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun unregisterElement(constructorElement: ConstructorElement) {
|
||||||
|
_constructorElements.remove(constructorElement)
|
||||||
|
}
|
||||||
|
}
|
@ -3,7 +3,6 @@ package space.kscience.controls.constructor
|
|||||||
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
|
||||||
import space.kscience.controls.spec.MutableDevicePropertySpec
|
|
||||||
import space.kscience.dataforge.context.Context
|
import space.kscience.dataforge.context.Context
|
||||||
import space.kscience.dataforge.context.Factory
|
import space.kscience.dataforge.context.Factory
|
||||||
import space.kscience.dataforge.meta.Meta
|
import space.kscience.dataforge.meta.Meta
|
||||||
@ -22,15 +21,15 @@ 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 _constructorElements: MutableSet<ConstructorElement> = mutableSetOf()
|
||||||
override val stateDescriptors: Set<StateDescriptor> get() = _stateDescriptors
|
override val constructorElements: Set<ConstructorElement> get() = _constructorElements
|
||||||
|
|
||||||
override fun registerState(stateDescriptor: StateDescriptor) {
|
override fun registerElement(constructorElement: ConstructorElement) {
|
||||||
_stateDescriptors.add(stateDescriptor)
|
_constructorElements.add(constructorElement)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun unregisterState(stateDescriptor: StateDescriptor) {
|
override fun unregisterElement(constructorElement: ConstructorElement) {
|
||||||
_stateDescriptors.remove(stateDescriptor)
|
_constructorElements.remove(constructorElement)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun <T> registerProperty(
|
override fun <T> registerProperty(
|
||||||
@ -39,7 +38,7 @@ public abstract class DeviceConstructor(
|
|||||||
state: DeviceState<T>,
|
state: DeviceState<T>,
|
||||||
) {
|
) {
|
||||||
super.registerProperty(converter, descriptor, state)
|
super.registerProperty(converter, descriptor, state)
|
||||||
registerState(StatePropertyDescriptor(this, descriptor.name, state))
|
registerElement(PropertyConstructorElement(this, descriptor.name, state))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -108,7 +107,7 @@ public fun <T : Any> DeviceConstructor.property(
|
|||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register a mutable external state as a property
|
* Create and register a mutable external state as a property
|
||||||
*/
|
*/
|
||||||
public fun <T : Any> DeviceConstructor.mutableProperty(
|
public fun <T : Any> DeviceConstructor.mutableProperty(
|
||||||
metaConverter: MetaConverter<T>,
|
metaConverter: MetaConverter<T>,
|
||||||
@ -141,22 +140,7 @@ public fun <T> DeviceConstructor.virtualProperty(
|
|||||||
nameOverride,
|
nameOverride,
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
public fun <T, S : DeviceState<T>> DeviceConstructor.property(
|
||||||
* Bind existing property provided by specification to this device
|
spec: DevicePropertySpec<*, T>,
|
||||||
*/
|
state: S,
|
||||||
public fun <T, D : Device> DeviceConstructor.deviceProperty(
|
): Unit = registerProperty(spec.converter, spec.descriptor, state)
|
||||||
device: D,
|
|
||||||
property: DevicePropertySpec<D, T>,
|
|
||||||
initialValue: T,
|
|
||||||
): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, DeviceState<T>>> =
|
|
||||||
property(property.converter, device.propertyAsState(property, initialValue))
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Bind existing property provided by specification to this device
|
|
||||||
*/
|
|
||||||
public fun <T, D : Device> DeviceConstructor.deviceProperty(
|
|
||||||
device: D,
|
|
||||||
property: MutableDevicePropertySpec<D, T>,
|
|
||||||
initialValue: T,
|
|
||||||
): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, MutableDeviceState<T>>> =
|
|
||||||
property(property.converter, device.mutablePropertyAsState(property, initialValue))
|
|
||||||
|
@ -1,32 +0,0 @@
|
|||||||
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 {
|
|
||||||
|
|
||||||
override val coroutineContext: CoroutineContext = context.newCoroutineContext(SupervisorJob())
|
|
||||||
|
|
||||||
|
|
||||||
private val _stateDescriptors: MutableSet<StateDescriptor> = mutableSetOf<StateDescriptor>().apply {
|
|
||||||
dependencies.forEach {
|
|
||||||
add(StateNodeDescriptor(it))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override val stateDescriptors: Set<StateDescriptor> get() = _stateDescriptors
|
|
||||||
|
|
||||||
override fun registerState(stateDescriptor: StateDescriptor) {
|
|
||||||
_stateDescriptors.add(stateDescriptor)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun unregisterState(stateDescriptor: StateDescriptor) {
|
|
||||||
_stateDescriptors.remove(stateDescriptor)
|
|
||||||
}
|
|
||||||
}
|
|
@ -48,6 +48,12 @@ public interface DeviceStateWithDependencies<T> : DeviceState<T> {
|
|||||||
public val dependencies: Collection<DeviceState<*>>
|
public val dependencies: Collection<DeviceState<*>>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public fun <T> DeviceState<T>.withDependencies(
|
||||||
|
dependencies: Collection<DeviceState<*>>
|
||||||
|
): DeviceStateWithDependencies<T> = object : DeviceStateWithDependencies<T>, DeviceState<T> by this {
|
||||||
|
override val dependencies: Collection<DeviceState<*>> = dependencies
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new read-only [DeviceState] that mirrors receiver state by mapping the value with [mapper].
|
* Create a new read-only [DeviceState] that mirrors receiver state by mapping the value with [mapper].
|
||||||
*/
|
*/
|
||||||
|
@ -92,11 +92,11 @@ public suspend fun <T> Device.mutablePropertyAsState(
|
|||||||
return mutablePropertyAsState(propertyName, metaConverter, initialValue)
|
return mutablePropertyAsState(propertyName, metaConverter, initialValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
public suspend fun <D : Device, T> D.mutablePropertyAsState(
|
public suspend fun <D : Device, T> D.propertyAsState(
|
||||||
propertySpec: MutableDevicePropertySpec<D, T>,
|
propertySpec: MutableDevicePropertySpec<D, T>,
|
||||||
): MutableDeviceState<T> = mutablePropertyAsState(propertySpec.name, propertySpec.converter)
|
): MutableDeviceState<T> = mutablePropertyAsState(propertySpec.name, propertySpec.converter)
|
||||||
|
|
||||||
public fun <D : Device, T> D.mutablePropertyAsState(
|
public fun <D : Device, T> D.propertyAsState(
|
||||||
propertySpec: MutableDevicePropertySpec<D, T>,
|
propertySpec: MutableDevicePropertySpec<D, T>,
|
||||||
initialValue: T,
|
initialValue: T,
|
||||||
): MutableDeviceState<T> = mutablePropertyAsState(propertySpec.name, propertySpec.converter, initialValue)
|
): MutableDeviceState<T> = mutablePropertyAsState(propertySpec.name, propertySpec.converter, initialValue)
|
||||||
|
@ -53,5 +53,5 @@ public fun StateContainer.doubleInRangeState(
|
|||||||
initialValue: Double,
|
initialValue: Double,
|
||||||
range: ClosedFloatingPointRange<Double>,
|
range: ClosedFloatingPointRange<Double>,
|
||||||
): DoubleInRangeState = DoubleInRangeState(initialValue, range).also {
|
): DoubleInRangeState = DoubleInRangeState(initialValue, range).also {
|
||||||
registerState(StateNodeDescriptor(it))
|
registerElement(StateConstructorElement(it))
|
||||||
}
|
}
|
@ -0,0 +1,20 @@
|
|||||||
|
package space.kscience.controls.constructor.library
|
||||||
|
|
||||||
|
import kotlinx.coroutines.flow.FlowCollector
|
||||||
|
import space.kscience.controls.constructor.DeviceConstructor
|
||||||
|
import space.kscience.controls.constructor.DeviceState
|
||||||
|
import space.kscience.controls.constructor.DeviceStateWithDependencies
|
||||||
|
import space.kscience.controls.constructor.flowState
|
||||||
|
import space.kscience.dataforge.context.Context
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A device that converts one type of physical quantity to another type
|
||||||
|
*/
|
||||||
|
public class Converter<T, R>(
|
||||||
|
context: Context,
|
||||||
|
input: DeviceState<T>,
|
||||||
|
initialValue: R,
|
||||||
|
transform: suspend FlowCollector<R>.(T) -> Unit,
|
||||||
|
) : DeviceConstructor(context) {
|
||||||
|
public val output: DeviceStateWithDependencies<R> = flowState(input, initialValue, transform)
|
||||||
|
}
|
@ -6,7 +6,7 @@ import kotlinx.coroutines.isActive
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import space.kscience.controls.api.Device
|
import space.kscience.controls.api.Device
|
||||||
import space.kscience.controls.constructor.MutableDeviceState
|
import space.kscience.controls.constructor.MutableDeviceState
|
||||||
import space.kscience.controls.constructor.mutablePropertyAsState
|
import space.kscience.controls.constructor.propertyAsState
|
||||||
import space.kscience.controls.manager.clock
|
import space.kscience.controls.manager.clock
|
||||||
import space.kscience.controls.spec.*
|
import space.kscience.controls.spec.*
|
||||||
import space.kscience.dataforge.context.Context
|
import space.kscience.dataforge.context.Context
|
||||||
@ -98,4 +98,4 @@ public class VirtualDrive(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public suspend fun Drive.stateOfForce(): MutableDeviceState<Double> = mutablePropertyAsState(Drive.force)
|
public suspend fun Drive.stateOfForce(): MutableDeviceState<Double> = propertyAsState(Drive.force)
|
||||||
|
@ -1,15 +1,14 @@
|
|||||||
package space.kscience.controls.constructor.library
|
package space.kscience.controls.constructor.library
|
||||||
|
|
||||||
import kotlinx.coroutines.flow.launchIn
|
|
||||||
import kotlinx.coroutines.flow.onEach
|
|
||||||
import space.kscience.controls.api.Device
|
import space.kscience.controls.api.Device
|
||||||
|
import space.kscience.controls.constructor.DeviceConstructor
|
||||||
import space.kscience.controls.constructor.DeviceState
|
import space.kscience.controls.constructor.DeviceState
|
||||||
import space.kscience.controls.spec.DeviceBySpec
|
import space.kscience.controls.constructor.property
|
||||||
import space.kscience.controls.spec.DevicePropertySpec
|
import space.kscience.controls.spec.DevicePropertySpec
|
||||||
import space.kscience.controls.spec.DeviceSpec
|
import space.kscience.controls.spec.DeviceSpec
|
||||||
import space.kscience.controls.spec.booleanProperty
|
import space.kscience.controls.spec.booleanProperty
|
||||||
import space.kscience.dataforge.context.Context
|
import space.kscience.dataforge.context.Context
|
||||||
import space.kscience.dataforge.context.Factory
|
import space.kscience.dataforge.meta.MetaConverter
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -17,13 +16,10 @@ import space.kscience.dataforge.context.Factory
|
|||||||
*/
|
*/
|
||||||
public interface LimitSwitch : Device {
|
public interface LimitSwitch : Device {
|
||||||
|
|
||||||
public val locked: Boolean
|
public fun isLocked(): Boolean
|
||||||
|
|
||||||
public companion object : DeviceSpec<LimitSwitch>() {
|
public companion object : DeviceSpec<LimitSwitch>() {
|
||||||
public val locked: DevicePropertySpec<LimitSwitch, Boolean> by booleanProperty { locked }
|
public val locked: DevicePropertySpec<LimitSwitch, Boolean> by booleanProperty { isLocked() }
|
||||||
public operator fun invoke(lockedState: DeviceState<Boolean>): Factory<LimitSwitch> = Factory { context, _ ->
|
|
||||||
VirtualLimitSwitch(context, lockedState)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -32,14 +28,10 @@ public interface LimitSwitch : Device {
|
|||||||
*/
|
*/
|
||||||
public class VirtualLimitSwitch(
|
public class VirtualLimitSwitch(
|
||||||
context: Context,
|
context: Context,
|
||||||
public val lockedState: DeviceState<Boolean>,
|
locked: DeviceState<Boolean>,
|
||||||
) : DeviceBySpec<LimitSwitch>(LimitSwitch, context), LimitSwitch {
|
) : DeviceConstructor(context), LimitSwitch {
|
||||||
|
|
||||||
override suspend fun onStart() {
|
public val locked: DeviceState<Boolean> by property(MetaConverter.boolean, locked)
|
||||||
lockedState.valueFlow.onEach {
|
|
||||||
propertyChanged(LimitSwitch.locked, it)
|
|
||||||
}.launchIn(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
override val locked: Boolean get() = lockedState.value
|
override fun isLocked(): Boolean = locked.value
|
||||||
}
|
}
|
@ -16,32 +16,22 @@ import kotlin.time.Duration
|
|||||||
import kotlin.time.Duration.Companion.milliseconds
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
import kotlin.time.DurationUnit
|
import kotlin.time.DurationUnit
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Pid regulator parameters
|
* Pid regulator parameters
|
||||||
*/
|
*/
|
||||||
public interface PidParameters {
|
public data class PidParameters(
|
||||||
public val kp: Double
|
val kp: Double,
|
||||||
public val ki: Double
|
val ki: Double,
|
||||||
public val kd: Double
|
val kd: Double,
|
||||||
public val timeStep: Duration
|
val timeStep: Duration = 1.milliseconds,
|
||||||
}
|
)
|
||||||
|
|
||||||
private data class PidParametersImpl(
|
|
||||||
override val kp: Double,
|
|
||||||
override val ki: Double,
|
|
||||||
override val kd: Double,
|
|
||||||
override val timeStep: Duration,
|
|
||||||
) : PidParameters
|
|
||||||
|
|
||||||
public fun PidParameters(kp: Double, ki: Double, kd: Double, timeStep: Duration = 1.milliseconds): PidParameters =
|
|
||||||
PidParametersImpl(kp, ki, kd, timeStep)
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A drive with PID regulator
|
* A drive with PID regulator
|
||||||
*/
|
*/
|
||||||
public class PidRegulator(
|
public class PidRegulator(
|
||||||
public val drive: Drive,
|
public val drive: Drive,
|
||||||
public val pidParameters: PidParameters,
|
public var pidParameters: PidParameters, // TODO expose as property
|
||||||
) : DeviceBySpec<Regulator>(Regulator, drive.context), Regulator {
|
) : DeviceBySpec<Regulator>(Regulator, drive.context), Regulator {
|
||||||
|
|
||||||
private val clock = drive.context.clock
|
private val clock = drive.context.clock
|
||||||
@ -65,7 +55,7 @@ public class PidRegulator(
|
|||||||
delay(pidParameters.timeStep)
|
delay(pidParameters.timeStep)
|
||||||
mutex.withLock {
|
mutex.withLock {
|
||||||
val realTime = clock.now()
|
val realTime = clock.now()
|
||||||
val delta = target - position
|
val delta = target - getPosition()
|
||||||
val dtSeconds = (realTime - lastTime).toDouble(DurationUnit.SECONDS)
|
val dtSeconds = (realTime - lastTime).toDouble(DurationUnit.SECONDS)
|
||||||
integral += delta * dtSeconds
|
integral += delta * dtSeconds
|
||||||
val derivative = (drive.position - lastPosition) / dtSeconds
|
val derivative = (drive.position - lastPosition) / dtSeconds
|
||||||
@ -87,7 +77,7 @@ public class PidRegulator(
|
|||||||
drive.stop()
|
drive.stop()
|
||||||
}
|
}
|
||||||
|
|
||||||
override val position: Double get() = drive.position
|
override suspend fun getPosition(): Double = drive.position
|
||||||
}
|
}
|
||||||
|
|
||||||
public fun DeviceGroup.pid(
|
public fun DeviceGroup.pid(
|
||||||
|
@ -17,11 +17,11 @@ public interface Regulator : Device {
|
|||||||
/**
|
/**
|
||||||
* Current position value
|
* Current position value
|
||||||
*/
|
*/
|
||||||
public val position: Double
|
public suspend fun getPosition(): Double
|
||||||
|
|
||||||
public companion object : DeviceSpec<Regulator>() {
|
public companion object : DeviceSpec<Regulator>() {
|
||||||
public val target: MutableDevicePropertySpec<Regulator, Double> by mutableProperty(MetaConverter.double, Regulator::target)
|
public val target: MutableDevicePropertySpec<Regulator, Double> by mutableProperty(MetaConverter.double, Regulator::target)
|
||||||
|
|
||||||
public val position: DevicePropertySpec<Regulator, Double> by doubleProperty { position }
|
public val position: DevicePropertySpec<Regulator, Double> by doubleProperty { getPosition() }
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -0,0 +1,10 @@
|
|||||||
|
package space.kscience.controls.constructor.units
|
||||||
|
|
||||||
|
import kotlin.jvm.JvmInline
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A value without identity coupled to units of measurements.
|
||||||
|
*/
|
||||||
|
@JvmInline
|
||||||
|
public value class NumericalValue<T: UnitsOfMeasurement>(public val value: Double)
|
@ -0,0 +1,30 @@
|
|||||||
|
package space.kscience.controls.constructor.units
|
||||||
|
|
||||||
|
|
||||||
|
public interface UnitsOfMeasurement
|
||||||
|
|
||||||
|
/**/
|
||||||
|
|
||||||
|
public interface UnitsOfLength : UnitsOfMeasurement
|
||||||
|
|
||||||
|
public data object Meters: UnitsOfLength
|
||||||
|
|
||||||
|
/**/
|
||||||
|
|
||||||
|
public interface UnitsOfTime: UnitsOfMeasurement
|
||||||
|
|
||||||
|
public data object Seconds: UnitsOfTime
|
||||||
|
|
||||||
|
/**/
|
||||||
|
|
||||||
|
public interface UnitsOfVelocity: UnitsOfMeasurement
|
||||||
|
|
||||||
|
public data object MetersPerSecond: UnitsOfVelocity
|
||||||
|
|
||||||
|
/**/
|
||||||
|
|
||||||
|
public interface UnitsAngularOfVelocity: UnitsOfMeasurement
|
||||||
|
|
||||||
|
public data object RadiansPerSecond: UnitsAngularOfVelocity
|
||||||
|
|
||||||
|
public data object DegreesPerSecond: UnitsAngularOfVelocity
|
@ -12,6 +12,7 @@ import kotlin.math.roundToLong
|
|||||||
|
|
||||||
@OptIn(InternalCoroutinesApi::class)
|
@OptIn(InternalCoroutinesApi::class)
|
||||||
private class CompressedTimeDispatcher(
|
private class CompressedTimeDispatcher(
|
||||||
|
val clockManager: ClockManager,
|
||||||
val dispatcher: CoroutineDispatcher,
|
val dispatcher: CoroutineDispatcher,
|
||||||
val compression: Double,
|
val compression: Double,
|
||||||
) : CoroutineDispatcher(), Delay {
|
) : CoroutineDispatcher(), Delay {
|
||||||
@ -74,7 +75,7 @@ public class ClockManager : AbstractPlugin() {
|
|||||||
): CoroutineDispatcher = if (timeCompression == 1.0) {
|
): CoroutineDispatcher = if (timeCompression == 1.0) {
|
||||||
dispatcher
|
dispatcher
|
||||||
} else {
|
} else {
|
||||||
CompressedTimeDispatcher(dispatcher, timeCompression)
|
CompressedTimeDispatcher(this, dispatcher, timeCompression)
|
||||||
}
|
}
|
||||||
|
|
||||||
public companion object : PluginFactory<ClockManager> {
|
public companion object : PluginFactory<ClockManager> {
|
||||||
|
@ -1,10 +1,7 @@
|
|||||||
package space.kscience.controls.spec
|
package space.kscience.controls.spec
|
||||||
|
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import space.kscience.controls.api.ActionDescriptor
|
import space.kscience.controls.api.*
|
||||||
import space.kscience.controls.api.Device
|
|
||||||
import space.kscience.controls.api.PropertyDescriptor
|
|
||||||
import space.kscience.controls.api.metaDescriptor
|
|
||||||
import space.kscience.dataforge.meta.Meta
|
import space.kscience.dataforge.meta.Meta
|
||||||
import space.kscience.dataforge.meta.MetaConverter
|
import space.kscience.dataforge.meta.MetaConverter
|
||||||
import space.kscience.dataforge.meta.descriptors.MetaDescriptor
|
import space.kscience.dataforge.meta.descriptors.MetaDescriptor
|
||||||
@ -159,7 +156,6 @@ public abstract class DeviceSpec<D : Device> {
|
|||||||
deviceAction
|
deviceAction
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -196,3 +192,16 @@ public fun <D : Device> DeviceSpec<D>.metaAction(
|
|||||||
execute(it)
|
execute(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Throw an exception if device does not have all properties and actions defined by this specification
|
||||||
|
*/
|
||||||
|
public fun DeviceSpec<*>.validate(device: Device) {
|
||||||
|
properties.map { it.value.descriptor }.forEach { specProperty ->
|
||||||
|
check(specProperty in device.propertyDescriptors) { "Property ${specProperty.name} not registered in ${device.id}" }
|
||||||
|
}
|
||||||
|
|
||||||
|
actions.map { it.value.descriptor }.forEach { specAction ->
|
||||||
|
check(specAction in device.actionDescriptors) { "Action ${specAction.name} not registered in ${device.id}" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -19,42 +19,37 @@ import kotlin.test.assertEquals
|
|||||||
class MagixLoopTest {
|
class MagixLoopTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun deviceHub() = runTest {
|
fun realDeviceHub() = runTest {
|
||||||
val context = Context {
|
|
||||||
plugin(DeviceManager)
|
|
||||||
}
|
|
||||||
|
|
||||||
val server = context.startMagixServer()
|
|
||||||
|
|
||||||
val deviceManager = context.request(DeviceManager)
|
|
||||||
|
|
||||||
val deviceEndpoint = MagixEndpoint.rSocketWithWebSockets("localhost")
|
|
||||||
|
|
||||||
// deviceEndpoint.subscribe().onEach {
|
|
||||||
// println(it)
|
|
||||||
// }.launchIn(this)
|
|
||||||
|
|
||||||
deviceManager.launchMagixService(deviceEndpoint, "device")
|
|
||||||
|
|
||||||
launch {
|
|
||||||
delay(50)
|
|
||||||
repeat(10) {
|
|
||||||
deviceManager.install("test[$it]", TestDevice)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val clientEndpoint = MagixEndpoint.rSocketWithWebSockets("localhost")
|
|
||||||
|
|
||||||
val remoteHub = clientEndpoint.remoteDeviceHub(context, "client", "device")
|
|
||||||
|
|
||||||
assertEquals(0, remoteHub.devices.size)
|
|
||||||
|
|
||||||
delay(60)
|
|
||||||
//switch context to use actual delay
|
|
||||||
withContext(Dispatchers.Default) {
|
withContext(Dispatchers.Default) {
|
||||||
|
val context = Context {
|
||||||
|
plugin(DeviceManager)
|
||||||
|
}
|
||||||
|
|
||||||
|
val server = context.startMagixServer()
|
||||||
|
|
||||||
|
val deviceManager = context.request(DeviceManager)
|
||||||
|
|
||||||
|
val deviceEndpoint = MagixEndpoint.rSocketWithWebSockets("localhost")
|
||||||
|
|
||||||
|
deviceManager.launchMagixService(deviceEndpoint, "device")
|
||||||
|
|
||||||
|
launch {
|
||||||
|
delay(50)
|
||||||
|
repeat(10) {
|
||||||
|
deviceManager.install("test[$it]", TestDevice)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val clientEndpoint = MagixEndpoint.rSocketWithWebSockets("localhost")
|
||||||
|
|
||||||
|
val remoteHub = clientEndpoint.remoteDeviceHub(context, "client", "device")
|
||||||
|
|
||||||
|
assertEquals(0, remoteHub.devices.size)
|
||||||
|
delay(60)
|
||||||
clientEndpoint.requestDeviceUpdate("client", "device")
|
clientEndpoint.requestDeviceUpdate("client", "device")
|
||||||
delay(60)
|
delay(60)
|
||||||
assertEquals(10, remoteHub.devices.size)
|
assertEquals(10, remoteHub.devices.size)
|
||||||
|
server.stop()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -34,7 +34,7 @@ public suspend inline fun <reified T: Any> OpcUaDevice.readOpcWithTime(
|
|||||||
converter: MetaConverter<T>,
|
converter: MetaConverter<T>,
|
||||||
magAge: Double = 500.0
|
magAge: Double = 500.0
|
||||||
): Pair<T, DateTime> {
|
): Pair<T, DateTime> {
|
||||||
val data = client.readValue(magAge, TimestampsToReturn.Server, nodeId).await()
|
val data: DataValue = client.readValue(magAge, TimestampsToReturn.Server, nodeId).await()
|
||||||
val time = data.serverTime ?: error("No server time provided")
|
val time = data.serverTime ?: error("No server time provided")
|
||||||
val meta: Meta = when (val content = data.value.value) {
|
val meta: Meta = when (val content = data.value.value) {
|
||||||
is T -> return content to time
|
is T -> return content to time
|
||||||
|
45
controls-visualisation-compose/build.gradle.kts
Normal file
45
controls-visualisation-compose/build.gradle.kts
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import org.jetbrains.compose.ExperimentalComposeLibrary
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
id("space.kscience.gradle.mpp")
|
||||||
|
alias(spclibs.plugins.compose)
|
||||||
|
`maven-publish`
|
||||||
|
}
|
||||||
|
|
||||||
|
description = """
|
||||||
|
Visualisation extension using compose-multiplatform
|
||||||
|
""".trimIndent()
|
||||||
|
|
||||||
|
kscience {
|
||||||
|
jvm()
|
||||||
|
useKtor()
|
||||||
|
useSerialization()
|
||||||
|
useContextReceivers()
|
||||||
|
commonMain {
|
||||||
|
api(projects.controlsConstructor)
|
||||||
|
api("io.github.koalaplot:koalaplot-core:0.6.0")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
sourceSets {
|
||||||
|
commonMain {
|
||||||
|
dependencies {
|
||||||
|
api(compose.foundation)
|
||||||
|
api(compose.material3)
|
||||||
|
@OptIn(ExperimentalComposeLibrary::class)
|
||||||
|
api(compose.desktop.components.splitPane)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// jvmMain {
|
||||||
|
// dependencies {
|
||||||
|
// implementation(compose.desktop.currentOs)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
readme {
|
||||||
|
maturity = space.kscience.gradle.Maturity.PROTOTYPE
|
||||||
|
}
|
@ -0,0 +1,45 @@
|
|||||||
|
package space.kscience.controls.compose
|
||||||
|
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import io.github.koalaplot.core.xygraph.AxisModel
|
||||||
|
import io.github.koalaplot.core.xygraph.TickValues
|
||||||
|
import kotlinx.datetime.Clock
|
||||||
|
import kotlinx.datetime.Instant
|
||||||
|
import kotlin.math.floor
|
||||||
|
import kotlin.time.Duration
|
||||||
|
import kotlin.time.times
|
||||||
|
|
||||||
|
public class TimeAxisModel(
|
||||||
|
override val minimumMajorTickSpacing: Dp = 50.dp,
|
||||||
|
private val rangeProvider: () -> ClosedRange<Instant>,
|
||||||
|
) : AxisModel<Instant> {
|
||||||
|
|
||||||
|
override fun computeTickValues(axisLength: Dp): TickValues<Instant> {
|
||||||
|
val currentRange = rangeProvider()
|
||||||
|
val rangeLength = currentRange.endInclusive - currentRange.start
|
||||||
|
val numTicks = floor(axisLength / minimumMajorTickSpacing).toInt()
|
||||||
|
val numMinorTicks = numTicks * 2
|
||||||
|
return object : TickValues<Instant> {
|
||||||
|
override val majorTickValues: List<Instant> = List(numTicks) {
|
||||||
|
currentRange.start + it.toDouble() / (numTicks - 1) * rangeLength
|
||||||
|
}
|
||||||
|
|
||||||
|
override val minorTickValues: List<Instant> = List(numMinorTicks) {
|
||||||
|
currentRange.start + it.toDouble() / (numMinorTicks - 1) * rangeLength
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun computeOffset(point: Instant): Float {
|
||||||
|
val currentRange = rangeProvider()
|
||||||
|
return ((point - currentRange.start) / (currentRange.endInclusive - currentRange.start)).toFloat()
|
||||||
|
}
|
||||||
|
|
||||||
|
public companion object {
|
||||||
|
public fun recent(duration: Duration, clock: Clock = Clock.System): TimeAxisModel = TimeAxisModel {
|
||||||
|
val now = clock.now()
|
||||||
|
(now - duration)..now
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,31 @@
|
|||||||
|
package space.kscience.controls.compose
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.State
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.snapshotFlow
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import space.kscience.controls.constructor.DeviceState
|
||||||
|
import kotlin.coroutines.CoroutineContext
|
||||||
|
import kotlin.coroutines.EmptyCoroutineContext
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represent this [DeviceState] as Compose multiplatform [State]
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
public fun <T> DeviceState<T>.asComposeState(
|
||||||
|
coroutineContext: CoroutineContext = EmptyCoroutineContext,
|
||||||
|
): State<T> = valueFlow.collectAsState(value, coroutineContext)
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represent this Compose [State] as [DeviceState]
|
||||||
|
*/
|
||||||
|
public fun <T> State<T>.asDeviceState(): DeviceState<T> = object : DeviceState<T> {
|
||||||
|
override val value: T get() = this@asDeviceState.value
|
||||||
|
|
||||||
|
override val valueFlow: Flow<T> get() = snapshotFlow { this@asDeviceState.value }
|
||||||
|
|
||||||
|
override fun toString(): String = "ComposeState(value=$value)"
|
||||||
|
}
|
@ -0,0 +1,2 @@
|
|||||||
|
package space.kscience.controls.compose
|
||||||
|
|
@ -0,0 +1,230 @@
|
|||||||
|
package space.kscience.controls.compose
|
||||||
|
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.graphics.SolidColor
|
||||||
|
import io.github.koalaplot.core.line.LinePlot
|
||||||
|
import io.github.koalaplot.core.style.LineStyle
|
||||||
|
import io.github.koalaplot.core.xygraph.DefaultPoint
|
||||||
|
import io.github.koalaplot.core.xygraph.XYGraphScope
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.flow.*
|
||||||
|
import kotlinx.coroutines.isActive
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.datetime.Clock
|
||||||
|
import kotlinx.datetime.Instant
|
||||||
|
import space.kscience.controls.api.Device
|
||||||
|
import space.kscience.controls.api.PropertyChangedMessage
|
||||||
|
import space.kscience.controls.api.propertyMessageFlow
|
||||||
|
import space.kscience.controls.constructor.DeviceState
|
||||||
|
import space.kscience.controls.manager.clock
|
||||||
|
import space.kscience.controls.misc.ValueWithTime
|
||||||
|
import space.kscience.controls.spec.DevicePropertySpec
|
||||||
|
import space.kscience.controls.spec.name
|
||||||
|
import space.kscience.dataforge.context.Context
|
||||||
|
import space.kscience.dataforge.meta.Meta
|
||||||
|
import space.kscience.dataforge.meta.double
|
||||||
|
import kotlin.time.Duration
|
||||||
|
import kotlin.time.Duration.Companion.minutes
|
||||||
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
|
|
||||||
|
private val defaultMaxAge get() = 10.minutes
|
||||||
|
private val defaultMaxPoints get() = 800
|
||||||
|
private val defaultMinPoints get() = 400
|
||||||
|
private val defaultSampling get() = 1.seconds
|
||||||
|
|
||||||
|
|
||||||
|
internal fun <T> Flow<ValueWithTime<T>>.collectAndTrim(
|
||||||
|
maxAge: Duration = defaultMaxAge,
|
||||||
|
maxPoints: Int = defaultMaxPoints,
|
||||||
|
minPoints: Int = defaultMinPoints,
|
||||||
|
clock: Clock = Clock.System,
|
||||||
|
): Flow<List<ValueWithTime<T>>> {
|
||||||
|
require(maxPoints > 2)
|
||||||
|
require(minPoints > 0)
|
||||||
|
require(maxPoints > minPoints)
|
||||||
|
val points = mutableListOf<ValueWithTime<T>>()
|
||||||
|
return transform { newPoint ->
|
||||||
|
points.add(newPoint)
|
||||||
|
val now = clock.now()
|
||||||
|
// filter old points
|
||||||
|
points.removeAll { now - it.time > maxAge }
|
||||||
|
|
||||||
|
if (points.size > maxPoints) {
|
||||||
|
val durationBetweenPoints = maxAge / minPoints
|
||||||
|
val markedForRemoval = buildList {
|
||||||
|
var lastTime: Instant? = null
|
||||||
|
points.forEach { point ->
|
||||||
|
if (lastTime?.let { point.time - it < durationBetweenPoints } == true) {
|
||||||
|
add(point)
|
||||||
|
} else {
|
||||||
|
lastTime = point.time
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
points.removeAll(markedForRemoval)
|
||||||
|
}
|
||||||
|
//return a protective copy
|
||||||
|
emit(ArrayList(points))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val defaultLineStyle: LineStyle = LineStyle(SolidColor(androidx.compose.ui.graphics.Color.Black))
|
||||||
|
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun <T> XYGraphScope<Instant, T>.PlotTimeSeries(
|
||||||
|
data: List<ValueWithTime<T>>,
|
||||||
|
lineStyle: LineStyle = defaultLineStyle,
|
||||||
|
) {
|
||||||
|
LinePlot(
|
||||||
|
data = data.map { DefaultPoint(it.time, it.value) },
|
||||||
|
lineStyle = lineStyle
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a trace that shows a [Device] property change over time. Show only latest [maxPoints] .
|
||||||
|
* @return a [Job] that handles the listener
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
public fun XYGraphScope<Instant, Double>.PlotDeviceProperty(
|
||||||
|
device: Device,
|
||||||
|
propertyName: String,
|
||||||
|
extractValue: Meta.() -> Double = { value?.double ?: Double.NaN },
|
||||||
|
maxAge: Duration = defaultMaxAge,
|
||||||
|
maxPoints: Int = defaultMaxPoints,
|
||||||
|
minPoints: Int = defaultMinPoints,
|
||||||
|
sampling: Duration = defaultSampling,
|
||||||
|
lineStyle: LineStyle = defaultLineStyle,
|
||||||
|
) {
|
||||||
|
var points by remember { mutableStateOf<List<ValueWithTime<Double>>>(emptyList()) }
|
||||||
|
|
||||||
|
LaunchedEffect(device, propertyName, maxAge, maxPoints, minPoints, sampling) {
|
||||||
|
device.propertyMessageFlow(propertyName)
|
||||||
|
.sample(sampling)
|
||||||
|
.map { ValueWithTime(it.value.extractValue(), it.time) }
|
||||||
|
.collectAndTrim(maxAge, maxPoints, minPoints, device.clock)
|
||||||
|
.onEach { points = it }
|
||||||
|
.launchIn(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
PlotTimeSeries(points, lineStyle)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
public fun XYGraphScope<Instant, Double>.PlotDeviceProperty(
|
||||||
|
device: Device,
|
||||||
|
property: DevicePropertySpec<*, out Number>,
|
||||||
|
maxAge: Duration = defaultMaxAge,
|
||||||
|
maxPoints: Int = defaultMaxPoints,
|
||||||
|
minPoints: Int = defaultMinPoints,
|
||||||
|
sampling: Duration = defaultSampling,
|
||||||
|
lineStyle: LineStyle = LineStyle(SolidColor(androidx.compose.ui.graphics.Color.Black)),
|
||||||
|
): Unit = PlotDeviceProperty(
|
||||||
|
device = device,
|
||||||
|
propertyName = property.name,
|
||||||
|
extractValue = { property.converter.readOrNull(this)?.toDouble() ?: Double.NaN },
|
||||||
|
maxAge = maxAge,
|
||||||
|
maxPoints = maxPoints,
|
||||||
|
minPoints = minPoints,
|
||||||
|
sampling = sampling,
|
||||||
|
lineStyle = lineStyle
|
||||||
|
)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
public fun XYGraphScope<Instant, Double>.PlotNumberState(
|
||||||
|
context: Context,
|
||||||
|
state: DeviceState<out Number>,
|
||||||
|
maxAge: Duration = defaultMaxAge,
|
||||||
|
maxPoints: Int = defaultMaxPoints,
|
||||||
|
minPoints: Int = defaultMinPoints,
|
||||||
|
sampling: Duration = defaultSampling,
|
||||||
|
lineStyle: LineStyle = defaultLineStyle,
|
||||||
|
): Unit {
|
||||||
|
var points by remember { mutableStateOf<List<ValueWithTime<Double>>>(emptyList()) }
|
||||||
|
|
||||||
|
|
||||||
|
LaunchedEffect(context, state, maxAge, maxPoints, minPoints, sampling) {
|
||||||
|
val clock = context.clock
|
||||||
|
|
||||||
|
state.valueFlow.sample(sampling)
|
||||||
|
.map { ValueWithTime(it.toDouble(), clock.now()) }
|
||||||
|
.collectAndTrim(maxAge, maxPoints, minPoints, clock)
|
||||||
|
.onEach { points = it }
|
||||||
|
.launchIn(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
PlotTimeSeries(points, lineStyle)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun List<Instant>.averageTime(): Instant {
|
||||||
|
val min = min()
|
||||||
|
val max = max()
|
||||||
|
val duration = max - min
|
||||||
|
return min + duration / 2
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun <T> Flow<T>.chunkedByPeriod(duration: Duration): Flow<List<T>> {
|
||||||
|
val collector: ArrayDeque<T> = ArrayDeque<T>()
|
||||||
|
return channelFlow {
|
||||||
|
launch {
|
||||||
|
while (isActive) {
|
||||||
|
delay(duration)
|
||||||
|
send(ArrayList(collector))
|
||||||
|
collector.clear()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this@chunkedByPeriod.collect {
|
||||||
|
collector.add(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Average property value by [averagingInterval]. Return [startValue] on each sample interval if no events arrived.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
public fun XYGraphScope<Instant, Double>.PlotAveragedDeviceProperty(
|
||||||
|
device: Device,
|
||||||
|
propertyName: String,
|
||||||
|
startValue: Double = 0.0,
|
||||||
|
extractValue: Meta.() -> Double = { value?.double ?: startValue },
|
||||||
|
maxAge: Duration = defaultMaxAge,
|
||||||
|
maxPoints: Int = defaultMaxPoints,
|
||||||
|
minPoints: Int = defaultMinPoints,
|
||||||
|
averagingInterval: Duration = defaultSampling,
|
||||||
|
lineStyle: LineStyle = defaultLineStyle,
|
||||||
|
) {
|
||||||
|
|
||||||
|
var points by remember { mutableStateOf<List<ValueWithTime<Double>>>(emptyList()) }
|
||||||
|
|
||||||
|
LaunchedEffect(device, propertyName, startValue, maxAge, maxPoints, minPoints, averagingInterval) {
|
||||||
|
val clock = device.clock
|
||||||
|
var lastValue = startValue
|
||||||
|
device.propertyMessageFlow(propertyName)
|
||||||
|
.chunkedByPeriod(averagingInterval)
|
||||||
|
.transform<List<PropertyChangedMessage>, ValueWithTime<Double>> { eventList ->
|
||||||
|
if (eventList.isEmpty()) {
|
||||||
|
ValueWithTime(lastValue, clock.now())
|
||||||
|
} else {
|
||||||
|
val time = eventList.map { it.time }.averageTime()
|
||||||
|
val value = eventList.map { extractValue(it.value) }.average()
|
||||||
|
ValueWithTime(value, time).also {
|
||||||
|
lastValue = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.collectAndTrim(maxAge, maxPoints, minPoints, clock)
|
||||||
|
.onEach { points = it }
|
||||||
|
.launchIn(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
PlotTimeSeries(points, lineStyle)
|
||||||
|
}
|
@ -0,0 +1,51 @@
|
|||||||
|
package space.kscience.controls.compose
|
||||||
|
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.material3.SliderColors
|
||||||
|
import androidx.compose.material3.SliderDefaults
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import space.kscience.controls.constructor.DeviceState
|
||||||
|
import space.kscience.controls.constructor.MutableDeviceState
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
public fun Slider(
|
||||||
|
deviceState: MutableDeviceState<Number>,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
enabled: Boolean = true,
|
||||||
|
valueRange: ClosedFloatingPointRange<Float> = 0f..1f,
|
||||||
|
steps: Int = 0,
|
||||||
|
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||||
|
colors: SliderColors = SliderDefaults.colors(),
|
||||||
|
) {
|
||||||
|
androidx.compose.material3.Slider(
|
||||||
|
value = deviceState.value.toFloat(),
|
||||||
|
onValueChange = { deviceState.value = it },
|
||||||
|
modifier = modifier,
|
||||||
|
enabled = enabled,
|
||||||
|
valueRange = valueRange,
|
||||||
|
steps = steps,
|
||||||
|
interactionSource = interactionSource,
|
||||||
|
colors = colors,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
public fun SliderIndicator(
|
||||||
|
deviceState: DeviceState<Number>,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
valueRange: ClosedFloatingPointRange<Float> = 0f..1f,
|
||||||
|
steps: Int = 0,
|
||||||
|
colors: SliderColors = SliderDefaults.colors(),
|
||||||
|
) {
|
||||||
|
androidx.compose.material3.Slider(
|
||||||
|
value = deviceState.value.toFloat(),
|
||||||
|
onValueChange = { /*do nothing*/ },
|
||||||
|
modifier = modifier,
|
||||||
|
enabled = false,
|
||||||
|
valueRange = valueRange,
|
||||||
|
steps = steps,
|
||||||
|
colors = colors,
|
||||||
|
)
|
||||||
|
}
|
@ -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
|
||||||
|
|
||||||
@ -8,14 +7,13 @@ plugins {
|
|||||||
}
|
}
|
||||||
|
|
||||||
kscience {
|
kscience {
|
||||||
jvm {
|
jvm()
|
||||||
withJava()
|
|
||||||
}
|
|
||||||
useKtor()
|
useKtor()
|
||||||
useSerialization()
|
useSerialization()
|
||||||
useContextReceivers()
|
useContextReceivers()
|
||||||
commonMain {
|
commonMain {
|
||||||
implementation(projects.controlsVision)
|
implementation(projects.controlsVisualisationCompose)
|
||||||
|
// implementation(projects.controlsVision)
|
||||||
implementation(projects.controlsConstructor)
|
implementation(projects.controlsConstructor)
|
||||||
// implementation("io.github.koalaplot:koalaplot-core:0.6.0")
|
// implementation("io.github.koalaplot:koalaplot-core:0.6.0")
|
||||||
}
|
}
|
||||||
@ -30,8 +28,6 @@ kotlin {
|
|||||||
jvmMain {
|
jvmMain {
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(compose.desktop.currentOs)
|
implementation(compose.desktop.currentOs)
|
||||||
@OptIn(ExperimentalComposeLibrary::class)
|
|
||||||
implementation(compose.desktop.components.splitPane)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,8 @@ import androidx.compose.foundation.layout.Box
|
|||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.material.MaterialTheme
|
import androidx.compose.material.MaterialTheme
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.geometry.Offset
|
import androidx.compose.ui.geometry.Offset
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
@ -13,10 +14,9 @@ import androidx.compose.ui.unit.dp
|
|||||||
import androidx.compose.ui.window.Window
|
import androidx.compose.ui.window.Window
|
||||||
import androidx.compose.ui.window.application
|
import androidx.compose.ui.window.application
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
import space.kscience.controls.compose.asComposeState
|
||||||
import space.kscience.controls.constructor.*
|
import space.kscience.controls.constructor.*
|
||||||
import space.kscience.dataforge.context.Context
|
import space.kscience.dataforge.context.Context
|
||||||
import kotlin.coroutines.CoroutineContext
|
|
||||||
import kotlin.coroutines.EmptyCoroutineContext
|
|
||||||
import kotlin.math.pow
|
import kotlin.math.pow
|
||||||
import kotlin.math.sqrt
|
import kotlin.math.sqrt
|
||||||
import kotlin.time.Duration.Companion.milliseconds
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
@ -29,16 +29,11 @@ data class XY(val x: Double, val y: Double) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val XY.length: Double get() = sqrt(x.pow(2) + y.pow(2))
|
||||||
|
|
||||||
operator fun XY.plus(other: XY): XY = XY(x + other.x, y + other.y)
|
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.times(c: Double): XY = XY(x * c, y * c)
|
||||||
operator fun XY.div(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(
|
class Spring(
|
||||||
context: Context,
|
context: Context,
|
||||||
@ -46,27 +41,25 @@ class Spring(
|
|||||||
val l0: Double,
|
val l0: Double,
|
||||||
val begin: DeviceState<XY>,
|
val begin: DeviceState<XY>,
|
||||||
val end: DeviceState<XY>,
|
val end: DeviceState<XY>,
|
||||||
) : DeviceConstructor(context) {
|
) : ConstructorModel(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
|
* vector from start to end
|
||||||
*/
|
*/
|
||||||
val direction = combineState(begin, end) { begin, end ->
|
val direction = combineState(begin, end) { begin: XY, end: XY ->
|
||||||
val dx = end.x - begin.x
|
val dx = end.x - begin.x
|
||||||
val dy = end.y - begin.y
|
val dy = end.y - begin.y
|
||||||
val l = sqrt((end.y - begin.y).pow(2) + (end.x - begin.x).pow(2))
|
val l = sqrt(dx.pow(2) + dy.pow(2))
|
||||||
XY(dx / l, dy / l)
|
XY(dx / l, dy / l)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val tension: DeviceState<Double> = combineState(begin, end) { begin: XY, end: XY ->
|
||||||
|
val dx = end.x - begin.x
|
||||||
|
val dy = end.y - begin.y
|
||||||
|
k * sqrt(dx.pow(2) + dy.pow(2))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
val beginForce = combineState(direction, tension) { direction: XY, tension: Double ->
|
val beginForce = combineState(direction, tension) { direction: XY, tension: Double ->
|
||||||
direction * (tension)
|
direction * (tension)
|
||||||
}
|
}
|
||||||
@ -83,13 +76,15 @@ class MaterialPoint(
|
|||||||
val force: DeviceState<XY>,
|
val force: DeviceState<XY>,
|
||||||
val position: MutableDeviceState<XY>,
|
val position: MutableDeviceState<XY>,
|
||||||
val velocity: MutableDeviceState<XY> = MutableDeviceState(XY.ZERO),
|
val velocity: MutableDeviceState<XY> = MutableDeviceState(XY.ZERO),
|
||||||
) : DeviceModel(context, force) {
|
) : ConstructorModel(context, force, position, velocity) {
|
||||||
|
|
||||||
private val timer: TimerState = timer(2.milliseconds)
|
private val timer: TimerState = timer(2.milliseconds)
|
||||||
|
|
||||||
|
//TODO synchronize force change
|
||||||
|
|
||||||
private val movement = timer.onChange(
|
private val movement = timer.onChange(
|
||||||
position, velocity,
|
writes = setOf(position, velocity),
|
||||||
alsoReads = setOf(force, velocity, position)
|
reads = setOf(force, velocity, position)
|
||||||
) { prev, next ->
|
) { prev, next ->
|
||||||
val dt = (next - prev).toDouble(DurationUnit.SECONDS)
|
val dt = (next - prev).toDouble(DurationUnit.SECONDS)
|
||||||
val a = force.value / mass
|
val a = force.value / mass
|
||||||
@ -105,31 +100,31 @@ class BodyOnSprings(
|
|||||||
k: Double,
|
k: Double,
|
||||||
startPosition: XY,
|
startPosition: XY,
|
||||||
l0: Double = 1.0,
|
l0: Double = 1.0,
|
||||||
val xLeft: Double = 0.0,
|
val xLeft: Double = -1.0,
|
||||||
val xRight: Double = 2.0,
|
val xRight: Double = 1.0,
|
||||||
val yBottom: Double = 0.0,
|
val yBottom: Double = -1.0,
|
||||||
val yTop: Double = 2.0,
|
val yTop: Double = 1.0,
|
||||||
) : DeviceConstructor(context) {
|
) : DeviceConstructor(context) {
|
||||||
|
|
||||||
val width = xRight - xLeft
|
val width = xRight - xLeft
|
||||||
val height = yTop - yBottom
|
val height = yTop - yBottom
|
||||||
|
|
||||||
val position = mutableState(startPosition)
|
val position = stateOf(startPosition)
|
||||||
|
|
||||||
private val leftAnchor = mutableState(XY(xLeft, yTop + yBottom / 2))
|
private val leftAnchor = stateOf(XY(xLeft, (yTop + yBottom) / 2))
|
||||||
|
|
||||||
val leftSpring by device(
|
val leftSpring = model(
|
||||||
Spring(context, k, l0, leftAnchor, position)
|
Spring(context, k, l0, leftAnchor, position)
|
||||||
)
|
)
|
||||||
|
|
||||||
private val rightAnchor = mutableState(XY(xRight, yTop + yBottom / 2))
|
private val rightAnchor = stateOf(XY(xRight, (yTop + yBottom) / 2))
|
||||||
|
|
||||||
val rightSpring by device(
|
val rightSpring = model(
|
||||||
Spring(context, k, l0, rightAnchor, position)
|
Spring(context, k, l0, rightAnchor, position)
|
||||||
)
|
)
|
||||||
|
|
||||||
val force: DeviceState<XY> = combineState(leftSpring.endForce, rightSpring.endForce) { left, rignt ->
|
val force: DeviceState<XY> = combineState(leftSpring.endForce, rightSpring.endForce) { left, right ->
|
||||||
left + rignt
|
left + right
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -138,18 +133,13 @@ class BodyOnSprings(
|
|||||||
context = context,
|
context = context,
|
||||||
mass = mass,
|
mass = mass,
|
||||||
force = force,
|
force = force,
|
||||||
position = position
|
position = position,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun <T> DeviceState<T>.collect(
|
|
||||||
coroutineContext: CoroutineContext = EmptyCoroutineContext,
|
|
||||||
): State<T> = valueFlow.collectAsState(value, coroutineContext)
|
|
||||||
|
|
||||||
fun main() = application {
|
fun main() = application {
|
||||||
val initialState = XY(1.1, 1.1)
|
val initialState = XY(0.1, 0.2)
|
||||||
|
|
||||||
Window(title = "Ball on springs", onCloseRequest = ::exitApplication) {
|
Window(title = "Ball on springs", onCloseRequest = ::exitApplication) {
|
||||||
MaterialTheme {
|
MaterialTheme {
|
||||||
@ -161,12 +151,20 @@ fun main() = application {
|
|||||||
BodyOnSprings(context, 100.0, 1000.0, initialState)
|
BodyOnSprings(context, 100.0, 1000.0, initialState)
|
||||||
}
|
}
|
||||||
|
|
||||||
val position: XY by model.body.position.collect()
|
//TODO add ability to freeze model
|
||||||
|
|
||||||
|
// LaunchedEffect(Unit){
|
||||||
|
// model.position.valueFlow.onEach {
|
||||||
|
// model.position.value = it.copy(y = model.position.value.y.coerceIn(-1.0..1.0))
|
||||||
|
// }.collect()
|
||||||
|
// }
|
||||||
|
|
||||||
|
val position: XY by model.body.position.asComposeState()
|
||||||
Box(Modifier.size(400.dp)) {
|
Box(Modifier.size(400.dp)) {
|
||||||
Canvas(modifier = Modifier.fillMaxSize()) {
|
Canvas(modifier = Modifier.fillMaxSize()) {
|
||||||
fun XY.toOffset() = Offset(
|
fun XY.toOffset() = Offset(
|
||||||
(x / model.width * size.width).toFloat(),
|
center.x + (x / model.width * size.width).toFloat(),
|
||||||
(y / model.height * size.height).toFloat()
|
center.y - (y / model.height * size.height).toFloat()
|
||||||
)
|
)
|
||||||
|
|
||||||
drawCircle(
|
drawCircle(
|
||||||
|
@ -1,38 +1,40 @@
|
|||||||
package space.kscience.controls.demo.constructor
|
package space.kscience.controls.demo.constructor
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.layout.width
|
|
||||||
import androidx.compose.material.*
|
import androidx.compose.material.*
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.SolidColor
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.window.Window
|
import androidx.compose.ui.window.Window
|
||||||
import androidx.compose.ui.window.application
|
import androidx.compose.ui.window.application
|
||||||
import space.kscience.controls.constructor.DeviceConstructor
|
import io.github.koalaplot.core.ChartLayout
|
||||||
import space.kscience.controls.constructor.DoubleInRangeState
|
import io.github.koalaplot.core.legend.FlowLegend
|
||||||
import space.kscience.controls.constructor.device
|
import io.github.koalaplot.core.style.LineStyle
|
||||||
import space.kscience.controls.constructor.deviceProperty
|
import io.github.koalaplot.core.util.ExperimentalKoalaPlotApi
|
||||||
|
import io.github.koalaplot.core.util.toString
|
||||||
|
import io.github.koalaplot.core.xygraph.XYGraph
|
||||||
|
import io.github.koalaplot.core.xygraph.rememberDoubleLinearAxisModel
|
||||||
|
import kotlinx.coroutines.flow.collect
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
import kotlinx.datetime.Instant
|
||||||
|
import org.jetbrains.compose.splitpane.ExperimentalSplitPaneApi
|
||||||
|
import org.jetbrains.compose.splitpane.HorizontalSplitPane
|
||||||
|
import space.kscience.controls.compose.PlotDeviceProperty
|
||||||
|
import space.kscience.controls.compose.PlotNumberState
|
||||||
|
import space.kscience.controls.compose.TimeAxisModel
|
||||||
|
import space.kscience.controls.constructor.*
|
||||||
import space.kscience.controls.constructor.library.*
|
import space.kscience.controls.constructor.library.*
|
||||||
import space.kscience.controls.manager.ClockManager
|
import space.kscience.controls.manager.ClockManager
|
||||||
import space.kscience.controls.manager.DeviceManager
|
import space.kscience.controls.manager.DeviceManager
|
||||||
import space.kscience.controls.manager.clock
|
import space.kscience.controls.manager.clock
|
||||||
import space.kscience.controls.manager.install
|
import space.kscience.controls.manager.install
|
||||||
import space.kscience.controls.spec.doRecurring
|
|
||||||
import space.kscience.controls.spec.name
|
|
||||||
import space.kscience.controls.vision.plot
|
|
||||||
import space.kscience.controls.vision.plotDeviceProperty
|
|
||||||
import space.kscience.controls.vision.plotNumberState
|
|
||||||
import space.kscience.controls.vision.showDashboard
|
|
||||||
import space.kscience.dataforge.context.Context
|
import space.kscience.dataforge.context.Context
|
||||||
import space.kscience.dataforge.meta.Meta
|
import space.kscience.dataforge.meta.Meta
|
||||||
import space.kscience.plotly.models.ScatterMode
|
import java.awt.Dimension
|
||||||
import space.kscience.visionforge.plotly.PlotlyPlugin
|
|
||||||
import kotlin.math.PI
|
import kotlin.math.PI
|
||||||
import kotlin.math.sin
|
import kotlin.math.sin
|
||||||
import kotlin.time.Duration
|
import kotlin.time.Duration
|
||||||
@ -49,15 +51,15 @@ class LinearDrive(
|
|||||||
meta: Meta = Meta.EMPTY,
|
meta: Meta = Meta.EMPTY,
|
||||||
) : DeviceConstructor(drive.context, meta) {
|
) : DeviceConstructor(drive.context, meta) {
|
||||||
|
|
||||||
val drive: Drive by device(drive)
|
val drive by device(drive)
|
||||||
val pid by device(PidRegulator(drive, pidParameters))
|
val pid by device(PidRegulator(drive, pidParameters))
|
||||||
|
|
||||||
val start by device(start)
|
val start by device(start)
|
||||||
val end by device(end)
|
val end by device(end)
|
||||||
|
|
||||||
val position by deviceProperty(drive, Drive.position, Double.NaN)
|
val position = drive.propertyAsState(Drive.position, Double.NaN)
|
||||||
|
|
||||||
val target by deviceProperty(pid, Regulator.target, 0.0)
|
val target = pid.propertyAsState(Regulator.target, 0.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -77,163 +79,205 @@ fun LinearDrive(
|
|||||||
meta = meta
|
meta = meta
|
||||||
)
|
)
|
||||||
|
|
||||||
|
class Modulator(
|
||||||
|
context: Context,
|
||||||
|
target: MutableDeviceState<Double>,
|
||||||
|
var freq: Double = 0.1,
|
||||||
|
var timeStep: Duration = 5.milliseconds,
|
||||||
|
) : DeviceConstructor(context) {
|
||||||
|
private val clockStart = clock.now()
|
||||||
|
|
||||||
|
val timer = timer(10.milliseconds)
|
||||||
|
|
||||||
|
private val modulation = timer.onNext {
|
||||||
|
val timeFromStart = clock.now() - clockStart
|
||||||
|
val t = timeFromStart.toDouble(DurationUnit.SECONDS)
|
||||||
|
target.value = 5 * sin(2.0 * PI * freq * t) +
|
||||||
|
sin(2 * PI * 21 * freq * t + 0.02 * (timeFromStart / timeStep))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private val maxAge = 10.seconds
|
||||||
|
|
||||||
|
@OptIn(ExperimentalSplitPaneApi::class, ExperimentalKoalaPlotApi::class)
|
||||||
fun main() = application {
|
fun main() = application {
|
||||||
val context = Context {
|
val context = remember {
|
||||||
plugin(DeviceManager)
|
Context {
|
||||||
plugin(PlotlyPlugin)
|
plugin(DeviceManager)
|
||||||
plugin(ClockManager)
|
plugin(ClockManager)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class MutablePidParameters(
|
val clock = remember { context.clock }
|
||||||
kp: Double,
|
|
||||||
ki: Double,
|
|
||||||
kd: Double,
|
var pidParameters by remember {
|
||||||
timeStep: Duration,
|
mutableStateOf(PidParameters(kp = 2.5, ki = 0.0, kd = -0.1, timeStep = 0.005.seconds))
|
||||||
) : PidParameters {
|
|
||||||
override var kp by mutableStateOf(kp)
|
|
||||||
override var ki by mutableStateOf(ki)
|
|
||||||
override var kd by mutableStateOf(kd)
|
|
||||||
override var timeStep by mutableStateOf(timeStep)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val pidParameters = remember {
|
val state = remember { DoubleInRangeState(0.0, -6.0..6.0) }
|
||||||
MutablePidParameters(
|
|
||||||
kp = 2.5,
|
val linearDrive = remember {
|
||||||
ki = 0.0,
|
context.install(
|
||||||
kd = -0.1,
|
"linearDrive",
|
||||||
timeStep = 0.005.seconds
|
LinearDrive(context, state, 0.05, pidParameters)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
val state = DoubleInRangeState(0.0, -6.0..6.0)
|
val modulator = remember {
|
||||||
|
context.install(
|
||||||
val linearDrive = context.install(
|
"modulator",
|
||||||
"linearDrive",
|
Modulator(context, linearDrive.target)
|
||||||
LinearDrive(context, state, 0.05, pidParameters)
|
)
|
||||||
)
|
|
||||||
|
|
||||||
val clockStart = context.clock.now()
|
|
||||||
linearDrive.doRecurring(10.milliseconds) {
|
|
||||||
val timeFromStart = clock.now() - clockStart
|
|
||||||
val t = timeFromStart.toDouble(DurationUnit.SECONDS)
|
|
||||||
val freq = 0.1
|
|
||||||
target.value = 5 * sin(2.0 * PI * freq * t) +
|
|
||||||
sin(2 * PI * 21 * freq * t + 0.02 * (timeFromStart / pidParameters.timeStep))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//bind pid parameters
|
||||||
val maxAge = 10.seconds
|
LaunchedEffect(Unit) {
|
||||||
|
snapshotFlow {
|
||||||
context.showDashboard {
|
pidParameters
|
||||||
plot {
|
}.onEach {
|
||||||
plotNumberState(context, state, maxAge = maxAge, sampling = 50.milliseconds) {
|
linearDrive.pid.pidParameters = pidParameters
|
||||||
name = "real position"
|
}.collect()
|
||||||
}
|
|
||||||
plotDeviceProperty(linearDrive.pid, Regulator.position.name, maxAge = maxAge, sampling = 50.milliseconds) {
|
|
||||||
name = "read position"
|
|
||||||
}
|
|
||||||
|
|
||||||
plotDeviceProperty(linearDrive.pid, Regulator.target.name, maxAge = maxAge, sampling = 50.milliseconds) {
|
|
||||||
name = "target"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
plot {
|
|
||||||
plotDeviceProperty(
|
|
||||||
linearDrive.start,
|
|
||||||
LimitSwitch.locked.name,
|
|
||||||
maxAge = maxAge,
|
|
||||||
sampling = 50.milliseconds
|
|
||||||
) {
|
|
||||||
name = "start measured"
|
|
||||||
mode = ScatterMode.markers
|
|
||||||
}
|
|
||||||
plotDeviceProperty(linearDrive.end, LimitSwitch.locked.name, maxAge = maxAge, sampling = 50.milliseconds) {
|
|
||||||
name = "end measured"
|
|
||||||
mode = ScatterMode.markers
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Window(title = "Pid regulator simulator", onCloseRequest = ::exitApplication) {
|
Window(title = "Pid regulator simulator", onCloseRequest = ::exitApplication) {
|
||||||
|
window.minimumSize = Dimension(800, 400)
|
||||||
MaterialTheme {
|
MaterialTheme {
|
||||||
Column {
|
HorizontalSplitPane {
|
||||||
Row {
|
first(400.dp) {
|
||||||
Text("kp:", Modifier.align(Alignment.CenterVertically).width(50.dp).padding(5.dp))
|
Column(modifier = Modifier.background(color = Color.LightGray).fillMaxHeight()) {
|
||||||
TextField(
|
Row {
|
||||||
String.format("%.2f", pidParameters.kp),
|
Text("kp:", Modifier.align(Alignment.CenterVertically).width(50.dp).padding(5.dp))
|
||||||
{ pidParameters.kp = it.toDouble() },
|
TextField(
|
||||||
Modifier.width(100.dp),
|
String.format("%.2f", pidParameters.kp),
|
||||||
enabled = false
|
{ pidParameters = pidParameters.copy(kp = it.toDouble()) },
|
||||||
)
|
Modifier.width(100.dp),
|
||||||
Slider(
|
enabled = false
|
||||||
pidParameters.kp.toFloat(),
|
)
|
||||||
{ pidParameters.kp = it.toDouble() },
|
Slider(
|
||||||
valueRange = 0f..20f,
|
pidParameters.kp.toFloat(),
|
||||||
steps = 100
|
{ pidParameters = pidParameters.copy(kp = it.toDouble()) },
|
||||||
)
|
valueRange = 0f..20f,
|
||||||
}
|
steps = 100
|
||||||
Row {
|
)
|
||||||
Text("ki:", Modifier.align(Alignment.CenterVertically).width(50.dp).padding(5.dp))
|
}
|
||||||
TextField(
|
Row {
|
||||||
String.format("%.2f", pidParameters.ki),
|
Text("ki:", Modifier.align(Alignment.CenterVertically).width(50.dp).padding(5.dp))
|
||||||
{ pidParameters.ki = it.toDouble() },
|
TextField(
|
||||||
Modifier.width(100.dp),
|
String.format("%.2f", pidParameters.ki),
|
||||||
enabled = false
|
{ pidParameters = pidParameters.copy(ki = it.toDouble()) },
|
||||||
)
|
Modifier.width(100.dp),
|
||||||
|
enabled = false
|
||||||
Slider(
|
)
|
||||||
pidParameters.ki.toFloat(),
|
|
||||||
{ pidParameters.ki = it.toDouble() },
|
Slider(
|
||||||
valueRange = -10f..10f,
|
pidParameters.ki.toFloat(),
|
||||||
steps = 100
|
{ pidParameters = pidParameters.copy(ki = it.toDouble()) },
|
||||||
)
|
valueRange = -10f..10f,
|
||||||
}
|
steps = 100
|
||||||
Row {
|
)
|
||||||
Text("kd:", Modifier.align(Alignment.CenterVertically).width(50.dp).padding(5.dp))
|
}
|
||||||
TextField(
|
Row {
|
||||||
String.format("%.2f", pidParameters.kd),
|
Text("kd:", Modifier.align(Alignment.CenterVertically).width(50.dp).padding(5.dp))
|
||||||
{ pidParameters.kd = it.toDouble() },
|
TextField(
|
||||||
Modifier.width(100.dp),
|
String.format("%.2f", pidParameters.kd),
|
||||||
enabled = false
|
{ pidParameters = pidParameters.copy(kd = it.toDouble()) },
|
||||||
)
|
Modifier.width(100.dp),
|
||||||
|
enabled = false
|
||||||
Slider(
|
)
|
||||||
pidParameters.kd.toFloat(),
|
|
||||||
{ pidParameters.kd = it.toDouble() },
|
Slider(
|
||||||
valueRange = -10f..10f,
|
pidParameters.kd.toFloat(),
|
||||||
steps = 100
|
{ pidParameters = pidParameters.copy(kd = it.toDouble()) },
|
||||||
)
|
valueRange = -10f..10f,
|
||||||
}
|
steps = 100
|
||||||
|
)
|
||||||
Row {
|
}
|
||||||
Text("dt:", Modifier.align(Alignment.CenterVertically).width(50.dp).padding(5.dp))
|
|
||||||
TextField(
|
Row {
|
||||||
pidParameters.timeStep.toString(DurationUnit.MILLISECONDS),
|
Text("dt:", Modifier.align(Alignment.CenterVertically).width(50.dp).padding(5.dp))
|
||||||
{ pidParameters.timeStep = it.toDouble().milliseconds },
|
TextField(
|
||||||
Modifier.width(100.dp),
|
pidParameters.timeStep.toString(DurationUnit.MILLISECONDS),
|
||||||
enabled = false
|
{ pidParameters = pidParameters.copy(timeStep = it.toDouble().milliseconds) },
|
||||||
)
|
Modifier.width(100.dp),
|
||||||
|
enabled = false
|
||||||
Slider(
|
)
|
||||||
pidParameters.timeStep.toDouble(DurationUnit.MILLISECONDS).toFloat(),
|
|
||||||
{ pidParameters.timeStep = it.toDouble().milliseconds },
|
Slider(
|
||||||
valueRange = 0f..100f,
|
pidParameters.timeStep.toDouble(DurationUnit.MILLISECONDS).toFloat(),
|
||||||
steps = 100
|
{ pidParameters = pidParameters.copy(timeStep = it.toDouble().milliseconds) },
|
||||||
)
|
valueRange = 0f..100f,
|
||||||
}
|
steps = 100
|
||||||
Row {
|
)
|
||||||
Button({
|
}
|
||||||
pidParameters.run {
|
Row {
|
||||||
kp = 2.5
|
Button({
|
||||||
ki = 0.0
|
pidParameters = PidParameters(
|
||||||
kd = -0.1
|
kp = 2.5,
|
||||||
timeStep = 0.005.seconds
|
ki = 0.0,
|
||||||
|
kd = -0.1,
|
||||||
|
timeStep = 0.005.seconds
|
||||||
|
)
|
||||||
|
}) {
|
||||||
|
Text("Reset")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
second(400.dp) {
|
||||||
|
ChartLayout {
|
||||||
|
XYGraph<Instant, Double>(
|
||||||
|
xAxisModel = remember { TimeAxisModel.recent(maxAge, clock) },
|
||||||
|
yAxisModel = rememberDoubleLinearAxisModel(state.range),
|
||||||
|
xAxisTitle = { Text("Time in seconds relative to current") },
|
||||||
|
xAxisLabels = { it: Instant ->
|
||||||
|
androidx.compose.material3.Text(
|
||||||
|
(clock.now() - it).toDouble(
|
||||||
|
DurationUnit.SECONDS
|
||||||
|
).toString(2)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
yAxisLabels = { it: Double -> Text(it.toString(2)) }
|
||||||
|
) {
|
||||||
|
PlotNumberState(
|
||||||
|
context = context,
|
||||||
|
state = state,
|
||||||
|
maxAge = maxAge,
|
||||||
|
sampling = 50.milliseconds,
|
||||||
|
lineStyle = LineStyle(SolidColor(Color.Blue))
|
||||||
|
)
|
||||||
|
PlotDeviceProperty(
|
||||||
|
linearDrive.pid,
|
||||||
|
Regulator.position,
|
||||||
|
maxAge = maxAge,
|
||||||
|
sampling = 50.milliseconds,
|
||||||
|
)
|
||||||
|
PlotDeviceProperty(
|
||||||
|
linearDrive.pid,
|
||||||
|
Regulator.target,
|
||||||
|
maxAge = maxAge,
|
||||||
|
sampling = 50.milliseconds,
|
||||||
|
lineStyle = LineStyle(SolidColor(Color.Red))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Surface {
|
||||||
|
FlowLegend(3, label = {
|
||||||
|
when (it) {
|
||||||
|
0 -> {
|
||||||
|
Text("Body position", color = Color.Blue)
|
||||||
|
}
|
||||||
|
|
||||||
|
1 -> {
|
||||||
|
Text("Regulator position", color = Color.Black)
|
||||||
|
}
|
||||||
|
|
||||||
|
2 -> {
|
||||||
|
Text("Regulator target", color = Color.Red)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}) {
|
|
||||||
Text("Reset")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
2
demo/constructor/src/jvmMain/kotlin/Plotter.kt
Normal file
2
demo/constructor/src/jvmMain/kotlin/Plotter.kt
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
package space.kscience.controls.demo.constructor
|
||||||
|
|
@ -64,6 +64,7 @@ include(
|
|||||||
":controls-storage",
|
":controls-storage",
|
||||||
":controls-storage:controls-xodus",
|
":controls-storage:controls-xodus",
|
||||||
":controls-constructor",
|
":controls-constructor",
|
||||||
|
":controls-visualisation-compose",
|
||||||
":controls-vision",
|
":controls-vision",
|
||||||
":controls-jupyter",
|
":controls-jupyter",
|
||||||
":magix",
|
":magix",
|
||||||
|
Loading…
Reference in New Issue
Block a user