Compare commits
8 Commits
2698cee80b
...
0c128bce36
Author | SHA1 | Date | |
---|---|---|---|
0c128bce36 | |||
4e17c9051c | |||
0f687c3c51 | |||
53fc240c75 | |||
825f1a4d04 | |||
0443fdc3c0 | |||
78b18ebda6 | |||
0e963a7b13 |
@ -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")
|
||||||
|
@ -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
|
||||||
|
)
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
||||||
|
@ -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)
|
@ -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))
|
|
||||||
|
@ -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))
|
|
@ -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))
|
@ -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)
|
@ -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)
|
@ -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) }
|
@ -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,
|
|
||||||
)
|
|
@ -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()
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
}
|
)
|
||||||
}
|
}
|
||||||
)
|
}
|
||||||
}
|
)
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
19
controls-jupyter/build.gradle.kts
Normal file
19
controls-jupyter/build.gradle.kts
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
14
controls-jupyter/src/jsMain/kotlin/commonJupyter.kt
Normal file
14
controls-jupyter/src/jsMain/kotlin/commonJupyter.kt
Normal 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)
|
||||||
|
}
|
||||||
|
|
71
controls-jupyter/src/jvmMain/kotlin/ControlsJupyter.kt
Normal file
71
controls-jupyter/src/jvmMain/kotlin/ControlsJupyter.kt
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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())
|
||||||
|
@ -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{
|
||||||
|
@ -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
|
||||||
}
|
}
|
157
controls-vision/src/commonMain/kotlin/plotExtensions.kt
Normal file
157
controls-vision/src/commonMain/kotlin/plotExtensions.kt
Normal 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)
|
||||||
|
}
|
@ -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)
|
|
||||||
}
|
|
@ -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)
|
||||||
}
|
}
|
61
controls-vision/src/jvmMain/kotlin/dashboard.kt
Normal file
61
controls-vision/src/jvmMain/kotlin/dashboard.kt
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
@ -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()
|
|
||||||
}
|
}
|
176
demo/notebooks/constructor.ipynb
Normal file
176
demo/notebooks/constructor.ipynb
Normal 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
|
||||||
|
}
|
@ -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
|
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@ -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
|
||||||
|
@ -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",
|
||||||
|
Loading…
Reference in New Issue
Block a user