Compare commits

..

No commits in common. "9edde7bdbd47688f208cb9f2a8fbe73519a8261a" and "05757aefdc1afbe674c67be6f0602ebf0ee37125" have entirely different histories.

36 changed files with 419 additions and 944 deletions

View File

@ -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

View File

@ -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 {

View File

@ -11,7 +11,6 @@ kscience{
jvm() jvm()
js() js()
useCoroutines() useCoroutines()
useSerialization()
commonMain { commonMain {
api(projects.controlsCore) api(projects.controlsCore)
} }

View File

@ -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)
}
}

View File

@ -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))

View File

@ -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)
}
}

View File

@ -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].
*/ */

View File

@ -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)
} }
} }
} }

View File

@ -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)

View File

@ -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))
} }

View File

@ -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)
}

View File

@ -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)

View File

@ -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
} }

View File

@ -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(

View File

@ -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 }
} }
} }

View File

@ -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)

View File

@ -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

View File

@ -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> {

View File

@ -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)}]"
} }
/** /**

View File

@ -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,

View File

@ -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
}

View File

@ -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}" }
}
}

View File

@ -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()
} }
} }
} }

View File

@ -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

View File

@ -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
}

View File

@ -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
}
}
}

View File

@ -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)"
}

View File

@ -1,2 +0,0 @@
package space.kscience.controls.compose

View File

@ -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)
}

View File

@ -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,
)
}

View File

@ -1,3 +1,4 @@
import org.jetbrains.compose.ExperimentalComposeLibrary
import org.jetbrains.compose.desktop.application.dsl.TargetFormat import org.jetbrains.compose.desktop.application.dsl.TargetFormat
import org.jetbrains.kotlin.gradle.dsl.ExplicitApiMode import org.jetbrains.kotlin.gradle.dsl.ExplicitApiMode
@ -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)
} }
} }
} }

View File

@ -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(

View File

@ -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")
} }
} }
} }

View File

@ -1,2 +0,0 @@
package space.kscience.controls.demo.constructor

View File

@ -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",