Compare commits

...

8 Commits

30 changed files with 1053 additions and 287 deletions

View File

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

View File

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

View File

@ -1,5 +1,7 @@
package space.kscience.controls.constructor package space.kscience.controls.constructor
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import space.kscience.controls.api.Device import space.kscience.controls.api.Device
@ -9,6 +11,7 @@ import space.kscience.controls.spec.MutableDevicePropertySpec
import space.kscience.controls.spec.name import space.kscience.controls.spec.name
import space.kscience.dataforge.meta.Meta import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.transformations.MetaConverter import space.kscience.dataforge.meta.transformations.MetaConverter
import kotlin.time.Duration
/** /**
* An observable state of a device * An observable state of a device
@ -18,6 +21,8 @@ public interface DeviceState<T> {
public val value: T public val value: T
public val valueFlow: Flow<T> public val valueFlow: Flow<T>
public companion object
} }
public val <T> DeviceState<T>.metaFlow: Flow<Meta> get() = valueFlow.map(converter::objectToMeta) public val <T> DeviceState<T>.metaFlow: Flow<Meta> get() = valueFlow.map(converter::objectToMeta)
@ -55,7 +60,7 @@ private open class BoundDeviceState<T>(
override val converter: MetaConverter<T>, override val converter: MetaConverter<T>,
val device: Device, val device: Device,
val propertyName: String, val propertyName: String,
private val initialValue: T, initialValue: T,
) : DeviceState<T> { ) : DeviceState<T> {
override val valueFlow: StateFlow<T> = device.messageFlow.filterIsInstance<PropertyChangedMessage>().filter { override val valueFlow: StateFlow<T> = device.messageFlow.filterIsInstance<PropertyChangedMessage>().filter {
@ -70,7 +75,7 @@ private open class BoundDeviceState<T>(
/** /**
* Bind a read-only [DeviceState] to a [Device] property * Bind a read-only [DeviceState] to a [Device] property
*/ */
public suspend fun <T> Device.bindStateToProperty( public suspend fun <T> Device.propertyAsState(
propertyName: String, propertyName: String,
metaConverter: MetaConverter<T>, metaConverter: MetaConverter<T>,
): DeviceState<T> { ): DeviceState<T> {
@ -78,9 +83,9 @@ public suspend fun <T> Device.bindStateToProperty(
return BoundDeviceState(metaConverter, this, propertyName, initialValue) return BoundDeviceState(metaConverter, this, propertyName, initialValue)
} }
public suspend fun <D : Device, T> D.bindStateToProperty( public suspend fun <D : Device, T> D.propertyAsState(
propertySpec: DevicePropertySpec<D, T>, propertySpec: DevicePropertySpec<D, T>,
): DeviceState<T> = bindStateToProperty(propertySpec.name, propertySpec.converter) ): DeviceState<T> = propertyAsState(propertySpec.name, propertySpec.converter)
public fun <T, R> DeviceState<T>.map( public fun <T, R> DeviceState<T>.map(
converter: MetaConverter<R>, mapper: (T) -> R, converter: MetaConverter<R>, mapper: (T) -> R,
@ -108,16 +113,82 @@ private class MutableBoundDeviceState<T>(
} }
} }
public suspend fun <T> Device.bindMutableStateToProperty( 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(
propertyName: String, propertyName: String,
metaConverter: MetaConverter<T>, metaConverter: MetaConverter<T>,
): MutableDeviceState<T> { ): MutableDeviceState<T> {
val initialValue = metaConverter.metaToObject(readProperty(propertyName)) ?: error("Conversion of property failed") val initialValue = metaConverter.metaToObject(readProperty(propertyName)) ?: error("Conversion of property failed")
return MutableBoundDeviceState(metaConverter, this, propertyName, initialValue) return mutablePropertyAsState(propertyName, metaConverter, initialValue)
} }
public suspend fun <D : Device, T> D.bindMutableStateToProperty( public suspend fun <D : Device, T> D.mutablePropertyAsState(
propertySpec: MutableDevicePropertySpec<D, T>, propertySpec: MutableDevicePropertySpec<D, T>,
): MutableDeviceState<T> = bindMutableStateToProperty(propertySpec.name, propertySpec.converter) ): 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)
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,6 +8,7 @@ import space.kscience.controls.api.Device
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
import space.kscience.dataforge.context.Factory
import space.kscience.dataforge.meta.double import space.kscience.dataforge.meta.double
import space.kscience.dataforge.meta.get import space.kscience.dataforge.meta.get
import space.kscience.dataforge.meta.transformations.MetaConverter import space.kscience.dataforge.meta.transformations.MetaConverter
@ -83,12 +84,15 @@ public class VirtualDrive(
override fun onStop() { override fun onStop() {
updateJob?.cancel() 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> = bindMutableStateToProperty(Drive.force) public suspend fun Drive.stateOfForce(): MutableDeviceState<Double> = mutablePropertyAsState(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.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.names.parseAsName import space.kscience.dataforge.context.Factory
/** /**
@ -20,6 +20,9 @@ public interface LimitSwitch : Device {
public companion object : DeviceSpec<LimitSwitch>() { public companion object : DeviceSpec<LimitSwitch>() {
public val locked: DevicePropertySpec<LimitSwitch, Boolean> by booleanProperty { locked } public val locked: DevicePropertySpec<LimitSwitch, Boolean> by booleanProperty { locked }
public fun factory(lockedState: DeviceState<Boolean>): Factory<LimitSwitch> = Factory { context, _ ->
VirtualLimitSwitch(context, lockedState)
}
} }
} }
@ -38,7 +41,4 @@ public class VirtualLimitSwitch(
} }
override val locked: Boolean get() = lockedState.value 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, name: String,
drive: Drive, drive: Drive,
pidParameters: PidParameters, pidParameters: PidParameters,
): PidRegulator = device(name, PidRegulator(drive, pidParameters)) ): PidRegulator = install(name, PidRegulator(drive, pidParameters))

View File

@ -38,4 +38,10 @@ public class DoubleRangeState(
* A state showing that the range is on its higher boundary * A state showing that the range is on its higher boundary
*/ */
public val atEndState: DeviceState<Boolean> = map(MetaConverter.boolean) { it >= range.endInclusive } 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

@ -0,0 +1,69 @@
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

@ -0,0 +1,40 @@
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

@ -1,18 +0,0 @@
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,23 +72,21 @@ public abstract class DeviceBase<D : Device>(
onBufferOverflow = BufferOverflow.DROP_OLDEST onBufferOverflow = BufferOverflow.DROP_OLDEST
) )
override val coroutineContext: CoroutineContext by lazy { override val coroutineContext: CoroutineContext = context.newCoroutineContext(
context.newCoroutineContext( SupervisorJob(context.coroutineContext[Job]) +
SupervisorJob(context.coroutineContext[Job]) + CoroutineName("Device $this") +
CoroutineName("Device $this") + CoroutineExceptionHandler { _, throwable ->
CoroutineExceptionHandler { _, throwable -> launch {
launch { sharedMessageFlow.emit(
sharedMessageFlow.emit( DeviceErrorMessage(
DeviceErrorMessage( errorMessage = throwable.message,
errorMessage = throwable.message, errorType = throwable::class.simpleName,
errorType = throwable::class.simpleName, errorStackTrace = throwable.stackTraceToString()
errorStackTrace = throwable.stackTraceToString()
)
) )
} )
} }
) }
} )
/** /**

View File

@ -0,0 +1,19 @@
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

@ -0,0 +1,14 @@
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

@ -0,0 +1,71 @@
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() "Float" -> member.value?.numberOrNull?.toFloat()
"Double" -> member.value?.numberOrNull?.toDouble() "Double" -> member.value?.numberOrNull?.toDouble()
"String" -> member.string "String" -> member.string
"DateTime" -> DateTime(member.instant().toJavaInstant()) "DateTime" -> member.instant?.toJavaInstant()?.let { DateTime(it) }
"Guid" -> member.string?.let { UUID.fromString(it) } "Guid" -> member.string?.let { UUID.fromString(it) }
"ByteString" -> member.value?.list?.let { list -> "ByteString" -> member.value?.list?.let { list ->
ByteString(list.map { it.number.toByte() }.toByteArray()) ByteString(list.map { it.number.toByte() }.toByteArray())

View File

@ -1,7 +1,7 @@
import space.kscience.gradle.Maturity import space.kscience.gradle.Maturity
plugins { plugins {
id("space.kscience.gradle.jvm") id("space.kscience.gradle.mpp")
`maven-publish` `maven-publish`
} }
@ -12,16 +12,20 @@ description = """
val dataforgeVersion: String by rootProject.extra val dataforgeVersion: String by rootProject.extra
val ktorVersion: String by rootProject.extra val ktorVersion: String by rootProject.extra
dependencies {
implementation(projects.controlsCore) kscience {
implementation(projects.controlsPortsKtor) jvm()
implementation(projects.magix.magixServer) dependencies {
implementation("io.ktor:ktor-server-cio:$ktorVersion") implementation(projects.controlsCore)
implementation("io.ktor:ktor-server-websockets:$ktorVersion") implementation(projects.controlsPortsKtor)
implementation("io.ktor:ktor-server-content-negotiation:$ktorVersion") implementation(projects.magix.magixServer)
implementation("io.ktor:ktor-serialization-kotlinx-json:$ktorVersion") implementation("io.ktor:ktor-server-cio:$ktorVersion")
implementation("io.ktor:ktor-server-html-builder:$ktorVersion") implementation("io.ktor:ktor-server-websockets:$ktorVersion")
implementation("io.ktor:ktor-server-status-pages:$ktorVersion") implementation("io.ktor:ktor-server-content-negotiation:$ktorVersion")
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{ readme{

View File

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

View File

@ -0,0 +1,157 @@
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

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

View File

@ -0,0 +1,61 @@
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,11 +1,16 @@
import org.jetbrains.kotlin.gradle.dsl.ExplicitApiMode
plugins { plugins {
id("space.kscience.gradle.mpp") id("space.kscience.gradle.mpp")
application application
} }
kscience { kscience {
fullStack("js/constructor.js", jvmConfig = {withJava()}) jvm{
withJava()
}
useKtor() useKtor()
useContextReceivers()
dependencies { dependencies {
api(projects.controlsVision) api(projects.controlsVision)
} }
@ -17,4 +22,6 @@ kscience {
application { application {
mainClass.set("space.kscience.controls.demo.constructor.MainKt") mainClass.set("space.kscience.controls.demo.constructor.MainKt")
} }
kotlin.explicitApi = ExplicitApiMode.Disabled

View File

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

View File

@ -0,0 +1,176 @@
{
"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,10 +4,7 @@ kotlin.native.ignoreDisabledTargets=true
org.gradle.parallel=true org.gradle.parallel=true
publishing.github=false
publishing.sonatype=false
org.gradle.configureondemand=true org.gradle.configureondemand=true
org.gradle.jvmargs=-Xmx4096m org.gradle.jvmargs=-Xmx4096m
toolsVersion=0.15.0-kotlin-1.9.20-RC2 toolsVersion=0.15.0-kotlin-1.9.20

View File

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

View File

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