Compare commits

..

No commits in common. "0c128bce36ada3c3b2b8f571ffd36b4a125bfd84" and "2698cee80b1de7f0bb2bbf8c48782c8061695b8c" have entirely different histories.

30 changed files with 288 additions and 1054 deletions

View File

@ -6,7 +6,7 @@ plugins {
}
val dataforgeVersion: String by extra("0.6.2")
val visionforgeVersion by extra("0.3.0-dev-14")
val visionforgeVersion by extra("0.3.0-dev-10")
val ktorVersion: String by extra(space.kscience.gradle.KScienceVersions.ktorVersion)
val rsocketVersion by extra("0.15.4")
val xodusVersion by extra("2.0.1")

View File

@ -1,126 +0,0 @@
package space.kscience.controls.constructor
import space.kscience.controls.api.Device
import space.kscience.controls.api.PropertyDescriptor
import space.kscience.controls.manager.DeviceManager
import space.kscience.dataforge.context.Factory
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.transformations.MetaConverter
import space.kscience.dataforge.names.Name
import space.kscience.dataforge.names.asName
import kotlin.properties.PropertyDelegateProvider
import kotlin.properties.ReadOnlyProperty
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
import kotlin.time.Duration
/**
* A base for strongly typed device constructor blocks. Has additional delegates for type-safe devices
*/
public abstract class DeviceConstructor(
deviceManager: DeviceManager,
meta: Meta,
) : DeviceGroup(deviceManager, meta) {
/**
* Register a device, provided by a given [factory] and
*/
public fun <D : Device> device(
factory: Factory<D>,
meta: Meta? = null,
nameOverride: Name? = null,
metaLocation: Name? = null,
): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, D>> =
PropertyDelegateProvider { _: DeviceConstructor, property: KProperty<*> ->
val name = nameOverride ?: property.name.asName()
val device = install(name, factory, meta, metaLocation ?: name)
ReadOnlyProperty { _: DeviceConstructor, _ ->
device
}
}
public fun <D : Device> device(
device: D,
nameOverride: Name? = null,
): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, D>> =
PropertyDelegateProvider { _: DeviceConstructor, property: KProperty<*> ->
val name = nameOverride ?: property.name.asName()
install(name, device)
ReadOnlyProperty { _: DeviceConstructor, _ ->
device
}
}
/**
* Register a property and provide a direct reader for it
*/
public fun <T : Any> property(
state: DeviceState<T>,
nameOverride: String? = null,
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, T>> =
PropertyDelegateProvider { _: DeviceConstructor, property ->
val name = nameOverride ?: property.name
val descriptor = PropertyDescriptor(name).apply(descriptorBuilder)
registerProperty(descriptor, state)
ReadOnlyProperty { _: DeviceConstructor, _ ->
state.value
}
}
/**
* Register external state as a property
*/
public fun <T : Any> property(
metaConverter: MetaConverter<T>,
reader: suspend () -> T,
readInterval: Duration,
initialState: T,
nameOverride: String? = null,
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, T>> = property(
DeviceState.external(this, metaConverter, readInterval, initialState, reader),
nameOverride, descriptorBuilder
)
/**
* Register a mutable property and provide a direct reader for it
*/
public fun <T : Any> mutableProperty(
state: MutableDeviceState<T>,
nameOverride: String? = null,
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
): PropertyDelegateProvider<DeviceConstructor, ReadWriteProperty<DeviceConstructor, T>> =
PropertyDelegateProvider { _: DeviceConstructor, property ->
val name = nameOverride ?: property.name
val descriptor = PropertyDescriptor(name).apply(descriptorBuilder)
registerProperty(descriptor, state)
object : ReadWriteProperty<DeviceConstructor, T> {
override fun getValue(thisRef: DeviceConstructor, property: KProperty<*>): T = state.value
override fun setValue(thisRef: DeviceConstructor, property: KProperty<*>, value: T) {
state.value = value
}
}
}
/**
* Register external state as a property
*/
public fun <T : Any> mutableProperty(
metaConverter: MetaConverter<T>,
reader: suspend () -> T,
writer: suspend (T) -> Unit,
readInterval: Duration,
initialState: T,
nameOverride: String? = null,
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
): PropertyDelegateProvider<DeviceConstructor, ReadWriteProperty<DeviceConstructor, T>> = mutableProperty(
DeviceState.external(this, metaConverter, readInterval, initialState, reader, writer),
nameOverride,
descriptorBuilder
)
}

View File

@ -3,15 +3,11 @@ package space.kscience.controls.constructor
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import space.kscience.controls.api.*
import space.kscience.controls.api.DeviceLifecycleState.*
import space.kscience.controls.manager.DeviceManager
import space.kscience.controls.manager.install
import space.kscience.dataforge.context.Context
import space.kscience.dataforge.context.Factory
import space.kscience.dataforge.context.request
import space.kscience.dataforge.meta.Laminate
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.MutableMeta
@ -26,7 +22,7 @@ import kotlin.coroutines.CoroutineContext
/**
* A mutable group of devices and properties to be used for lightweight design and simulations.
*/
public open class DeviceGroup(
public class DeviceGroup(
public val deviceManager: DeviceManager,
override val meta: Meta,
) : DeviceHub, CachingDevice {
@ -42,19 +38,14 @@ public open class DeviceGroup(
)
override final val context: Context get() = deviceManager.context
override val context: Context get() = deviceManager.context
private val sharedMessageFlow = MutableSharedFlow<DeviceMessage>()
override val messageFlow: Flow<DeviceMessage>
get() = sharedMessageFlow
override val coroutineContext: CoroutineContext = context.newCoroutineContext(
override val coroutineContext: CoroutineContext by lazy {
context.newCoroutineContext(
SupervisorJob(context.coroutineContext[Job]) +
CoroutineName("Device $this") +
CoroutineExceptionHandler { _, throwable ->
context.launch {
launch {
sharedMessageFlow.emit(
DeviceErrorMessage(
errorMessage = throwable.message,
@ -65,41 +56,24 @@ public open class DeviceGroup(
}
}
)
}
private val _devices = hashMapOf<NameToken, Device>()
override val devices: Map<NameToken, Device> = _devices
/**
* Register and initialize (synchronize child's lifecycle state with group state) a new device in this group
*/
@OptIn(DFExperimental::class)
public fun <D : Device> install(token: NameToken, device: D): D {
require(_devices[token] == null) { "A child device with name $token already exists" }
//start the child device if needed
if(lifecycleState == STARTED || lifecycleState == STARTING) launch { device.start() }
public fun <D : Device> device(token: NameToken, device: D): D {
check(_devices[token] == null) { "A child device with name $token already exists" }
_devices[token] = device
return device
}
private val properties: MutableMap<Name, Property> = hashMapOf()
/**
* Register a new property based on [DeviceState]. Properties could be modified dynamically
*/
public fun registerProperty(descriptor: PropertyDescriptor, state: DeviceState<out Any>) {
public fun property(descriptor: PropertyDescriptor, state: DeviceState<out Any>) {
val name = descriptor.name.parseAsName()
require(properties[name] == null) { "Can't add property with name $name. It already exists." }
properties[name] = Property(state, descriptor)
state.metaFlow.onEach {
sharedMessageFlow.emit(
PropertyChangedMessage(
descriptor.name,
it
)
)
}.launchIn(this)
}
private val actions: MutableMap<Name, Action> = hashMapOf()
@ -126,6 +100,10 @@ public open class DeviceGroup(
property.valueAsMeta = value
}
private val sharedMessageFlow = MutableSharedFlow<DeviceMessage>()
override val messageFlow: Flow<DeviceMessage>
get() = sharedMessageFlow
override suspend fun execute(actionName: String, argument: Meta?): Meta? {
val action = actions[actionName] ?: error("Action with name $actionName not found")
@ -133,8 +111,8 @@ public open class DeviceGroup(
}
@DFExperimental
override var lifecycleState: DeviceLifecycleState = STOPPED
protected set(value) {
override var lifecycleState: DeviceLifecycleState = DeviceLifecycleState.STOPPED
private set(value) {
if (field != value) {
launch {
sharedMessageFlow.emit(
@ -148,12 +126,12 @@ public open class DeviceGroup(
@OptIn(DFExperimental::class)
override suspend fun start() {
lifecycleState = STARTING
lifecycleState = DeviceLifecycleState.STARTING
super.start()
devices.values.forEach {
it.start()
}
lifecycleState = STARTED
lifecycleState = DeviceLifecycleState.STARTED
}
@OptIn(DFExperimental::class)
@ -162,7 +140,7 @@ public open class DeviceGroup(
it.stop()
}
super.stop()
lifecycleState = STOPPED
lifecycleState = DeviceLifecycleState.STOPPED
}
public companion object {
@ -170,7 +148,7 @@ public open class DeviceGroup(
}
}
public fun DeviceManager.registerDeviceGroup(
public fun DeviceManager.deviceGroup(
name: String = "@group",
meta: Meta = Meta.EMPTY,
block: DeviceGroup.() -> Unit,
@ -180,19 +158,13 @@ public fun DeviceManager.registerDeviceGroup(
return group
}
public fun Context.registerDeviceGroup(
name: String = "@group",
meta: Meta = Meta.EMPTY,
block: DeviceGroup.() -> Unit,
): DeviceGroup = request(DeviceManager).registerDeviceGroup(name, meta, block)
private fun DeviceGroup.getOrCreateGroup(name: Name): DeviceGroup {
return when (name.length) {
0 -> this
1 -> {
val token = name.first()
when (val d = devices[token]) {
null -> install(
null -> device(
token,
DeviceGroup(deviceManager, meta[token] ?: Meta.EMPTY)
)
@ -208,102 +180,79 @@ private fun DeviceGroup.getOrCreateGroup(name: Name): DeviceGroup {
/**
* Register a device at given [name] path
*/
public fun <D : Device> DeviceGroup.install(name: Name, device: D): D {
public fun <D : Device> DeviceGroup.device(name: Name, device: D): D {
return when (name.length) {
0 -> error("Can't use empty name for a child device")
1 -> install(name.first(), device)
else -> getOrCreateGroup(name.cutLast()).install(name.tokens.last(), device)
1 -> device(name.first(), device)
else -> getOrCreateGroup(name.cutLast()).device(name.tokens.last(), device)
}
}
public fun <D : Device> DeviceGroup.install(name: String, device: D): D =
install(name.parseAsName(), device)
public fun <D : Device> Context.install(name: String, device: D): D = request(DeviceManager).install(name, device)
public fun <D: Device> DeviceGroup.device(name: String, device: D): D = device(name.parseAsName(), device)
/**
* Add a device creating intermediate groups if necessary. If device with given [name] already exists, throws an error.
* @param name the name of the device in the group
* @param factory a factory used to create a device
* @param deviceMeta meta override for this specific device
* @param metaLocation location of the template meta in parent group meta
*/
public fun <D : Device> DeviceGroup.install(
name: Name,
factory: Factory<D>,
deviceMeta: Meta? = null,
metaLocation: Name = name,
): D {
val newDevice = factory.build(deviceManager.context, Laminate(deviceMeta, meta[metaLocation]))
install(name, newDevice)
public fun DeviceGroup.device(name: Name, factory: Factory<Device>, deviceMeta: Meta? = null): Device {
val newDevice = factory.build(deviceManager.context, Laminate(deviceMeta, meta[name]))
device(name, newDevice)
return newDevice
}
public fun <D : Device> DeviceGroup.install(
public fun DeviceGroup.device(
name: String,
factory: Factory<D>,
metaLocation: Name = name.parseAsName(),
factory: Factory<Device>,
metaBuilder: (MutableMeta.() -> Unit)? = null,
): D = install(name.parseAsName(), factory, metaBuilder?.let { Meta(it) }, metaLocation)
): Device = device(name.parseAsName(), factory, metaBuilder?.let { Meta(it) })
/**
* Create or edit a group with a given [name].
*/
public fun DeviceGroup.registerDeviceGroup(name: Name, block: DeviceGroup.() -> Unit): DeviceGroup =
public fun DeviceGroup.deviceGroup(name: Name, block: DeviceGroup.() -> Unit): DeviceGroup =
getOrCreateGroup(name).apply(block)
public fun DeviceGroup.registerDeviceGroup(name: String, block: DeviceGroup.() -> Unit): DeviceGroup =
registerDeviceGroup(name.parseAsName(), block)
public fun DeviceGroup.deviceGroup(name: String, block: DeviceGroup.() -> Unit): DeviceGroup =
deviceGroup(name.parseAsName(), block)
/**
* Register read-only property based on [state]
*/
public fun <T : Any> DeviceGroup.registerProperty(
public fun <T : Any> DeviceGroup.property(
name: String,
state: DeviceState<T>,
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
) {
registerProperty(
): DeviceState<T> {
property(
PropertyDescriptor(name).apply(descriptorBuilder),
state
)
return state
}
/**
* Register a mutable property based on mutable [state]
*/
public fun <T : Any> DeviceGroup.registerMutableProperty(
public fun <T : Any> DeviceGroup.mutableProperty(
name: String,
state: MutableDeviceState<T>,
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
) {
registerProperty(
): MutableDeviceState<T> {
property(
PropertyDescriptor(name).apply(descriptorBuilder),
state
)
return state
}
/**
* Create a virtual [MutableDeviceState], but do not register it to a device
*/
@Suppress("UnusedReceiverParameter")
public fun <T : Any> DeviceGroup.state(
converter: MetaConverter<T>,
initialValue: T,
): MutableDeviceState<T> = VirtualDeviceState<T>(converter, initialValue)
/**
* Create a new virtual mutable state and a property based on it.
* @return the mutable state used in property
*/
public fun <T : Any> DeviceGroup.registerVirtualProperty(
public fun <T : Any> DeviceGroup.virtualProperty(
name: String,
initialValue: T,
converter: MetaConverter<T>,
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
): MutableDeviceState<T> {
val state = state(converter, initialValue)
registerMutableProperty(name, state, descriptorBuilder)
return state
val state = VirtualDeviceState<T>(converter, initialValue)
return mutableProperty(name, state, descriptorBuilder)
}
/**
* Create a virtual [MutableDeviceState], but do not register it to a device
*/
@Suppress("UnusedReceiverParameter")
public fun <T : Any> DeviceGroup.standAloneProperty(
initialValue: T,
converter: MetaConverter<T>,
): MutableDeviceState<T> = VirtualDeviceState<T>(converter, initialValue)

View File

@ -1,7 +1,5 @@
package space.kscience.controls.constructor
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import space.kscience.controls.api.Device
@ -11,7 +9,6 @@ import space.kscience.controls.spec.MutableDevicePropertySpec
import space.kscience.controls.spec.name
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.transformations.MetaConverter
import kotlin.time.Duration
/**
* An observable state of a device
@ -21,8 +18,6 @@ public interface DeviceState<T> {
public val value: T
public val valueFlow: Flow<T>
public companion object
}
public val <T> DeviceState<T>.metaFlow: Flow<Meta> get() = valueFlow.map(converter::objectToMeta)
@ -60,7 +55,7 @@ private open class BoundDeviceState<T>(
override val converter: MetaConverter<T>,
val device: Device,
val propertyName: String,
initialValue: T,
private val initialValue: T,
) : DeviceState<T> {
override val valueFlow: StateFlow<T> = device.messageFlow.filterIsInstance<PropertyChangedMessage>().filter {
@ -75,7 +70,7 @@ private open class BoundDeviceState<T>(
/**
* Bind a read-only [DeviceState] to a [Device] property
*/
public suspend fun <T> Device.propertyAsState(
public suspend fun <T> Device.bindStateToProperty(
propertyName: String,
metaConverter: MetaConverter<T>,
): DeviceState<T> {
@ -83,9 +78,9 @@ public suspend fun <T> Device.propertyAsState(
return BoundDeviceState(metaConverter, this, propertyName, initialValue)
}
public suspend fun <D : Device, T> D.propertyAsState(
public suspend fun <D : Device, T> D.bindStateToProperty(
propertySpec: DevicePropertySpec<D, T>,
): DeviceState<T> = propertyAsState(propertySpec.name, propertySpec.converter)
): DeviceState<T> = bindStateToProperty(propertySpec.name, propertySpec.converter)
public fun <T, R> DeviceState<T>.map(
converter: MetaConverter<R>, mapper: (T) -> R,
@ -113,82 +108,16 @@ private class MutableBoundDeviceState<T>(
}
}
public fun <T> Device.mutablePropertyAsState(
propertyName: String,
metaConverter: MetaConverter<T>,
initialValue: T,
): MutableDeviceState<T> = MutableBoundDeviceState(metaConverter, this, propertyName, initialValue)
public suspend fun <T> Device.mutablePropertyAsState(
public suspend fun <T> Device.bindMutableStateToProperty(
propertyName: String,
metaConverter: MetaConverter<T>,
): MutableDeviceState<T> {
val initialValue = metaConverter.metaToObject(readProperty(propertyName)) ?: error("Conversion of property failed")
return mutablePropertyAsState(propertyName, metaConverter, initialValue)
return MutableBoundDeviceState(metaConverter, this, propertyName, initialValue)
}
public suspend fun <D : Device, T> D.mutablePropertyAsState(
public suspend fun <D : Device, T> D.bindMutableStateToProperty(
propertySpec: MutableDevicePropertySpec<D, T>,
): MutableDeviceState<T> = mutablePropertyAsState(propertySpec.name, propertySpec.converter)
public fun <D : Device, T> D.mutablePropertyAsState(
propertySpec: MutableDevicePropertySpec<D, T>,
initialValue: T,
): MutableDeviceState<T> = mutablePropertyAsState(propertySpec.name, propertySpec.converter, initialValue)
): MutableDeviceState<T> = bindMutableStateToProperty(propertySpec.name, propertySpec.converter)
private open class ExternalState<T>(
val scope: CoroutineScope,
override val converter: MetaConverter<T>,
val readInterval: Duration,
initialValue: T,
val reader: suspend () -> T,
) : DeviceState<T> {
protected val flow: StateFlow<T> = flow {
while (true) {
delay(readInterval)
emit(reader())
}
}.stateIn(scope, SharingStarted.Eagerly, initialValue)
override val value: T get() = flow.value
override val valueFlow: Flow<T> get() = flow
}
/**
* Create a [DeviceState] which is constructed by periodically reading external value
*/
public fun <T> DeviceState.Companion.external(
scope: CoroutineScope,
converter: MetaConverter<T>,
readInterval: Duration,
initialValue: T,
reader: suspend () -> T,
): DeviceState<T> = ExternalState(scope, converter, readInterval, initialValue, reader)
private class MutableExternalState<T>(
scope: CoroutineScope,
converter: MetaConverter<T>,
readInterval: Duration,
initialValue: T,
reader: suspend () -> T,
val writer: suspend (T) -> Unit,
) : ExternalState<T>(scope, converter, readInterval, initialValue, reader), MutableDeviceState<T> {
override var value: T
get() = super.value
set(value) {
scope.launch {
writer(value)
}
}
}
public fun <T> DeviceState.Companion.external(
scope: CoroutineScope,
converter: MetaConverter<T>,
readInterval: Duration,
initialValue: T,
reader: suspend () -> T,
writer: suspend (T) -> Unit,
): MutableDeviceState<T> = MutableExternalState(scope, converter, readInterval, initialValue, reader, writer)

View File

@ -8,7 +8,6 @@ import space.kscience.controls.api.Device
import space.kscience.controls.manager.clock
import space.kscience.controls.spec.*
import space.kscience.dataforge.context.Context
import space.kscience.dataforge.context.Factory
import space.kscience.dataforge.meta.double
import space.kscience.dataforge.meta.get
import space.kscience.dataforge.meta.transformations.MetaConverter
@ -84,15 +83,12 @@ public class VirtualDrive(
override fun onStop() {
updateJob?.cancel()
}
public companion object {
public fun factory(
mass: Double,
positionState: MutableDeviceState<Double>,
): Factory<Drive> = Factory { context, _ ->
VirtualDrive(context, mass, positionState)
}
}
}
public suspend fun Drive.stateOfForce(): MutableDeviceState<Double> = mutablePropertyAsState(Drive.force)
public suspend fun Drive.stateOfForce(): MutableDeviceState<Double> = bindMutableStateToProperty(Drive.force)
public fun DeviceGroup.virtualDrive(
name: String,
mass: Double,
positionState: MutableDeviceState<Double>,
): VirtualDrive = device(name, VirtualDrive(context, mass, positionState))

View File

@ -8,7 +8,7 @@ import space.kscience.controls.spec.DevicePropertySpec
import space.kscience.controls.spec.DeviceSpec
import space.kscience.controls.spec.booleanProperty
import space.kscience.dataforge.context.Context
import space.kscience.dataforge.context.Factory
import space.kscience.dataforge.names.parseAsName
/**
@ -20,9 +20,6 @@ public interface LimitSwitch : Device {
public companion object : DeviceSpec<LimitSwitch>() {
public val locked: DevicePropertySpec<LimitSwitch, Boolean> by booleanProperty { locked }
public fun factory(lockedState: DeviceState<Boolean>): Factory<LimitSwitch> = Factory { context, _ ->
VirtualLimitSwitch(context, lockedState)
}
}
}
@ -42,3 +39,6 @@ public class VirtualLimitSwitch(
override val locked: Boolean get() = lockedState.value
}
public fun DeviceGroup.virtualLimitSwitch(name: String, lockedState: DeviceState<Boolean>): VirtualLimitSwitch =
device(name.parseAsName(), VirtualLimitSwitch(context, lockedState))

View File

@ -78,4 +78,4 @@ public fun DeviceGroup.pid(
name: String,
drive: Drive,
pidParameters: PidParameters,
): PidRegulator = install(name, PidRegulator(drive, pidParameters))
): PidRegulator = device(name, PidRegulator(drive, pidParameters))

View File

@ -39,9 +39,3 @@ public class DoubleRangeState(
*/
public val atEndState: DeviceState<Boolean> = map(MetaConverter.boolean) { it >= range.endInclusive }
}
@Suppress("UnusedReceiverParameter")
public fun DeviceGroup.rangeState(
initialValue: Double,
range: ClosedFloatingPointRange<Double>,
): DoubleRangeState = DoubleRangeState(initialValue, range)

View File

@ -1,69 +0,0 @@
package space.kscience.controls.misc
import io.ktor.utils.io.core.Input
import io.ktor.utils.io.core.Output
import kotlinx.datetime.Instant
import space.kscience.dataforge.io.IOFormat
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.get
import space.kscience.dataforge.meta.transformations.MetaConverter
import kotlin.reflect.KType
import kotlin.reflect.typeOf
/**
* A value coupled to a time it was obtained at
*/
public data class ValueWithTime<T>(val value: T, val time: Instant) {
public companion object {
/**
* Create a [ValueWithTime] format for given value value [IOFormat]
*/
public fun <T> ioFormat(
valueFormat: IOFormat<T>,
): IOFormat<ValueWithTime<T>> = ValueWithTimeIOFormat(valueFormat)
/**
* Create a [MetaConverter] with time for given value [MetaConverter]
*/
public fun <T> metaConverter(
valueConverter: MetaConverter<T>,
): MetaConverter<ValueWithTime<T>> = ValueWithTimeMetaConverter(valueConverter)
public const val META_TIME_KEY: String = "time"
public const val META_VALUE_KEY: String = "value"
}
}
private class ValueWithTimeIOFormat<T>(val valueFormat: IOFormat<T>) : IOFormat<ValueWithTime<T>> {
override val type: KType get() = typeOf<ValueWithTime<T>>()
override fun readObject(input: Input): ValueWithTime<T> {
val timestamp = InstantIOFormat.readObject(input)
val value = valueFormat.readObject(input)
return ValueWithTime(value, timestamp)
}
override fun writeObject(output: Output, obj: ValueWithTime<T>) {
InstantIOFormat.writeObject(output, obj.time)
valueFormat.writeObject(output, obj.value)
}
}
private class ValueWithTimeMetaConverter<T>(
val valueConverter: MetaConverter<T>,
) : MetaConverter<ValueWithTime<T>> {
override fun metaToObject(
meta: Meta,
): ValueWithTime<T>? = valueConverter.metaToObject(meta[ValueWithTime.META_VALUE_KEY] ?: Meta.EMPTY)?.let {
ValueWithTime(it, meta[ValueWithTime.META_TIME_KEY]?.instant ?: Instant.DISTANT_PAST)
}
override fun objectToMeta(obj: ValueWithTime<T>): Meta = Meta {
ValueWithTime.META_TIME_KEY put obj.time.toMeta()
ValueWithTime.META_VALUE_KEY put valueConverter.objectToMeta(obj.value)
}
}
public fun <T : Any> MetaConverter<T>.withTime(): MetaConverter<ValueWithTime<T>> = ValueWithTimeMetaConverter(this)

View File

@ -1,40 +0,0 @@
package space.kscience.controls.misc
import io.ktor.utils.io.core.*
import kotlinx.datetime.Instant
import space.kscience.dataforge.context.Context
import space.kscience.dataforge.io.IOFormat
import space.kscience.dataforge.io.IOFormatFactory
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.string
import space.kscience.dataforge.names.Name
import space.kscience.dataforge.names.asName
import kotlin.reflect.KType
import kotlin.reflect.typeOf
/**
* An [IOFormat] for [Instant]
*/
public object InstantIOFormat : IOFormat<Instant>, IOFormatFactory<Instant> {
override fun build(context: Context, meta: Meta): IOFormat<Instant> = this
override val name: Name = "instant".asName()
override val type: KType get() = typeOf<Instant>()
override fun writeObject(output: Output, obj: Instant) {
output.writeLong(obj.epochSeconds)
output.writeInt(obj.nanosecondsOfSecond)
}
override fun readObject(input: Input): Instant {
val seconds = input.readLong()
val nanoseconds = input.readInt()
return Instant.fromEpochSeconds(seconds, nanoseconds)
}
}
public fun Instant.toMeta(): Meta = Meta(toString())
public val Meta.instant: Instant? get() = value?.string?.let { Instant.parse(it) }

View File

@ -0,0 +1,18 @@
package space.kscience.controls.misc
import kotlinx.datetime.Instant
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.get
import space.kscience.dataforge.meta.long
// TODO move to core
public fun Instant.toMeta(): Meta = Meta {
"seconds" put epochSeconds
"nanos" put nanosecondsOfSecond
}
public fun Meta.instant(): Instant = value?.long?.let { Instant.fromEpochMilliseconds(it) } ?: Instant.fromEpochSeconds(
get("seconds")?.long ?: 0L,
get("nanos")?.long ?: 0L,
)

View File

@ -72,7 +72,8 @@ public abstract class DeviceBase<D : Device>(
onBufferOverflow = BufferOverflow.DROP_OLDEST
)
override val coroutineContext: CoroutineContext = context.newCoroutineContext(
override val coroutineContext: CoroutineContext by lazy {
context.newCoroutineContext(
SupervisorJob(context.coroutineContext[Job]) +
CoroutineName("Device $this") +
CoroutineExceptionHandler { _, throwable ->
@ -87,6 +88,7 @@ public abstract class DeviceBase<D : Device>(
}
}
)
}
/**

View File

@ -1,19 +0,0 @@
plugins {
id("space.kscience.gradle.mpp")
}
val visionforgeVersion: String by rootProject.extra
kscience {
fullStack("js/controls-jupyter.js")
useKtor()
useContextReceivers()
jupyterLibrary()
dependencies {
implementation(projects.controlsVision)
implementation("space.kscience:visionforge-jupyter:$visionforgeVersion")
}
jvmMain {
implementation(spclibs.logback.classic)
}
}

View File

@ -1,14 +0,0 @@
import space.kscience.visionforge.jupyter.VFNotebookClient
import space.kscience.visionforge.markup.MarkupPlugin
import space.kscience.visionforge.plotly.PlotlyPlugin
import space.kscience.visionforge.runVisionClient
public fun main(): Unit = runVisionClient {
// plugin(DeviceManager)
// plugin(ClockManager)
plugin(PlotlyPlugin)
plugin(MarkupPlugin)
// plugin(TableVisionJsPlugin)
plugin(VFNotebookClient)
}

View File

@ -1,71 +0,0 @@
import org.jetbrains.kotlinx.jupyter.api.declare
import org.jetbrains.kotlinx.jupyter.api.libraries.resources
import space.kscience.controls.manager.ClockManager
import space.kscience.controls.manager.DeviceManager
import space.kscience.dataforge.context.Context
import space.kscience.dataforge.misc.DFExperimental
import space.kscience.plotly.Plot
import space.kscience.tables.Table
import space.kscience.visionforge.jupyter.VisionForge
import space.kscience.visionforge.jupyter.VisionForgeIntegration
import space.kscience.visionforge.markup.MarkupPlugin
import space.kscience.visionforge.plotly.PlotlyPlugin
import space.kscience.visionforge.plotly.asVision
import space.kscience.visionforge.tables.toVision
import space.kscience.visionforge.visionManager
@OptIn(DFExperimental::class)
public class ControlsJupyter : VisionForgeIntegration(CONTEXT.visionManager) {
override fun Builder.afterLoaded(vf: VisionForge) {
resources {
js("controls-jupyter") {
classPath("js/controls-jupyter.js")
}
}
onLoaded {
declare("context" to CONTEXT)
}
import(
"kotlin.time.*",
"kotlin.time.Duration.Companion.milliseconds",
"kotlin.time.Duration.Companion.seconds",
"space.kscience.tables.*",
"space.kscience.dataforge.meta.*",
"space.kscience.dataforge.context.*",
"space.kscience.plotly.*",
"space.kscience.plotly.models.*",
"space.kscience.visionforge.plotly.*",
"space.kscience.controls.manager.*",
"space.kscience.controls.constructor.*",
"space.kscience.controls.vision.*",
"space.kscience.controls.spec.*"
)
render<Table<*>> { table ->
vf.produceHtml {
vision { table.toVision() }
}
}
render<Plot> { plot ->
vf.produceHtml {
vision { plot.asVision() }
}
}
}
public companion object {
private val CONTEXT: Context = Context("controls-jupyter") {
plugin(DeviceManager)
plugin(ClockManager)
plugin(PlotlyPlugin)
// plugin(TableVisionPlugin)
plugin(MarkupPlugin)
}
}
}

View File

@ -147,7 +147,7 @@ internal class MetaStructureCodec(
"Float" -> member.value?.numberOrNull?.toFloat()
"Double" -> member.value?.numberOrNull?.toDouble()
"String" -> member.string
"DateTime" -> member.instant?.toJavaInstant()?.let { DateTime(it) }
"DateTime" -> DateTime(member.instant().toJavaInstant())
"Guid" -> member.string?.let { UUID.fromString(it) }
"ByteString" -> member.value?.list?.let { list ->
ByteString(list.map { it.number.toByte() }.toByteArray())

View File

@ -1,7 +1,7 @@
import space.kscience.gradle.Maturity
plugins {
id("space.kscience.gradle.mpp")
id("space.kscience.gradle.jvm")
`maven-publish`
}
@ -12,10 +12,7 @@ description = """
val dataforgeVersion: String by rootProject.extra
val ktorVersion: String by rootProject.extra
kscience {
jvm()
dependencies {
dependencies {
implementation(projects.controlsCore)
implementation(projects.controlsPortsKtor)
implementation(projects.magix.magixServer)
@ -25,7 +22,6 @@ kscience {
implementation("io.ktor:ktor-serialization-kotlinx-json:$ktorVersion")
implementation("io.ktor:ktor-server-html-builder:$ktorVersion")
implementation("io.ktor:ktor-server-status-pages:$ktorVersion")
}
}
readme{

View File

@ -7,23 +7,20 @@ description = """
Dashboard and visualization extensions for devices
""".trimIndent()
val visionforgeVersion: String by rootProject.extra
val visionforgeVersion = "0.3.0-dev-10"
kscience {
fullStack("js/controls-vision.js")
useKtor()
useContextReceivers()
jvm()
js()
dependencies {
api(projects.controlsCore)
api(projects.controlsConstructor)
api("space.kscience:visionforge-plotly:$visionforgeVersion")
api("space.kscience:visionforge-markdown:$visionforgeVersion")
api("space.kscience:visionforge-tables:$visionforgeVersion")
}
jvmMain{
api("space.kscience:visionforge-server:$visionforgeVersion")
api("io.ktor:ktor-server-cio")
}
}

View File

@ -1,157 +0,0 @@
package space.kscience.controls.vision
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.transform
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.datetime.Clock
import kotlinx.datetime.Instant
import space.kscience.controls.api.Device
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.dataforge.context.Context
import space.kscience.dataforge.meta.*
import space.kscience.plotly.Plot
import space.kscience.plotly.bar
import space.kscience.plotly.models.Bar
import space.kscience.plotly.models.Scatter
import space.kscience.plotly.models.Trace
import space.kscience.plotly.models.TraceValues
import space.kscience.plotly.scatter
import kotlin.time.Duration
import kotlin.time.Duration.Companion.hours
private var TraceValues.values: List<Value>
get() = value?.list ?: emptyList()
set(newValues) {
value = ListValue(newValues)
}
private var TraceValues.times: List<Instant>
get() = value?.list?.map { Instant.parse(it.string) } ?: emptyList()
set(newValues) {
value = ListValue(newValues.map { it.toString().asValue() })
}
private class TimeData(private var points: MutableList<ValueWithTime<Value>> = mutableListOf()) {
private val mutex = Mutex()
suspend fun append(time: Instant, value: Value) = mutex.withLock {
points.add(ValueWithTime(value, time))
}
suspend fun trim(maxAge: Duration, maxPoints: Int = 800, minPoints: Int = 400) {
require(maxPoints > 2)
require(minPoints > 0)
require(maxPoints > minPoints)
val now = Clock.System.now()
// filter old points
points.removeAll { now - it.time > maxAge }
if (points.size > maxPoints) {
val durationBetweenPoints = maxAge / minPoints
val markedForRemoval = buildList<ValueWithTime<Value>> {
var lastTime: Instant? = null
points.forEach { point ->
if (lastTime?.let { point.time - it < durationBetweenPoints } == true) {
add(point)
} else {
lastTime = point.time
}
}
}
points.removeAll(markedForRemoval)
}
}
suspend fun fillPlot(x: TraceValues, y: TraceValues) = mutex.withLock {
x.strings = points.map { it.time.toString() }
y.values = points.map { it.value }
}
}
/**
* Add a trace that shows a [Device] property change over time. Show only latest [maxPoints] .
* @return a [Job] that handles the listener
*/
public fun Plot.plotDeviceProperty(
device: Device,
propertyName: String,
extractValue: Meta.() -> Value = { value ?: Null },
maxAge: Duration = 1.hours,
maxPoints: Int = 800,
minPoints: Int = 400,
coroutineScope: CoroutineScope = device.context,
configuration: Scatter.() -> Unit = {},
): Job = scatter(configuration).run {
val clock = device.context.clock
val data = TimeData()
device.propertyMessageFlow(propertyName).transform {
data.append(it.time ?: clock.now(), it.value.extractValue())
data.trim(maxAge, maxPoints, minPoints)
emit(data)
}.onEach {
it.fillPlot(x, y)
}.launchIn(coroutineScope)
}
private fun <T> Trace.updateFromState(
context: Context,
state: DeviceState<T>,
extractValue: T.() -> Value = { state.converter.objectToMeta(this).value ?: space.kscience.dataforge.meta.Null },
maxAge: Duration = 1.hours,
maxPoints: Int = 800,
minPoints: Int = 400,
): Job{
val clock = context.clock
val data = TimeData()
return state.valueFlow.transform<T, TimeData> {
data.append(clock.now(), it.extractValue())
data.trim(maxAge, maxPoints, minPoints)
}.onEach {
it.fillPlot(x, y)
}.launchIn(context)
}
public fun <T> Plot.plotDeviceState(
context: Context,
state: DeviceState<T>,
extractValue: T.() -> Value = { state.converter.objectToMeta(this).value ?: Null },
maxAge: Duration = 1.hours,
maxPoints: Int = 800,
minPoints: Int = 400,
configuration: Scatter.() -> Unit = {},
): Job = scatter(configuration).run {
updateFromState(context, state, extractValue, maxAge, maxPoints, minPoints)
}
public fun Plot.plotNumberState(
context: Context,
state: DeviceState<out Number>,
maxAge: Duration = 1.hours,
maxPoints: Int = 800,
minPoints: Int = 400,
configuration: Scatter.() -> Unit = {},
): Job = scatter(configuration).run {
updateFromState(context, state, { asValue() }, maxAge, maxPoints, minPoints)
}
public fun Plot.plotBooleanState(
context: Context,
state: DeviceState<Boolean>,
maxAge: Duration = 1.hours,
maxPoints: Int = 800,
minPoints: Int = 400,
configuration: Bar.() -> Unit = {},
): Job = bar(configuration).run {
updateFromState(context, state, { asValue() }, maxAge, maxPoints, minPoints)
}

View File

@ -0,0 +1,70 @@
package space.kscience.controls.vision
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import space.kscience.controls.api.Device
import space.kscience.controls.api.propertyMessageFlow
import space.kscience.controls.constructor.DeviceState
import space.kscience.controls.manager.clock
import space.kscience.dataforge.context.Context
import space.kscience.dataforge.meta.*
import space.kscience.plotly.Plot
import space.kscience.plotly.bar
import space.kscience.plotly.models.Bar
import space.kscience.plotly.models.Scatter
import space.kscience.plotly.models.TraceValues
import space.kscience.plotly.scatter
private var TraceValues.values: List<Value>
get() = value?.list ?: emptyList()
set(newValues) {
value = ListValue(newValues)
}
/**
* Add a trace that shows a [Device] property change over time. Show only latest [pointsNumber] .
* @return a [Job] that handles the listener
*/
public fun Plot.plotDeviceProperty(
device: Device,
propertyName: String,
extractValue: Meta.() -> Value = { value ?: Null },
pointsNumber: Int = 400,
coroutineScope: CoroutineScope = device.context,
configuration: Scatter.() -> Unit = {},
): Job = scatter(configuration).run {
val clock = device.context.clock
device.propertyMessageFlow(propertyName).onEach { message ->
x.strings = (x.strings + (message.time ?: clock.now()).toString()).takeLast(pointsNumber)
y.values = (y.values + message.value.extractValue()).takeLast(pointsNumber)
}.launchIn(coroutineScope)
}
public fun Plot.plotNumberState(
context: Context,
state: DeviceState<out Number>,
pointsNumber: Int = 400,
configuration: Scatter.() -> Unit = {},
): Job = scatter(configuration).run {
val clock = context.clock
state.valueFlow.onEach {
x.strings = (x.strings + clock.now().toString()).takeLast(pointsNumber)
y.numbers = (y.numbers + it).takeLast(pointsNumber)
}.launchIn(context)
}
public fun Plot.plotBooleanState(
context: Context,
state: DeviceState<Boolean>,
pointsNumber: Int = 400,
configuration: Bar.() -> Unit = {},
): Job = bar(configuration).run {
val clock = context.clock
state.valueFlow.onEach {
x.strings = (x.strings + clock.now().toString()).takeLast(pointsNumber)
y.values = (y.values + it.asValue()).takeLast(pointsNumber)
}.launchIn(context)
}

View File

@ -1,61 +0,0 @@
package space.kscience.controls.vision
import io.ktor.server.cio.CIO
import io.ktor.server.engine.ApplicationEngine
import io.ktor.server.engine.embeddedServer
import io.ktor.server.http.content.staticResources
import io.ktor.server.routing.Routing
import io.ktor.server.routing.routing
import kotlinx.html.TagConsumer
import space.kscience.dataforge.context.Context
import space.kscience.plotly.Plot
import space.kscience.plotly.PlotlyConfig
import space.kscience.visionforge.html.HtmlVisionFragment
import space.kscience.visionforge.html.VisionPage
import space.kscience.visionforge.html.VisionTagConsumer
import space.kscience.visionforge.plotly.plotly
import space.kscience.visionforge.server.VisionRoute
import space.kscience.visionforge.server.close
import space.kscience.visionforge.server.openInBrowser
import space.kscience.visionforge.server.visionPage
import space.kscience.visionforge.visionManager
public fun Context.showDashboard(
port: Int = 7777,
routes: Routing.() -> Unit = {},
configurationBuilder: VisionRoute.() -> Unit = {},
visionFragment: HtmlVisionFragment,
): ApplicationEngine = embeddedServer(CIO, port = port) {
routing {
staticResources("", null, null)
routes()
}
visionPage(
visionManager,
VisionPage.scriptHeader("js/controls-vision.js"),
configurationBuilder = configurationBuilder,
visionFragment = visionFragment
)
}.also {
it.start(false)
it.openInBrowser()
println("Enter 'exit' to close server")
while (readlnOrNull() != "exit") {
//
}
it.close()
}
context(VisionTagConsumer<*>)
public fun TagConsumer<*>.plot(
config: PlotlyConfig = PlotlyConfig(),
block: Plot.() -> Unit,
) {
vision {
plotly(config, block)
}
}

View File

@ -1,16 +1,11 @@
import org.jetbrains.kotlin.gradle.dsl.ExplicitApiMode
plugins {
id("space.kscience.gradle.mpp")
application
}
kscience {
jvm{
withJava()
}
fullStack("js/constructor.js", jvmConfig = {withJava()})
useKtor()
useContextReceivers()
dependencies {
api(projects.controlsVision)
}
@ -23,5 +18,3 @@ kscience {
application {
mainClass.set("space.kscience.controls.demo.constructor.MainKt")
}
kotlin.explicitApi = ExplicitApiMode.Disabled

View File

@ -1,11 +1,6 @@
package space.kscience.controls.vision
import space.kscience.visionforge.markup.MarkupPlugin
import space.kscience.visionforge.plotly.PlotlyPlugin
import space.kscience.visionforge.runVisionClient
public fun main(): Unit = runVisionClient {
plugin(PlotlyPlugin)
plugin(MarkupPlugin)
// plugin(TableVisionJsPlugin)
}

View File

@ -1,54 +1,47 @@
package space.kscience.controls.demo.constructor
import io.ktor.server.cio.CIO
import io.ktor.server.engine.embeddedServer
import io.ktor.server.http.content.staticResources
import io.ktor.server.routing.routing
import space.kscience.controls.api.get
import space.kscience.controls.constructor.*
import space.kscience.controls.manager.ClockManager
import space.kscience.controls.manager.DeviceManager
import space.kscience.controls.manager.clock
import space.kscience.controls.spec.doRecurring
import space.kscience.controls.spec.name
import space.kscience.controls.vision.plot
import space.kscience.controls.spec.read
import space.kscience.controls.spec.write
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.request
import space.kscience.dataforge.meta.Meta
import space.kscience.plotly.models.ScatterMode
import space.kscience.visionforge.VisionManager
import space.kscience.visionforge.html.VisionPage
import space.kscience.visionforge.plotly.PlotlyPlugin
import space.kscience.visionforge.plotly.plotly
import space.kscience.visionforge.server.close
import space.kscience.visionforge.server.openInBrowser
import space.kscience.visionforge.server.visionPage
import kotlin.math.PI
import kotlin.math.sin
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds
import kotlin.time.DurationUnit
class LinearDrive(
context: Context,
state: DoubleRangeState,
mass: Double,
pidParameters: PidParameters,
meta: Meta = Meta.EMPTY,
) : DeviceConstructor(context.request(DeviceManager), meta) {
val drive by device(VirtualDrive.factory(mass, state))
val pid by device(PidRegulator(drive, pidParameters))
val start by device(LimitSwitch.factory(state.atStartState))
val end by device(LimitSwitch.factory(state.atEndState))
val position by property(state)
var target by mutableProperty(pid.mutablePropertyAsState(Regulator.target, 0.0))
}
fun main() {
@Suppress("ExtractKtorModule")
public fun main() {
val context = Context {
plugin(DeviceManager)
plugin(PlotlyPlugin)
plugin(ClockManager)
}
val deviceManager = context.request(DeviceManager)
val visionManager = context.request(VisionManager)
val state = DoubleRangeState(0.0, -5.0..5.0)
val pidParameters = PidParameters(
@ -58,46 +51,79 @@ fun main() {
timeStep = 0.005.seconds
)
val device = context.install("device", LinearDrive(context, state, 0.005, pidParameters)).apply {
val device = deviceManager.deviceGroup {
val drive = virtualDrive("drive", 0.005, state)
val pid = pid("pid", drive, pidParameters)
virtualLimitSwitch("start", state.atStartState)
virtualLimitSwitch("end", state.atEndState)
val clock = context.clock
val clockStart = clock.now()
doRecurring(10.milliseconds) {
val timeFromStart = clock.now() - clockStart
val t = timeFromStart.toDouble(DurationUnit.SECONDS)
val freq = 0.1
target = 5 * sin(2.0 * PI * freq * t) +
sin(2 * PI * 21 * freq * t + 0.02 * (timeFromStart / pidParameters.timeStep))
val target = 5 * sin(2.0 * PI * freq * t) +
sin(2 * PI * 21 * freq * t + 0.1 * (timeFromStart / pidParameters.timeStep))
pid.write(Regulator.target, target)
pid.read(Regulator.position)
}
}
val server = embeddedServer(CIO, port = 7777) {
routing {
staticResources("", null, null)
}
val maxAge = 10.seconds
context.showDashboard {
plot {
plotNumberState(context, state, maxAge = maxAge) {
visionPage(
visionManager,
VisionPage.scriptHeader("js/constructor.js")
) {
vision {
plotly {
plotNumberState(context, state) {
name = "real position"
}
plotDeviceProperty(device.pid, Regulator.position.name, maxAge = maxAge) {
plotDeviceProperty(device["pid"], Regulator.position.name) {
name = "read position"
}
plotDeviceProperty(device.pid, Regulator.target.name, maxAge = maxAge) {
plotDeviceProperty(device["pid"], Regulator.target.name) {
name = "target"
}
}
}
plot {
plotDeviceProperty(device.start, LimitSwitch.locked.name, maxAge = maxAge) {
vision {
plotly {
// plotBooleanState(context, state.atStartState) {
// name = "start"
// }
// plotBooleanState(context, state.atEndState) {
// name = "end"
// }
plotDeviceProperty(device["start"], LimitSwitch.locked.name) {
name = "start measured"
mode = ScatterMode.markers
}
plotDeviceProperty(device.end, LimitSwitch.locked.name, maxAge = maxAge) {
plotDeviceProperty(device["end"], LimitSwitch.locked.name) {
name = "end measured"
mode = ScatterMode.markers
}
}
}
}
}.start(false)
server.openInBrowser()
println("Enter 'exit' to close server")
while (readlnOrNull() != "exit") {
//
}
server.close()
}

View File

@ -1,176 +0,0 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"collapsed": true
},
"outputs": [],
"source": [
"USE(ControlsJupyter())"
]
},
{
"cell_type": "code",
"execution_count": null,
"outputs": [],
"source": [
"class LinearDrive(\n",
" context: Context,\n",
" state: DoubleRangeState,\n",
" mass: Double,\n",
" pidParameters: PidParameters,\n",
" meta: Meta = Meta.EMPTY,\n",
") : DeviceConstructor(context.request(DeviceManager), meta) {\n",
"\n",
" val drive by device(VirtualDrive.factory(mass, state))\n",
" val pid by device(PidRegulator(drive, pidParameters))\n",
"\n",
" val start by device(LimitSwitch.factory(state.atStartState))\n",
" val end by device(LimitSwitch.factory(state.atEndState))\n",
"\n",
"\n",
" val position by property(state)\n",
" var target by mutableProperty(pid.mutablePropertyAsState(Regulator.target, 0.0))\n",
"}\n"
],
"metadata": {
"collapsed": false
}
},
{
"cell_type": "code",
"execution_count": null,
"outputs": [],
"source": [
"import kotlin.time.Duration.Companion.milliseconds\n",
"import kotlin.time.Duration.Companion.seconds\n",
"\n",
"val state = DoubleRangeState(0.0, -5.0..5.0)\n",
"\n",
"val pidParameters = PidParameters(\n",
" kp = 2.5,\n",
" ki = 0.0,\n",
" kd = -0.1,\n",
" timeStep = 0.005.seconds\n",
")\n",
"\n",
"val device = context.install(\"device\", LinearDrive(context, state, 0.005, pidParameters))"
],
"metadata": {
"collapsed": false
}
},
{
"cell_type": "code",
"execution_count": null,
"outputs": [],
"source": [
"\n",
"val job = device.run {\n",
" val clock = context.clock\n",
" val clockStart = clock.now()\n",
" doRecurring(10.milliseconds) {\n",
" val timeFromStart = clock.now() - clockStart\n",
" val t = timeFromStart.toDouble(DurationUnit.SECONDS)\n",
" val freq = 0.1\n",
"\n",
" target = 5 * sin(2.0 * PI * freq * t) +\n",
" sin(2 * PI * 21 * freq * t + 0.02 * (timeFromStart / pidParameters.timeStep))\n",
" }\n",
"}"
],
"metadata": {
"collapsed": false
}
},
{
"cell_type": "code",
"execution_count": null,
"outputs": [],
"source": [
"val maxAge = 10.seconds\n",
"\n",
"\n",
"VisionForge.fragment {\n",
" vision {\n",
" plotly {\n",
" plotNumberState(context, state, maxAge = maxAge) {\n",
" name = \"real position\"\n",
" }\n",
" plotDeviceProperty(device.pid, Regulator.position.name, maxAge = maxAge) {\n",
" name = \"read position\"\n",
" }\n",
"\n",
" plotDeviceProperty(device.pid, Regulator.target.name, maxAge = maxAge) {\n",
" name = \"target\"\n",
" }\n",
" }\n",
" }\n",
"\n",
" vision {\n",
" plotly {\n",
" plotDeviceProperty(device.start, LimitSwitch.locked.name, maxAge = maxAge) {\n",
" name = \"start measured\"\n",
" mode = ScatterMode.markers\n",
" }\n",
" plotDeviceProperty(device.end, LimitSwitch.locked.name, maxAge = maxAge) {\n",
" name = \"end measured\"\n",
" mode = ScatterMode.markers\n",
" }\n",
" }\n",
" }\n",
"}"
],
"metadata": {
"collapsed": false
}
},
{
"cell_type": "code",
"execution_count": null,
"outputs": [],
"source": [
"import kotlinx.coroutines.cancel\n",
"\n",
"job.cancel()"
],
"metadata": {
"collapsed": false
}
},
{
"cell_type": "code",
"execution_count": null,
"outputs": [],
"source": [],
"metadata": {
"collapsed": false
}
}
],
"metadata": {
"kernelspec": {
"display_name": "Kotlin",
"language": "kotlin",
"name": "kotlin"
},
"language_info": {
"name": "kotlin",
"version": "1.9.0",
"mimetype": "text/x-kotlin",
"file_extension": ".kt",
"pygments_lexer": "kotlin",
"codemirror_mode": "text/x-kotlin",
"nbconvert_exporter": ""
},
"ktnbPluginMetadata": {
"projectDependencies": [
"controls-kt.controls-jupyter.jvmMain"
]
}
},
"nbformat": 4,
"nbformat_minor": 0
}

View File

@ -4,7 +4,10 @@ kotlin.native.ignoreDisabledTargets=true
org.gradle.parallel=true
publishing.github=false
publishing.sonatype=false
org.gradle.configureondemand=true
org.gradle.jvmargs=-Xmx4096m
toolsVersion=0.15.0-kotlin-1.9.20
toolsVersion=0.15.0-kotlin-1.9.20-RC2

View File

@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

View File

@ -52,7 +52,6 @@ include(
":controls-storage:controls-xodus",
":controls-constructor",
":controls-vision",
":controls-jupyter",
":magix",
":magix:magix-api",
":magix:magix-server",