Compare commits
4 Commits
4b05f46fa7
...
a9592d0372
Author | SHA1 | Date | |
---|---|---|---|
a9592d0372 | |||
44514cd477 | |||
24b6856f15 | |||
381da970bf |
@ -3,12 +3,11 @@ import space.kscience.gradle.useSPCTeam
|
|||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id("space.kscience.gradle.project")
|
id("space.kscience.gradle.project")
|
||||||
alias(libs.plugins.versions)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
allprojects {
|
allprojects {
|
||||||
group = "space.kscience"
|
group = "space.kscience"
|
||||||
version = "0.3.1-dev-1"
|
version = "0.4.0-dev-1"
|
||||||
repositories{
|
repositories{
|
||||||
maven("https://maven.pkg.jetbrains.space/spc/p/sci/dev")
|
maven("https://maven.pkg.jetbrains.space/spc/p/sci/dev")
|
||||||
}
|
}
|
||||||
|
@ -10,9 +10,14 @@ description = """
|
|||||||
kscience{
|
kscience{
|
||||||
jvm()
|
jvm()
|
||||||
js()
|
js()
|
||||||
dependencies {
|
useCoroutines()
|
||||||
|
commonMain {
|
||||||
api(projects.controlsCore)
|
api(projects.controlsCore)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
commonTest{
|
||||||
|
implementation(spclibs.logback.classic)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
readme{
|
readme{
|
||||||
|
@ -0,0 +1,26 @@
|
|||||||
|
package space.kscience.controls.constructor
|
||||||
|
|
||||||
|
import space.kscience.controls.api.Device
|
||||||
|
|
||||||
|
public sealed interface ConstructorBinding
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A binding that exposes device property as read-only state
|
||||||
|
*/
|
||||||
|
public class PropertyBinding<T>(
|
||||||
|
public val device: Device,
|
||||||
|
public val propertyName: String,
|
||||||
|
public val state: DeviceState<T>,
|
||||||
|
) : ConstructorBinding
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A binding for independent state like a timer
|
||||||
|
*/
|
||||||
|
public class StateBinding<T>(
|
||||||
|
public val state: DeviceState<T>
|
||||||
|
) : ConstructorBinding
|
||||||
|
|
||||||
|
public class ActionBinding(
|
||||||
|
public val reads: Collection<DeviceState<*>>,
|
||||||
|
public val writes: Collection<DeviceState<*>>
|
||||||
|
): ConstructorBinding
|
@ -1,9 +1,16 @@
|
|||||||
package space.kscience.controls.constructor
|
package space.kscience.controls.constructor
|
||||||
|
|
||||||
|
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.Device
|
||||||
import space.kscience.controls.api.PropertyDescriptor
|
import space.kscience.controls.api.PropertyDescriptor
|
||||||
|
import space.kscience.controls.manager.ClockManager
|
||||||
|
import space.kscience.controls.spec.DevicePropertySpec
|
||||||
|
import space.kscience.controls.spec.MutableDevicePropertySpec
|
||||||
import space.kscience.dataforge.context.Context
|
import space.kscience.dataforge.context.Context
|
||||||
import space.kscience.dataforge.context.Factory
|
import space.kscience.dataforge.context.Factory
|
||||||
|
import space.kscience.dataforge.context.request
|
||||||
import space.kscience.dataforge.meta.Meta
|
import space.kscience.dataforge.meta.Meta
|
||||||
import space.kscience.dataforge.meta.MetaConverter
|
import space.kscience.dataforge.meta.MetaConverter
|
||||||
import space.kscience.dataforge.names.Name
|
import space.kscience.dataforge.names.Name
|
||||||
@ -14,105 +21,154 @@ import kotlin.reflect.KProperty
|
|||||||
import kotlin.time.Duration
|
import kotlin.time.Duration
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A base for strongly typed device constructor blocks. Has additional delegates for type-safe devices
|
* A base for strongly typed device constructor block. Has additional delegates for type-safe devices
|
||||||
*/
|
*/
|
||||||
public abstract class DeviceConstructor(
|
public abstract class DeviceConstructor(
|
||||||
context: Context,
|
context: Context,
|
||||||
meta: Meta,
|
meta: Meta = Meta.EMPTY,
|
||||||
) : DeviceGroup(context, meta) {
|
) : DeviceGroup(context, meta) {
|
||||||
|
private val _bindings: MutableList<ConstructorBinding> = mutableListOf()
|
||||||
|
public val bindings: List<ConstructorBinding> get() = _bindings
|
||||||
|
|
||||||
|
public fun registerBinding(binding: ConstructorBinding) {
|
||||||
|
_bindings.add(binding)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun registerProperty(descriptor: PropertyDescriptor, state: DeviceState<*>) {
|
||||||
|
super.registerProperty(descriptor, state)
|
||||||
|
registerBinding(PropertyBinding(this, descriptor.name, state))
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register a device, provided by a given [factory] and
|
* Create and register a timer. Timer is not counted as a device property.
|
||||||
*/
|
*/
|
||||||
public fun <D : Device> device(
|
public fun timer(tick: Duration): TimerState = TimerState(context.request(ClockManager), tick)
|
||||||
factory: Factory<D>,
|
.also { registerBinding(StateBinding(it)) }
|
||||||
meta: Meta? = null,
|
|
||||||
nameOverride: Name? = null,
|
/**
|
||||||
metaLocation: Name? = null,
|
* Launch action that is performed on each [DeviceState] value change.
|
||||||
): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, D>> =
|
*
|
||||||
PropertyDelegateProvider { _: DeviceConstructor, property: KProperty<*> ->
|
* Optionally provide [writes] - a set of states that this change affects.
|
||||||
val name = nameOverride ?: property.name.asName()
|
*/
|
||||||
val device = install(name, factory, meta, metaLocation ?: name)
|
public fun <T> DeviceState<T>.onChange(
|
||||||
ReadOnlyProperty { _: DeviceConstructor, _ ->
|
vararg writes: DeviceState<*>,
|
||||||
device
|
reads: Collection<DeviceState<*>>,
|
||||||
}
|
onChange: suspend (T) -> Unit,
|
||||||
|
): Job = valueFlow.onEach(onChange).launchIn(this@DeviceConstructor).also {
|
||||||
|
registerBinding(ActionBinding(setOf(this, *reads.toTypedArray()), setOf(*writes)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register a device, provided by a given [factory] and
|
||||||
|
*/
|
||||||
|
public fun <D : Device> DeviceConstructor.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(
|
public fun <D : Device> DeviceConstructor.device(
|
||||||
device: D,
|
device: D,
|
||||||
nameOverride: Name? = null,
|
nameOverride: Name? = null,
|
||||||
): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, D>> =
|
): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, D>> =
|
||||||
PropertyDelegateProvider { _: DeviceConstructor, property: KProperty<*> ->
|
PropertyDelegateProvider { _: DeviceConstructor, property: KProperty<*> ->
|
||||||
val name = nameOverride ?: property.name.asName()
|
val name = nameOverride ?: property.name.asName()
|
||||||
install(name, device)
|
install(name, device)
|
||||||
ReadOnlyProperty { _: DeviceConstructor, _ ->
|
ReadOnlyProperty { _: DeviceConstructor, _ ->
|
||||||
device
|
device
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
/**
|
* Register a property and provide a direct reader for it
|
||||||
* Register a property and provide a direct reader for it
|
*/
|
||||||
*/
|
public fun <T, S : DeviceState<T>> DeviceConstructor.property(
|
||||||
public fun <T, S: DeviceState<T>> property(
|
state: S,
|
||||||
state: S,
|
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
nameOverride: String? = null,
|
||||||
nameOverride: String? = null,
|
): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, S>> =
|
||||||
): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, S>> =
|
PropertyDelegateProvider { _: DeviceConstructor, property ->
|
||||||
PropertyDelegateProvider { _: DeviceConstructor, property ->
|
val name = nameOverride ?: property.name
|
||||||
val name = nameOverride ?: property.name
|
val descriptor = PropertyDescriptor(name).apply(descriptorBuilder)
|
||||||
val descriptor = PropertyDescriptor(name).apply(descriptorBuilder)
|
registerProperty(descriptor, state)
|
||||||
registerProperty(descriptor, state)
|
ReadOnlyProperty { _: DeviceConstructor, _ ->
|
||||||
ReadOnlyProperty { _: DeviceConstructor, _ ->
|
state
|
||||||
state
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register external state as a property
|
* Register external state as a property
|
||||||
*/
|
*/
|
||||||
public fun <T : Any> property(
|
public fun <T : Any> DeviceConstructor.property(
|
||||||
metaConverter: MetaConverter<T>,
|
metaConverter: MetaConverter<T>,
|
||||||
reader: suspend () -> T,
|
reader: suspend () -> T,
|
||||||
readInterval: Duration,
|
readInterval: Duration,
|
||||||
initialState: T,
|
initialState: T,
|
||||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||||
nameOverride: String? = null,
|
nameOverride: String? = null,
|
||||||
): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, DeviceState<T>>> = property(
|
): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, DeviceState<T>>> = property(
|
||||||
DeviceState.external(this, metaConverter, readInterval, initialState, reader),
|
DeviceState.external(this, metaConverter, readInterval, initialState, reader),
|
||||||
descriptorBuilder,
|
descriptorBuilder,
|
||||||
nameOverride,
|
nameOverride,
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register a mutable external state as a property
|
* Register a mutable external state as a property
|
||||||
*/
|
*/
|
||||||
public fun <T : Any> mutableProperty(
|
public fun <T : Any> DeviceConstructor.mutableProperty(
|
||||||
metaConverter: MetaConverter<T>,
|
metaConverter: MetaConverter<T>,
|
||||||
reader: suspend () -> T,
|
reader: suspend () -> T,
|
||||||
writer: suspend (T) -> Unit,
|
writer: suspend (T) -> Unit,
|
||||||
readInterval: Duration,
|
readInterval: Duration,
|
||||||
initialState: T,
|
initialState: T,
|
||||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||||
nameOverride: String? = null,
|
nameOverride: String? = null,
|
||||||
): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, MutableDeviceState<T>>> = property(
|
): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, MutableDeviceState<T>>> = property(
|
||||||
DeviceState.external(this, metaConverter, readInterval, initialState, reader, writer),
|
DeviceState.external(this, metaConverter, readInterval, initialState, reader, writer),
|
||||||
descriptorBuilder,
|
descriptorBuilder,
|
||||||
nameOverride,
|
nameOverride,
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create and register a virtual mutable property with optional [callback]
|
* Create and register a virtual mutable property with optional [callback]
|
||||||
*/
|
*/
|
||||||
public fun <T> virtualProperty(
|
public fun <T> DeviceConstructor.virtualProperty(
|
||||||
metaConverter: MetaConverter<T>,
|
metaConverter: MetaConverter<T>,
|
||||||
initialState: T,
|
initialState: T,
|
||||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||||
nameOverride: String? = null,
|
nameOverride: String? = null,
|
||||||
callback: (T) -> Unit = {},
|
callback: (T) -> Unit = {},
|
||||||
): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, MutableDeviceState<T>>> = property(
|
): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, MutableDeviceState<T>>> = property(
|
||||||
DeviceState.virtual(metaConverter, initialState, callback),
|
DeviceState.internal(metaConverter, initialState, callback),
|
||||||
descriptorBuilder,
|
descriptorBuilder,
|
||||||
nameOverride,
|
nameOverride,
|
||||||
)
|
)
|
||||||
}
|
|
||||||
|
/**
|
||||||
|
* Bind existing property provided by specification to this device
|
||||||
|
*/
|
||||||
|
public fun <T, D : Device> DeviceConstructor.deviceProperty(
|
||||||
|
device: D,
|
||||||
|
property: DevicePropertySpec<D, T>,
|
||||||
|
initialValue: T,
|
||||||
|
): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, DeviceState<T>>> =
|
||||||
|
property(device.propertyAsState(property, initialValue))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bind existing property provided by specification to this device
|
||||||
|
*/
|
||||||
|
public fun <T, D : Device> DeviceConstructor.deviceProperty(
|
||||||
|
device: D,
|
||||||
|
property: MutableDevicePropertySpec<D, T>,
|
||||||
|
initialValue: T,
|
||||||
|
): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, MutableDeviceState<T>>> =
|
||||||
|
property(device.mutablePropertyAsState(property, initialValue))
|
@ -9,9 +9,7 @@ import space.kscience.controls.api.*
|
|||||||
import space.kscience.controls.api.DeviceLifecycleState.*
|
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.*
|
||||||
import space.kscience.dataforge.context.Factory
|
|
||||||
import space.kscience.dataforge.context.request
|
|
||||||
import space.kscience.dataforge.meta.*
|
import space.kscience.dataforge.meta.*
|
||||||
import space.kscience.dataforge.misc.DFExperimental
|
import space.kscience.dataforge.misc.DFExperimental
|
||||||
import space.kscience.dataforge.names.*
|
import space.kscience.dataforge.names.*
|
||||||
@ -57,6 +55,7 @@ public open class DeviceGroup(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
logger.error(throwable) { "Exception in device $id" }
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -69,7 +68,7 @@ public open class DeviceGroup(
|
|||||||
* Register and initialize (synchronize child's lifecycle state with group state) a new device in this group
|
* Register and initialize (synchronize child's lifecycle state with group state) a new device in this group
|
||||||
*/
|
*/
|
||||||
@OptIn(DFExperimental::class)
|
@OptIn(DFExperimental::class)
|
||||||
public fun <D : Device> install(token: NameToken, device: D): D {
|
public open fun <D : Device> install(token: NameToken, device: D): D {
|
||||||
require(_devices[token] == null) { "A child device with name $token already exists" }
|
require(_devices[token] == null) { "A child device with name $token already exists" }
|
||||||
//start the child device if needed
|
//start the child device if needed
|
||||||
if (lifecycleState == STARTED || lifecycleState == STARTING) launch { device.start() }
|
if (lifecycleState == STARTED || lifecycleState == STARTING) launch { device.start() }
|
||||||
@ -82,7 +81,7 @@ public open class DeviceGroup(
|
|||||||
/**
|
/**
|
||||||
* Register a new property based on [DeviceState]. Properties could be modified dynamically
|
* Register a new property based on [DeviceState]. Properties could be modified dynamically
|
||||||
*/
|
*/
|
||||||
public fun registerProperty(descriptor: PropertyDescriptor, state: DeviceState<*>) {
|
public open fun registerProperty(descriptor: PropertyDescriptor, state: DeviceState<*>) {
|
||||||
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)
|
||||||
@ -126,37 +125,33 @@ public open class DeviceGroup(
|
|||||||
return action.invoke(argument)
|
return action.invoke(argument)
|
||||||
}
|
}
|
||||||
|
|
||||||
@DFExperimental
|
final override var lifecycleState: DeviceLifecycleState = DeviceLifecycleState.STOPPED
|
||||||
override var lifecycleState: DeviceLifecycleState = STOPPED
|
private set
|
||||||
protected set(value) {
|
|
||||||
if (field != value) {
|
|
||||||
launch {
|
private suspend fun setLifecycleState(lifecycleState: DeviceLifecycleState) {
|
||||||
sharedMessageFlow.emit(
|
this.lifecycleState = lifecycleState
|
||||||
DeviceLifeCycleMessage(value)
|
sharedMessageFlow.emit(
|
||||||
)
|
DeviceLifeCycleMessage(lifecycleState)
|
||||||
}
|
)
|
||||||
}
|
}
|
||||||
field = value
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@OptIn(DFExperimental::class)
|
|
||||||
override suspend fun start() {
|
override suspend fun start() {
|
||||||
lifecycleState = STARTING
|
setLifecycleState(STARTING)
|
||||||
super.start()
|
super.start()
|
||||||
devices.values.forEach {
|
devices.values.forEach {
|
||||||
it.start()
|
it.start()
|
||||||
}
|
}
|
||||||
lifecycleState = STARTED
|
setLifecycleState(STARTED)
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(DFExperimental::class)
|
override suspend fun stop() {
|
||||||
override fun stop() {
|
|
||||||
devices.values.forEach {
|
devices.values.forEach {
|
||||||
it.stop()
|
it.stop()
|
||||||
}
|
}
|
||||||
|
setLifecycleState(STOPPED)
|
||||||
super.stop()
|
super.stop()
|
||||||
lifecycleState = STOPPED
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public companion object {
|
public companion object {
|
||||||
@ -210,13 +205,9 @@ public fun <D : Device> DeviceGroup.install(name: Name, device: D): D {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public fun <D : Device> DeviceGroup.install(name: String, device: D): D =
|
public fun <D : Device> DeviceGroup.install(name: String, device: D): D = install(name.parseAsName(), device)
|
||||||
install(name.parseAsName(), device)
|
|
||||||
|
|
||||||
public fun <D : Device> DeviceGroup.install(device: D): D =
|
public fun <D : Device> DeviceGroup.install(device: D): D = install(device.id, device)
|
||||||
install(device.id, 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.
|
||||||
@ -292,7 +283,7 @@ public fun <T : Any> DeviceGroup.registerVirtualProperty(
|
|||||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||||
callback: (T) -> Unit = {},
|
callback: (T) -> Unit = {},
|
||||||
): MutableDeviceState<T> {
|
): MutableDeviceState<T> {
|
||||||
val state = DeviceState.virtual<T>(converter, initialValue, callback)
|
val state = DeviceState.internal<T>(converter, initialValue, callback)
|
||||||
registerMutableProperty(name, state, descriptorBuilder)
|
registerMutableProperty(name, state, descriptorBuilder)
|
||||||
return state
|
return state
|
||||||
}
|
}
|
||||||
|
@ -2,18 +2,13 @@ package space.kscience.controls.constructor
|
|||||||
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.*
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.flow.map
|
||||||
import space.kscience.controls.api.Device
|
import kotlinx.coroutines.flow.onEach
|
||||||
import space.kscience.controls.api.PropertyChangedMessage
|
|
||||||
import space.kscience.controls.spec.DevicePropertySpec
|
|
||||||
import space.kscience.controls.spec.MutableDevicePropertySpec
|
|
||||||
import space.kscience.controls.spec.name
|
|
||||||
import space.kscience.dataforge.meta.Meta
|
import space.kscience.dataforge.meta.Meta
|
||||||
import space.kscience.dataforge.meta.MetaConverter
|
import space.kscience.dataforge.meta.MetaConverter
|
||||||
import kotlin.reflect.KProperty
|
import kotlin.reflect.KProperty
|
||||||
import kotlin.time.Duration
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An observable state of a device
|
* An observable state of a device
|
||||||
@ -24,6 +19,8 @@ public interface DeviceState<T> {
|
|||||||
|
|
||||||
public val valueFlow: Flow<T>
|
public val valueFlow: Flow<T>
|
||||||
|
|
||||||
|
override fun toString(): String
|
||||||
|
|
||||||
public companion object
|
public companion object
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -36,7 +33,7 @@ public operator fun <T> DeviceState<T>.getValue(thisRef: Any?, property: KProper
|
|||||||
/**
|
/**
|
||||||
* Collect values in a given [scope]
|
* Collect values in a given [scope]
|
||||||
*/
|
*/
|
||||||
public fun <T> DeviceState<T>.collectValuesIn(scope: CoroutineScope, block: suspend (T)->Unit): Job =
|
public fun <T> DeviceState<T>.collectValuesIn(scope: CoroutineScope, block: suspend (T) -> Unit): Job =
|
||||||
valueFlow.onEach(block).launchIn(scope)
|
valueFlow.onEach(block).launchIn(scope)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -57,186 +54,46 @@ public var <T> MutableDeviceState<T>.valueAsMeta: Meta
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A [MutableDeviceState] that does not correspond to a physical state
|
* Device state with a value that depends on other device states
|
||||||
*
|
|
||||||
* @param callback a synchronous callback that could be used without a scope
|
|
||||||
*/
|
*/
|
||||||
private class VirtualDeviceState<T>(
|
public interface DeviceStateWithDependencies<T> : DeviceState<T> {
|
||||||
override val converter: MetaConverter<T>,
|
public val dependencies: Collection<DeviceState<*>>
|
||||||
initialValue: T,
|
|
||||||
private val callback: (T) -> Unit = {},
|
|
||||||
) : MutableDeviceState<T> {
|
|
||||||
private val flow = MutableStateFlow(initialValue)
|
|
||||||
override val valueFlow: Flow<T> get() = flow
|
|
||||||
|
|
||||||
override var value: T
|
|
||||||
get() = flow.value
|
|
||||||
set(value) {
|
|
||||||
flow.value = value
|
|
||||||
callback(value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A [MutableDeviceState] that does not correspond to a physical state
|
|
||||||
*
|
|
||||||
* @param callback a synchronous callback that could be used without a scope
|
|
||||||
*/
|
|
||||||
public fun <T> DeviceState.Companion.virtual(
|
|
||||||
converter: MetaConverter<T>,
|
|
||||||
initialValue: T,
|
|
||||||
callback: (T) -> Unit = {},
|
|
||||||
): MutableDeviceState<T> = VirtualDeviceState(converter, initialValue, callback)
|
|
||||||
|
|
||||||
private class StateFlowAsState<T>(
|
|
||||||
override val converter: MetaConverter<T>,
|
|
||||||
val flow: MutableStateFlow<T>,
|
|
||||||
) : MutableDeviceState<T> {
|
|
||||||
override var value: T by flow::value
|
|
||||||
override val valueFlow: Flow<T> get() = flow
|
|
||||||
}
|
|
||||||
|
|
||||||
public fun <T> MutableStateFlow<T>.asDeviceState(converter: MetaConverter<T>): DeviceState<T> =
|
|
||||||
StateFlowAsState(converter, this)
|
|
||||||
|
|
||||||
|
|
||||||
private open class BoundDeviceState<T>(
|
|
||||||
override val converter: MetaConverter<T>,
|
|
||||||
val device: Device,
|
|
||||||
val propertyName: String,
|
|
||||||
initialValue: T,
|
|
||||||
) : DeviceState<T> {
|
|
||||||
|
|
||||||
override val valueFlow: StateFlow<T> = device.messageFlow.filterIsInstance<PropertyChangedMessage>().filter {
|
|
||||||
it.property == propertyName
|
|
||||||
}.mapNotNull {
|
|
||||||
converter.read(it.value)
|
|
||||||
}.stateIn(device.context, SharingStarted.Eagerly, initialValue)
|
|
||||||
|
|
||||||
override val value: T get() = valueFlow.value
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Bind a read-only [DeviceState] to a [Device] property
|
* Create a new read-only [DeviceState] that mirrors receiver state by mapping the value with [mapper].
|
||||||
*/
|
*/
|
||||||
public suspend fun <T> Device.propertyAsState(
|
|
||||||
propertyName: String,
|
|
||||||
metaConverter: MetaConverter<T>,
|
|
||||||
): DeviceState<T> {
|
|
||||||
val initialValue = metaConverter.readOrNull(readProperty(propertyName)) ?: error("Conversion of property failed")
|
|
||||||
return BoundDeviceState(metaConverter, this, propertyName, initialValue)
|
|
||||||
}
|
|
||||||
|
|
||||||
public suspend fun <D : Device, T> D.propertyAsState(
|
|
||||||
propertySpec: DevicePropertySpec<D, T>,
|
|
||||||
): 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,
|
||||||
): DeviceState<R> = object : DeviceState<R> {
|
): DeviceStateWithDependencies<R> = object : DeviceStateWithDependencies<R> {
|
||||||
|
override val dependencies = listOf(this)
|
||||||
|
|
||||||
override val converter: MetaConverter<R> = converter
|
override val converter: MetaConverter<R> = converter
|
||||||
|
|
||||||
override val value: R
|
override val value: R
|
||||||
get() = mapper(this@map.value)
|
get() = mapper(this@map.value)
|
||||||
|
|
||||||
override val valueFlow: Flow<R> = this@map.valueFlow.map(mapper)
|
override val valueFlow: Flow<R> = this@map.valueFlow.map(mapper)
|
||||||
}
|
|
||||||
|
|
||||||
private class MutableBoundDeviceState<T>(
|
override fun toString(): String = "DeviceState.map(arg=${this@map}, converter=$converter)"
|
||||||
converter: MetaConverter<T>,
|
|
||||||
device: Device,
|
|
||||||
propertyName: String,
|
|
||||||
initialValue: T,
|
|
||||||
) : BoundDeviceState<T>(converter, device, propertyName, initialValue), MutableDeviceState<T> {
|
|
||||||
|
|
||||||
override var value: T
|
|
||||||
get() = valueFlow.value
|
|
||||||
set(newValue) {
|
|
||||||
device.launch {
|
|
||||||
device.writeProperty(propertyName, converter.convert(newValue))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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,
|
|
||||||
metaConverter: MetaConverter<T>,
|
|
||||||
): MutableDeviceState<T> {
|
|
||||||
val initialValue = metaConverter.readOrNull(readProperty(propertyName)) ?: error("Conversion of property failed")
|
|
||||||
return mutablePropertyAsState(propertyName, metaConverter, initialValue)
|
|
||||||
}
|
|
||||||
|
|
||||||
public suspend fun <D : Device, T> D.mutablePropertyAsState(
|
|
||||||
propertySpec: MutableDevicePropertySpec<D, T>,
|
|
||||||
): MutableDeviceState<T> = mutablePropertyAsState(propertySpec.name, propertySpec.converter)
|
|
||||||
|
|
||||||
public fun <D : Device, T> D.mutablePropertyAsState(
|
|
||||||
propertySpec: MutableDevicePropertySpec<D, T>,
|
|
||||||
initialValue: T,
|
|
||||||
): MutableDeviceState<T> = mutablePropertyAsState(propertySpec.name, propertySpec.converter, initialValue)
|
|
||||||
|
|
||||||
|
|
||||||
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
|
* Combine two device states into one read-only [DeviceState]. Only the latest value of each state is used.
|
||||||
*/
|
*/
|
||||||
public fun <T> DeviceState.Companion.external(
|
public fun <T1, T2, R> combine(
|
||||||
scope: CoroutineScope,
|
state1: DeviceState<T1>,
|
||||||
converter: MetaConverter<T>,
|
state2: DeviceState<T2>,
|
||||||
readInterval: Duration,
|
converter: MetaConverter<R>,
|
||||||
initialValue: T,
|
mapper: (T1, T2) -> R,
|
||||||
reader: suspend () -> T,
|
): DeviceStateWithDependencies<R> = object : DeviceStateWithDependencies<R> {
|
||||||
): DeviceState<T> = ExternalState(scope, converter, readInterval, initialValue, reader)
|
override val dependencies = listOf(state1, state2)
|
||||||
|
|
||||||
private class MutableExternalState<T>(
|
override val converter: MetaConverter<R> = converter
|
||||||
scope: CoroutineScope,
|
|
||||||
converter: MetaConverter<T>,
|
override val value: R get() = mapper(state1.value, state2.value)
|
||||||
readInterval: Duration,
|
|
||||||
initialValue: T,
|
override val valueFlow: Flow<R> = kotlinx.coroutines.flow.combine(state1.valueFlow, state2.valueFlow, mapper)
|
||||||
reader: suspend () -> T,
|
|
||||||
val writer: suspend (T) -> Unit,
|
override fun toString(): String = "DeviceState.combine(state1=$state1, state2=$state2)"
|
||||||
) : ExternalState<T>(scope, converter, readInterval, initialValue, reader), MutableDeviceState<T> {
|
|
||||||
override var value: T
|
|
||||||
get() = super.value
|
|
||||||
set(value) {
|
|
||||||
scope.launch {
|
|
||||||
writer(value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a [DeviceState] that regularly reads and caches an external 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)
|
|
@ -0,0 +1,41 @@
|
|||||||
|
package space.kscience.controls.constructor
|
||||||
|
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.isActive
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.datetime.Instant
|
||||||
|
import space.kscience.controls.manager.ClockManager
|
||||||
|
import space.kscience.controls.spec.instant
|
||||||
|
import space.kscience.dataforge.meta.MetaConverter
|
||||||
|
import kotlin.time.Duration
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A dedicated [DeviceState] that operates with time.
|
||||||
|
* The state changes with [tick] interval and always shows the time of the last update.
|
||||||
|
*
|
||||||
|
* Both [tick] and current time are computed by [clockManager] enabling time manipulation.
|
||||||
|
*
|
||||||
|
* The timer runs indefinitely until the parent context is closed
|
||||||
|
*/
|
||||||
|
public class TimerState(
|
||||||
|
public val clockManager: ClockManager,
|
||||||
|
public val tick: Duration,
|
||||||
|
) : DeviceState<Instant> {
|
||||||
|
override val converter: MetaConverter<Instant> get() = MetaConverter.instant
|
||||||
|
|
||||||
|
private val clock = MutableStateFlow(clockManager.clock.now())
|
||||||
|
|
||||||
|
private val updateJob = clockManager.context.launch {
|
||||||
|
while (isActive) {
|
||||||
|
clockManager.delay(tick)
|
||||||
|
clock.value = clockManager.clock.now()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override val valueFlow: Flow<Instant> get() = clock
|
||||||
|
|
||||||
|
override val value: Instant get() = clock.value
|
||||||
|
|
||||||
|
override fun toString(): String = "TimerState(tick=$tick)"
|
||||||
|
}
|
@ -0,0 +1,103 @@
|
|||||||
|
package space.kscience.controls.constructor
|
||||||
|
|
||||||
|
import kotlinx.coroutines.flow.*
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import space.kscience.controls.api.Device
|
||||||
|
import space.kscience.controls.api.PropertyChangedMessage
|
||||||
|
import space.kscience.controls.api.id
|
||||||
|
import space.kscience.controls.spec.DevicePropertySpec
|
||||||
|
import space.kscience.controls.spec.MutableDevicePropertySpec
|
||||||
|
import space.kscience.controls.spec.name
|
||||||
|
import space.kscience.dataforge.meta.MetaConverter
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A copy-free [DeviceState] bound to a device property
|
||||||
|
*/
|
||||||
|
private open class BoundDeviceState<T>(
|
||||||
|
override val converter: MetaConverter<T>,
|
||||||
|
val device: Device,
|
||||||
|
val propertyName: String,
|
||||||
|
initialValue: T,
|
||||||
|
) : DeviceState<T> {
|
||||||
|
|
||||||
|
override val valueFlow: StateFlow<T> = device.messageFlow.filterIsInstance<PropertyChangedMessage>().filter {
|
||||||
|
it.property == propertyName
|
||||||
|
}.mapNotNull {
|
||||||
|
converter.read(it.value)
|
||||||
|
}.stateIn(device.context, SharingStarted.Eagerly, initialValue)
|
||||||
|
|
||||||
|
override val value: T get() = valueFlow.value
|
||||||
|
override fun toString(): String =
|
||||||
|
"BoundDeviceState(converter=$converter, device=${device.id}, propertyName='$propertyName')"
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public fun <T> Device.propertyAsState(
|
||||||
|
propertyName: String,
|
||||||
|
metaConverter: MetaConverter<T>,
|
||||||
|
initialValue: T,
|
||||||
|
): DeviceState<T> = BoundDeviceState(metaConverter, this, propertyName, initialValue)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bind a read-only [DeviceState] to a [Device] property
|
||||||
|
*/
|
||||||
|
public suspend fun <T> Device.propertyAsState(
|
||||||
|
propertyName: String,
|
||||||
|
metaConverter: MetaConverter<T>,
|
||||||
|
): DeviceState<T> = propertyAsState(
|
||||||
|
propertyName,
|
||||||
|
metaConverter,
|
||||||
|
metaConverter.readOrNull(readProperty(propertyName)) ?: error("Conversion of property failed")
|
||||||
|
)
|
||||||
|
|
||||||
|
public suspend fun <D : Device, T> D.propertyAsState(
|
||||||
|
propertySpec: DevicePropertySpec<D, T>,
|
||||||
|
): DeviceState<T> = propertyAsState(propertySpec.name, propertySpec.converter)
|
||||||
|
|
||||||
|
public fun <D : Device, T> D.propertyAsState(
|
||||||
|
propertySpec: DevicePropertySpec<D, T>,
|
||||||
|
initialValue: T,
|
||||||
|
): DeviceState<T> = propertyAsState(propertySpec.name, propertySpec.converter, initialValue)
|
||||||
|
|
||||||
|
|
||||||
|
private class MutableBoundDeviceState<T>(
|
||||||
|
converter: MetaConverter<T>,
|
||||||
|
device: Device,
|
||||||
|
propertyName: String,
|
||||||
|
initialValue: T,
|
||||||
|
) : BoundDeviceState<T>(converter, device, propertyName, initialValue), MutableDeviceState<T> {
|
||||||
|
|
||||||
|
override var value: T
|
||||||
|
get() = valueFlow.value
|
||||||
|
set(newValue) {
|
||||||
|
device.launch {
|
||||||
|
device.writeProperty(propertyName, converter.convert(newValue))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
metaConverter: MetaConverter<T>,
|
||||||
|
): MutableDeviceState<T> {
|
||||||
|
val initialValue = metaConverter.readOrNull(readProperty(propertyName)) ?: error("Conversion of property failed")
|
||||||
|
return mutablePropertyAsState(propertyName, metaConverter, initialValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
public suspend fun <D : Device, T> D.mutablePropertyAsState(
|
||||||
|
propertySpec: MutableDevicePropertySpec<D, T>,
|
||||||
|
): MutableDeviceState<T> = mutablePropertyAsState(propertySpec.name, propertySpec.converter)
|
||||||
|
|
||||||
|
public fun <D : Device, T> D.mutablePropertyAsState(
|
||||||
|
propertySpec: MutableDevicePropertySpec<D, T>,
|
||||||
|
initialValue: T,
|
||||||
|
): MutableDeviceState<T> = mutablePropertyAsState(propertySpec.name, propertySpec.converter, initialValue)
|
||||||
|
|
@ -38,6 +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 }
|
||||||
|
|
||||||
|
override fun toString(): String = "DoubleRangeState(range=$range, converter=$converter)"
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("UnusedReceiverParameter")
|
@Suppress("UnusedReceiverParameter")
|
||||||
|
@ -0,0 +1,70 @@
|
|||||||
|
package space.kscience.controls.constructor
|
||||||
|
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.flow.*
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import space.kscience.dataforge.meta.MetaConverter
|
||||||
|
import kotlin.time.Duration
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
override fun toString(): String = "ExternalState(converter=$converter)"
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a [DeviceState] which is constructed by regularly 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a [MutableDeviceState] which is constructed by regularly reading external value and allows writing
|
||||||
|
*/
|
||||||
|
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)
|
@ -0,0 +1,23 @@
|
|||||||
|
package space.kscience.controls.constructor
|
||||||
|
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import space.kscience.dataforge.meta.MetaConverter
|
||||||
|
|
||||||
|
|
||||||
|
private class StateFlowAsState<T>(
|
||||||
|
override val converter: MetaConverter<T>,
|
||||||
|
val flow: MutableStateFlow<T>,
|
||||||
|
) : MutableDeviceState<T> {
|
||||||
|
override var value: T by flow::value
|
||||||
|
override val valueFlow: Flow<T> get() = flow
|
||||||
|
|
||||||
|
override fun toString(): String = "FlowAsState(converter=$converter)"
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a read-only [DeviceState] that wraps [MutableStateFlow].
|
||||||
|
* No data copy is performed.
|
||||||
|
*/
|
||||||
|
public fun <T> MutableStateFlow<T>.asDeviceState(converter: MetaConverter<T>): DeviceState<T> =
|
||||||
|
StateFlowAsState(converter, this)
|
@ -0,0 +1,42 @@
|
|||||||
|
package space.kscience.controls.constructor
|
||||||
|
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import space.kscience.dataforge.meta.MetaConverter
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A [MutableDeviceState] that does not correspond to a physical state
|
||||||
|
*
|
||||||
|
* @param callback a synchronous callback that could be used without a scope
|
||||||
|
*/
|
||||||
|
private class VirtualDeviceState<T>(
|
||||||
|
override val converter: MetaConverter<T>,
|
||||||
|
initialValue: T,
|
||||||
|
private val callback: (T) -> Unit = {},
|
||||||
|
) : MutableDeviceState<T> {
|
||||||
|
private val flow = MutableStateFlow(initialValue)
|
||||||
|
override val valueFlow: Flow<T> get() = flow
|
||||||
|
|
||||||
|
override var value: T
|
||||||
|
get() = flow.value
|
||||||
|
set(value) {
|
||||||
|
flow.value = value
|
||||||
|
callback(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toString(): String = "VirtualDeviceState(converter=$converter)"
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A [MutableDeviceState] that does not correspond to a physical state
|
||||||
|
*
|
||||||
|
* @param callback a synchronous callback that could be used without a scope
|
||||||
|
*/
|
||||||
|
public fun <T> DeviceState.Companion.internal(
|
||||||
|
converter: MetaConverter<T>,
|
||||||
|
initialValue: T,
|
||||||
|
callback: (T) -> Unit = {},
|
||||||
|
): MutableDeviceState<T> = VirtualDeviceState(converter, initialValue, callback)
|
@ -1,10 +1,12 @@
|
|||||||
package space.kscience.controls.constructor
|
package space.kscience.controls.constructor.library
|
||||||
|
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.isActive
|
import kotlinx.coroutines.isActive
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import space.kscience.controls.api.Device
|
import space.kscience.controls.api.Device
|
||||||
|
import space.kscience.controls.constructor.MutableDeviceState
|
||||||
|
import space.kscience.controls.constructor.mutablePropertyAsState
|
||||||
import space.kscience.controls.manager.clock
|
import space.kscience.controls.manager.clock
|
||||||
import space.kscience.controls.spec.*
|
import space.kscience.controls.spec.*
|
||||||
import space.kscience.dataforge.context.Context
|
import space.kscience.dataforge.context.Context
|
||||||
@ -49,7 +51,7 @@ public class VirtualDrive(
|
|||||||
public val positionState: MutableDeviceState<Double>,
|
public val positionState: MutableDeviceState<Double>,
|
||||||
) : Drive, DeviceBySpec<Drive>(Drive, context) {
|
) : Drive, DeviceBySpec<Drive>(Drive, context) {
|
||||||
|
|
||||||
private val dt = meta["time.step"].double?.milliseconds ?: 1.milliseconds
|
private val dt = meta["time.step"].double?.milliseconds ?: 5.milliseconds
|
||||||
private val clock = context.clock
|
private val clock = context.clock
|
||||||
|
|
||||||
override var force: Double = 0.0
|
override var force: Double = 0.0
|
||||||
@ -82,7 +84,7 @@ public class VirtualDrive(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStop() {
|
override suspend fun onStop() {
|
||||||
updateJob?.cancel()
|
updateJob?.cancel()
|
||||||
}
|
}
|
||||||
|
|
@ -1,8 +1,9 @@
|
|||||||
package space.kscience.controls.constructor
|
package space.kscience.controls.constructor.library
|
||||||
|
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import space.kscience.controls.api.Device
|
import space.kscience.controls.api.Device
|
||||||
|
import space.kscience.controls.constructor.DeviceState
|
||||||
import space.kscience.controls.spec.DeviceBySpec
|
import space.kscience.controls.spec.DeviceBySpec
|
||||||
import space.kscience.controls.spec.DevicePropertySpec
|
import space.kscience.controls.spec.DevicePropertySpec
|
||||||
import space.kscience.controls.spec.DeviceSpec
|
import space.kscience.controls.spec.DeviceSpec
|
||||||
@ -20,7 +21,7 @@ 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, _ ->
|
public operator fun invoke(lockedState: DeviceState<Boolean>): Factory<LimitSwitch> = Factory { context, _ ->
|
||||||
VirtualLimitSwitch(context, lockedState)
|
VirtualLimitSwitch(context, lockedState)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -34,7 +35,7 @@ public class VirtualLimitSwitch(
|
|||||||
public val lockedState: DeviceState<Boolean>,
|
public val lockedState: DeviceState<Boolean>,
|
||||||
) : DeviceBySpec<LimitSwitch>(LimitSwitch, context), LimitSwitch {
|
) : DeviceBySpec<LimitSwitch>(LimitSwitch, context), LimitSwitch {
|
||||||
|
|
||||||
init {
|
override suspend fun onStart() {
|
||||||
lockedState.valueFlow.onEach {
|
lockedState.valueFlow.onEach {
|
||||||
propertyChanged(LimitSwitch.locked, it)
|
propertyChanged(LimitSwitch.locked, it)
|
||||||
}.launchIn(this)
|
}.launchIn(this)
|
@ -1,4 +1,4 @@
|
|||||||
package space.kscience.controls.constructor
|
package space.kscience.controls.constructor.library
|
||||||
|
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
@ -7,8 +7,11 @@ import kotlinx.coroutines.launch
|
|||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.sync.Mutex
|
||||||
import kotlinx.coroutines.sync.withLock
|
import kotlinx.coroutines.sync.withLock
|
||||||
import kotlinx.datetime.Instant
|
import kotlinx.datetime.Instant
|
||||||
|
import space.kscience.controls.constructor.DeviceGroup
|
||||||
|
import space.kscience.controls.constructor.install
|
||||||
import space.kscience.controls.manager.clock
|
import space.kscience.controls.manager.clock
|
||||||
import space.kscience.controls.spec.DeviceBySpec
|
import space.kscience.controls.spec.DeviceBySpec
|
||||||
|
import space.kscience.controls.spec.write
|
||||||
import kotlin.time.Duration
|
import kotlin.time.Duration
|
||||||
import kotlin.time.Duration.Companion.milliseconds
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
import kotlin.time.DurationUnit
|
import kotlin.time.DurationUnit
|
||||||
@ -71,15 +74,17 @@ public class PidRegulator(
|
|||||||
lastTime = realTime
|
lastTime = realTime
|
||||||
lastPosition = drive.position
|
lastPosition = drive.position
|
||||||
|
|
||||||
drive.force = pidParameters.kp * delta + pidParameters.ki * integral + pidParameters.kd * derivative
|
drive.write(Drive.force,pidParameters.kp * delta + pidParameters.ki * integral + pidParameters.kd * derivative)
|
||||||
|
//drive.force = pidParameters.kp * delta + pidParameters.ki * integral + pidParameters.kd * derivative
|
||||||
propertyChanged(Regulator.position, drive.position)
|
propertyChanged(Regulator.position, drive.position)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStop() {
|
override suspend fun onStop() {
|
||||||
updateJob?.cancel()
|
updateJob?.cancel()
|
||||||
|
drive.stop()
|
||||||
}
|
}
|
||||||
|
|
||||||
override val position: Double get() = drive.position
|
override val position: Double get() = drive.position
|
@ -1,4 +1,4 @@
|
|||||||
package space.kscience.controls.constructor
|
package space.kscience.controls.constructor.library
|
||||||
|
|
||||||
import space.kscience.controls.api.Device
|
import space.kscience.controls.api.Device
|
||||||
import space.kscience.controls.spec.*
|
import space.kscience.controls.spec.*
|
@ -0,0 +1,43 @@
|
|||||||
|
package space.kscience.controls.constructor
|
||||||
|
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import space.kscience.controls.api.Device
|
||||||
|
import space.kscience.controls.api.DeviceLifeCycleMessage
|
||||||
|
import space.kscience.controls.api.DeviceLifecycleState
|
||||||
|
import space.kscience.controls.manager.DeviceManager
|
||||||
|
import space.kscience.controls.manager.install
|
||||||
|
import space.kscience.controls.spec.doRecurring
|
||||||
|
import space.kscience.dataforge.context.Context
|
||||||
|
import space.kscience.dataforge.context.Factory
|
||||||
|
import space.kscience.dataforge.context.Global
|
||||||
|
import space.kscience.dataforge.context.request
|
||||||
|
import space.kscience.dataforge.meta.Meta
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
|
|
||||||
|
class DeviceGroupTest {
|
||||||
|
|
||||||
|
class TestDevice(context: Context) : DeviceConstructor(context) {
|
||||||
|
|
||||||
|
companion object : Factory<Device> {
|
||||||
|
override fun build(context: Context, meta: Meta): Device = TestDevice(context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testRecurringRead() = runTest {
|
||||||
|
var counter = 10
|
||||||
|
val testDevice = Global.request(DeviceManager).install("test", TestDevice)
|
||||||
|
testDevice.doRecurring(1.milliseconds) {
|
||||||
|
counter--
|
||||||
|
println(counter)
|
||||||
|
if (counter <= 0) {
|
||||||
|
testDevice.stop()
|
||||||
|
}
|
||||||
|
error("Error!")
|
||||||
|
}
|
||||||
|
testDevice.messageFlow.first { it is DeviceLifeCycleMessage && it.state == DeviceLifecycleState.STOPPED }
|
||||||
|
println("stopped")
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,22 @@
|
|||||||
|
package space.kscience.controls.constructor
|
||||||
|
|
||||||
|
import kotlinx.coroutines.flow.collect
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
import kotlinx.coroutines.flow.take
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import space.kscience.controls.manager.ClockManager
|
||||||
|
import space.kscience.dataforge.context.Global
|
||||||
|
import space.kscience.dataforge.context.request
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
|
|
||||||
|
class TimerTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun timer() = runTest {
|
||||||
|
val timer = TimerState(Global.request(ClockManager), 10.milliseconds)
|
||||||
|
timer.valueFlow.take(10).onEach {
|
||||||
|
println(it)
|
||||||
|
}.collect()
|
||||||
|
}
|
||||||
|
}
|
@ -12,7 +12,6 @@ import space.kscience.dataforge.context.logger
|
|||||||
import space.kscience.dataforge.meta.Meta
|
import space.kscience.dataforge.meta.Meta
|
||||||
import space.kscience.dataforge.meta.get
|
import space.kscience.dataforge.meta.get
|
||||||
import space.kscience.dataforge.meta.string
|
import space.kscience.dataforge.meta.string
|
||||||
import space.kscience.dataforge.misc.DFExperimental
|
|
||||||
import space.kscience.dataforge.misc.DfType
|
import space.kscience.dataforge.misc.DfType
|
||||||
import space.kscience.dataforge.names.parseAsName
|
import space.kscience.dataforge.names.parseAsName
|
||||||
|
|
||||||
@ -100,12 +99,11 @@ public interface Device : ContextAware, CoroutineScope {
|
|||||||
/**
|
/**
|
||||||
* Close and terminate the device. This function does not wait for the device to be closed.
|
* Close and terminate the device. This function does not wait for the device to be closed.
|
||||||
*/
|
*/
|
||||||
public fun stop() {
|
public suspend fun stop() {
|
||||||
logger.info { "Device $this is closed" }
|
logger.info { "Device $this is closed" }
|
||||||
cancel("The device is closed")
|
cancel("The device is closed")
|
||||||
}
|
}
|
||||||
|
|
||||||
@DFExperimental
|
|
||||||
public val lifecycleState: DeviceLifecycleState
|
public val lifecycleState: DeviceLifecycleState
|
||||||
|
|
||||||
public companion object {
|
public companion object {
|
||||||
|
@ -48,7 +48,7 @@ public operator fun DeviceHub.get(nameToken: NameToken): Device =
|
|||||||
|
|
||||||
public fun DeviceHub.getOrNull(name: Name): Device? = when {
|
public fun DeviceHub.getOrNull(name: Name): Device? = when {
|
||||||
name.isEmpty() -> this as? Device
|
name.isEmpty() -> this as? Device
|
||||||
name.length == 1 -> get(name.firstOrNull()!!)
|
name.length == 1 -> devices[name.firstOrNull()!!]
|
||||||
else -> (get(name.firstOrNull()!!) as? DeviceHub)?.getOrNull(name.cutFirst())
|
else -> (get(name.firstOrNull()!!) as? DeviceHub)?.getOrNull(name.cutFirst())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -15,18 +15,23 @@ public class PropertyDescriptor(
|
|||||||
public var description: String? = null,
|
public var description: String? = null,
|
||||||
public var metaDescriptor: MetaDescriptor = MetaDescriptor(),
|
public var metaDescriptor: MetaDescriptor = MetaDescriptor(),
|
||||||
public var readable: Boolean = true,
|
public var readable: Boolean = true,
|
||||||
public var mutable: Boolean = false
|
public var mutable: Boolean = false,
|
||||||
)
|
)
|
||||||
|
|
||||||
public fun PropertyDescriptor.metaDescriptor(block: MetaDescriptorBuilder.()->Unit){
|
public fun PropertyDescriptor.metaDescriptor(block: MetaDescriptorBuilder.() -> Unit) {
|
||||||
metaDescriptor = MetaDescriptor(block)
|
metaDescriptor = MetaDescriptor {
|
||||||
|
from(metaDescriptor)
|
||||||
|
block()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A descriptor for property
|
* A descriptor for property
|
||||||
*/
|
*/
|
||||||
@Serializable
|
@Serializable
|
||||||
public class ActionDescriptor(public val name: String) {
|
public class ActionDescriptor(
|
||||||
public var description: String? = null
|
public val name: String,
|
||||||
}
|
public var description: String? = null,
|
||||||
|
public var inputMetaDescriptor: MetaDescriptor = MetaDescriptor(),
|
||||||
|
public var outputMetaDescriptor: MetaDescriptor = MetaDescriptor()
|
||||||
|
)
|
||||||
|
@ -6,15 +6,21 @@ import space.kscience.dataforge.context.Context
|
|||||||
import space.kscience.dataforge.context.PluginFactory
|
import space.kscience.dataforge.context.PluginFactory
|
||||||
import space.kscience.dataforge.context.PluginTag
|
import space.kscience.dataforge.context.PluginTag
|
||||||
import space.kscience.dataforge.meta.Meta
|
import space.kscience.dataforge.meta.Meta
|
||||||
|
import kotlin.time.Duration
|
||||||
|
|
||||||
public class ClockManager : AbstractPlugin() {
|
public class ClockManager : AbstractPlugin() {
|
||||||
override val tag: PluginTag get() = DeviceManager.tag
|
override val tag: PluginTag get() = Companion.tag
|
||||||
|
|
||||||
public val clock: Clock by lazy {
|
public val clock: Clock by lazy {
|
||||||
//TODO add clock customization
|
//TODO add clock customization
|
||||||
Clock.System
|
Clock.System
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public suspend fun delay(duration: Duration) {
|
||||||
|
//TODO add time compression
|
||||||
|
kotlinx.coroutines.delay(duration)
|
||||||
|
}
|
||||||
|
|
||||||
public companion object : PluginFactory<ClockManager> {
|
public companion object : PluginFactory<ClockManager> {
|
||||||
override val tag: PluginTag = PluginTag("clock", group = PluginTag.DATAFORGE_GROUP)
|
override val tag: PluginTag = PluginTag("clock", group = PluginTag.DATAFORGE_GROUP)
|
||||||
|
|
||||||
|
@ -49,6 +49,10 @@ public fun <D : Device> DeviceManager.install(name: String, device: D): D {
|
|||||||
public fun <D : Device> DeviceManager.install(device: D): D = install(device.id, device)
|
public fun <D : Device> DeviceManager.install(device: D): D = install(device.id, device)
|
||||||
|
|
||||||
|
|
||||||
|
public fun <D : Device> Context.install(name: String, device: D): D = request(DeviceManager).install(name, device)
|
||||||
|
|
||||||
|
public fun <D : Device> Context.install(device: D): D = request(DeviceManager).install(device.id, device)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register and start a device built by [factory] with current [Context] and [meta].
|
* Register and start a device built by [factory] with current [Context] and [meta].
|
||||||
*/
|
*/
|
||||||
@ -75,5 +79,4 @@ public inline fun <D : Device> DeviceManager.installing(
|
|||||||
current as D
|
current as D
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -100,6 +100,7 @@ public abstract class AbstractAsynchronousPort(
|
|||||||
* Send a data packet via the port
|
* Send a data packet via the port
|
||||||
*/
|
*/
|
||||||
override suspend fun send(data: ByteArray) {
|
override suspend fun send(data: ByteArray) {
|
||||||
|
check(isOpen){"The port is not opened"}
|
||||||
outgoing.send(data)
|
outgoing.send(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -9,11 +9,11 @@ import kotlinx.coroutines.sync.withLock
|
|||||||
import space.kscience.controls.api.*
|
import space.kscience.controls.api.*
|
||||||
import space.kscience.dataforge.context.Context
|
import space.kscience.dataforge.context.Context
|
||||||
import space.kscience.dataforge.context.debug
|
import space.kscience.dataforge.context.debug
|
||||||
|
import space.kscience.dataforge.context.error
|
||||||
import space.kscience.dataforge.context.logger
|
import space.kscience.dataforge.context.logger
|
||||||
import space.kscience.dataforge.meta.Meta
|
import space.kscience.dataforge.meta.Meta
|
||||||
import space.kscience.dataforge.meta.get
|
import space.kscience.dataforge.meta.get
|
||||||
import space.kscience.dataforge.meta.int
|
import space.kscience.dataforge.meta.int
|
||||||
import space.kscience.dataforge.misc.DFExperimental
|
|
||||||
import kotlin.coroutines.CoroutineContext
|
import kotlin.coroutines.CoroutineContext
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -72,10 +72,10 @@ public abstract class DeviceBase<D : Device>(
|
|||||||
onBufferOverflow = BufferOverflow.DROP_OLDEST
|
onBufferOverflow = BufferOverflow.DROP_OLDEST
|
||||||
)
|
)
|
||||||
|
|
||||||
@OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
override val coroutineContext: CoroutineContext = context.newCoroutineContext(
|
override val coroutineContext: CoroutineContext = context.newCoroutineContext(
|
||||||
SupervisorJob(context.coroutineContext[Job]) +
|
SupervisorJob(context.coroutineContext[Job]) +
|
||||||
CoroutineName("Device $this") +
|
CoroutineName("Device $id") +
|
||||||
CoroutineExceptionHandler { _, throwable ->
|
CoroutineExceptionHandler { _, throwable ->
|
||||||
launch {
|
launch {
|
||||||
sharedMessageFlow.emit(
|
sharedMessageFlow.emit(
|
||||||
@ -86,6 +86,7 @@ public abstract class DeviceBase<D : Device>(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
logger.error(throwable) { "Exception in device $id" }
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -187,43 +188,39 @@ public abstract class DeviceBase<D : Device>(
|
|||||||
return spec.executeWithMeta(self, argument ?: Meta.EMPTY)
|
return spec.executeWithMeta(self, argument ?: Meta.EMPTY)
|
||||||
}
|
}
|
||||||
|
|
||||||
@DFExperimental
|
|
||||||
final override var lifecycleState: DeviceLifecycleState = DeviceLifecycleState.STOPPED
|
final override var lifecycleState: DeviceLifecycleState = DeviceLifecycleState.STOPPED
|
||||||
private set(value) {
|
private set
|
||||||
if (field != value) {
|
|
||||||
launch {
|
|
||||||
sharedMessageFlow.emit(
|
private suspend fun setLifecycleState(lifecycleState: DeviceLifecycleState) {
|
||||||
DeviceLifeCycleMessage(value)
|
this.lifecycleState = lifecycleState
|
||||||
)
|
sharedMessageFlow.emit(
|
||||||
}
|
DeviceLifeCycleMessage(lifecycleState)
|
||||||
}
|
)
|
||||||
field = value
|
}
|
||||||
}
|
|
||||||
|
|
||||||
protected open suspend fun onStart() {
|
protected open suspend fun onStart() {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(DFExperimental::class)
|
|
||||||
final override suspend fun start() {
|
final override suspend fun start() {
|
||||||
if (lifecycleState == DeviceLifecycleState.STOPPED) {
|
if (lifecycleState == DeviceLifecycleState.STOPPED) {
|
||||||
super.start()
|
super.start()
|
||||||
lifecycleState = DeviceLifecycleState.STARTING
|
setLifecycleState(DeviceLifecycleState.STARTING)
|
||||||
onStart()
|
onStart()
|
||||||
lifecycleState = DeviceLifecycleState.STARTED
|
setLifecycleState(DeviceLifecycleState.STARTED)
|
||||||
} else {
|
} else {
|
||||||
logger.debug { "Device $this is already started" }
|
logger.debug { "Device $this is already started" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected open fun onStop() {
|
protected open suspend fun onStop() {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(DFExperimental::class)
|
final override suspend fun stop() {
|
||||||
final override fun stop() {
|
|
||||||
onStop()
|
onStop()
|
||||||
lifecycleState = DeviceLifecycleState.STOPPED
|
setLifecycleState(DeviceLifecycleState.STOPPED)
|
||||||
super.stop()
|
super.stop()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -20,7 +20,7 @@ public open class DeviceBySpec<D : Device>(
|
|||||||
self.onOpen()
|
self.onOpen()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStop(): Unit = with(spec){
|
override suspend fun onStop(): Unit = with(spec){
|
||||||
self.onClose()
|
self.onClose()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,8 +4,10 @@ import kotlinx.coroutines.withContext
|
|||||||
import space.kscience.controls.api.ActionDescriptor
|
import space.kscience.controls.api.ActionDescriptor
|
||||||
import space.kscience.controls.api.Device
|
import space.kscience.controls.api.Device
|
||||||
import space.kscience.controls.api.PropertyDescriptor
|
import space.kscience.controls.api.PropertyDescriptor
|
||||||
|
import space.kscience.controls.api.metaDescriptor
|
||||||
import space.kscience.dataforge.meta.Meta
|
import space.kscience.dataforge.meta.Meta
|
||||||
import space.kscience.dataforge.meta.MetaConverter
|
import space.kscience.dataforge.meta.MetaConverter
|
||||||
|
import space.kscience.dataforge.meta.descriptors.MetaDescriptor
|
||||||
import kotlin.properties.PropertyDelegateProvider
|
import kotlin.properties.PropertyDelegateProvider
|
||||||
import kotlin.properties.ReadOnlyProperty
|
import kotlin.properties.ReadOnlyProperty
|
||||||
import kotlin.reflect.KProperty
|
import kotlin.reflect.KProperty
|
||||||
@ -54,6 +56,11 @@ public abstract class DeviceSpec<D : Device> {
|
|||||||
val deviceProperty = object : DevicePropertySpec<D, T> {
|
val deviceProperty = object : DevicePropertySpec<D, T> {
|
||||||
|
|
||||||
override val descriptor: PropertyDescriptor = PropertyDescriptor(propertyName).apply {
|
override val descriptor: PropertyDescriptor = PropertyDescriptor(propertyName).apply {
|
||||||
|
converter.descriptor?.let { converterDescriptor ->
|
||||||
|
metaDescriptor {
|
||||||
|
from(converterDescriptor)
|
||||||
|
}
|
||||||
|
}
|
||||||
fromSpec(property)
|
fromSpec(property)
|
||||||
descriptorBuilder()
|
descriptorBuilder()
|
||||||
}
|
}
|
||||||
@ -83,6 +90,11 @@ public abstract class DeviceSpec<D : Device> {
|
|||||||
propertyName,
|
propertyName,
|
||||||
mutable = true
|
mutable = true
|
||||||
).apply {
|
).apply {
|
||||||
|
converter.descriptor?.let { converterDescriptor ->
|
||||||
|
metaDescriptor {
|
||||||
|
from(converterDescriptor)
|
||||||
|
}
|
||||||
|
}
|
||||||
fromSpec(property)
|
fromSpec(property)
|
||||||
descriptorBuilder()
|
descriptorBuilder()
|
||||||
}
|
}
|
||||||
@ -118,6 +130,19 @@ public abstract class DeviceSpec<D : Device> {
|
|||||||
val actionName = name ?: property.name
|
val actionName = name ?: property.name
|
||||||
val deviceAction = object : DeviceActionSpec<D, I, O> {
|
val deviceAction = object : DeviceActionSpec<D, I, O> {
|
||||||
override val descriptor: ActionDescriptor = ActionDescriptor(actionName).apply {
|
override val descriptor: ActionDescriptor = ActionDescriptor(actionName).apply {
|
||||||
|
inputConverter.descriptor?.let { converterDescriptor ->
|
||||||
|
inputMetaDescriptor = MetaDescriptor {
|
||||||
|
from(converterDescriptor)
|
||||||
|
from(inputMetaDescriptor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
outputConverter.descriptor?.let { converterDescriptor ->
|
||||||
|
outputMetaDescriptor = MetaDescriptor {
|
||||||
|
from(converterDescriptor)
|
||||||
|
from(outputMetaDescriptor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fromSpec(property)
|
fromSpec(property)
|
||||||
descriptorBuilder()
|
descriptorBuilder()
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,41 @@
|
|||||||
|
package space.kscience.controls.spec
|
||||||
|
|
||||||
|
import kotlinx.datetime.Instant
|
||||||
|
import space.kscience.dataforge.meta.*
|
||||||
|
import kotlin.time.Duration
|
||||||
|
import kotlin.time.DurationUnit
|
||||||
|
import kotlin.time.toDuration
|
||||||
|
|
||||||
|
public fun Double.asMeta(): Meta = Meta(asValue())
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a nullable [MetaConverter] from non-nullable one
|
||||||
|
*/
|
||||||
|
public fun <T : Any> MetaConverter<T>.nullable(): MetaConverter<T?> = object : MetaConverter<T?> {
|
||||||
|
override fun convert(obj: T?): Meta = obj?.let { this@nullable.convert(it) }?: Meta(Null)
|
||||||
|
|
||||||
|
override fun readOrNull(source: Meta): T? = if(source.value == Null) null else this@nullable.readOrNull(source)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
//TODO to be moved to DF
|
||||||
|
private object DurationConverter : MetaConverter<Duration> {
|
||||||
|
override fun readOrNull(source: Meta): Duration = source.value?.double?.toDuration(DurationUnit.SECONDS)
|
||||||
|
?: run {
|
||||||
|
val unit: DurationUnit = source["unit"].enum<DurationUnit>() ?: DurationUnit.SECONDS
|
||||||
|
val value = source[Meta.VALUE_KEY].double ?: error("No value present for Duration")
|
||||||
|
return@run value.toDuration(unit)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun convert(obj: Duration): Meta = obj.toDouble(DurationUnit.SECONDS).asMeta()
|
||||||
|
}
|
||||||
|
|
||||||
|
public val MetaConverter.Companion.duration: MetaConverter<Duration> get() = DurationConverter
|
||||||
|
|
||||||
|
|
||||||
|
private object InstantConverter : MetaConverter<Instant> {
|
||||||
|
override fun readOrNull(source: Meta): Instant? = source.string?.let { Instant.parse(it) }
|
||||||
|
override fun convert(obj: Instant): Meta = Meta(obj.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
public val MetaConverter.Companion.instant: MetaConverter<Instant> get() = InstantConverter
|
@ -1,14 +1,31 @@
|
|||||||
package space.kscience.controls.spec
|
package space.kscience.controls.spec
|
||||||
|
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.flow
|
import kotlinx.coroutines.flow.flow
|
||||||
import kotlinx.coroutines.isActive
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import space.kscience.controls.api.Device
|
import space.kscience.controls.api.Device
|
||||||
import kotlin.time.Duration
|
import kotlin.time.Duration
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Do a recurring (with a fixed delay) task on a device.
|
||||||
|
*/
|
||||||
|
public fun <D : Device> D.doRecurring(
|
||||||
|
interval: Duration,
|
||||||
|
debugTaskName: String? = null,
|
||||||
|
task: suspend D.() -> Unit,
|
||||||
|
): Job {
|
||||||
|
val taskName = debugTaskName ?: "task[${task.hashCode().toString(16)}]"
|
||||||
|
return launch(CoroutineName(taskName)) {
|
||||||
|
while (isActive) {
|
||||||
|
delay(interval)
|
||||||
|
//launch in parent scope to properly evaluate exceptions
|
||||||
|
this@doRecurring.launch {
|
||||||
|
task()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Perform a recurring asynchronous read action and return a flow of results.
|
* Perform a recurring asynchronous read action and return a flow of results.
|
||||||
* The flow is lazy, so action is not performed unless flow is consumed.
|
* The flow is lazy, so action is not performed unless flow is consumed.
|
||||||
@ -16,23 +33,12 @@ import kotlin.time.Duration
|
|||||||
*
|
*
|
||||||
* The flow is canceled when the device scope is canceled
|
* The flow is canceled when the device scope is canceled
|
||||||
*/
|
*/
|
||||||
public fun <D : Device, R> D.readRecurring(interval: Duration, reader: suspend D.() -> R): Flow<R> = flow {
|
public fun <D : Device, R> D.readRecurring(
|
||||||
while (isActive) {
|
interval: Duration,
|
||||||
delay(interval)
|
debugTaskName: String? = null,
|
||||||
launch {
|
reader: suspend D.() -> R,
|
||||||
emit(reader())
|
): Flow<R> = flow {
|
||||||
}
|
doRecurring(interval, debugTaskName) {
|
||||||
}
|
emit(reader())
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Do a recurring (with a fixed delay) task on a device.
|
|
||||||
*/
|
|
||||||
public fun <D : Device> D.doRecurring(interval: Duration, task: suspend D.() -> Unit): Job = launch {
|
|
||||||
while (isActive) {
|
|
||||||
delay(interval)
|
|
||||||
launch {
|
|
||||||
task()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -4,8 +4,6 @@ import space.kscience.controls.api.ActionDescriptor
|
|||||||
import space.kscience.controls.api.PropertyDescriptor
|
import space.kscience.controls.api.PropertyDescriptor
|
||||||
import kotlin.reflect.KProperty
|
import kotlin.reflect.KProperty
|
||||||
|
|
||||||
@Target(AnnotationTarget.CLASS, AnnotationTarget.PROPERTY, AnnotationTarget.FIELD)
|
|
||||||
public annotation class Description(val content: String)
|
|
||||||
|
|
||||||
internal expect fun PropertyDescriptor.fromSpec(property: KProperty<*>)
|
internal expect fun PropertyDescriptor.fromSpec(property: KProperty<*>)
|
||||||
|
|
||||||
|
@ -1,22 +0,0 @@
|
|||||||
package space.kscience.controls.spec
|
|
||||||
|
|
||||||
import space.kscience.dataforge.meta.*
|
|
||||||
import kotlin.time.Duration
|
|
||||||
import kotlin.time.DurationUnit
|
|
||||||
import kotlin.time.toDuration
|
|
||||||
|
|
||||||
public fun Double.asMeta(): Meta = Meta(asValue())
|
|
||||||
|
|
||||||
//TODO to be moved to DF
|
|
||||||
public object DurationConverter : MetaConverter<Duration> {
|
|
||||||
override fun readOrNull(source: Meta): Duration = source.value?.double?.toDuration(DurationUnit.SECONDS)
|
|
||||||
?: run {
|
|
||||||
val unit: DurationUnit = source["unit"].enum<DurationUnit>() ?: DurationUnit.SECONDS
|
|
||||||
val value = source[Meta.VALUE_KEY].double ?: error("No value present for Duration")
|
|
||||||
return@run value.toDuration(unit)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun convert(obj: Duration): Meta = obj.toDouble(DurationUnit.SECONDS).asMeta()
|
|
||||||
}
|
|
||||||
|
|
||||||
public val MetaConverter.Companion.duration: MetaConverter<Duration> get() = DurationConverter
|
|
@ -2,17 +2,18 @@ package space.kscience.controls.spec
|
|||||||
|
|
||||||
import space.kscience.controls.api.ActionDescriptor
|
import space.kscience.controls.api.ActionDescriptor
|
||||||
import space.kscience.controls.api.PropertyDescriptor
|
import space.kscience.controls.api.PropertyDescriptor
|
||||||
|
import space.kscience.dataforge.descriptors.Description
|
||||||
import kotlin.reflect.KProperty
|
import kotlin.reflect.KProperty
|
||||||
import kotlin.reflect.full.findAnnotation
|
import kotlin.reflect.full.findAnnotation
|
||||||
|
|
||||||
internal actual fun PropertyDescriptor.fromSpec(property: KProperty<*>) {
|
internal actual fun PropertyDescriptor.fromSpec(property: KProperty<*>) {
|
||||||
property.findAnnotation<Description>()?.let {
|
property.findAnnotation<Description>()?.let {
|
||||||
description = it.content
|
description = it.value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal actual fun ActionDescriptor.fromSpec(property: KProperty<*>){
|
internal actual fun ActionDescriptor.fromSpec(property: KProperty<*>){
|
||||||
property.findAnnotation<Description>()?.let {
|
property.findAnnotation<Description>()?.let {
|
||||||
description = it.content
|
description = it.value
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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`
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -9,10 +9,12 @@ description = """
|
|||||||
A plugin for Controls-kt device server on top of modbus-rtu/modbus-tcp protocols
|
A plugin for Controls-kt device server on top of modbus-rtu/modbus-tcp protocols
|
||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
|
|
||||||
|
kscience {
|
||||||
dependencies {
|
jvm()
|
||||||
api(projects.controlsCore)
|
jvmMain {
|
||||||
api(libs.j2mod)
|
api(projects.controlsCore)
|
||||||
|
api(libs.j2mod)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
readme{
|
readme{
|
||||||
|
@ -24,7 +24,7 @@ public open class ModbusDeviceBySpec<D: Device>(
|
|||||||
master.connect()
|
master.connect()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStop() {
|
override suspend fun onStop() {
|
||||||
if(disposeMasterOnClose){
|
if(disposeMasterOnClose){
|
||||||
master.disconnect()
|
master.disconnect()
|
||||||
}
|
}
|
@ -63,7 +63,7 @@ public open class OpcUaDeviceBySpec<D : Device>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStop() {
|
override suspend fun onStop() {
|
||||||
client.disconnect()
|
client.disconnect()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
plugins {
|
plugins {
|
||||||
id("space.kscience.gradle.jvm")
|
id("space.kscience.gradle.mpp")
|
||||||
`maven-publish`
|
`maven-publish`
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -7,10 +7,15 @@ description = """
|
|||||||
Utils to work with controls-kt on Raspberry pi
|
Utils to work with controls-kt on Raspberry pi
|
||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
|
|
||||||
dependencies{
|
kscience {
|
||||||
api(project(":controls-core"))
|
jvm()
|
||||||
api(libs.pi4j.ktx) // Kotlin DSL
|
|
||||||
api(libs.pi4j.core)
|
|
||||||
api(libs.pi4j.plugin.raspberrypi)
|
jvmMain {
|
||||||
api(libs.pi4j.plugin.pigpio)
|
api(project(":controls-core"))
|
||||||
|
api(libs.pi4j.ktx) // Kotlin DSL
|
||||||
|
api(libs.pi4j.core)
|
||||||
|
api(libs.pi4j.plugin.raspberrypi)
|
||||||
|
api(libs.pi4j.plugin.pigpio)
|
||||||
|
}
|
||||||
}
|
}
|
@ -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`
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -9,9 +9,12 @@ description = """
|
|||||||
Implementation of byte ports on top os ktor-io asynchronous API
|
Implementation of byte ports on top os ktor-io asynchronous API
|
||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
|
|
||||||
dependencies {
|
kscience {
|
||||||
api(projects.controlsCore)
|
jvm()
|
||||||
api(spclibs.ktor.network)
|
jvmMain {
|
||||||
|
api(projects.controlsCore)
|
||||||
|
api(spclibs.ktor.network)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
readme{
|
readme{
|
||||||
|
@ -1,15 +1,18 @@
|
|||||||
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`
|
||||||
}
|
}
|
||||||
|
|
||||||
description = "Implementation of direct serial port communication with JSerialComm"
|
description = "Implementation of direct serial port communication with JSerialComm"
|
||||||
|
|
||||||
dependencies{
|
kscience {
|
||||||
api(project(":controls-core"))
|
jvm()
|
||||||
implementation(libs.jSerialComm)
|
jvmMain {
|
||||||
|
api(project(":controls-core"))
|
||||||
|
implementation(libs.jSerialComm)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
readme{
|
readme{
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
plugins {
|
plugins {
|
||||||
id("space.kscience.gradle.jvm")
|
id("space.kscience.gradle.mpp")
|
||||||
`maven-publish`
|
`maven-publish`
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -7,13 +7,18 @@ description = """
|
|||||||
An implementation of controls-storage on top of JetBrains Xodus.
|
An implementation of controls-storage on top of JetBrains Xodus.
|
||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
|
|
||||||
dependencies {
|
kscience {
|
||||||
api(projects.controlsStorage)
|
jvm()
|
||||||
implementation(libs.xodus.entity.store)
|
jvmMain {
|
||||||
|
api(projects.controlsStorage)
|
||||||
|
implementation(libs.xodus.entity.store)
|
||||||
// implementation("org.jetbrains.xodus:xodus-environment:$xodusVersion")
|
// implementation("org.jetbrains.xodus:xodus-environment:$xodusVersion")
|
||||||
// implementation("org.jetbrains.xodus:xodus-vfs:$xodusVersion")
|
// implementation("org.jetbrains.xodus:xodus-vfs:$xodusVersion")
|
||||||
|
|
||||||
testImplementation(spclibs.kotlinx.coroutines.test)
|
}
|
||||||
|
jvmTest{
|
||||||
|
implementation(spclibs.kotlinx.coroutines.test)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
readme{
|
readme{
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
plugins {
|
plugins {
|
||||||
kotlin("jvm")
|
kotlin("jvm")
|
||||||
id("org.openjfx.javafxplugin") version "0.0.13"
|
alias(spclibs.plugins.compose)
|
||||||
application
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -20,28 +19,46 @@ dependencies {
|
|||||||
implementation(projects.controlsOpcua)
|
implementation(projects.controlsOpcua)
|
||||||
|
|
||||||
implementation(spclibs.ktor.client.cio)
|
implementation(spclibs.ktor.client.cio)
|
||||||
implementation(libs.tornadofx)
|
|
||||||
implementation(libs.plotlykt.server)
|
implementation(libs.plotlykt.server)
|
||||||
// implementation("com.github.Ricky12Awesome:json-schema-serialization:0.6.6")
|
// implementation("com.github.Ricky12Awesome:json-schema-serialization:0.6.6")
|
||||||
|
|
||||||
|
implementation(compose.runtime)
|
||||||
|
implementation(compose.desktop.currentOs)
|
||||||
|
implementation(compose.material3)
|
||||||
|
// implementation("org.pushing-pixels:aurora-window:1.3.0")
|
||||||
|
// implementation("org.pushing-pixels:aurora-component:1.3.0")
|
||||||
|
// implementation("org.pushing-pixels:aurora-theming:1.3.0")
|
||||||
|
|
||||||
implementation(spclibs.logback.classic)
|
implementation(spclibs.logback.classic)
|
||||||
}
|
}
|
||||||
|
|
||||||
kotlin{
|
kotlin{
|
||||||
jvmToolchain(11)
|
jvmToolchain(17)
|
||||||
}
|
compilerOptions {
|
||||||
|
freeCompilerArgs.addAll("-Xjvm-default=all", "-Xopt-in=kotlin.RequiresOptIn")
|
||||||
|
|
||||||
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach {
|
|
||||||
kotlinOptions {
|
|
||||||
freeCompilerArgs = freeCompilerArgs + listOf("-Xjvm-default=all", "-Xopt-in=kotlin.RequiresOptIn")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
javafx {
|
compose{
|
||||||
version = "17"
|
desktop{
|
||||||
modules("javafx.controls")
|
application{
|
||||||
|
mainClass = "space.kscience.controls.demo.DemoControllerViewKt"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
//
|
||||||
application {
|
//
|
||||||
mainClass.set("space.kscience.controls.demo.DemoControllerViewKt")
|
//tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach {
|
||||||
}
|
// kotlinOptions {
|
||||||
|
// freeCompilerArgs = freeCompilerArgs + listOf("-Xjvm-default=all", "-Xopt-in=kotlin.RequiresOptIn")
|
||||||
|
// }
|
||||||
|
//}
|
||||||
|
//
|
||||||
|
//javafx {
|
||||||
|
// version = "17"
|
||||||
|
// modules("javafx.controls")
|
||||||
|
//}
|
||||||
|
//
|
||||||
|
//application {
|
||||||
|
// mainClass.set("space.kscience.controls.demo.DemoControllerViewKt")
|
||||||
|
//}
|
@ -1,10 +1,16 @@
|
|||||||
package space.kscience.controls.demo
|
package space.kscience.controls.demo
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.window.Window
|
||||||
|
import androidx.compose.ui.window.application
|
||||||
|
import androidx.compose.ui.window.rememberWindowState
|
||||||
import io.ktor.server.engine.ApplicationEngine
|
import io.ktor.server.engine.ApplicationEngine
|
||||||
import javafx.scene.Parent
|
import kotlinx.coroutines.Job
|
||||||
import javafx.scene.control.Slider
|
|
||||||
import javafx.scene.layout.Priority
|
|
||||||
import javafx.stage.Stage
|
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@ -16,9 +22,6 @@ import space.kscience.controls.api.GetDescriptionMessage
|
|||||||
import space.kscience.controls.api.PropertyChangedMessage
|
import space.kscience.controls.api.PropertyChangedMessage
|
||||||
import space.kscience.controls.client.launchMagixService
|
import space.kscience.controls.client.launchMagixService
|
||||||
import space.kscience.controls.client.magixFormat
|
import space.kscience.controls.client.magixFormat
|
||||||
import space.kscience.controls.demo.DemoDevice.Companion.cosScale
|
|
||||||
import space.kscience.controls.demo.DemoDevice.Companion.sinScale
|
|
||||||
import space.kscience.controls.demo.DemoDevice.Companion.timeScale
|
|
||||||
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.controls.opcua.server.OpcUaServer
|
import space.kscience.controls.opcua.server.OpcUaServer
|
||||||
@ -34,16 +37,15 @@ import space.kscience.magix.rsocket.rSocketWithWebSockets
|
|||||||
import space.kscience.magix.server.RSocketMagixFlowPlugin
|
import space.kscience.magix.server.RSocketMagixFlowPlugin
|
||||||
import space.kscience.magix.server.startMagixServer
|
import space.kscience.magix.server.startMagixServer
|
||||||
import space.kscince.magix.zmq.ZmqMagixFlowPlugin
|
import space.kscince.magix.zmq.ZmqMagixFlowPlugin
|
||||||
import tornadofx.*
|
|
||||||
import java.awt.Desktop
|
import java.awt.Desktop
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
|
|
||||||
class DemoController : Controller(), ContextAware {
|
class DemoController : ContextAware {
|
||||||
|
|
||||||
var device: DemoDevice? = null
|
var device: DemoDevice? = null
|
||||||
var magixServer: ApplicationEngine? = null
|
var magixServer: ApplicationEngine? = null
|
||||||
var visualizer: ApplicationEngine? = null
|
var visualizer: ApplicationEngine? = null
|
||||||
var opcUaServer: OpcUaServer = OpcUaServer {
|
val opcUaServer: OpcUaServer = OpcUaServer {
|
||||||
setApplicationName(LocalizedText.english("space.kscience.controls.opcua"))
|
setApplicationName(LocalizedText.english("space.kscience.controls.opcua"))
|
||||||
|
|
||||||
endpoint {
|
endpoint {
|
||||||
@ -59,39 +61,37 @@ class DemoController : Controller(), ContextAware {
|
|||||||
private val deviceManager = context.request(DeviceManager)
|
private val deviceManager = context.request(DeviceManager)
|
||||||
|
|
||||||
|
|
||||||
fun init() {
|
fun start(): Job = context.launch {
|
||||||
context.launch {
|
device = deviceManager.install("demo", DemoDevice)
|
||||||
device = deviceManager.install("demo", DemoDevice)
|
//starting magix event loop
|
||||||
//starting magix event loop
|
magixServer = startMagixServer(
|
||||||
magixServer = startMagixServer(
|
RSocketMagixFlowPlugin(), //TCP rsocket support
|
||||||
RSocketMagixFlowPlugin(), //TCP rsocket support
|
ZmqMagixFlowPlugin() //ZMQ support
|
||||||
ZmqMagixFlowPlugin() //ZMQ support
|
)
|
||||||
)
|
//Launch a device client and connect it to the server
|
||||||
//Launch a device client and connect it to the server
|
val deviceEndpoint = MagixEndpoint.rSocketWithTcp("localhost")
|
||||||
val deviceEndpoint = MagixEndpoint.rSocketWithTcp("localhost")
|
deviceManager.launchMagixService(deviceEndpoint)
|
||||||
deviceManager.launchMagixService(deviceEndpoint)
|
//connect visualization to a magix endpoint
|
||||||
//connect visualization to a magix endpoint
|
val visualEndpoint = MagixEndpoint.rSocketWithWebSockets("localhost")
|
||||||
val visualEndpoint = MagixEndpoint.rSocketWithWebSockets("localhost")
|
visualizer = startDemoDeviceServer(visualEndpoint)
|
||||||
visualizer = startDemoDeviceServer(visualEndpoint)
|
|
||||||
|
|
||||||
//serve devices as OPC-UA namespace
|
//serve devices as OPC-UA namespace
|
||||||
opcUaServer.startup()
|
opcUaServer.startup()
|
||||||
opcUaServer.serveDevices(deviceManager)
|
opcUaServer.serveDevices(deviceManager)
|
||||||
|
|
||||||
|
|
||||||
val listenerEndpoint = MagixEndpoint.rSocketWithWebSockets("localhost")
|
val listenerEndpoint = MagixEndpoint.rSocketWithWebSockets("localhost")
|
||||||
listenerEndpoint.subscribe(DeviceManager.magixFormat).onEach { (_, deviceMessage)->
|
listenerEndpoint.subscribe(DeviceManager.magixFormat).onEach { (_, deviceMessage) ->
|
||||||
// print all messages that are not property change message
|
// print all messages that are not property change message
|
||||||
if(deviceMessage !is PropertyChangedMessage){
|
if (deviceMessage !is PropertyChangedMessage) {
|
||||||
println(">> ${Json.encodeToString(DeviceMessage.serializer(), deviceMessage)}")
|
println(">> ${Json.encodeToString(DeviceMessage.serializer(), deviceMessage)}")
|
||||||
}
|
}
|
||||||
}.launchIn(this)
|
}.launchIn(this)
|
||||||
listenerEndpoint.send(DeviceManager.magixFormat, GetDescriptionMessage(), "listener", "controls-kt")
|
listenerEndpoint.send(DeviceManager.magixFormat, GetDescriptionMessage(), "listener", "controls-kt")
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun shutdown() {
|
fun shutdown(): Job = context.launch {
|
||||||
logger.info { "Shutting down..." }
|
logger.info { "Shutting down..." }
|
||||||
opcUaServer.shutdown()
|
opcUaServer.shutdown()
|
||||||
logger.info { "OpcUa server stopped" }
|
logger.info { "OpcUa server stopped" }
|
||||||
@ -101,90 +101,82 @@ class DemoController : Controller(), ContextAware {
|
|||||||
logger.info { "Magix server stopped" }
|
logger.info { "Magix server stopped" }
|
||||||
device?.stop()
|
device?.stop()
|
||||||
logger.info { "Device server stopped" }
|
logger.info { "Device server stopped" }
|
||||||
context.close()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun DemoControls(controller: DemoController) {
|
||||||
|
var timeScale by remember { mutableStateOf(5000f) }
|
||||||
|
var xScale by remember { mutableStateOf(1f) }
|
||||||
|
var yScale by remember { mutableStateOf(1f) }
|
||||||
|
|
||||||
class DemoControllerView : View(title = " Demo controller remote") {
|
Surface(Modifier.padding(5.dp)) {
|
||||||
private val controller: DemoController by inject()
|
Column {
|
||||||
private var timeScaleSlider: Slider by singleAssign()
|
Row(Modifier.fillMaxWidth()) {
|
||||||
private var xScaleSlider: Slider by singleAssign()
|
Text("Time Scale", modifier = Modifier.align(Alignment.CenterVertically).width(100.dp))
|
||||||
private var yScaleSlider: Slider by singleAssign()
|
TextField(String.format("%.2f", timeScale),{}, enabled = false, modifier = Modifier.align(Alignment.CenterVertically).width(100.dp))
|
||||||
|
Slider(timeScale, onValueChange = { timeScale = it }, steps = 20, valueRange = 1000f..5000f)
|
||||||
override val root: Parent = vbox {
|
|
||||||
hbox {
|
|
||||||
label("Time scale")
|
|
||||||
pane {
|
|
||||||
hgrow = Priority.ALWAYS
|
|
||||||
}
|
}
|
||||||
timeScaleSlider = slider(1000..10000, 5000) {
|
Row(Modifier.fillMaxWidth()) {
|
||||||
isShowTickLabels = true
|
Text("X scale", modifier = Modifier.align(Alignment.CenterVertically).width(100.dp))
|
||||||
isShowTickMarks = true
|
TextField(String.format("%.2f", xScale),{}, enabled = false, modifier = Modifier.align(Alignment.CenterVertically).width(100.dp))
|
||||||
|
Slider(xScale, onValueChange = { xScale = it }, steps = 20, valueRange = 0.1f..2.0f)
|
||||||
}
|
}
|
||||||
}
|
Row(Modifier.fillMaxWidth()) {
|
||||||
hbox {
|
Text("Y scale", modifier = Modifier.align(Alignment.CenterVertically).width(100.dp))
|
||||||
label("X scale")
|
TextField(String.format("%.2f", yScale),{}, enabled = false, modifier = Modifier.align(Alignment.CenterVertically).width(100.dp))
|
||||||
pane {
|
Slider(yScale, onValueChange = { yScale = it }, steps = 20, valueRange = 0.1f..2.0f)
|
||||||
hgrow = Priority.ALWAYS
|
|
||||||
}
|
}
|
||||||
xScaleSlider = slider(0.1..2.0, 1.0) {
|
Row(Modifier.fillMaxWidth()) {
|
||||||
isShowTickLabels = true
|
Button(
|
||||||
isShowTickMarks = true
|
onClick = {
|
||||||
|
controller.device?.run {
|
||||||
|
launch {
|
||||||
|
write(DemoDevice.timeScale, timeScale.toDouble())
|
||||||
|
write(DemoDevice.sinScale, xScale.toDouble())
|
||||||
|
write(DemoDevice.cosScale, yScale.toDouble())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Text("Submit")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
Row(Modifier.fillMaxWidth()) {
|
||||||
hbox {
|
Button(
|
||||||
label("Y scale")
|
onClick = {
|
||||||
pane {
|
controller.visualizer?.run {
|
||||||
hgrow = Priority.ALWAYS
|
val host = "localhost"//environment.connectors.first().host
|
||||||
}
|
val port = environment.connectors.first().port
|
||||||
yScaleSlider = slider(0.1..2.0, 1.0) {
|
val uri = URI("http", null, host, port, "/", null, null)
|
||||||
isShowTickLabels = true
|
Desktop.getDesktop().browse(uri)
|
||||||
isShowTickMarks = true
|
}
|
||||||
}
|
},
|
||||||
}
|
Modifier.fillMaxWidth()
|
||||||
button("Submit") {
|
) {
|
||||||
useMaxWidth = true
|
Text("Show plots")
|
||||||
action {
|
|
||||||
controller.device?.run {
|
|
||||||
launch {
|
|
||||||
write(timeScale, timeScaleSlider.value)
|
|
||||||
write(sinScale, xScaleSlider.value)
|
|
||||||
write(cosScale, yScaleSlider.value)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
button("Show plots") {
|
|
||||||
useMaxWidth = true
|
}
|
||||||
action {
|
}
|
||||||
controller.visualizer?.run {
|
|
||||||
val host = "localhost"//environment.connectors.first().host
|
|
||||||
val port = environment.connectors.first().port
|
fun main() = application {
|
||||||
val uri = URI("http", null, host, port, "/", null, null)
|
val controller = remember { DemoController().apply { start() } }
|
||||||
Desktop.getDesktop().browse(uri)
|
|
||||||
}
|
Window(
|
||||||
}
|
title = "All things control",
|
||||||
|
onCloseRequest = {
|
||||||
|
controller.shutdown()
|
||||||
|
exitApplication()
|
||||||
|
},
|
||||||
|
state = rememberWindowState(width = 400.dp, height = 320.dp)
|
||||||
|
) {
|
||||||
|
MaterialTheme {
|
||||||
|
DemoControls(controller)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class DemoControllerApp : App(DemoControllerView::class) {
|
|
||||||
private val controller: DemoController by inject()
|
|
||||||
|
|
||||||
override fun start(stage: Stage) {
|
|
||||||
super.start(stage)
|
|
||||||
controller.init()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun stop() {
|
|
||||||
controller.shutdown()
|
|
||||||
super.stop()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
fun main() {
|
|
||||||
launch<DemoControllerApp>()
|
|
||||||
}
|
}
|
@ -1,10 +0,0 @@
|
|||||||
package space.kscience.controls.demo
|
|
||||||
|
|
||||||
//import com.github.ricky12awesome.jss.encodeToSchema
|
|
||||||
//import com.github.ricky12awesome.jss.globalJson
|
|
||||||
//import space.kscience.controls.api.DeviceMessage
|
|
||||||
|
|
||||||
//fun main() {
|
|
||||||
// val schema = globalJson.encodeToSchema(DeviceMessage.serializer(), generateDefinitions = false)
|
|
||||||
// println(schema)
|
|
||||||
//}
|
|
@ -8,6 +8,7 @@ import javafx.scene.layout.Priority
|
|||||||
import javafx.stage.Stage
|
import javafx.stage.Stage
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
import space.kscience.controls.client.launchMagixService
|
import space.kscience.controls.client.launchMagixService
|
||||||
import space.kscience.controls.demo.car.IVirtualCar.Companion.acceleration
|
import space.kscience.controls.demo.car.IVirtualCar.Companion.acceleration
|
||||||
import space.kscience.controls.manager.DeviceManager
|
import space.kscience.controls.manager.DeviceManager
|
||||||
@ -67,7 +68,7 @@ class VirtualCarController : Controller(), ContextAware {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun shutdown() {
|
suspend fun shutdown() {
|
||||||
logger.info { "Shutting down..." }
|
logger.info { "Shutting down..." }
|
||||||
magixServer?.stop(1000, 5000)
|
magixServer?.stop(1000, 5000)
|
||||||
logger.info { "Magix server stopped" }
|
logger.info { "Magix server stopped" }
|
||||||
@ -137,7 +138,9 @@ class VirtualCarControllerApp : App(VirtualCarControllerView::class) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun stop() {
|
override fun stop() {
|
||||||
controller.shutdown()
|
runBlocking {
|
||||||
|
controller.shutdown()
|
||||||
|
}
|
||||||
super.stop()
|
super.stop()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,9 +16,11 @@ import androidx.compose.ui.window.Window
|
|||||||
import androidx.compose.ui.window.application
|
import androidx.compose.ui.window.application
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import space.kscience.controls.constructor.*
|
import space.kscience.controls.constructor.*
|
||||||
|
import space.kscience.controls.constructor.library.*
|
||||||
import space.kscience.controls.manager.ClockManager
|
import space.kscience.controls.manager.ClockManager
|
||||||
import space.kscience.controls.manager.DeviceManager
|
import space.kscience.controls.manager.DeviceManager
|
||||||
import space.kscience.controls.manager.clock
|
import space.kscience.controls.manager.clock
|
||||||
|
import space.kscience.controls.manager.install
|
||||||
import space.kscience.controls.spec.doRecurring
|
import space.kscience.controls.spec.doRecurring
|
||||||
import space.kscience.controls.spec.name
|
import space.kscience.controls.spec.name
|
||||||
import space.kscience.controls.vision.plot
|
import space.kscience.controls.vision.plot
|
||||||
@ -48,13 +50,14 @@ class LinearDrive(
|
|||||||
val drive by device(VirtualDrive.factory(mass, state))
|
val drive by device(VirtualDrive.factory(mass, state))
|
||||||
val pid by device(PidRegulator(drive, pidParameters))
|
val pid by device(PidRegulator(drive, pidParameters))
|
||||||
|
|
||||||
val start by device(LimitSwitch.factory(state.atStartState))
|
val start by device(LimitSwitch(state.atStartState))
|
||||||
val end by device(LimitSwitch.factory(state.atEndState))
|
val end by device(LimitSwitch(state.atEndState))
|
||||||
|
|
||||||
|
|
||||||
val positionState: DoubleRangeState by property(state)
|
val positionState: DoubleRangeState by property(state)
|
||||||
private val targetState: MutableDeviceState<Double> by property(pid.mutablePropertyAsState(Regulator.target, 0.0))
|
|
||||||
var target by targetState
|
private val targetState: MutableDeviceState<Double> by deviceProperty(pid, Regulator.target, 0.0)
|
||||||
|
var target: Double by targetState
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -73,7 +76,6 @@ private fun Context.launchPidDevice(
|
|||||||
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
|
||||||
|
|
||||||
target = 5 * sin(2.0 * PI * freq * t) +
|
target = 5 * sin(2.0 * PI * freq * t) +
|
||||||
sin(2 * PI * 21 * freq * t + 0.02 * (timeFromStart / pidParameters.timeStep))
|
sin(2 * PI * 21 * freq * t + 0.02 * (timeFromStart / pidParameters.timeStep))
|
||||||
}
|
}
|
||||||
@ -150,7 +152,7 @@ fun main() = application {
|
|||||||
Row {
|
Row {
|
||||||
Text("kp:", Modifier.align(Alignment.CenterVertically).width(50.dp).padding(5.dp))
|
Text("kp:", Modifier.align(Alignment.CenterVertically).width(50.dp).padding(5.dp))
|
||||||
TextField(
|
TextField(
|
||||||
String.format("%.2f",pidParameters.kp),
|
String.format("%.2f", pidParameters.kp),
|
||||||
{ pidParameters.kp = it.toDouble() },
|
{ pidParameters.kp = it.toDouble() },
|
||||||
Modifier.width(100.dp),
|
Modifier.width(100.dp),
|
||||||
enabled = false
|
enabled = false
|
||||||
@ -165,7 +167,7 @@ fun main() = application {
|
|||||||
Row {
|
Row {
|
||||||
Text("ki:", Modifier.align(Alignment.CenterVertically).width(50.dp).padding(5.dp))
|
Text("ki:", Modifier.align(Alignment.CenterVertically).width(50.dp).padding(5.dp))
|
||||||
TextField(
|
TextField(
|
||||||
String.format("%.2f",pidParameters.ki),
|
String.format("%.2f", pidParameters.ki),
|
||||||
{ pidParameters.ki = it.toDouble() },
|
{ pidParameters.ki = it.toDouble() },
|
||||||
Modifier.width(100.dp),
|
Modifier.width(100.dp),
|
||||||
enabled = false
|
enabled = false
|
||||||
@ -181,7 +183,7 @@ fun main() = application {
|
|||||||
Row {
|
Row {
|
||||||
Text("kd:", Modifier.align(Alignment.CenterVertically).width(50.dp).padding(5.dp))
|
Text("kd:", Modifier.align(Alignment.CenterVertically).width(50.dp).padding(5.dp))
|
||||||
TextField(
|
TextField(
|
||||||
String.format("%.2f",pidParameters.kd),
|
String.format("%.2f", pidParameters.kd),
|
||||||
{ pidParameters.kd = it.toDouble() },
|
{ pidParameters.kd = it.toDouble() },
|
||||||
Modifier.width(100.dp),
|
Modifier.width(100.dp),
|
||||||
enabled = false
|
enabled = false
|
||||||
|
@ -1,18 +1,6 @@
|
|||||||
plugins {
|
plugins {
|
||||||
id("space.kscience.gradle.jvm")
|
id("space.kscience.gradle.jvm")
|
||||||
application
|
alias(spclibs.plugins.compose)
|
||||||
id("org.openjfx.javafxplugin")
|
|
||||||
}
|
|
||||||
|
|
||||||
//TODO to be moved to a separate project
|
|
||||||
|
|
||||||
javafx {
|
|
||||||
version = "17"
|
|
||||||
modules = listOf("javafx.controls")
|
|
||||||
}
|
|
||||||
|
|
||||||
application{
|
|
||||||
mainClass.set("ru.mipt.npm.devices.pimotionmaster.PiMotionMasterAppKt")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
kotlin{
|
kotlin{
|
||||||
@ -25,5 +13,17 @@ val dataforgeVersion: String by extra
|
|||||||
dependencies {
|
dependencies {
|
||||||
implementation(project(":controls-ports-ktor"))
|
implementation(project(":controls-ports-ktor"))
|
||||||
implementation(projects.controlsMagix)
|
implementation(projects.controlsMagix)
|
||||||
implementation(libs.tornadofx)
|
|
||||||
|
implementation(compose.runtime)
|
||||||
|
implementation(compose.desktop.currentOs)
|
||||||
|
implementation(compose.material3)
|
||||||
|
implementation(spclibs.logback.classic)
|
||||||
|
}
|
||||||
|
|
||||||
|
compose{
|
||||||
|
desktop{
|
||||||
|
application{
|
||||||
|
mainClass = "ru.mipt.npm.devices.pimotionmaster.PiMotionMasterAppKt"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,31 +1,195 @@
|
|||||||
package ru.mipt.npm.devices.pimotionmaster
|
package ru.mipt.npm.devices.pimotionmaster
|
||||||
|
|
||||||
import javafx.beans.property.ReadOnlyProperty
|
|
||||||
import javafx.beans.property.SimpleIntegerProperty
|
import androidx.compose.foundation.layout.Column
|
||||||
import javafx.beans.property.SimpleObjectProperty
|
import androidx.compose.foundation.layout.ColumnScope
|
||||||
import javafx.beans.property.SimpleStringProperty
|
import androidx.compose.foundation.layout.Row
|
||||||
import javafx.geometry.Pos
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import javafx.scene.Parent
|
import androidx.compose.material.Button
|
||||||
import javafx.scene.layout.Priority
|
import androidx.compose.material.OutlinedTextField
|
||||||
import javafx.scene.layout.VBox
|
import androidx.compose.material.Slider
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import androidx.compose.material.Text
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.window.Window
|
||||||
|
import androidx.compose.ui.window.application
|
||||||
|
import androidx.compose.ui.window.rememberWindowState
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import ru.mipt.npm.devices.pimotionmaster.PiMotionMasterDevice.Axis.Companion.maxPosition
|
|
||||||
import ru.mipt.npm.devices.pimotionmaster.PiMotionMasterDevice.Axis.Companion.minPosition
|
|
||||||
import ru.mipt.npm.devices.pimotionmaster.PiMotionMasterDevice.Axis.Companion.position
|
|
||||||
import space.kscience.controls.manager.DeviceManager
|
import space.kscience.controls.manager.DeviceManager
|
||||||
import space.kscience.controls.manager.installing
|
import space.kscience.controls.manager.installing
|
||||||
import space.kscience.controls.spec.read
|
import space.kscience.controls.spec.read
|
||||||
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 tornadofx.*
|
|
||||||
|
|
||||||
class PiMotionMasterApp : App(PiMotionMasterView::class)
|
//class PiMotionMasterApp : App(PiMotionMasterView::class)
|
||||||
|
//
|
||||||
|
//class PiMotionMasterController : Controller() {
|
||||||
|
// //initialize context
|
||||||
|
// val context = Context("piMotionMaster") {
|
||||||
|
// plugin(DeviceManager)
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// //initialize deviceManager plugin
|
||||||
|
// val deviceManager: DeviceManager = context.request(DeviceManager)
|
||||||
|
//
|
||||||
|
// // install device
|
||||||
|
// val motionMaster: PiMotionMasterDevice by deviceManager.installing(PiMotionMasterDevice)
|
||||||
|
//}
|
||||||
|
|
||||||
class PiMotionMasterController : Controller() {
|
@Composable
|
||||||
//initialize context
|
fun ColumnScope.piMotionMasterAxis(
|
||||||
val context = Context("piMotionMaster"){
|
axisName: String,
|
||||||
|
axis: PiMotionMasterDevice.Axis,
|
||||||
|
) {
|
||||||
|
var min by remember { mutableStateOf(0f) }
|
||||||
|
var max by remember { mutableStateOf(1f) }
|
||||||
|
var targetPosition by remember { mutableStateOf(0f) }
|
||||||
|
val position: Double by axis.composeState(PiMotionMasterDevice.Axis.position, 0.0)
|
||||||
|
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
LaunchedEffect(axis) {
|
||||||
|
min = axis.read(PiMotionMasterDevice.Axis.minPosition).toFloat()
|
||||||
|
max = axis.read(PiMotionMasterDevice.Axis.maxPosition).toFloat()
|
||||||
|
targetPosition = axis.read(PiMotionMasterDevice.Axis.position).toFloat()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Row {
|
||||||
|
Text(axisName)
|
||||||
|
|
||||||
|
Column {
|
||||||
|
Slider(
|
||||||
|
value = position.toFloat(),
|
||||||
|
enabled = false,
|
||||||
|
onValueChange = { },
|
||||||
|
valueRange = min..max
|
||||||
|
)
|
||||||
|
Slider(
|
||||||
|
value = targetPosition,
|
||||||
|
onValueChange = { newPosition ->
|
||||||
|
targetPosition = newPosition
|
||||||
|
scope.launch {
|
||||||
|
axis.move(newPosition.toDouble())
|
||||||
|
}
|
||||||
|
},
|
||||||
|
valueRange = min..max
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun AxisPane(axes: Map<String, PiMotionMasterDevice.Axis>) {
|
||||||
|
Column {
|
||||||
|
axes.forEach { (name, axis) ->
|
||||||
|
this.piMotionMasterAxis(name, axis)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun PiMotionMasterApp(device: PiMotionMasterDevice) {
|
||||||
|
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
val connected by device.composeState(PiMotionMasterDevice.connected, false)
|
||||||
|
var debugServerJob by remember { mutableStateOf<Job?>(null) }
|
||||||
|
var axes by remember { mutableStateOf<Map<String, PiMotionMasterDevice.Axis>?>(null) }
|
||||||
|
//private val axisList = FXCollections.observableArrayList<Map.Entry<String, PiMotionMasterDevice.Axis>>()
|
||||||
|
var host by remember { mutableStateOf("127.0.0.1") }
|
||||||
|
var port by remember { mutableStateOf(10024) }
|
||||||
|
|
||||||
|
Scaffold {
|
||||||
|
Column {
|
||||||
|
|
||||||
|
|
||||||
|
Text("Address:")
|
||||||
|
Row {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = host,
|
||||||
|
onValueChange = { host = it },
|
||||||
|
label = { Text("Host") },
|
||||||
|
enabled = debugServerJob == null,
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
)
|
||||||
|
var portError by remember { mutableStateOf(false) }
|
||||||
|
OutlinedTextField(
|
||||||
|
value = port.toString(),
|
||||||
|
onValueChange = {
|
||||||
|
it.toIntOrNull()?.let { value ->
|
||||||
|
port = value
|
||||||
|
portError = false
|
||||||
|
} ?: run {
|
||||||
|
portError = true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
label = { Text("Port") },
|
||||||
|
enabled = debugServerJob == null,
|
||||||
|
isError = portError,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Row {
|
||||||
|
Button(
|
||||||
|
onClick = {
|
||||||
|
if (debugServerJob == null) {
|
||||||
|
debugServerJob = device.context.launchPiDebugServer(port, listOf("1", "2", "3", "4"))
|
||||||
|
} else {
|
||||||
|
debugServerJob?.cancel()
|
||||||
|
debugServerJob = null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
if (debugServerJob == null) {
|
||||||
|
Text("Start debug server")
|
||||||
|
} else {
|
||||||
|
Text("Stop debug server")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Row {
|
||||||
|
Button(
|
||||||
|
onClick = {
|
||||||
|
if (!connected) {
|
||||||
|
device.launch {
|
||||||
|
device.connect(host, port)
|
||||||
|
axes = device.axes
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
device.launch {
|
||||||
|
device.disconnect()
|
||||||
|
axes = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
if (!connected) {
|
||||||
|
Text("Connect")
|
||||||
|
} else {
|
||||||
|
Text("Disconnect")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
axes?.let { axes ->
|
||||||
|
AxisPane(axes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun main() = application {
|
||||||
|
|
||||||
|
val context = Context("piMotionMaster") {
|
||||||
plugin(DeviceManager)
|
plugin(DeviceManager)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -34,131 +198,14 @@ class PiMotionMasterController : Controller() {
|
|||||||
|
|
||||||
// install device
|
// install device
|
||||||
val motionMaster: PiMotionMasterDevice by deviceManager.installing(PiMotionMasterDevice)
|
val motionMaster: PiMotionMasterDevice by deviceManager.installing(PiMotionMasterDevice)
|
||||||
}
|
|
||||||
|
|
||||||
fun VBox.piMotionMasterAxis(
|
Window(
|
||||||
axisName: String,
|
title = "Pi motion master demo",
|
||||||
axis: PiMotionMasterDevice.Axis,
|
onCloseRequest = { exitApplication() },
|
||||||
coroutineScope: CoroutineScope,
|
state = rememberWindowState(width = 400.dp, height = 300.dp)
|
||||||
) = hbox {
|
) {
|
||||||
alignment = Pos.CENTER
|
MaterialTheme {
|
||||||
label(axisName)
|
PiMotionMasterApp(motionMaster)
|
||||||
coroutineScope.launch {
|
|
||||||
with(axis) {
|
|
||||||
val min: Double = read(minPosition)
|
|
||||||
val max: Double = read(maxPosition)
|
|
||||||
val positionProperty = fxProperty(position)
|
|
||||||
val startPosition = read(position)
|
|
||||||
runLater {
|
|
||||||
vbox {
|
|
||||||
hgrow = Priority.ALWAYS
|
|
||||||
slider(min..max, startPosition) {
|
|
||||||
minWidth = 300.0
|
|
||||||
isShowTickLabels = true
|
|
||||||
isShowTickMarks = true
|
|
||||||
minorTickCount = 10
|
|
||||||
majorTickUnit = 1.0
|
|
||||||
valueProperty().onChange {
|
|
||||||
coroutineScope.launch {
|
|
||||||
axis.move(value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
slider(min..max) {
|
|
||||||
isDisable = true
|
|
||||||
valueProperty().bind(positionProperty)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
fun Parent.axisPane(axes: Map<String, PiMotionMasterDevice.Axis>, coroutineScope: CoroutineScope) {
|
|
||||||
vbox {
|
|
||||||
axes.forEach { (name, axis) ->
|
|
||||||
this.piMotionMasterAxis(name, axis, coroutineScope)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class PiMotionMasterView : View() {
|
|
||||||
|
|
||||||
private val controller: PiMotionMasterController by inject()
|
|
||||||
val device = controller.motionMaster
|
|
||||||
|
|
||||||
private val connectedProperty: ReadOnlyProperty<Boolean> = device.fxProperty(PiMotionMasterDevice.connected)
|
|
||||||
private val debugServerJobProperty = SimpleObjectProperty<Job>()
|
|
||||||
private val debugServerStarted = debugServerJobProperty.booleanBinding { it != null }
|
|
||||||
//private val axisList = FXCollections.observableArrayList<Map.Entry<String, PiMotionMasterDevice.Axis>>()
|
|
||||||
|
|
||||||
override val root: Parent = borderpane {
|
|
||||||
top {
|
|
||||||
form {
|
|
||||||
val host = SimpleStringProperty("127.0.0.1")
|
|
||||||
val port = SimpleIntegerProperty(10024)
|
|
||||||
fieldset("Address:") {
|
|
||||||
field("Host:") {
|
|
||||||
textfield(host) {
|
|
||||||
enableWhen(debugServerStarted.not())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
field("Port:") {
|
|
||||||
textfield(port) {
|
|
||||||
stripNonNumeric()
|
|
||||||
}
|
|
||||||
button {
|
|
||||||
hgrow = Priority.ALWAYS
|
|
||||||
textProperty().bind(debugServerStarted.stringBinding {
|
|
||||||
if (it != true) {
|
|
||||||
"Start debug server"
|
|
||||||
} else {
|
|
||||||
"Stop debug server"
|
|
||||||
}
|
|
||||||
})
|
|
||||||
action {
|
|
||||||
if (!debugServerStarted.get()) {
|
|
||||||
debugServerJobProperty.value =
|
|
||||||
controller.context.launchPiDebugServer(port.get(), listOf("1", "2", "3", "4"))
|
|
||||||
} else {
|
|
||||||
debugServerJobProperty.get().cancel()
|
|
||||||
debugServerJobProperty.value = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
hgrow = Priority.ALWAYS
|
|
||||||
textProperty().bind(connectedProperty.stringBinding {
|
|
||||||
if (it == false) {
|
|
||||||
"Connect"
|
|
||||||
} else {
|
|
||||||
"Disconnect"
|
|
||||||
}
|
|
||||||
})
|
|
||||||
action {
|
|
||||||
if (!connectedProperty.value) {
|
|
||||||
device.connect(host.get(), port.get())
|
|
||||||
center {
|
|
||||||
axisPane(device.axes,controller.context)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this@borderpane.center = null
|
|
||||||
device.disconnect()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun main() {
|
|
||||||
launch<PiMotionMasterApp>()
|
|
||||||
}
|
}
|
@ -7,13 +7,15 @@ import kotlinx.coroutines.delay
|
|||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.flow.toList
|
import kotlinx.coroutines.flow.toList
|
||||||
import kotlinx.coroutines.flow.transformWhile
|
import kotlinx.coroutines.flow.transformWhile
|
||||||
import kotlinx.coroutines.runBlocking
|
|
||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.sync.Mutex
|
||||||
import kotlinx.coroutines.sync.withLock
|
import kotlinx.coroutines.sync.withLock
|
||||||
import kotlinx.coroutines.withTimeout
|
import kotlinx.coroutines.withTimeout
|
||||||
import space.kscience.controls.api.DeviceHub
|
import space.kscience.controls.api.DeviceHub
|
||||||
import space.kscience.controls.api.PropertyDescriptor
|
import space.kscience.controls.api.PropertyDescriptor
|
||||||
import space.kscience.controls.ports.*
|
import space.kscience.controls.ports.AsynchronousPort
|
||||||
|
import space.kscience.controls.ports.KtorTcpPort
|
||||||
|
import space.kscience.controls.ports.send
|
||||||
|
import space.kscience.controls.ports.withStringDelimiter
|
||||||
import space.kscience.controls.spec.*
|
import space.kscience.controls.spec.*
|
||||||
import space.kscience.dataforge.context.*
|
import space.kscience.dataforge.context.*
|
||||||
import space.kscience.dataforge.meta.*
|
import space.kscience.dataforge.meta.*
|
||||||
@ -33,10 +35,8 @@ class PiMotionMasterDevice(
|
|||||||
//PortProxy { portFactory(address ?: error("The device is not connected"), context) }
|
//PortProxy { portFactory(address ?: error("The device is not connected"), context) }
|
||||||
|
|
||||||
|
|
||||||
fun disconnect() {
|
suspend fun disconnect() {
|
||||||
runBlocking {
|
execute(disconnect)
|
||||||
execute(disconnect)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var timeoutValue: Duration = 200.milliseconds
|
var timeoutValue: Duration = 200.milliseconds
|
||||||
@ -54,13 +54,11 @@ class PiMotionMasterDevice(
|
|||||||
if (errorCode != 0) error(message(errorCode))
|
if (errorCode != 0) error(message(errorCode))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun connect(host: String, port: Int) {
|
suspend fun connect(host: String, port: Int) {
|
||||||
runBlocking {
|
execute(connect, Meta {
|
||||||
execute(connect, Meta {
|
"host" put host
|
||||||
"host" put host
|
"port" put port
|
||||||
"port" put port
|
})
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private val mutex = Mutex()
|
private val mutex = Mutex()
|
||||||
@ -103,7 +101,7 @@ class PiMotionMasterDevice(
|
|||||||
}.toList()
|
}.toList()
|
||||||
}
|
}
|
||||||
} catch (ex: Throwable) {
|
} catch (ex: Throwable) {
|
||||||
logger.warn { "Error during PIMotionMaster request. Requesting error code." }
|
logger.error(ex) { "Error during PIMotionMaster request. Requesting error code." }
|
||||||
val errorCode = getErrorCode()
|
val errorCode = getErrorCode()
|
||||||
dispatchError(errorCode)
|
dispatchError(errorCode)
|
||||||
logger.warn { "Error code $errorCode" }
|
logger.warn { "Error code $errorCode" }
|
||||||
@ -167,12 +165,12 @@ class PiMotionMasterDevice(
|
|||||||
}
|
}
|
||||||
//Update port
|
//Update port
|
||||||
//address = portSpec.node
|
//address = portSpec.node
|
||||||
port = portFactory(portSpec, context)
|
port = portFactory(portSpec, context).apply { open() }
|
||||||
propertyChanged(connected, true)
|
|
||||||
// connector.open()
|
// connector.open()
|
||||||
//Initialize axes
|
//Initialize axes
|
||||||
val idn = read(identity)
|
val idn = read(identity)
|
||||||
failIfError { "Can't connect to $portSpec. Error code: $it" }
|
failIfError { "Can't connect to $portSpec. Error code: $it" }
|
||||||
|
propertyChanged(connected, true)
|
||||||
logger.info { "Connected to $idn on $portSpec" }
|
logger.info { "Connected to $idn on $portSpec" }
|
||||||
val ids = request("SAI?").map { it.trim() }
|
val ids = request("SAI?").map { it.trim() }
|
||||||
if (ids != axes.keys.toList()) {
|
if (ids != axes.keys.toList()) {
|
||||||
|
@ -0,0 +1,15 @@
|
|||||||
|
package ru.mipt.npm.devices.pimotionmaster
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.State
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import space.kscience.controls.api.Device
|
||||||
|
import space.kscience.controls.spec.DevicePropertySpec
|
||||||
|
import space.kscience.controls.spec.propertyFlow
|
||||||
|
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun <D : Device, T : Any> D.composeState(
|
||||||
|
spec: DevicePropertySpec<D, T>,
|
||||||
|
initialState: T,
|
||||||
|
): State<T> = propertyFlow(spec).collectAsState(initialState)
|
@ -1,58 +0,0 @@
|
|||||||
package ru.mipt.npm.devices.pimotionmaster
|
|
||||||
|
|
||||||
import javafx.beans.property.ObjectPropertyBase
|
|
||||||
import javafx.beans.property.Property
|
|
||||||
import javafx.beans.property.ReadOnlyProperty
|
|
||||||
import space.kscience.controls.api.Device
|
|
||||||
import space.kscience.controls.spec.*
|
|
||||||
import space.kscience.dataforge.context.info
|
|
||||||
import space.kscience.dataforge.context.logger
|
|
||||||
import tornadofx.*
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Bind a FX property to a device property with a given [spec]
|
|
||||||
*/
|
|
||||||
fun <D : Device, T : Any> D.fxProperty(
|
|
||||||
spec: DevicePropertySpec<D, T>,
|
|
||||||
): ReadOnlyProperty<T> = object : ObjectPropertyBase<T>() {
|
|
||||||
override fun getBean(): Any = this
|
|
||||||
override fun getName(): String = spec.name
|
|
||||||
|
|
||||||
init {
|
|
||||||
//Read incoming changes
|
|
||||||
onPropertyChange(spec) {
|
|
||||||
runLater {
|
|
||||||
try {
|
|
||||||
set(it)
|
|
||||||
} catch (ex: Throwable) {
|
|
||||||
logger.info { "Failed to set property $name to $it" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun <D : Device, T : Any> D.fxProperty(spec: MutableDevicePropertySpec<D, T>): Property<T> =
|
|
||||||
object : ObjectPropertyBase<T>() {
|
|
||||||
override fun getBean(): Any = this
|
|
||||||
override fun getName(): String = spec.name
|
|
||||||
|
|
||||||
init {
|
|
||||||
//Read incoming changes
|
|
||||||
onPropertyChange(spec) {
|
|
||||||
runLater {
|
|
||||||
try {
|
|
||||||
set(it)
|
|
||||||
} catch (ex: Throwable) {
|
|
||||||
logger.info { "Failed to set property $name to $it" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onChange { newValue ->
|
|
||||||
if (newValue != null) {
|
|
||||||
writeAsync(spec, newValue)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -8,6 +8,8 @@ import io.ktor.util.InternalAPI
|
|||||||
import io.ktor.util.moveToByteArray
|
import io.ktor.util.moveToByteArray
|
||||||
import io.ktor.utils.io.writeAvailable
|
import io.ktor.utils.io.writeAvailable
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
import space.kscience.dataforge.context.Context
|
import space.kscience.dataforge.context.Context
|
||||||
import space.kscience.dataforge.context.Global
|
import space.kscience.dataforge.context.Global
|
||||||
|
|
||||||
@ -18,41 +20,40 @@ val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
|
|||||||
@OptIn(InternalAPI::class)
|
@OptIn(InternalAPI::class)
|
||||||
fun Context.launchPiDebugServer(port: Int, axes: List<String>): Job = launch(exceptionHandler) {
|
fun Context.launchPiDebugServer(port: Int, axes: List<String>): Job = launch(exceptionHandler) {
|
||||||
val virtualDevice = PiMotionMasterVirtualDevice(this@launchPiDebugServer, axes)
|
val virtualDevice = PiMotionMasterVirtualDevice(this@launchPiDebugServer, axes)
|
||||||
val server = aSocket(ActorSelectorManager(Dispatchers.IO)).tcp().bind("localhost", port)
|
aSocket(ActorSelectorManager(Dispatchers.IO)).tcp().bind("localhost", port).use { server ->
|
||||||
println("Started virtual port server at ${server.localAddress}")
|
println("Started virtual port server at ${server.localAddress}")
|
||||||
|
|
||||||
while (isActive) {
|
while (isActive) {
|
||||||
val socket = server.accept()
|
val socket = server.accept()
|
||||||
launch(SupervisorJob(coroutineContext[Job])) {
|
launch(SupervisorJob(coroutineContext[Job])) {
|
||||||
println("Socket accepted: ${socket.remoteAddress}")
|
println("Socket accepted: ${socket.remoteAddress}")
|
||||||
val input = socket.openReadChannel()
|
val input = socket.openReadChannel()
|
||||||
val output = socket.openWriteChannel()
|
val output = socket.openWriteChannel()
|
||||||
|
|
||||||
val sendJob = launch {
|
val sendJob = virtualDevice.subscribe().onEach {
|
||||||
virtualDevice.subscribe().collect {
|
|
||||||
//println("Sending: ${it.decodeToString()}")
|
//println("Sending: ${it.decodeToString()}")
|
||||||
output.writeAvailable(it)
|
output.writeAvailable(it)
|
||||||
output.flush()
|
output.flush()
|
||||||
}
|
}.launchIn(this)
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
while (isActive) {
|
try {
|
||||||
input.read { buffer ->
|
while (isActive) {
|
||||||
val array = buffer.moveToByteArray()
|
input.read { buffer ->
|
||||||
launch {
|
val array = buffer.moveToByteArray()
|
||||||
virtualDevice.send(array)
|
launch {
|
||||||
|
virtualDevice.send(array)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
e.printStackTrace()
|
||||||
|
sendJob.cancel()
|
||||||
|
socket.close()
|
||||||
|
} finally {
|
||||||
|
println("Client socket closed")
|
||||||
}
|
}
|
||||||
} catch (e: Throwable) {
|
|
||||||
e.printStackTrace()
|
|
||||||
sendJob.cancel()
|
|
||||||
socket.close()
|
|
||||||
} finally {
|
|
||||||
println("Socket closed")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,36 +1,38 @@
|
|||||||
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`
|
||||||
application
|
|
||||||
}
|
}
|
||||||
|
|
||||||
description = """
|
description = """
|
||||||
A magix event loop implementation in Kotlin. Includes HTTP/SSE and RSocket routes.
|
A magix event loop implementation in Kotlin. Includes HTTP/SSE and RSocket routes.
|
||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
|
|
||||||
kscience {
|
|
||||||
useSerialization{
|
|
||||||
json()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val dataforgeVersion: String by rootProject.extra
|
val dataforgeVersion: String by rootProject.extra
|
||||||
val ktorVersion: String = space.kscience.gradle.KScienceVersions.ktorVersion
|
val ktorVersion: String = space.kscience.gradle.KScienceVersions.ktorVersion
|
||||||
|
|
||||||
dependencies{
|
kscience {
|
||||||
api(projects.magix.magixApi)
|
jvm()
|
||||||
api("io.ktor:ktor-server-cio:$ktorVersion")
|
useSerialization{
|
||||||
api("io.ktor:ktor-server-websockets:$ktorVersion")
|
json()
|
||||||
api("io.ktor:ktor-server-content-negotiation:$ktorVersion")
|
}
|
||||||
api("io.ktor:ktor-serialization-kotlinx-json:$ktorVersion")
|
|
||||||
api("io.ktor:ktor-server-html-builder:$ktorVersion")
|
jvmMain{
|
||||||
|
api(projects.magix.magixApi)
|
||||||
|
api("io.ktor:ktor-server-cio:$ktorVersion")
|
||||||
|
api("io.ktor:ktor-server-websockets:$ktorVersion")
|
||||||
|
api("io.ktor:ktor-server-content-negotiation:$ktorVersion")
|
||||||
|
api("io.ktor:ktor-serialization-kotlinx-json:$ktorVersion")
|
||||||
|
api("io.ktor:ktor-server-html-builder:$ktorVersion")
|
||||||
|
|
||||||
|
api(libs.rsocket.ktor.server)
|
||||||
|
api(libs.rsocket.transport.ktor.tcp)
|
||||||
|
}
|
||||||
|
|
||||||
api(libs.rsocket.ktor.server)
|
|
||||||
api(libs.rsocket.transport.ktor.tcp)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
readme{
|
readme{
|
||||||
maturity = Maturity.EXPERIMENTAL
|
maturity = Maturity.EXPERIMENTAL
|
||||||
}
|
}
|
@ -18,7 +18,6 @@ pluginManagement {
|
|||||||
id("space.kscience.gradle.mpp") version toolsVersion
|
id("space.kscience.gradle.mpp") version toolsVersion
|
||||||
id("space.kscience.gradle.jvm") version toolsVersion
|
id("space.kscience.gradle.jvm") version toolsVersion
|
||||||
id("space.kscience.gradle.js") version toolsVersion
|
id("space.kscience.gradle.js") version toolsVersion
|
||||||
id("org.openjfx.javafxplugin") version "0.0.13"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user