Compare commits
No commits in common. "9edde7bdbd47688f208cb9f2a8fbe73519a8261a" and "05757aefdc1afbe674c67be6f0602ebf0ee37125" have entirely different histories.
9edde7bdbd
...
05757aefdc
@ -13,7 +13,6 @@
|
|||||||
- Refactored ports. Now we have `AsynchronousPort` as well as `SynchronousPort`
|
- Refactored ports. Now we have `AsynchronousPort` as well as `SynchronousPort`
|
||||||
- `DeviceClient` now initializes property and action descriptors eagerly.
|
- `DeviceClient` now initializes property and action descriptors eagerly.
|
||||||
- `DeviceHub` now works with `Name` instead of `NameToken`. Tree-like structure is made using `Path`. Device messages no longer have access to sub-devices.
|
- `DeviceHub` now works with `Name` instead of `NameToken`. Tree-like structure is made using `Path`. Device messages no longer have access to sub-devices.
|
||||||
- Add some utility methods to ports. Synchronous port response could be now consumed as `Source`.
|
|
||||||
|
|
||||||
### Deprecated
|
### Deprecated
|
||||||
|
|
||||||
|
@ -8,9 +8,6 @@ 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,7 +11,6 @@ kscience{
|
|||||||
jvm()
|
jvm()
|
||||||
js()
|
js()
|
||||||
useCoroutines()
|
useCoroutines()
|
||||||
useSerialization()
|
|
||||||
commonMain {
|
commonMain {
|
||||||
api(projects.controlsCore)
|
api(projects.controlsCore)
|
||||||
}
|
}
|
||||||
|
@ -1,33 +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 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,6 +3,7 @@ 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
|
||||||
@ -21,15 +22,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 _constructorElements: MutableSet<ConstructorElement> = mutableSetOf()
|
private val _stateDescriptors: MutableSet<StateDescriptor> = mutableSetOf()
|
||||||
override val constructorElements: Set<ConstructorElement> get() = _constructorElements
|
override val stateDescriptors: Set<StateDescriptor> get() = _stateDescriptors
|
||||||
|
|
||||||
override fun registerElement(constructorElement: ConstructorElement) {
|
override fun registerState(stateDescriptor: StateDescriptor) {
|
||||||
_constructorElements.add(constructorElement)
|
_stateDescriptors.add(stateDescriptor)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun unregisterElement(constructorElement: ConstructorElement) {
|
override fun unregisterState(stateDescriptor: StateDescriptor) {
|
||||||
_constructorElements.remove(constructorElement)
|
_stateDescriptors.remove(stateDescriptor)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun <T> registerProperty(
|
override fun <T> registerProperty(
|
||||||
@ -38,7 +39,7 @@ public abstract class DeviceConstructor(
|
|||||||
state: DeviceState<T>,
|
state: DeviceState<T>,
|
||||||
) {
|
) {
|
||||||
super.registerProperty(converter, descriptor, state)
|
super.registerProperty(converter, descriptor, state)
|
||||||
registerElement(PropertyConstructorElement(this, descriptor.name, state))
|
registerState(StatePropertyDescriptor(this, descriptor.name, state))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -107,7 +108,7 @@ public fun <T : Any> DeviceConstructor.property(
|
|||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create and register a mutable external state as a property
|
* 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>,
|
||||||
@ -140,7 +141,22 @@ public fun <T> DeviceConstructor.virtualProperty(
|
|||||||
nameOverride,
|
nameOverride,
|
||||||
)
|
)
|
||||||
|
|
||||||
public fun <T, S : DeviceState<T>> DeviceConstructor.property(
|
/**
|
||||||
spec: DevicePropertySpec<*, T>,
|
* Bind existing property provided by specification to this device
|
||||||
state: S,
|
*/
|
||||||
): Unit = registerProperty(spec.converter, spec.descriptor, state)
|
public fun <T, D : Device> DeviceConstructor.deviceProperty(
|
||||||
|
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))
|
||||||
|
@ -0,0 +1,32 @@
|
|||||||
|
package space.kscience.controls.constructor
|
||||||
|
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.SupervisorJob
|
||||||
|
import kotlinx.coroutines.newCoroutineContext
|
||||||
|
import space.kscience.dataforge.context.Context
|
||||||
|
import kotlin.coroutines.CoroutineContext
|
||||||
|
|
||||||
|
public abstract class DeviceModel(
|
||||||
|
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,12 +48,6 @@ 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].
|
||||||
*/
|
*/
|
||||||
|
@ -2,7 +2,9 @@ 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.*
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
import kotlinx.coroutines.flow.runningFold
|
||||||
import space.kscience.controls.api.Device
|
import space.kscience.controls.api.Device
|
||||||
import space.kscience.controls.manager.ClockManager
|
import space.kscience.controls.manager.ClockManager
|
||||||
import space.kscience.dataforge.context.ContextAware
|
import space.kscience.dataforge.context.ContextAware
|
||||||
@ -12,38 +14,34 @@ 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 ConstructorElement
|
public sealed interface StateDescriptor
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A binding that exposes device property as read-only state
|
* A binding that exposes device property as read-only state
|
||||||
*/
|
*/
|
||||||
public class PropertyConstructorElement<T>(
|
public class StatePropertyDescriptor<T>(
|
||||||
public val device: Device,
|
public val device: Device,
|
||||||
public val propertyName: String,
|
public val propertyName: String,
|
||||||
public val state: DeviceState<T>,
|
public val state: DeviceState<T>,
|
||||||
) : ConstructorElement
|
) : StateDescriptor
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A binding for independent state like a timer
|
* A binding for independent state like a timer
|
||||||
*/
|
*/
|
||||||
public class StateConstructorElement<T>(
|
public class StateNodeDescriptor<T>(
|
||||||
public val state: DeviceState<T>,
|
public val state: DeviceState<T>,
|
||||||
) : ConstructorElement
|
) : StateDescriptor
|
||||||
|
|
||||||
public class ConnectionConstrucorElement(
|
public class StateConnectionDescriptor(
|
||||||
public val reads: Collection<DeviceState<*>>,
|
public val reads: Collection<DeviceState<*>>,
|
||||||
public val writes: Collection<DeviceState<*>>,
|
public val writes: Collection<DeviceState<*>>,
|
||||||
) : ConstructorElement
|
) : StateDescriptor
|
||||||
|
|
||||||
public class ModelConstructorElement(
|
|
||||||
public val model: ConstructorModel
|
|
||||||
) : ConstructorElement
|
|
||||||
|
|
||||||
|
|
||||||
public interface StateContainer : ContextAware, CoroutineScope {
|
public interface StateContainer : ContextAware, CoroutineScope {
|
||||||
public val constructorElements: Set<ConstructorElement>
|
public val stateDescriptors: Set<StateDescriptor>
|
||||||
public fun registerElement(constructorElement: ConstructorElement)
|
public fun registerState(stateDescriptor: StateDescriptor)
|
||||||
public fun unregisterElement(constructorElement: ConstructorElement)
|
public fun unregisterState(stateDescriptor: StateDescriptor)
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -52,16 +50,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(
|
||||||
writes: Collection<DeviceState<*>> = emptySet(),
|
vararg writes: DeviceState<*>,
|
||||||
reads: Collection<DeviceState<*>> = emptySet(),
|
alsoReads: 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 {
|
||||||
registerElement(ConnectionConstrucorElement(reads + this, writes))
|
registerState(StateConnectionDescriptor(setOf(this, *alsoReads.toTypedArray()), setOf(*writes)))
|
||||||
}
|
}
|
||||||
|
|
||||||
public fun <T> DeviceState<T>.onChange(
|
public fun <T> DeviceState<T>.onChange(
|
||||||
writes: Collection<DeviceState<*>> = emptySet(),
|
vararg writes: DeviceState<*>,
|
||||||
reads: Collection<DeviceState<*>> = emptySet(),
|
alsoReads: 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)
|
||||||
@ -70,7 +68,7 @@ public interface StateContainer : ContextAware, CoroutineScope {
|
|||||||
onChange(pair.first, pair.second)
|
onChange(pair.first, pair.second)
|
||||||
}
|
}
|
||||||
}.launchIn(this@StateContainer).also {
|
}.launchIn(this@StateContainer).also {
|
||||||
registerElement(ConnectionConstrucorElement(reads + this, writes))
|
registerState(StateConnectionDescriptor(setOf(this, *alsoReads.toTypedArray()), setOf(*writes)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -78,19 +76,21 @@ 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 {
|
||||||
registerElement(StateConstructorElement(state))
|
registerState(StateNodeDescriptor(state))
|
||||||
return state
|
return state
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a register a [MutableDeviceState] with a given [converter]
|
* Create a register a [MutableDeviceState] with a given [converter]
|
||||||
*/
|
*/
|
||||||
public fun <T> StateContainer.stateOf(initialValue: T): MutableDeviceState<T> = state(
|
public fun <T> StateContainer.mutableState(initialValue: T): MutableDeviceState<T> = state(
|
||||||
MutableDeviceState(initialValue)
|
MutableDeviceState(initialValue)
|
||||||
)
|
)
|
||||||
|
|
||||||
public fun <T : ConstructorModel> StateContainer.model(model: T): T {
|
public fun <T : DeviceModel> StateContainer.model(model: T): T {
|
||||||
registerElement(ModelConstructorElement(model))
|
model.stateDescriptors.forEach {
|
||||||
|
registerState(it)
|
||||||
|
}
|
||||||
return model
|
return model
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -101,20 +101,9 @@ public fun StateContainer.timer(tick: Duration): TimerState = state(TimerState(c
|
|||||||
|
|
||||||
|
|
||||||
public fun <T, R> StateContainer.mapState(
|
public fun <T, R> StateContainer.mapState(
|
||||||
origin: DeviceState<T>,
|
state: DeviceState<T>,
|
||||||
transformation: (T) -> R,
|
transformation: (T) -> R,
|
||||||
): DeviceStateWithDependencies<R> = state(DeviceState.map(origin, transformation))
|
): DeviceStateWithDependencies<R> = state(DeviceState.map(state, 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
|
||||||
@ -133,13 +122,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 = ConnectionConstrucorElement(setOf(sourceState), setOf(targetState))
|
val descriptor = StateConnectionDescriptor(setOf(sourceState), setOf(targetState))
|
||||||
registerElement(descriptor)
|
registerState(descriptor)
|
||||||
return sourceState.valueFlow.onEach {
|
return sourceState.valueFlow.onEach {
|
||||||
targetState.value = it
|
targetState.value = it
|
||||||
}.launchIn(this).apply {
|
}.launchIn(this).apply {
|
||||||
invokeOnCompletion {
|
invokeOnCompletion {
|
||||||
unregisterElement(descriptor)
|
unregisterState(descriptor)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -155,19 +144,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 = ConnectionConstrucorElement(setOf(sourceState), setOf(targetState))
|
val descriptor = StateConnectionDescriptor(setOf(sourceState), setOf(targetState))
|
||||||
registerElement(descriptor)
|
registerState(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 {
|
||||||
unregisterElement(descriptor)
|
unregisterState(descriptor)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register [ConstructorElement] that combines values from [sourceState1] and [sourceState2] using [transformation].
|
* Register [StateDescriptor] 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
|
||||||
*/
|
*/
|
||||||
@ -177,19 +166,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 = ConnectionConstrucorElement(setOf(sourceState1, sourceState2), setOf(targetState))
|
val descriptor = StateConnectionDescriptor(setOf(sourceState1, sourceState2), setOf(targetState))
|
||||||
registerElement(descriptor)
|
registerState(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 {
|
||||||
unregisterElement(descriptor)
|
unregisterState(descriptor)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register [ConstructorElement] that combines values from [sourceStates] using [transformation].
|
* Register [StateDescriptor] that combines values from [sourceStates] using [transformation].
|
||||||
*
|
*
|
||||||
* On resulting [Job] cancel the binding is unregistered
|
* On resulting [Job] cancel the binding is unregistered
|
||||||
*/
|
*/
|
||||||
@ -198,13 +187,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 = ConnectionConstrucorElement(sourceStates, setOf(targetState))
|
val descriptor = StateConnectionDescriptor(sourceStates, setOf(targetState))
|
||||||
registerElement(descriptor)
|
registerState(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 {
|
||||||
unregisterElement(descriptor)
|
unregisterState(descriptor)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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.propertyAsState(
|
public suspend fun <D : Device, T> D.mutablePropertyAsState(
|
||||||
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.propertyAsState(
|
public fun <D : Device, T> D.mutablePropertyAsState(
|
||||||
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 {
|
||||||
registerElement(StateConstructorElement(it))
|
registerState(StateNodeDescriptor(it))
|
||||||
}
|
}
|
@ -1,20 +0,0 @@
|
|||||||
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.propertyAsState
|
import space.kscience.controls.constructor.mutablePropertyAsState
|
||||||
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> = propertyAsState(Drive.force)
|
public suspend fun Drive.stateOfForce(): MutableDeviceState<Double> = mutablePropertyAsState(Drive.force)
|
||||||
|
@ -1,14 +1,15 @@
|
|||||||
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.constructor.property
|
import space.kscience.controls.spec.DeviceBySpec
|
||||||
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.meta.MetaConverter
|
import space.kscience.dataforge.context.Factory
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -16,10 +17,13 @@ import space.kscience.dataforge.meta.MetaConverter
|
|||||||
*/
|
*/
|
||||||
public interface LimitSwitch : Device {
|
public interface LimitSwitch : Device {
|
||||||
|
|
||||||
public fun isLocked(): Boolean
|
public val locked: Boolean
|
||||||
|
|
||||||
public companion object : DeviceSpec<LimitSwitch>() {
|
public companion object : DeviceSpec<LimitSwitch>() {
|
||||||
public val locked: DevicePropertySpec<LimitSwitch, Boolean> by booleanProperty { isLocked() }
|
public val locked: DevicePropertySpec<LimitSwitch, Boolean> by booleanProperty { locked }
|
||||||
|
public operator fun invoke(lockedState: DeviceState<Boolean>): Factory<LimitSwitch> = Factory { context, _ ->
|
||||||
|
VirtualLimitSwitch(context, lockedState)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -28,10 +32,14 @@ public interface LimitSwitch : Device {
|
|||||||
*/
|
*/
|
||||||
public class VirtualLimitSwitch(
|
public class VirtualLimitSwitch(
|
||||||
context: Context,
|
context: Context,
|
||||||
locked: DeviceState<Boolean>,
|
public val lockedState: DeviceState<Boolean>,
|
||||||
) : DeviceConstructor(context), LimitSwitch {
|
) : DeviceBySpec<LimitSwitch>(LimitSwitch, context), LimitSwitch {
|
||||||
|
|
||||||
public val locked: DeviceState<Boolean> by property(MetaConverter.boolean, locked)
|
override suspend fun onStart() {
|
||||||
|
lockedState.valueFlow.onEach {
|
||||||
|
propertyChanged(LimitSwitch.locked, it)
|
||||||
|
}.launchIn(this)
|
||||||
|
}
|
||||||
|
|
||||||
override fun isLocked(): Boolean = locked.value
|
override val locked: Boolean get() = lockedState.value
|
||||||
}
|
}
|
@ -16,22 +16,32 @@ 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 data class PidParameters(
|
public interface PidParameters {
|
||||||
val kp: Double,
|
public val kp: Double
|
||||||
val ki: Double,
|
public val ki: Double
|
||||||
val kd: Double,
|
public val kd: Double
|
||||||
val timeStep: Duration = 1.milliseconds,
|
public val timeStep: Duration
|
||||||
)
|
}
|
||||||
|
|
||||||
|
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 var pidParameters: PidParameters, // TODO expose as property
|
public val pidParameters: PidParameters,
|
||||||
) : DeviceBySpec<Regulator>(Regulator, drive.context), Regulator {
|
) : DeviceBySpec<Regulator>(Regulator, drive.context), Regulator {
|
||||||
|
|
||||||
private val clock = drive.context.clock
|
private val clock = drive.context.clock
|
||||||
@ -55,7 +65,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 - getPosition()
|
val delta = target - position
|
||||||
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
|
||||||
@ -77,7 +87,7 @@ public class PidRegulator(
|
|||||||
drive.stop()
|
drive.stop()
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getPosition(): Double = drive.position
|
override val position: Double get() = 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 suspend fun getPosition(): Double
|
public val position: 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 { getPosition() }
|
public val position: DevicePropertySpec<Regulator, Double> by doubleProperty { position }
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,10 +0,0 @@
|
|||||||
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)
|
|
@ -1,30 +0,0 @@
|
|||||||
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,7 +12,6 @@ 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 {
|
||||||
@ -75,7 +74,7 @@ public class ClockManager : AbstractPlugin() {
|
|||||||
): CoroutineDispatcher = if (timeCompression == 1.0) {
|
): CoroutineDispatcher = if (timeCompression == 1.0) {
|
||||||
dispatcher
|
dispatcher
|
||||||
} else {
|
} else {
|
||||||
CompressedTimeDispatcher(this, dispatcher, timeCompression)
|
CompressedTimeDispatcher(dispatcher, timeCompression)
|
||||||
}
|
}
|
||||||
|
|
||||||
public companion object : PluginFactory<ClockManager> {
|
public companion object : PluginFactory<ClockManager> {
|
||||||
|
@ -3,7 +3,10 @@ package space.kscience.controls.ports
|
|||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.channels.Channel
|
import kotlinx.coroutines.channels.Channel
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.flow.receiveAsFlow
|
import kotlinx.coroutines.flow.receiveAsFlow
|
||||||
|
import kotlinx.io.Buffer
|
||||||
import kotlinx.io.Source
|
import kotlinx.io.Source
|
||||||
import space.kscience.controls.api.AsynchronousSocket
|
import space.kscience.controls.api.AsynchronousSocket
|
||||||
import space.kscience.dataforge.context.*
|
import space.kscience.dataforge.context.*
|
||||||
@ -23,7 +26,15 @@ public interface AsynchronousPort : ContextAware, AsynchronousSocket<ByteArray>
|
|||||||
* [scope] controls the consummation.
|
* [scope] controls the consummation.
|
||||||
* If the scope is canceled, the source stops producing.
|
* If the scope is canceled, the source stops producing.
|
||||||
*/
|
*/
|
||||||
public fun AsynchronousPort.receiveAsSource(scope: CoroutineScope): Source = subscribe().consumeAsSource(scope)
|
public fun AsynchronousPort.receiveAsSource(scope: CoroutineScope): Source {
|
||||||
|
val buffer = Buffer()
|
||||||
|
|
||||||
|
subscribe().onEach {
|
||||||
|
buffer.write(it)
|
||||||
|
}.launchIn(scope)
|
||||||
|
|
||||||
|
return buffer
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -40,13 +51,13 @@ public abstract class AbstractAsynchronousPort(
|
|||||||
CoroutineScope(
|
CoroutineScope(
|
||||||
coroutineContext +
|
coroutineContext +
|
||||||
SupervisorJob(coroutineContext[Job]) +
|
SupervisorJob(coroutineContext[Job]) +
|
||||||
CoroutineExceptionHandler { _, throwable -> logger.error(throwable) { "Asynchronous port error: " + throwable.stackTraceToString() } } +
|
CoroutineExceptionHandler { _, throwable -> logger.error(throwable) { throwable.stackTraceToString() } } +
|
||||||
CoroutineName(toString())
|
CoroutineName(toString())
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val outgoing = Channel<ByteArray>(meta["outgoing.capacity"].int ?: 100)
|
private val outgoing = Channel<ByteArray>(meta["outgoing.capacity"].int?:100)
|
||||||
private val incoming = Channel<ByteArray>(meta["incoming.capacity"].int ?: 100)
|
private val incoming = Channel<ByteArray>(meta["incoming.capacity"].int?:100)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal method to synchronously send data
|
* Internal method to synchronously send data
|
||||||
@ -89,7 +100,7 @@ public abstract class AbstractAsynchronousPort(
|
|||||||
* Send a data packet via the port
|
* Send a data packet via the port
|
||||||
*/
|
*/
|
||||||
override suspend fun send(data: ByteArray) {
|
override suspend fun send(data: ByteArray) {
|
||||||
check(isOpen) { "The port is not opened" }
|
check(isOpen){"The port is not opened"}
|
||||||
outgoing.send(data)
|
outgoing.send(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -106,7 +117,7 @@ public abstract class AbstractAsynchronousPort(
|
|||||||
sendJob?.cancel()
|
sendJob?.cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun toString(): String = meta["name"].string ?: "ChannelPort[${hashCode().toString(16)}]"
|
override fun toString(): String = meta["name"].string?:"ChannelPort[${hashCode().toString(16)}]"
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
package space.kscience.controls.ports
|
package space.kscience.controls.ports
|
||||||
|
|
||||||
import kotlinx.coroutines.coroutineScope
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.*
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.flow.takeWhile
|
||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.sync.Mutex
|
||||||
import kotlinx.coroutines.sync.withLock
|
import kotlinx.coroutines.sync.withLock
|
||||||
import kotlinx.io.Buffer
|
import kotlinx.io.Buffer
|
||||||
import kotlinx.io.Source
|
|
||||||
import kotlinx.io.readByteArray
|
import kotlinx.io.readByteArray
|
||||||
import space.kscience.dataforge.context.Context
|
import space.kscience.dataforge.context.Context
|
||||||
import space.kscience.dataforge.context.ContextAware
|
import space.kscience.dataforge.context.ContextAware
|
||||||
@ -46,24 +46,6 @@ public interface SynchronousPort : ContextAware, AutoCloseable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Read response to a given message using [Source] abstraction
|
|
||||||
*/
|
|
||||||
public suspend fun <R> SynchronousPort.respondAsSource(
|
|
||||||
request: ByteArray,
|
|
||||||
transform: suspend Source.() -> R,
|
|
||||||
): R = respond(request) {
|
|
||||||
//suspend until the response is fully read
|
|
||||||
coroutineScope {
|
|
||||||
val buffer = Buffer()
|
|
||||||
val collectJob = onEach { buffer.write(it) }.launchIn(this)
|
|
||||||
val res = transform(buffer)
|
|
||||||
//cancel collection when the result is achieved
|
|
||||||
collectJob.cancel()
|
|
||||||
res
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class SynchronousOverAsynchronousPort(
|
private class SynchronousOverAsynchronousPort(
|
||||||
val port: AsynchronousPort,
|
val port: AsynchronousPort,
|
||||||
val mutex: Mutex,
|
val mutex: Mutex,
|
||||||
|
@ -1,24 +1,5 @@
|
|||||||
package space.kscience.controls.ports
|
package space.kscience.controls.ports
|
||||||
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
import kotlinx.coroutines.flow.launchIn
|
|
||||||
import kotlinx.coroutines.flow.onEach
|
|
||||||
import kotlinx.io.Buffer
|
|
||||||
import kotlinx.io.Source
|
|
||||||
import space.kscience.dataforge.io.Binary
|
import space.kscience.dataforge.io.Binary
|
||||||
|
|
||||||
public fun Binary.readShort(position: Int): Short = read(position) { readShort() }
|
public fun Binary.readShort(position: Int): Short = read(position) { readShort() }
|
||||||
|
|
||||||
/**
|
|
||||||
* Consume given flow of [ByteArray] as [Source]. The subscription is canceled when [scope] is closed.
|
|
||||||
*/
|
|
||||||
public fun Flow<ByteArray>.consumeAsSource(scope: CoroutineScope): Source {
|
|
||||||
val buffer = Buffer()
|
|
||||||
//subscription is canceled when the scope is canceled
|
|
||||||
onEach {
|
|
||||||
buffer.write(it)
|
|
||||||
}.launchIn(scope)
|
|
||||||
|
|
||||||
return buffer
|
|
||||||
}
|
|
@ -1,7 +1,10 @@
|
|||||||
package space.kscience.controls.spec
|
package space.kscience.controls.spec
|
||||||
|
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import space.kscience.controls.api.*
|
import space.kscience.controls.api.ActionDescriptor
|
||||||
|
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
|
||||||
@ -156,6 +159,7 @@ public abstract class DeviceSpec<D : Device> {
|
|||||||
deviceAction
|
deviceAction
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -192,16 +196,3 @@ 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,37 +19,42 @@ import kotlin.test.assertEquals
|
|||||||
class MagixLoopTest {
|
class MagixLoopTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun realDeviceHub() = runTest {
|
fun deviceHub() = 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: DataValue = client.readValue(magAge, TimestampsToReturn.Server, nodeId).await()
|
val data = 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
|
||||||
|
@ -1,45 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
@ -1,45 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,31 +0,0 @@
|
|||||||
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)"
|
|
||||||
}
|
|
@ -1,2 +0,0 @@
|
|||||||
package space.kscience.controls.compose
|
|
||||||
|
|
@ -1,230 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
@ -1,51 +0,0 @@
|
|||||||
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,3 +1,4 @@
|
|||||||
|
import org.jetbrains.compose.ExperimentalComposeLibrary
|
||||||
import org.jetbrains.compose.desktop.application.dsl.TargetFormat
|
import org.jetbrains.compose.desktop.application.dsl.TargetFormat
|
||||||
import org.jetbrains.kotlin.gradle.dsl.ExplicitApiMode
|
import org.jetbrains.kotlin.gradle.dsl.ExplicitApiMode
|
||||||
|
|
||||||
@ -7,13 +8,14 @@ plugins {
|
|||||||
}
|
}
|
||||||
|
|
||||||
kscience {
|
kscience {
|
||||||
jvm()
|
jvm {
|
||||||
|
withJava()
|
||||||
|
}
|
||||||
useKtor()
|
useKtor()
|
||||||
useSerialization()
|
useSerialization()
|
||||||
useContextReceivers()
|
useContextReceivers()
|
||||||
commonMain {
|
commonMain {
|
||||||
implementation(projects.controlsVisualisationCompose)
|
implementation(projects.controlsVision)
|
||||||
// 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")
|
||||||
}
|
}
|
||||||
@ -28,6 +30,8 @@ kotlin {
|
|||||||
jvmMain {
|
jvmMain {
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(compose.desktop.currentOs)
|
implementation(compose.desktop.currentOs)
|
||||||
|
@OptIn(ExperimentalComposeLibrary::class)
|
||||||
|
implementation(compose.desktop.components.splitPane)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,8 +5,7 @@ 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.getValue
|
import androidx.compose.runtime.*
|
||||||
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
|
||||||
@ -14,9 +13,10 @@ 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,11 +29,16 @@ 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,
|
||||||
@ -41,25 +46,27 @@ class Spring(
|
|||||||
val l0: Double,
|
val l0: Double,
|
||||||
val begin: DeviceState<XY>,
|
val begin: DeviceState<XY>,
|
||||||
val end: DeviceState<XY>,
|
val end: DeviceState<XY>,
|
||||||
) : ConstructorModel(context) {
|
) : 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
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* vector from start to end
|
* direction from start to end
|
||||||
*/
|
*/
|
||||||
val direction = combineState(begin, end) { begin: XY, end: XY ->
|
val direction = combineState(begin, end) { begin, end ->
|
||||||
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(dx.pow(2) + dy.pow(2))
|
val l = sqrt((end.y - begin.y).pow(2) + (end.x - begin.x).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)
|
||||||
}
|
}
|
||||||
@ -76,15 +83,13 @@ 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),
|
||||||
) : ConstructorModel(context, force, position, velocity) {
|
) : DeviceModel(context, force) {
|
||||||
|
|
||||||
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(
|
||||||
writes = setOf(position, velocity),
|
position, velocity,
|
||||||
reads = setOf(force, velocity, position)
|
alsoReads = 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
|
||||||
@ -100,31 +105,31 @@ class BodyOnSprings(
|
|||||||
k: Double,
|
k: Double,
|
||||||
startPosition: XY,
|
startPosition: XY,
|
||||||
l0: Double = 1.0,
|
l0: Double = 1.0,
|
||||||
val xLeft: Double = -1.0,
|
val xLeft: Double = 0.0,
|
||||||
val xRight: Double = 1.0,
|
val xRight: Double = 2.0,
|
||||||
val yBottom: Double = -1.0,
|
val yBottom: Double = 0.0,
|
||||||
val yTop: Double = 1.0,
|
val yTop: Double = 2.0,
|
||||||
) : DeviceConstructor(context) {
|
) : DeviceConstructor(context) {
|
||||||
|
|
||||||
val width = xRight - xLeft
|
val width = xRight - xLeft
|
||||||
val height = yTop - yBottom
|
val height = yTop - yBottom
|
||||||
|
|
||||||
val position = stateOf(startPosition)
|
val position = mutableState(startPosition)
|
||||||
|
|
||||||
private val leftAnchor = stateOf(XY(xLeft, (yTop + yBottom) / 2))
|
private val leftAnchor = mutableState(XY(xLeft, yTop + yBottom / 2))
|
||||||
|
|
||||||
val leftSpring = model(
|
val leftSpring by device(
|
||||||
Spring(context, k, l0, leftAnchor, position)
|
Spring(context, k, l0, leftAnchor, position)
|
||||||
)
|
)
|
||||||
|
|
||||||
private val rightAnchor = stateOf(XY(xRight, (yTop + yBottom) / 2))
|
private val rightAnchor = mutableState(XY(xRight, yTop + yBottom / 2))
|
||||||
|
|
||||||
val rightSpring = model(
|
val rightSpring by device(
|
||||||
Spring(context, k, l0, rightAnchor, position)
|
Spring(context, k, l0, rightAnchor, position)
|
||||||
)
|
)
|
||||||
|
|
||||||
val force: DeviceState<XY> = combineState(leftSpring.endForce, rightSpring.endForce) { left, right ->
|
val force: DeviceState<XY> = combineState(leftSpring.endForce, rightSpring.endForce) { left, rignt ->
|
||||||
left + right
|
left + rignt
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -133,13 +138,18 @@ 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(0.1, 0.2)
|
val initialState = XY(1.1, 1.1)
|
||||||
|
|
||||||
Window(title = "Ball on springs", onCloseRequest = ::exitApplication) {
|
Window(title = "Ball on springs", onCloseRequest = ::exitApplication) {
|
||||||
MaterialTheme {
|
MaterialTheme {
|
||||||
@ -151,20 +161,12 @@ fun main() = application {
|
|||||||
BodyOnSprings(context, 100.0, 1000.0, initialState)
|
BodyOnSprings(context, 100.0, 1000.0, initialState)
|
||||||
}
|
}
|
||||||
|
|
||||||
//TODO add ability to freeze model
|
val position: XY by model.body.position.collect()
|
||||||
|
|
||||||
// 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(
|
||||||
center.x + (x / model.width * size.width).toFloat(),
|
(x / model.width * size.width).toFloat(),
|
||||||
center.y - (y / model.height * size.height).toFloat()
|
(y / model.height * size.height).toFloat()
|
||||||
)
|
)
|
||||||
|
|
||||||
drawCircle(
|
drawCircle(
|
||||||
|
@ -1,40 +1,38 @@
|
|||||||
package space.kscience.controls.demo.constructor
|
package space.kscience.controls.demo.constructor
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.material.*
|
import androidx.compose.material.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.getValue
|
||||||
|
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 io.github.koalaplot.core.ChartLayout
|
import space.kscience.controls.constructor.DeviceConstructor
|
||||||
import io.github.koalaplot.core.legend.FlowLegend
|
import space.kscience.controls.constructor.DoubleInRangeState
|
||||||
import io.github.koalaplot.core.style.LineStyle
|
import space.kscience.controls.constructor.device
|
||||||
import io.github.koalaplot.core.util.ExperimentalKoalaPlotApi
|
import space.kscience.controls.constructor.deviceProperty
|
||||||
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 java.awt.Dimension
|
import space.kscience.plotly.models.ScatterMode
|
||||||
|
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
|
||||||
@ -51,15 +49,15 @@ class LinearDrive(
|
|||||||
meta: Meta = Meta.EMPTY,
|
meta: Meta = Meta.EMPTY,
|
||||||
) : DeviceConstructor(drive.context, meta) {
|
) : DeviceConstructor(drive.context, meta) {
|
||||||
|
|
||||||
val drive by device(drive)
|
val drive: 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 = drive.propertyAsState(Drive.position, Double.NaN)
|
val position by deviceProperty(drive, Drive.position, Double.NaN)
|
||||||
|
|
||||||
val target = pid.propertyAsState(Regulator.target, 0.0)
|
val target by deviceProperty(pid, Regulator.target, 0.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -79,205 +77,163 @@ 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)
|
fun main() = application {
|
||||||
|
val context = Context {
|
||||||
|
plugin(DeviceManager)
|
||||||
|
plugin(PlotlyPlugin)
|
||||||
|
plugin(ClockManager)
|
||||||
|
}
|
||||||
|
|
||||||
private val modulation = timer.onNext {
|
class MutablePidParameters(
|
||||||
|
kp: Double,
|
||||||
|
ki: Double,
|
||||||
|
kd: Double,
|
||||||
|
timeStep: Duration,
|
||||||
|
) : 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 {
|
||||||
|
MutablePidParameters(
|
||||||
|
kp = 2.5,
|
||||||
|
ki = 0.0,
|
||||||
|
kd = -0.1,
|
||||||
|
timeStep = 0.005.seconds
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val state = DoubleInRangeState(0.0, -6.0..6.0)
|
||||||
|
|
||||||
|
val linearDrive = context.install(
|
||||||
|
"linearDrive",
|
||||||
|
LinearDrive(context, state, 0.05, pidParameters)
|
||||||
|
)
|
||||||
|
|
||||||
|
val clockStart = context.clock.now()
|
||||||
|
linearDrive.doRecurring(10.milliseconds) {
|
||||||
val timeFromStart = clock.now() - clockStart
|
val timeFromStart = clock.now() - clockStart
|
||||||
val t = timeFromStart.toDouble(DurationUnit.SECONDS)
|
val t = timeFromStart.toDouble(DurationUnit.SECONDS)
|
||||||
|
val freq = 0.1
|
||||||
target.value = 5 * sin(2.0 * PI * freq * t) +
|
target.value = 5 * sin(2.0 * PI * freq * t) +
|
||||||
sin(2 * PI * 21 * freq * t + 0.02 * (timeFromStart / timeStep))
|
sin(2 * PI * 21 * freq * t + 0.02 * (timeFromStart / pidParameters.timeStep))
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private val maxAge = 10.seconds
|
val maxAge = 10.seconds
|
||||||
|
|
||||||
@OptIn(ExperimentalSplitPaneApi::class, ExperimentalKoalaPlotApi::class)
|
context.showDashboard {
|
||||||
fun main() = application {
|
plot {
|
||||||
val context = remember {
|
plotNumberState(context, state, maxAge = maxAge, sampling = 50.milliseconds) {
|
||||||
Context {
|
name = "real position"
|
||||||
plugin(DeviceManager)
|
}
|
||||||
plugin(ClockManager)
|
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"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
val clock = remember { context.clock }
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
var pidParameters by remember {
|
|
||||||
mutableStateOf(PidParameters(kp = 2.5, ki = 0.0, kd = -0.1, timeStep = 0.005.seconds))
|
|
||||||
}
|
|
||||||
|
|
||||||
val state = remember { DoubleInRangeState(0.0, -6.0..6.0) }
|
|
||||||
|
|
||||||
val linearDrive = remember {
|
|
||||||
context.install(
|
|
||||||
"linearDrive",
|
|
||||||
LinearDrive(context, state, 0.05, pidParameters)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
val modulator = remember {
|
|
||||||
context.install(
|
|
||||||
"modulator",
|
|
||||||
Modulator(context, linearDrive.target)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
//bind pid parameters
|
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
snapshotFlow {
|
|
||||||
pidParameters
|
|
||||||
}.onEach {
|
|
||||||
linearDrive.pid.pidParameters = pidParameters
|
|
||||||
}.collect()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Window(title = "Pid regulator simulator", onCloseRequest = ::exitApplication) {
|
Window(title = "Pid regulator simulator", onCloseRequest = ::exitApplication) {
|
||||||
window.minimumSize = Dimension(800, 400)
|
|
||||||
MaterialTheme {
|
MaterialTheme {
|
||||||
HorizontalSplitPane {
|
Column {
|
||||||
first(400.dp) {
|
Row {
|
||||||
Column(modifier = Modifier.background(color = Color.LightGray).fillMaxHeight()) {
|
Text("kp:", Modifier.align(Alignment.CenterVertically).width(50.dp).padding(5.dp))
|
||||||
Row {
|
TextField(
|
||||||
Text("kp:", Modifier.align(Alignment.CenterVertically).width(50.dp).padding(5.dp))
|
String.format("%.2f", pidParameters.kp),
|
||||||
TextField(
|
{ pidParameters.kp = it.toDouble() },
|
||||||
String.format("%.2f", pidParameters.kp),
|
Modifier.width(100.dp),
|
||||||
{ pidParameters = pidParameters.copy(kp = it.toDouble()) },
|
enabled = false
|
||||||
Modifier.width(100.dp),
|
)
|
||||||
enabled = false
|
Slider(
|
||||||
)
|
pidParameters.kp.toFloat(),
|
||||||
Slider(
|
{ pidParameters.kp = it.toDouble() },
|
||||||
pidParameters.kp.toFloat(),
|
valueRange = 0f..20f,
|
||||||
{ pidParameters = pidParameters.copy(kp = it.toDouble()) },
|
steps = 100
|
||||||
valueRange = 0f..20f,
|
)
|
||||||
steps = 100
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Row {
|
|
||||||
Text("ki:", Modifier.align(Alignment.CenterVertically).width(50.dp).padding(5.dp))
|
|
||||||
TextField(
|
|
||||||
String.format("%.2f", pidParameters.ki),
|
|
||||||
{ pidParameters = pidParameters.copy(ki = it.toDouble()) },
|
|
||||||
Modifier.width(100.dp),
|
|
||||||
enabled = false
|
|
||||||
)
|
|
||||||
|
|
||||||
Slider(
|
|
||||||
pidParameters.ki.toFloat(),
|
|
||||||
{ 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(
|
|
||||||
String.format("%.2f", pidParameters.kd),
|
|
||||||
{ pidParameters = pidParameters.copy(kd = it.toDouble()) },
|
|
||||||
Modifier.width(100.dp),
|
|
||||||
enabled = false
|
|
||||||
)
|
|
||||||
|
|
||||||
Slider(
|
|
||||||
pidParameters.kd.toFloat(),
|
|
||||||
{ 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(
|
|
||||||
pidParameters.timeStep.toString(DurationUnit.MILLISECONDS),
|
|
||||||
{ pidParameters = pidParameters.copy(timeStep = it.toDouble().milliseconds) },
|
|
||||||
Modifier.width(100.dp),
|
|
||||||
enabled = false
|
|
||||||
)
|
|
||||||
|
|
||||||
Slider(
|
|
||||||
pidParameters.timeStep.toDouble(DurationUnit.MILLISECONDS).toFloat(),
|
|
||||||
{ pidParameters = pidParameters.copy(timeStep = it.toDouble().milliseconds) },
|
|
||||||
valueRange = 0f..100f,
|
|
||||||
steps = 100
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Row {
|
|
||||||
Button({
|
|
||||||
pidParameters = PidParameters(
|
|
||||||
kp = 2.5,
|
|
||||||
ki = 0.0,
|
|
||||||
kd = -0.1,
|
|
||||||
timeStep = 0.005.seconds
|
|
||||||
)
|
|
||||||
}) {
|
|
||||||
Text("Reset")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
second(400.dp) {
|
Row {
|
||||||
ChartLayout {
|
Text("ki:", Modifier.align(Alignment.CenterVertically).width(50.dp).padding(5.dp))
|
||||||
XYGraph<Instant, Double>(
|
TextField(
|
||||||
xAxisModel = remember { TimeAxisModel.recent(maxAge, clock) },
|
String.format("%.2f", pidParameters.ki),
|
||||||
yAxisModel = rememberDoubleLinearAxisModel(state.range),
|
{ pidParameters.ki = it.toDouble() },
|
||||||
xAxisTitle = { Text("Time in seconds relative to current") },
|
Modifier.width(100.dp),
|
||||||
xAxisLabels = { it: Instant ->
|
enabled = false
|
||||||
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 -> {
|
Slider(
|
||||||
Text("Regulator position", color = Color.Black)
|
pidParameters.ki.toFloat(),
|
||||||
}
|
{ pidParameters.ki = it.toDouble() },
|
||||||
|
valueRange = -10f..10f,
|
||||||
|
steps = 100
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Row {
|
||||||
|
Text("kd:", Modifier.align(Alignment.CenterVertically).width(50.dp).padding(5.dp))
|
||||||
|
TextField(
|
||||||
|
String.format("%.2f", pidParameters.kd),
|
||||||
|
{ pidParameters.kd = it.toDouble() },
|
||||||
|
Modifier.width(100.dp),
|
||||||
|
enabled = false
|
||||||
|
)
|
||||||
|
|
||||||
2 -> {
|
Slider(
|
||||||
Text("Regulator target", color = Color.Red)
|
pidParameters.kd.toFloat(),
|
||||||
}
|
{ pidParameters.kd = it.toDouble() },
|
||||||
}
|
valueRange = -10f..10f,
|
||||||
})
|
steps = 100
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
Text("dt:", Modifier.align(Alignment.CenterVertically).width(50.dp).padding(5.dp))
|
||||||
|
TextField(
|
||||||
|
pidParameters.timeStep.toString(DurationUnit.MILLISECONDS),
|
||||||
|
{ pidParameters.timeStep = it.toDouble().milliseconds },
|
||||||
|
Modifier.width(100.dp),
|
||||||
|
enabled = false
|
||||||
|
)
|
||||||
|
|
||||||
|
Slider(
|
||||||
|
pidParameters.timeStep.toDouble(DurationUnit.MILLISECONDS).toFloat(),
|
||||||
|
{ pidParameters.timeStep = it.toDouble().milliseconds },
|
||||||
|
valueRange = 0f..100f,
|
||||||
|
steps = 100
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Row {
|
||||||
|
Button({
|
||||||
|
pidParameters.run {
|
||||||
|
kp = 2.5
|
||||||
|
ki = 0.0
|
||||||
|
kd = -0.1
|
||||||
|
timeStep = 0.005.seconds
|
||||||
}
|
}
|
||||||
|
}) {
|
||||||
|
Text("Reset")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,2 +0,0 @@
|
|||||||
package space.kscience.controls.demo.constructor
|
|
||||||
|
|
@ -64,7 +64,6 @@ 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