Update constructor
This commit is contained in:
parent
4f028ccee8
commit
1fcdbdc9f4
@ -3,6 +3,8 @@
|
||||
## Unreleased
|
||||
|
||||
### Added
|
||||
- Device lifecycle message
|
||||
- Low-code constructor
|
||||
|
||||
### Changed
|
||||
|
||||
|
@ -4,7 +4,7 @@ plugins {
|
||||
}
|
||||
|
||||
description = """
|
||||
A low-code constructor foe composite devices simulation
|
||||
A low-code constructor for composite devices simulation
|
||||
""".trimIndent()
|
||||
|
||||
kscience{
|
||||
|
@ -0,0 +1,25 @@
|
||||
package space.kscience.controls.constructor
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import space.kscience.dataforge.meta.Meta
|
||||
import space.kscience.dataforge.meta.transformations.MetaConverter
|
||||
|
||||
/**
|
||||
* An observable state of a device
|
||||
*/
|
||||
public interface DeviceState<T> {
|
||||
public val converter: MetaConverter<T>
|
||||
public val value: T
|
||||
|
||||
public val valueFlow: Flow<T>
|
||||
|
||||
public val metaFlow: Flow<Meta>
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* A mutable state of a device
|
||||
*/
|
||||
public interface MutableDeviceState<T> : DeviceState<T>{
|
||||
override var value: T
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package space.kscience.controls.spec
|
||||
package space.kscience.controls.constructor
|
||||
|
||||
import space.kscience.controls.api.Device
|
||||
import space.kscience.controls.api.DeviceHub
|
||||
@ -7,11 +7,8 @@ import space.kscience.dataforge.context.Factory
|
||||
import space.kscience.dataforge.meta.Meta
|
||||
import space.kscience.dataforge.meta.get
|
||||
import space.kscience.dataforge.names.NameToken
|
||||
import kotlin.collections.Map
|
||||
import kotlin.collections.component1
|
||||
import kotlin.collections.component2
|
||||
import kotlin.collections.mapValues
|
||||
import kotlin.collections.mutableMapOf
|
||||
import kotlin.collections.set
|
||||
|
||||
|
@ -0,0 +1,91 @@
|
||||
package space.kscience.controls.constructor
|
||||
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.datetime.Clock
|
||||
import space.kscience.controls.api.Device
|
||||
import space.kscience.controls.spec.DeviceBySpec
|
||||
import space.kscience.controls.spec.DevicePropertySpec
|
||||
import space.kscience.controls.spec.DeviceSpec
|
||||
import space.kscience.controls.spec.doubleProperty
|
||||
import space.kscience.dataforge.context.Context
|
||||
import space.kscience.dataforge.meta.double
|
||||
import space.kscience.dataforge.meta.get
|
||||
import space.kscience.dataforge.meta.transformations.MetaConverter
|
||||
import kotlin.math.pow
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import kotlin.time.DurationUnit
|
||||
|
||||
/**
|
||||
* A classic drive regulated by force with encoder
|
||||
*/
|
||||
public interface Drive : Device {
|
||||
/**
|
||||
* Get or set drive force or momentum
|
||||
*/
|
||||
public var force: Double
|
||||
|
||||
/**
|
||||
* Current position value
|
||||
*/
|
||||
public val position: Double
|
||||
|
||||
public companion object : DeviceSpec<Drive>() {
|
||||
public val force: DevicePropertySpec<Drive, Double> by Drive.property(
|
||||
MetaConverter.double,
|
||||
Drive::force
|
||||
)
|
||||
|
||||
public val position: DevicePropertySpec<Drive, Double> by doubleProperty { position }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A virtual drive
|
||||
*/
|
||||
public class VirtualDrive(
|
||||
context: Context,
|
||||
private val mass: Double,
|
||||
position: Double,
|
||||
) : Drive, DeviceBySpec<Drive>(Drive, context) {
|
||||
|
||||
private val dt = meta["time.step"].double?.milliseconds ?: 5.milliseconds
|
||||
private val clock = Clock.System
|
||||
|
||||
override var force: Double = 0.0
|
||||
|
||||
override var position: Double = position
|
||||
private set
|
||||
|
||||
public var velocity: Double = 0.0
|
||||
private set
|
||||
|
||||
private var updateJob: Job? = null
|
||||
|
||||
override suspend fun onStart() {
|
||||
updateJob = launch {
|
||||
var lastTime = clock.now()
|
||||
while (isActive) {
|
||||
delay(dt)
|
||||
val realTime = clock.now()
|
||||
val dtSeconds = (realTime - lastTime).toDouble(DurationUnit.SECONDS)
|
||||
|
||||
//set last time and value to new values
|
||||
lastTime = realTime
|
||||
|
||||
// compute new value based on velocity and acceleration from the previous step
|
||||
position += velocity * dtSeconds + force/mass * dtSeconds.pow(2) / 2
|
||||
|
||||
// compute new velocity based on acceleration on the previous step
|
||||
velocity += force/mass * dtSeconds
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
updateJob?.cancel()
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
package center.sciprog.controls.devices.misc
|
||||
package space.kscience.controls.constructor
|
||||
|
||||
import space.kscience.controls.api.Device
|
||||
import space.kscience.controls.spec.DeviceBySpec
|
||||
|
@ -1,4 +1,4 @@
|
||||
package center.sciprog.controls.devices.misc
|
||||
package space.kscience.controls.constructor
|
||||
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
@ -8,28 +8,27 @@ import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.datetime.Clock
|
||||
import kotlinx.datetime.Instant
|
||||
import space.kscience.controls.api.DeviceLifecycleState
|
||||
import space.kscience.controls.spec.DeviceBySpec
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import kotlin.time.DurationUnit
|
||||
|
||||
/**
|
||||
* PID controller on top of a [Regulator]
|
||||
* A drive with PID regulator
|
||||
*/
|
||||
public class PidRegulator(
|
||||
public val regulator: Regulator,
|
||||
public val drive: Drive,
|
||||
public val kp: Double,
|
||||
public val ki: Double,
|
||||
public val kd: Double,
|
||||
private val dt: Duration = 0.5.milliseconds,
|
||||
private val dt: Duration = 1.milliseconds,
|
||||
private val clock: Clock = Clock.System,
|
||||
) : DeviceBySpec<Regulator>(Regulator, regulator.context), Regulator {
|
||||
) : DeviceBySpec<Regulator>(Regulator, drive.context), Regulator {
|
||||
|
||||
override var target: Double = regulator.target
|
||||
override var target: Double = drive.position
|
||||
|
||||
private var lastTime: Instant = clock.now()
|
||||
private var lastRegulatorTarget: Double = target
|
||||
private var lastPosition: Double = target
|
||||
|
||||
private var integral: Double = 0.0
|
||||
|
||||
@ -39,10 +38,7 @@ public class PidRegulator(
|
||||
|
||||
|
||||
override suspend fun onStart() {
|
||||
if(regulator.lifecycleState == DeviceLifecycleState.STOPPED){
|
||||
regulator.start()
|
||||
}
|
||||
regulator.start()
|
||||
drive.start()
|
||||
updateJob = launch {
|
||||
while (isActive) {
|
||||
delay(dt)
|
||||
@ -51,13 +47,13 @@ public class PidRegulator(
|
||||
val delta = target - position
|
||||
val dtSeconds = (realTime - lastTime).toDouble(DurationUnit.SECONDS)
|
||||
integral += delta * dtSeconds
|
||||
val derivative = (regulator.target - lastRegulatorTarget) / dtSeconds
|
||||
val derivative = (drive.position - lastPosition) / dtSeconds
|
||||
|
||||
//set last time and value to new values
|
||||
lastTime = realTime
|
||||
lastRegulatorTarget = regulator.target
|
||||
lastPosition = drive.position
|
||||
|
||||
regulator.target = regulator.position + kp * delta + ki * integral + kd * derivative
|
||||
drive.force = kp * delta + ki * integral + kd * derivative
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -67,129 +63,6 @@ public class PidRegulator(
|
||||
updateJob?.cancel()
|
||||
}
|
||||
|
||||
override val position: Double get() = regulator.position
|
||||
override val position: Double get() = drive.position
|
||||
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
//interface PidRegulator : Device {
|
||||
// /**
|
||||
// * Proportional coefficient
|
||||
// */
|
||||
// val kp: Double
|
||||
//
|
||||
// /**
|
||||
// * Integral coefficient
|
||||
// */
|
||||
// val ki: Double
|
||||
//
|
||||
// /**
|
||||
// * Differential coefficient
|
||||
// */
|
||||
// val kd: Double
|
||||
//
|
||||
// /**
|
||||
// * The target value for PID
|
||||
// */
|
||||
// var target: Double
|
||||
//
|
||||
// /**
|
||||
// * Read current value
|
||||
// */
|
||||
// suspend fun read(): Double
|
||||
//
|
||||
// companion object : DeviceSpec<PidRegulator>() {
|
||||
// val target by property(MetaConverter.double, PidRegulator::target)
|
||||
// val value by doubleProperty { read() }
|
||||
// }
|
||||
//}
|
||||
//
|
||||
///**
|
||||
// *
|
||||
// */
|
||||
//class VirtualPid(
|
||||
// context: Context,
|
||||
// override val kp: Double,
|
||||
// override val ki: Double,
|
||||
// override val kd: Double,
|
||||
// val mass: Double,
|
||||
// override var target: Double = 0.0,
|
||||
// private val dt: Duration = 0.5.milliseconds,
|
||||
// private val clock: Clock = Clock.System,
|
||||
//) : DeviceBySpec<PidRegulator>(PidRegulator, context), PidRegulator {
|
||||
//
|
||||
// private val mutex = Mutex()
|
||||
//
|
||||
//
|
||||
// private var lastTime: Instant = clock.now()
|
||||
// private var lastValue: Double = target
|
||||
//
|
||||
// private var value: Double = target
|
||||
// private var velocity: Double = 0.0
|
||||
// private var acceleration: Double = 0.0
|
||||
// private var integral: Double = 0.0
|
||||
//
|
||||
//
|
||||
// private var updateJob: Job? = null
|
||||
//
|
||||
// override suspend fun onStart() {
|
||||
// updateJob = launch {
|
||||
// while (isActive) {
|
||||
// delay(dt)
|
||||
// mutex.withLock {
|
||||
// val realTime = clock.now()
|
||||
// val delta = target - value
|
||||
// val dtSeconds = (realTime - lastTime).toDouble(DurationUnit.SECONDS)
|
||||
// integral += delta * dtSeconds
|
||||
// val derivative = (value - lastValue) / dtSeconds
|
||||
//
|
||||
// //set last time and value to new values
|
||||
// lastTime = realTime
|
||||
// lastValue = value
|
||||
//
|
||||
// // compute new value based on velocity and acceleration from the previous step
|
||||
// value += velocity * dtSeconds + acceleration * dtSeconds.pow(2) / 2
|
||||
//
|
||||
// // compute new velocity based on acceleration on the previous step
|
||||
// velocity += acceleration * dtSeconds
|
||||
//
|
||||
// //compute force for the next step based on current values
|
||||
// acceleration = (kp * delta + ki * integral + kd * derivative) / mass
|
||||
//
|
||||
//
|
||||
// check(value.isFinite() && velocity.isFinite()) {
|
||||
// "Value $value is not finite"
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// override fun onStop() {
|
||||
// updateJob?.cancel()
|
||||
// super<PidRegulator>.stop()
|
||||
// }
|
||||
//
|
||||
// override suspend fun read(): Double = value
|
||||
//
|
||||
// suspend fun readVelocity(): Double = velocity
|
||||
//
|
||||
// suspend fun readAcceleration(): Double = acceleration
|
||||
//
|
||||
// suspend fun write(newTarget: Double) = mutex.withLock {
|
||||
// require(newTarget.isFinite()) { "Value $newTarget is not valid" }
|
||||
// target = newTarget
|
||||
// }
|
||||
//
|
||||
// companion object : Factory<Device> {
|
||||
// override fun build(context: Context, meta: Meta) = VirtualPid(
|
||||
// context,
|
||||
// meta["kp"].double ?: error("Kp is not defined"),
|
||||
// meta["ki"].double ?: error("Ki is not defined"),
|
||||
// meta["kd"].double ?: error("Kd is not defined"),
|
||||
// meta["m"].double ?: error("Mass is not defined"),
|
||||
// )
|
||||
//
|
||||
// }
|
||||
//}
|
@ -1,17 +1,14 @@
|
||||
package center.sciprog.controls.devices.misc
|
||||
package space.kscience.controls.constructor
|
||||
|
||||
import kotlinx.coroutines.Job
|
||||
import space.kscience.controls.api.Device
|
||||
import space.kscience.controls.spec.DeviceBySpec
|
||||
import space.kscience.controls.spec.DevicePropertySpec
|
||||
import space.kscience.controls.spec.DeviceSpec
|
||||
import space.kscience.controls.spec.doubleProperty
|
||||
import space.kscience.dataforge.context.Context
|
||||
import space.kscience.dataforge.meta.transformations.MetaConverter
|
||||
|
||||
|
||||
/**
|
||||
* A single axis drive
|
||||
* A regulator with target value and current position
|
||||
*/
|
||||
public interface Regulator : Device {
|
||||
/**
|
||||
@ -30,22 +27,3 @@ public interface Regulator : Device {
|
||||
public val position: DevicePropertySpec<Regulator, Double> by doubleProperty { position }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Virtual [Regulator] with speed limit
|
||||
*/
|
||||
public class VirtualRegulator(
|
||||
context: Context,
|
||||
value: Double,
|
||||
private val speed: Double,
|
||||
) : DeviceBySpec<Regulator>(Regulator, context), Regulator {
|
||||
|
||||
private var moveJob: Job? = null
|
||||
|
||||
override var position: Double = value
|
||||
private set
|
||||
|
||||
override var target: Double = value
|
||||
|
||||
|
||||
}
|
@ -7,6 +7,7 @@ import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.filterIsInstance
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.serialization.Serializable
|
||||
import space.kscience.controls.api.Device.Companion.DEVICE_TARGET
|
||||
import space.kscience.dataforge.context.ContextAware
|
||||
import space.kscience.dataforge.context.info
|
||||
@ -19,6 +20,7 @@ import space.kscience.dataforge.names.Name
|
||||
/**
|
||||
* A lifecycle state of a device
|
||||
*/
|
||||
@Serializable
|
||||
public enum class DeviceLifecycleState {
|
||||
|
||||
/**
|
||||
@ -34,7 +36,12 @@ public enum class DeviceLifecycleState {
|
||||
/**
|
||||
* The Device is closed
|
||||
*/
|
||||
STOPPED
|
||||
STOPPED,
|
||||
|
||||
/**
|
||||
* The device encountered irrecoverable error
|
||||
*/
|
||||
ERROR
|
||||
}
|
||||
|
||||
/**
|
||||
@ -98,7 +105,8 @@ public interface Device : ContextAware, CoroutineScope {
|
||||
public suspend fun execute(actionName: String, argument: Meta? = null): Meta?
|
||||
|
||||
/**
|
||||
* Initialize the device. This function suspends until the device is finished initialization
|
||||
* Initialize the device. This function suspends until the device is finished initialization.
|
||||
* Does nothing if the device is started or is starting
|
||||
*/
|
||||
public suspend fun start(): Unit = Unit
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
@file:OptIn(ExperimentalSerializationApi::class)
|
||||
@file:OptIn(ExperimentalSerializationApi::class, ExperimentalSerializationApi::class)
|
||||
|
||||
package space.kscience.controls.api
|
||||
|
||||
@ -228,6 +228,21 @@ public data class DeviceErrorMessage(
|
||||
override fun changeSource(block: (Name) -> Name): DeviceMessage = copy(sourceDevice = block(sourceDevice))
|
||||
}
|
||||
|
||||
/**
|
||||
* Device [Device.lifecycleState] is changed
|
||||
*/
|
||||
@Serializable
|
||||
@SerialName("lifecycle")
|
||||
public data class DeviceLifeCycleMessage(
|
||||
val state: DeviceLifecycleState,
|
||||
override val sourceDevice: Name = Name.EMPTY,
|
||||
override val targetDevice: Name? = null,
|
||||
override val comment: String? = null,
|
||||
@EncodeDefault override val time: Instant? = Clock.System.now(),
|
||||
) : DeviceMessage() {
|
||||
override fun changeSource(block: (Name) -> Name): DeviceMessage = copy(sourceDevice = block(sourceDevice))
|
||||
}
|
||||
|
||||
|
||||
public fun DeviceMessage.toMeta(): Meta = Json.encodeToJsonElement(this).toMeta()
|
||||
|
||||
|
@ -64,6 +64,7 @@ public suspend fun Device.respondMessage(deviceTarget: Name, request: DeviceMess
|
||||
is DeviceErrorMessage,
|
||||
is EmptyDeviceMessage,
|
||||
is DeviceLogMessage,
|
||||
is DeviceLifeCycleMessage,
|
||||
-> null
|
||||
}
|
||||
} catch (ex: Exception) {
|
||||
|
@ -190,7 +190,16 @@ public abstract class DeviceBase<D : Device>(
|
||||
|
||||
@DFExperimental
|
||||
override var lifecycleState: DeviceLifecycleState = DeviceLifecycleState.STOPPED
|
||||
protected set
|
||||
protected set(value) {
|
||||
if (field != value) {
|
||||
launch {
|
||||
sharedMessageFlow.emit(
|
||||
DeviceLifeCycleMessage(value)
|
||||
)
|
||||
}
|
||||
}
|
||||
field = value
|
||||
}
|
||||
|
||||
protected open suspend fun onStart() {
|
||||
|
||||
@ -198,7 +207,7 @@ public abstract class DeviceBase<D : Device>(
|
||||
|
||||
@OptIn(DFExperimental::class)
|
||||
final override suspend fun start() {
|
||||
if(lifecycleState == DeviceLifecycleState.STOPPED) {
|
||||
if (lifecycleState == DeviceLifecycleState.STOPPED) {
|
||||
super.start()
|
||||
lifecycleState = DeviceLifecycleState.STARTING
|
||||
onStart()
|
||||
|
20
controls-vision/build.gradle.kts
Normal file
20
controls-vision/build.gradle.kts
Normal file
@ -0,0 +1,20 @@
|
||||
plugins {
|
||||
id("space.kscience.gradle.mpp")
|
||||
`maven-publish`
|
||||
}
|
||||
|
||||
description = """
|
||||
Dashboard and visualization extensions for devices
|
||||
""".trimIndent()
|
||||
|
||||
kscience{
|
||||
jvm()
|
||||
js()
|
||||
dependencies {
|
||||
api(projects.controlsCore)
|
||||
}
|
||||
}
|
||||
|
||||
readme{
|
||||
maturity = space.kscience.gradle.Maturity.PROTOTYPE
|
||||
}
|
@ -51,6 +51,7 @@ include(
|
||||
":controls-storage",
|
||||
":controls-storage:controls-xodus",
|
||||
":controls-constructor",
|
||||
":controls-vision",
|
||||
":magix",
|
||||
":magix:magix-api",
|
||||
":magix:magix-server",
|
||||
|
Loading…
Reference in New Issue
Block a user