Compare commits
No commits in common. "a9592d0372e07a32f9461a8493d2fb6573fca5ca" and "4b05f46fa72dad09e4eb103bc39326eed63289fe" have entirely different histories.
a9592d0372
...
4b05f46fa7
@ -3,11 +3,12 @@ import space.kscience.gradle.useSPCTeam
|
||||
|
||||
plugins {
|
||||
id("space.kscience.gradle.project")
|
||||
alias(libs.plugins.versions)
|
||||
}
|
||||
|
||||
allprojects {
|
||||
group = "space.kscience"
|
||||
version = "0.4.0-dev-1"
|
||||
version = "0.3.1-dev-1"
|
||||
repositories{
|
||||
maven("https://maven.pkg.jetbrains.space/spc/p/sci/dev")
|
||||
}
|
||||
|
@ -10,14 +10,9 @@ description = """
|
||||
kscience{
|
||||
jvm()
|
||||
js()
|
||||
useCoroutines()
|
||||
commonMain {
|
||||
dependencies {
|
||||
api(projects.controlsCore)
|
||||
}
|
||||
|
||||
commonTest{
|
||||
implementation(spclibs.logback.classic)
|
||||
}
|
||||
}
|
||||
|
||||
readme{
|
||||
|
@ -1,26 +0,0 @@
|
||||
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,16 +1,9 @@
|
||||
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.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.Factory
|
||||
import space.kscience.dataforge.context.request
|
||||
import space.kscience.dataforge.meta.Meta
|
||||
import space.kscience.dataforge.meta.MetaConverter
|
||||
import space.kscience.dataforge.names.Name
|
||||
@ -21,154 +14,105 @@ import kotlin.reflect.KProperty
|
||||
import kotlin.time.Duration
|
||||
|
||||
/**
|
||||
* A base for strongly typed device constructor block. Has additional delegates for type-safe devices
|
||||
* A base for strongly typed device constructor blocks. Has additional delegates for type-safe devices
|
||||
*/
|
||||
public abstract class DeviceConstructor(
|
||||
context: Context,
|
||||
meta: Meta = Meta.EMPTY,
|
||||
meta: 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))
|
||||
}
|
||||
|
||||
/**
|
||||
* Create and register a timer. Timer is not counted as a device property.
|
||||
* Register a device, provided by a given [factory] and
|
||||
*/
|
||||
public fun timer(tick: Duration): TimerState = TimerState(context.request(ClockManager), tick)
|
||||
.also { registerBinding(StateBinding(it)) }
|
||||
public fun <D : Device> device(
|
||||
factory: Factory<D>,
|
||||
meta: Meta? = null,
|
||||
nameOverride: Name? = null,
|
||||
metaLocation: Name? = null,
|
||||
): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, D>> =
|
||||
PropertyDelegateProvider { _: DeviceConstructor, property: KProperty<*> ->
|
||||
val name = nameOverride ?: property.name.asName()
|
||||
val device = install(name, factory, meta, metaLocation ?: name)
|
||||
ReadOnlyProperty { _: DeviceConstructor, _ ->
|
||||
device
|
||||
}
|
||||
}
|
||||
|
||||
public fun <D : Device> device(
|
||||
device: D,
|
||||
nameOverride: Name? = null,
|
||||
): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, D>> =
|
||||
PropertyDelegateProvider { _: DeviceConstructor, property: KProperty<*> ->
|
||||
val name = nameOverride ?: property.name.asName()
|
||||
install(name, device)
|
||||
ReadOnlyProperty { _: DeviceConstructor, _ ->
|
||||
device
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Launch action that is performed on each [DeviceState] value change.
|
||||
*
|
||||
* Optionally provide [writes] - a set of states that this change affects.
|
||||
* Register a property and provide a direct reader for it
|
||||
*/
|
||||
public fun <T> DeviceState<T>.onChange(
|
||||
vararg writes: DeviceState<*>,
|
||||
reads: Collection<DeviceState<*>>,
|
||||
onChange: suspend (T) -> Unit,
|
||||
): Job = valueFlow.onEach(onChange).launchIn(this@DeviceConstructor).also {
|
||||
registerBinding(ActionBinding(setOf(this, *reads.toTypedArray()), setOf(*writes)))
|
||||
}
|
||||
public fun <T, S: DeviceState<T>> property(
|
||||
state: S,
|
||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||
nameOverride: String? = null,
|
||||
): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, S>> =
|
||||
PropertyDelegateProvider { _: DeviceConstructor, property ->
|
||||
val name = nameOverride ?: property.name
|
||||
val descriptor = PropertyDescriptor(name).apply(descriptorBuilder)
|
||||
registerProperty(descriptor, state)
|
||||
ReadOnlyProperty { _: DeviceConstructor, _ ->
|
||||
state
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register external state as a property
|
||||
*/
|
||||
public fun <T : Any> property(
|
||||
metaConverter: MetaConverter<T>,
|
||||
reader: suspend () -> T,
|
||||
readInterval: Duration,
|
||||
initialState: T,
|
||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||
nameOverride: String? = null,
|
||||
): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, DeviceState<T>>> = property(
|
||||
DeviceState.external(this, metaConverter, readInterval, initialState, reader),
|
||||
descriptorBuilder,
|
||||
nameOverride,
|
||||
)
|
||||
|
||||
/**
|
||||
* Register a mutable external state as a property
|
||||
*/
|
||||
public fun <T : Any> mutableProperty(
|
||||
metaConverter: MetaConverter<T>,
|
||||
reader: suspend () -> T,
|
||||
writer: suspend (T) -> Unit,
|
||||
readInterval: Duration,
|
||||
initialState: T,
|
||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||
nameOverride: String? = null,
|
||||
): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, MutableDeviceState<T>>> = property(
|
||||
DeviceState.external(this, metaConverter, readInterval, initialState, reader, writer),
|
||||
descriptorBuilder,
|
||||
nameOverride,
|
||||
)
|
||||
|
||||
/**
|
||||
* Create and register a virtual mutable property with optional [callback]
|
||||
*/
|
||||
public fun <T> virtualProperty(
|
||||
metaConverter: MetaConverter<T>,
|
||||
initialState: T,
|
||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||
nameOverride: String? = null,
|
||||
callback: (T) -> Unit = {},
|
||||
): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, MutableDeviceState<T>>> = property(
|
||||
DeviceState.virtual(metaConverter, initialState, callback),
|
||||
descriptorBuilder,
|
||||
nameOverride,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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> DeviceConstructor.device(
|
||||
device: D,
|
||||
nameOverride: Name? = null,
|
||||
): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, D>> =
|
||||
PropertyDelegateProvider { _: DeviceConstructor, property: KProperty<*> ->
|
||||
val name = nameOverride ?: property.name.asName()
|
||||
install(name, device)
|
||||
ReadOnlyProperty { _: DeviceConstructor, _ ->
|
||||
device
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a property and provide a direct reader for it
|
||||
*/
|
||||
public fun <T, S : DeviceState<T>> DeviceConstructor.property(
|
||||
state: S,
|
||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||
nameOverride: String? = null,
|
||||
): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, S>> =
|
||||
PropertyDelegateProvider { _: DeviceConstructor, property ->
|
||||
val name = nameOverride ?: property.name
|
||||
val descriptor = PropertyDescriptor(name).apply(descriptorBuilder)
|
||||
registerProperty(descriptor, state)
|
||||
ReadOnlyProperty { _: DeviceConstructor, _ ->
|
||||
state
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register external state as a property
|
||||
*/
|
||||
public fun <T : Any> DeviceConstructor.property(
|
||||
metaConverter: MetaConverter<T>,
|
||||
reader: suspend () -> T,
|
||||
readInterval: Duration,
|
||||
initialState: T,
|
||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||
nameOverride: String? = null,
|
||||
): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, DeviceState<T>>> = property(
|
||||
DeviceState.external(this, metaConverter, readInterval, initialState, reader),
|
||||
descriptorBuilder,
|
||||
nameOverride,
|
||||
)
|
||||
|
||||
/**
|
||||
* Register a mutable external state as a property
|
||||
*/
|
||||
public fun <T : Any> DeviceConstructor.mutableProperty(
|
||||
metaConverter: MetaConverter<T>,
|
||||
reader: suspend () -> T,
|
||||
writer: suspend (T) -> Unit,
|
||||
readInterval: Duration,
|
||||
initialState: T,
|
||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||
nameOverride: String? = null,
|
||||
): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, MutableDeviceState<T>>> = property(
|
||||
DeviceState.external(this, metaConverter, readInterval, initialState, reader, writer),
|
||||
descriptorBuilder,
|
||||
nameOverride,
|
||||
)
|
||||
|
||||
/**
|
||||
* Create and register a virtual mutable property with optional [callback]
|
||||
*/
|
||||
public fun <T> DeviceConstructor.virtualProperty(
|
||||
metaConverter: MetaConverter<T>,
|
||||
initialState: T,
|
||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||
nameOverride: String? = null,
|
||||
callback: (T) -> Unit = {},
|
||||
): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, MutableDeviceState<T>>> = property(
|
||||
DeviceState.internal(metaConverter, initialState, callback),
|
||||
descriptorBuilder,
|
||||
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,7 +9,9 @@ import space.kscience.controls.api.*
|
||||
import space.kscience.controls.api.DeviceLifecycleState.*
|
||||
import space.kscience.controls.manager.DeviceManager
|
||||
import space.kscience.controls.manager.install
|
||||
import space.kscience.dataforge.context.*
|
||||
import space.kscience.dataforge.context.Context
|
||||
import space.kscience.dataforge.context.Factory
|
||||
import space.kscience.dataforge.context.request
|
||||
import space.kscience.dataforge.meta.*
|
||||
import space.kscience.dataforge.misc.DFExperimental
|
||||
import space.kscience.dataforge.names.*
|
||||
@ -55,7 +57,6 @@ public open class DeviceGroup(
|
||||
)
|
||||
)
|
||||
}
|
||||
logger.error(throwable) { "Exception in device $id" }
|
||||
}
|
||||
)
|
||||
|
||||
@ -68,7 +69,7 @@ public open class DeviceGroup(
|
||||
* Register and initialize (synchronize child's lifecycle state with group state) a new device in this group
|
||||
*/
|
||||
@OptIn(DFExperimental::class)
|
||||
public open fun <D : Device> install(token: NameToken, device: D): D {
|
||||
public fun <D : Device> install(token: NameToken, device: D): D {
|
||||
require(_devices[token] == null) { "A child device with name $token already exists" }
|
||||
//start the child device if needed
|
||||
if (lifecycleState == STARTED || lifecycleState == STARTING) launch { device.start() }
|
||||
@ -81,7 +82,7 @@ public open class DeviceGroup(
|
||||
/**
|
||||
* Register a new property based on [DeviceState]. Properties could be modified dynamically
|
||||
*/
|
||||
public open fun registerProperty(descriptor: PropertyDescriptor, state: DeviceState<*>) {
|
||||
public fun registerProperty(descriptor: PropertyDescriptor, state: DeviceState<*>) {
|
||||
val name = descriptor.name.parseAsName()
|
||||
require(properties[name] == null) { "Can't add property with name $name. It already exists." }
|
||||
properties[name] = Property(state, descriptor)
|
||||
@ -125,33 +126,37 @@ public open class DeviceGroup(
|
||||
return action.invoke(argument)
|
||||
}
|
||||
|
||||
final override var lifecycleState: DeviceLifecycleState = DeviceLifecycleState.STOPPED
|
||||
private set
|
||||
|
||||
|
||||
private suspend fun setLifecycleState(lifecycleState: DeviceLifecycleState) {
|
||||
this.lifecycleState = lifecycleState
|
||||
sharedMessageFlow.emit(
|
||||
DeviceLifeCycleMessage(lifecycleState)
|
||||
)
|
||||
}
|
||||
@DFExperimental
|
||||
override var lifecycleState: DeviceLifecycleState = STOPPED
|
||||
protected set(value) {
|
||||
if (field != value) {
|
||||
launch {
|
||||
sharedMessageFlow.emit(
|
||||
DeviceLifeCycleMessage(value)
|
||||
)
|
||||
}
|
||||
}
|
||||
field = value
|
||||
}
|
||||
|
||||
|
||||
@OptIn(DFExperimental::class)
|
||||
override suspend fun start() {
|
||||
setLifecycleState(STARTING)
|
||||
lifecycleState = STARTING
|
||||
super.start()
|
||||
devices.values.forEach {
|
||||
it.start()
|
||||
}
|
||||
setLifecycleState(STARTED)
|
||||
lifecycleState = STARTED
|
||||
}
|
||||
|
||||
override suspend fun stop() {
|
||||
@OptIn(DFExperimental::class)
|
||||
override fun stop() {
|
||||
devices.values.forEach {
|
||||
it.stop()
|
||||
}
|
||||
setLifecycleState(STOPPED)
|
||||
super.stop()
|
||||
lifecycleState = STOPPED
|
||||
}
|
||||
|
||||
public companion object {
|
||||
@ -205,9 +210,13 @@ public fun <D : Device> DeviceGroup.install(name: Name, device: D): D {
|
||||
}
|
||||
}
|
||||
|
||||
public fun <D : Device> DeviceGroup.install(name: String, device: D): D = install(name.parseAsName(), device)
|
||||
public fun <D : Device> DeviceGroup.install(name: String, device: D): D =
|
||||
install(name.parseAsName(), device)
|
||||
|
||||
public fun <D : Device> DeviceGroup.install(device: D): D = install(device.id, device)
|
||||
public fun <D : Device> DeviceGroup.install(device: D): D =
|
||||
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.
|
||||
@ -283,7 +292,7 @@ public fun <T : Any> DeviceGroup.registerVirtualProperty(
|
||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||
callback: (T) -> Unit = {},
|
||||
): MutableDeviceState<T> {
|
||||
val state = DeviceState.internal<T>(converter, initialValue, callback)
|
||||
val state = DeviceState.virtual<T>(converter, initialValue, callback)
|
||||
registerMutableProperty(name, state, descriptorBuilder)
|
||||
return state
|
||||
}
|
||||
|
@ -2,13 +2,18 @@ package space.kscience.controls.constructor
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import space.kscience.controls.api.Device
|
||||
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.MetaConverter
|
||||
import kotlin.reflect.KProperty
|
||||
import kotlin.time.Duration
|
||||
|
||||
/**
|
||||
* An observable state of a device
|
||||
@ -19,8 +24,6 @@ public interface DeviceState<T> {
|
||||
|
||||
public val valueFlow: Flow<T>
|
||||
|
||||
override fun toString(): String
|
||||
|
||||
public companion object
|
||||
}
|
||||
|
||||
@ -33,7 +36,7 @@ public operator fun <T> DeviceState<T>.getValue(thisRef: Any?, property: KProper
|
||||
/**
|
||||
* 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)
|
||||
|
||||
/**
|
||||
@ -54,46 +57,186 @@ public var <T> MutableDeviceState<T>.valueAsMeta: Meta
|
||||
}
|
||||
|
||||
/**
|
||||
* Device state with a value that depends on other device states
|
||||
* A [MutableDeviceState] that does not correspond to a physical state
|
||||
*
|
||||
* @param callback a synchronous callback that could be used without a scope
|
||||
*/
|
||||
public interface DeviceStateWithDependencies<T> : DeviceState<T> {
|
||||
public val dependencies: Collection<DeviceState<*>>
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new read-only [DeviceState] that mirrors receiver state by mapping the value with [mapper].
|
||||
* Bind a read-only [DeviceState] to a [Device] property
|
||||
*/
|
||||
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(
|
||||
converter: MetaConverter<R>, mapper: (T) -> R,
|
||||
): DeviceStateWithDependencies<R> = object : DeviceStateWithDependencies<R> {
|
||||
override val dependencies = listOf(this)
|
||||
|
||||
): DeviceState<R> = object : DeviceState<R> {
|
||||
override val converter: MetaConverter<R> = converter
|
||||
|
||||
override val value: R
|
||||
get() = mapper(this@map.value)
|
||||
|
||||
override val valueFlow: Flow<R> = this@map.valueFlow.map(mapper)
|
||||
}
|
||||
|
||||
override fun toString(): String = "DeviceState.map(arg=${this@map}, converter=$converter)"
|
||||
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)
|
||||
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
/**
|
||||
* Combine two device states into one read-only [DeviceState]. Only the latest value of each state is used.
|
||||
* Create a [DeviceState] which is constructed by periodically reading external value
|
||||
*/
|
||||
public fun <T1, T2, R> combine(
|
||||
state1: DeviceState<T1>,
|
||||
state2: DeviceState<T2>,
|
||||
converter: MetaConverter<R>,
|
||||
mapper: (T1, T2) -> R,
|
||||
): DeviceStateWithDependencies<R> = object : DeviceStateWithDependencies<R> {
|
||||
override val dependencies = listOf(state1, state2)
|
||||
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)
|
||||
|
||||
override val converter: MetaConverter<R> = converter
|
||||
|
||||
override val value: R get() = mapper(state1.value, state2.value)
|
||||
|
||||
override val valueFlow: Flow<R> = kotlinx.coroutines.flow.combine(state1.valueFlow, state2.valueFlow, mapper)
|
||||
|
||||
override fun toString(): String = "DeviceState.combine(state1=$state1, state2=$state2)"
|
||||
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 [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)
|
@ -1,12 +1,10 @@
|
||||
package space.kscience.controls.constructor.library
|
||||
package space.kscience.controls.constructor
|
||||
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
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.spec.*
|
||||
import space.kscience.dataforge.context.Context
|
||||
@ -51,7 +49,7 @@ public class VirtualDrive(
|
||||
public val positionState: MutableDeviceState<Double>,
|
||||
) : Drive, DeviceBySpec<Drive>(Drive, context) {
|
||||
|
||||
private val dt = meta["time.step"].double?.milliseconds ?: 5.milliseconds
|
||||
private val dt = meta["time.step"].double?.milliseconds ?: 1.milliseconds
|
||||
private val clock = context.clock
|
||||
|
||||
override var force: Double = 0.0
|
||||
@ -84,7 +82,7 @@ public class VirtualDrive(
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun onStop() {
|
||||
override fun onStop() {
|
||||
updateJob?.cancel()
|
||||
}
|
||||
|
@ -1,9 +1,8 @@
|
||||
package space.kscience.controls.constructor.library
|
||||
package space.kscience.controls.constructor
|
||||
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import space.kscience.controls.api.Device
|
||||
import space.kscience.controls.constructor.DeviceState
|
||||
import space.kscience.controls.spec.DeviceBySpec
|
||||
import space.kscience.controls.spec.DevicePropertySpec
|
||||
import space.kscience.controls.spec.DeviceSpec
|
||||
@ -21,7 +20,7 @@ public interface LimitSwitch : Device {
|
||||
|
||||
public companion object : DeviceSpec<LimitSwitch>() {
|
||||
public val locked: DevicePropertySpec<LimitSwitch, Boolean> by booleanProperty { locked }
|
||||
public operator fun invoke(lockedState: DeviceState<Boolean>): Factory<LimitSwitch> = Factory { context, _ ->
|
||||
public fun factory(lockedState: DeviceState<Boolean>): Factory<LimitSwitch> = Factory { context, _ ->
|
||||
VirtualLimitSwitch(context, lockedState)
|
||||
}
|
||||
}
|
||||
@ -35,7 +34,7 @@ public class VirtualLimitSwitch(
|
||||
public val lockedState: DeviceState<Boolean>,
|
||||
) : DeviceBySpec<LimitSwitch>(LimitSwitch, context), LimitSwitch {
|
||||
|
||||
override suspend fun onStart() {
|
||||
init {
|
||||
lockedState.valueFlow.onEach {
|
||||
propertyChanged(LimitSwitch.locked, it)
|
||||
}.launchIn(this)
|
@ -1,4 +1,4 @@
|
||||
package space.kscience.controls.constructor.library
|
||||
package space.kscience.controls.constructor
|
||||
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
@ -7,11 +7,8 @@ import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
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.spec.DeviceBySpec
|
||||
import space.kscience.controls.spec.write
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import kotlin.time.DurationUnit
|
||||
@ -74,17 +71,15 @@ public class PidRegulator(
|
||||
lastTime = realTime
|
||||
lastPosition = drive.position
|
||||
|
||||
drive.write(Drive.force,pidParameters.kp * delta + pidParameters.ki * integral + pidParameters.kd * derivative)
|
||||
//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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun onStop() {
|
||||
override fun onStop() {
|
||||
updateJob?.cancel()
|
||||
drive.stop()
|
||||
}
|
||||
|
||||
override val position: Double get() = drive.position
|
@ -1,4 +1,4 @@
|
||||
package space.kscience.controls.constructor.library
|
||||
package space.kscience.controls.constructor
|
||||
|
||||
import space.kscience.controls.api.Device
|
||||
import space.kscience.controls.spec.*
|
@ -1,41 +0,0 @@
|
||||
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)"
|
||||
}
|
@ -1,103 +0,0 @@
|
||||
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,10 +38,6 @@ public class DoubleRangeState(
|
||||
* A state showing that the range is on its higher boundary
|
||||
*/
|
||||
public val atEndState: DeviceState<Boolean> = map(MetaConverter.boolean) { it >= range.endInclusive }
|
||||
|
||||
override fun toString(): String = "DoubleRangeState(range=$range, converter=$converter)"
|
||||
|
||||
|
||||
}
|
||||
|
||||
@Suppress("UnusedReceiverParameter")
|
||||
|
@ -1,70 +0,0 @@
|
||||
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)
|
@ -1,23 +0,0 @@
|
||||
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)
|
@ -1,42 +0,0 @@
|
||||
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,43 +0,0 @@
|
||||
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")
|
||||
}
|
||||
}
|
@ -1,22 +0,0 @@
|
||||
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,6 +12,7 @@ import space.kscience.dataforge.context.logger
|
||||
import space.kscience.dataforge.meta.Meta
|
||||
import space.kscience.dataforge.meta.get
|
||||
import space.kscience.dataforge.meta.string
|
||||
import space.kscience.dataforge.misc.DFExperimental
|
||||
import space.kscience.dataforge.misc.DfType
|
||||
import space.kscience.dataforge.names.parseAsName
|
||||
|
||||
@ -99,11 +100,12 @@ public interface Device : ContextAware, CoroutineScope {
|
||||
/**
|
||||
* Close and terminate the device. This function does not wait for the device to be closed.
|
||||
*/
|
||||
public suspend fun stop() {
|
||||
public fun stop() {
|
||||
logger.info { "Device $this is closed" }
|
||||
cancel("The device is closed")
|
||||
}
|
||||
|
||||
@DFExperimental
|
||||
public val lifecycleState: DeviceLifecycleState
|
||||
|
||||
public companion object {
|
||||
|
@ -48,7 +48,7 @@ public operator fun DeviceHub.get(nameToken: NameToken): Device =
|
||||
|
||||
public fun DeviceHub.getOrNull(name: Name): Device? = when {
|
||||
name.isEmpty() -> this as? Device
|
||||
name.length == 1 -> devices[name.firstOrNull()!!]
|
||||
name.length == 1 -> get(name.firstOrNull()!!)
|
||||
else -> (get(name.firstOrNull()!!) as? DeviceHub)?.getOrNull(name.cutFirst())
|
||||
}
|
||||
|
||||
|
@ -15,23 +15,18 @@ public class PropertyDescriptor(
|
||||
public var description: String? = null,
|
||||
public var metaDescriptor: MetaDescriptor = MetaDescriptor(),
|
||||
public var readable: Boolean = true,
|
||||
public var mutable: Boolean = false,
|
||||
public var mutable: Boolean = false
|
||||
)
|
||||
|
||||
public fun PropertyDescriptor.metaDescriptor(block: MetaDescriptorBuilder.() -> Unit) {
|
||||
metaDescriptor = MetaDescriptor {
|
||||
from(metaDescriptor)
|
||||
block()
|
||||
}
|
||||
public fun PropertyDescriptor.metaDescriptor(block: MetaDescriptorBuilder.()->Unit){
|
||||
metaDescriptor = MetaDescriptor(block)
|
||||
}
|
||||
|
||||
/**
|
||||
* A descriptor for property
|
||||
*/
|
||||
@Serializable
|
||||
public class ActionDescriptor(
|
||||
public val name: String,
|
||||
public var description: String? = null,
|
||||
public var inputMetaDescriptor: MetaDescriptor = MetaDescriptor(),
|
||||
public var outputMetaDescriptor: MetaDescriptor = MetaDescriptor()
|
||||
)
|
||||
public class ActionDescriptor(public val name: String) {
|
||||
public var description: String? = null
|
||||
}
|
||||
|
||||
|
@ -6,21 +6,15 @@ import space.kscience.dataforge.context.Context
|
||||
import space.kscience.dataforge.context.PluginFactory
|
||||
import space.kscience.dataforge.context.PluginTag
|
||||
import space.kscience.dataforge.meta.Meta
|
||||
import kotlin.time.Duration
|
||||
|
||||
public class ClockManager : AbstractPlugin() {
|
||||
override val tag: PluginTag get() = Companion.tag
|
||||
override val tag: PluginTag get() = DeviceManager.tag
|
||||
|
||||
public val clock: Clock by lazy {
|
||||
//TODO add clock customization
|
||||
Clock.System
|
||||
}
|
||||
|
||||
public suspend fun delay(duration: Duration) {
|
||||
//TODO add time compression
|
||||
kotlinx.coroutines.delay(duration)
|
||||
}
|
||||
|
||||
public companion object : PluginFactory<ClockManager> {
|
||||
override val tag: PluginTag = PluginTag("clock", group = PluginTag.DATAFORGE_GROUP)
|
||||
|
||||
|
@ -49,10 +49,6 @@ 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> 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].
|
||||
*/
|
||||
@ -80,3 +76,4 @@ public inline fun <D : Device> DeviceManager.installing(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -100,7 +100,6 @@ public abstract class AbstractAsynchronousPort(
|
||||
* Send a data packet via the port
|
||||
*/
|
||||
override suspend fun send(data: ByteArray) {
|
||||
check(isOpen){"The port is not opened"}
|
||||
outgoing.send(data)
|
||||
}
|
||||
|
||||
|
@ -9,11 +9,11 @@ import kotlinx.coroutines.sync.withLock
|
||||
import space.kscience.controls.api.*
|
||||
import space.kscience.dataforge.context.Context
|
||||
import space.kscience.dataforge.context.debug
|
||||
import space.kscience.dataforge.context.error
|
||||
import space.kscience.dataforge.context.logger
|
||||
import space.kscience.dataforge.meta.Meta
|
||||
import space.kscience.dataforge.meta.get
|
||||
import space.kscience.dataforge.meta.int
|
||||
import space.kscience.dataforge.misc.DFExperimental
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
/**
|
||||
@ -72,10 +72,10 @@ public abstract class DeviceBase<D : Device>(
|
||||
onBufferOverflow = BufferOverflow.DROP_OLDEST
|
||||
)
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
|
||||
override val coroutineContext: CoroutineContext = context.newCoroutineContext(
|
||||
SupervisorJob(context.coroutineContext[Job]) +
|
||||
CoroutineName("Device $id") +
|
||||
CoroutineName("Device $this") +
|
||||
CoroutineExceptionHandler { _, throwable ->
|
||||
launch {
|
||||
sharedMessageFlow.emit(
|
||||
@ -86,7 +86,6 @@ public abstract class DeviceBase<D : Device>(
|
||||
)
|
||||
)
|
||||
}
|
||||
logger.error(throwable) { "Exception in device $id" }
|
||||
}
|
||||
)
|
||||
|
||||
@ -188,39 +187,43 @@ public abstract class DeviceBase<D : Device>(
|
||||
return spec.executeWithMeta(self, argument ?: Meta.EMPTY)
|
||||
}
|
||||
|
||||
@DFExperimental
|
||||
final override var lifecycleState: DeviceLifecycleState = DeviceLifecycleState.STOPPED
|
||||
private set
|
||||
|
||||
|
||||
private suspend fun setLifecycleState(lifecycleState: DeviceLifecycleState) {
|
||||
this.lifecycleState = lifecycleState
|
||||
sharedMessageFlow.emit(
|
||||
DeviceLifeCycleMessage(lifecycleState)
|
||||
)
|
||||
}
|
||||
private set(value) {
|
||||
if (field != value) {
|
||||
launch {
|
||||
sharedMessageFlow.emit(
|
||||
DeviceLifeCycleMessage(value)
|
||||
)
|
||||
}
|
||||
}
|
||||
field = value
|
||||
}
|
||||
|
||||
protected open suspend fun onStart() {
|
||||
|
||||
}
|
||||
|
||||
@OptIn(DFExperimental::class)
|
||||
final override suspend fun start() {
|
||||
if (lifecycleState == DeviceLifecycleState.STOPPED) {
|
||||
super.start()
|
||||
setLifecycleState(DeviceLifecycleState.STARTING)
|
||||
lifecycleState = DeviceLifecycleState.STARTING
|
||||
onStart()
|
||||
setLifecycleState(DeviceLifecycleState.STARTED)
|
||||
lifecycleState = DeviceLifecycleState.STARTED
|
||||
} else {
|
||||
logger.debug { "Device $this is already started" }
|
||||
}
|
||||
}
|
||||
|
||||
protected open suspend fun onStop() {
|
||||
protected open fun onStop() {
|
||||
|
||||
}
|
||||
|
||||
final override suspend fun stop() {
|
||||
@OptIn(DFExperimental::class)
|
||||
final override fun stop() {
|
||||
onStop()
|
||||
setLifecycleState(DeviceLifecycleState.STOPPED)
|
||||
lifecycleState = DeviceLifecycleState.STOPPED
|
||||
super.stop()
|
||||
}
|
||||
|
||||
|
@ -20,7 +20,7 @@ public open class DeviceBySpec<D : Device>(
|
||||
self.onOpen()
|
||||
}
|
||||
|
||||
override suspend fun onStop(): Unit = with(spec){
|
||||
override fun onStop(): Unit = with(spec){
|
||||
self.onClose()
|
||||
}
|
||||
|
||||
|
@ -4,10 +4,8 @@ import kotlinx.coroutines.withContext
|
||||
import space.kscience.controls.api.ActionDescriptor
|
||||
import space.kscience.controls.api.Device
|
||||
import space.kscience.controls.api.PropertyDescriptor
|
||||
import space.kscience.controls.api.metaDescriptor
|
||||
import space.kscience.dataforge.meta.Meta
|
||||
import space.kscience.dataforge.meta.MetaConverter
|
||||
import space.kscience.dataforge.meta.descriptors.MetaDescriptor
|
||||
import kotlin.properties.PropertyDelegateProvider
|
||||
import kotlin.properties.ReadOnlyProperty
|
||||
import kotlin.reflect.KProperty
|
||||
@ -56,11 +54,6 @@ public abstract class DeviceSpec<D : Device> {
|
||||
val deviceProperty = object : DevicePropertySpec<D, T> {
|
||||
|
||||
override val descriptor: PropertyDescriptor = PropertyDescriptor(propertyName).apply {
|
||||
converter.descriptor?.let { converterDescriptor ->
|
||||
metaDescriptor {
|
||||
from(converterDescriptor)
|
||||
}
|
||||
}
|
||||
fromSpec(property)
|
||||
descriptorBuilder()
|
||||
}
|
||||
@ -90,11 +83,6 @@ public abstract class DeviceSpec<D : Device> {
|
||||
propertyName,
|
||||
mutable = true
|
||||
).apply {
|
||||
converter.descriptor?.let { converterDescriptor ->
|
||||
metaDescriptor {
|
||||
from(converterDescriptor)
|
||||
}
|
||||
}
|
||||
fromSpec(property)
|
||||
descriptorBuilder()
|
||||
}
|
||||
@ -130,19 +118,6 @@ public abstract class DeviceSpec<D : Device> {
|
||||
val actionName = name ?: property.name
|
||||
val deviceAction = object : DeviceActionSpec<D, I, O> {
|
||||
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)
|
||||
descriptorBuilder()
|
||||
}
|
||||
|
@ -1,41 +0,0 @@
|
||||
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,31 +1,14 @@
|
||||
package space.kscience.controls.spec
|
||||
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
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 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.
|
||||
* The flow is lazy, so action is not performed unless flow is consumed.
|
||||
@ -33,12 +16,23 @@ public fun <D : Device> D.doRecurring(
|
||||
*
|
||||
* The flow is canceled when the device scope is canceled
|
||||
*/
|
||||
public fun <D : Device, R> D.readRecurring(
|
||||
interval: Duration,
|
||||
debugTaskName: String? = null,
|
||||
reader: suspend D.() -> R,
|
||||
): Flow<R> = flow {
|
||||
doRecurring(interval, debugTaskName) {
|
||||
emit(reader())
|
||||
public fun <D : Device, R> D.readRecurring(interval: Duration, reader: suspend D.() -> R): Flow<R> = flow {
|
||||
while (isActive) {
|
||||
delay(interval)
|
||||
launch {
|
||||
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,6 +4,8 @@ import space.kscience.controls.api.ActionDescriptor
|
||||
import space.kscience.controls.api.PropertyDescriptor
|
||||
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<*>)
|
||||
|
||||
|
@ -0,0 +1,22 @@
|
||||
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,18 +2,17 @@ package space.kscience.controls.spec
|
||||
|
||||
import space.kscience.controls.api.ActionDescriptor
|
||||
import space.kscience.controls.api.PropertyDescriptor
|
||||
import space.kscience.dataforge.descriptors.Description
|
||||
import kotlin.reflect.KProperty
|
||||
import kotlin.reflect.full.findAnnotation
|
||||
|
||||
internal actual fun PropertyDescriptor.fromSpec(property: KProperty<*>) {
|
||||
property.findAnnotation<Description>()?.let {
|
||||
description = it.value
|
||||
description = it.content
|
||||
}
|
||||
}
|
||||
|
||||
internal actual fun ActionDescriptor.fromSpec(property: KProperty<*>){
|
||||
property.findAnnotation<Description>()?.let {
|
||||
description = it.value
|
||||
description = it.content
|
||||
}
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
import space.kscience.gradle.Maturity
|
||||
|
||||
plugins {
|
||||
id("space.kscience.gradle.mpp")
|
||||
id("space.kscience.gradle.jvm")
|
||||
`maven-publish`
|
||||
}
|
||||
|
||||
@ -9,12 +9,10 @@ description = """
|
||||
A plugin for Controls-kt device server on top of modbus-rtu/modbus-tcp protocols
|
||||
""".trimIndent()
|
||||
|
||||
kscience {
|
||||
jvm()
|
||||
jvmMain {
|
||||
api(projects.controlsCore)
|
||||
api(libs.j2mod)
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api(projects.controlsCore)
|
||||
api(libs.j2mod)
|
||||
}
|
||||
|
||||
readme{
|
||||
|
@ -24,7 +24,7 @@ public open class ModbusDeviceBySpec<D: Device>(
|
||||
master.connect()
|
||||
}
|
||||
|
||||
override suspend fun onStop() {
|
||||
override fun onStop() {
|
||||
if(disposeMasterOnClose){
|
||||
master.disconnect()
|
||||
}
|
@ -63,7 +63,7 @@ public open class OpcUaDeviceBySpec<D : Device>(
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun onStop() {
|
||||
override fun onStop() {
|
||||
client.disconnect()
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
plugins {
|
||||
id("space.kscience.gradle.mpp")
|
||||
id("space.kscience.gradle.jvm")
|
||||
`maven-publish`
|
||||
}
|
||||
|
||||
@ -7,15 +7,10 @@ description = """
|
||||
Utils to work with controls-kt on Raspberry pi
|
||||
""".trimIndent()
|
||||
|
||||
kscience {
|
||||
jvm()
|
||||
|
||||
|
||||
jvmMain {
|
||||
api(project(":controls-core"))
|
||||
api(libs.pi4j.ktx) // Kotlin DSL
|
||||
api(libs.pi4j.core)
|
||||
api(libs.pi4j.plugin.raspberrypi)
|
||||
api(libs.pi4j.plugin.pigpio)
|
||||
}
|
||||
dependencies{
|
||||
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
|
||||
|
||||
plugins {
|
||||
id("space.kscience.gradle.mpp")
|
||||
id("space.kscience.gradle.jvm")
|
||||
`maven-publish`
|
||||
}
|
||||
|
||||
@ -9,12 +9,9 @@ description = """
|
||||
Implementation of byte ports on top os ktor-io asynchronous API
|
||||
""".trimIndent()
|
||||
|
||||
kscience {
|
||||
jvm()
|
||||
jvmMain {
|
||||
api(projects.controlsCore)
|
||||
api(spclibs.ktor.network)
|
||||
}
|
||||
dependencies {
|
||||
api(projects.controlsCore)
|
||||
api(spclibs.ktor.network)
|
||||
}
|
||||
|
||||
readme{
|
||||
|
@ -1,18 +1,15 @@
|
||||
import space.kscience.gradle.Maturity
|
||||
|
||||
plugins {
|
||||
id("space.kscience.gradle.mpp")
|
||||
id("space.kscience.gradle.jvm")
|
||||
`maven-publish`
|
||||
}
|
||||
|
||||
description = "Implementation of direct serial port communication with JSerialComm"
|
||||
|
||||
kscience {
|
||||
jvm()
|
||||
jvmMain {
|
||||
api(project(":controls-core"))
|
||||
implementation(libs.jSerialComm)
|
||||
}
|
||||
dependencies{
|
||||
api(project(":controls-core"))
|
||||
implementation(libs.jSerialComm)
|
||||
}
|
||||
|
||||
readme{
|
||||
|
@ -1,5 +1,5 @@
|
||||
plugins {
|
||||
id("space.kscience.gradle.mpp")
|
||||
id("space.kscience.gradle.jvm")
|
||||
`maven-publish`
|
||||
}
|
||||
|
||||
@ -7,18 +7,13 @@ description = """
|
||||
An implementation of controls-storage on top of JetBrains Xodus.
|
||||
""".trimIndent()
|
||||
|
||||
kscience {
|
||||
jvm()
|
||||
jvmMain {
|
||||
api(projects.controlsStorage)
|
||||
implementation(libs.xodus.entity.store)
|
||||
dependencies {
|
||||
api(projects.controlsStorage)
|
||||
implementation(libs.xodus.entity.store)
|
||||
// implementation("org.jetbrains.xodus:xodus-environment:$xodusVersion")
|
||||
// implementation("org.jetbrains.xodus:xodus-vfs:$xodusVersion")
|
||||
|
||||
}
|
||||
jvmTest{
|
||||
implementation(spclibs.kotlinx.coroutines.test)
|
||||
}
|
||||
testImplementation(spclibs.kotlinx.coroutines.test)
|
||||
}
|
||||
|
||||
readme{
|
||||
|
@ -1,6 +1,7 @@
|
||||
plugins {
|
||||
kotlin("jvm")
|
||||
alias(spclibs.plugins.compose)
|
||||
id("org.openjfx.javafxplugin") version "0.0.13"
|
||||
application
|
||||
}
|
||||
|
||||
|
||||
@ -19,46 +20,28 @@ dependencies {
|
||||
implementation(projects.controlsOpcua)
|
||||
|
||||
implementation(spclibs.ktor.client.cio)
|
||||
implementation(libs.tornadofx)
|
||||
implementation(libs.plotlykt.server)
|
||||
// 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)
|
||||
}
|
||||
|
||||
kotlin{
|
||||
jvmToolchain(17)
|
||||
compilerOptions {
|
||||
freeCompilerArgs.addAll("-Xjvm-default=all", "-Xopt-in=kotlin.RequiresOptIn")
|
||||
jvmToolchain(11)
|
||||
}
|
||||
|
||||
|
||||
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach {
|
||||
kotlinOptions {
|
||||
freeCompilerArgs = freeCompilerArgs + listOf("-Xjvm-default=all", "-Xopt-in=kotlin.RequiresOptIn")
|
||||
}
|
||||
}
|
||||
|
||||
compose{
|
||||
desktop{
|
||||
application{
|
||||
mainClass = "space.kscience.controls.demo.DemoControllerViewKt"
|
||||
}
|
||||
}
|
||||
javafx {
|
||||
version = "17"
|
||||
modules("javafx.controls")
|
||||
}
|
||||
|
||||
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,16 +1,10 @@
|
||||
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 kotlinx.coroutines.Job
|
||||
import javafx.scene.Parent
|
||||
import javafx.scene.control.Slider
|
||||
import javafx.scene.layout.Priority
|
||||
import javafx.stage.Stage
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
@ -22,6 +16,9 @@ import space.kscience.controls.api.GetDescriptionMessage
|
||||
import space.kscience.controls.api.PropertyChangedMessage
|
||||
import space.kscience.controls.client.launchMagixService
|
||||
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.install
|
||||
import space.kscience.controls.opcua.server.OpcUaServer
|
||||
@ -37,15 +34,16 @@ import space.kscience.magix.rsocket.rSocketWithWebSockets
|
||||
import space.kscience.magix.server.RSocketMagixFlowPlugin
|
||||
import space.kscience.magix.server.startMagixServer
|
||||
import space.kscince.magix.zmq.ZmqMagixFlowPlugin
|
||||
import tornadofx.*
|
||||
import java.awt.Desktop
|
||||
import java.net.URI
|
||||
|
||||
class DemoController : ContextAware {
|
||||
class DemoController : Controller(), ContextAware {
|
||||
|
||||
var device: DemoDevice? = null
|
||||
var magixServer: ApplicationEngine? = null
|
||||
var visualizer: ApplicationEngine? = null
|
||||
val opcUaServer: OpcUaServer = OpcUaServer {
|
||||
var opcUaServer: OpcUaServer = OpcUaServer {
|
||||
setApplicationName(LocalizedText.english("space.kscience.controls.opcua"))
|
||||
|
||||
endpoint {
|
||||
@ -61,37 +59,39 @@ class DemoController : ContextAware {
|
||||
private val deviceManager = context.request(DeviceManager)
|
||||
|
||||
|
||||
fun start(): Job = context.launch {
|
||||
device = deviceManager.install("demo", DemoDevice)
|
||||
//starting magix event loop
|
||||
magixServer = startMagixServer(
|
||||
RSocketMagixFlowPlugin(), //TCP rsocket support
|
||||
ZmqMagixFlowPlugin() //ZMQ support
|
||||
)
|
||||
//Launch a device client and connect it to the server
|
||||
val deviceEndpoint = MagixEndpoint.rSocketWithTcp("localhost")
|
||||
deviceManager.launchMagixService(deviceEndpoint)
|
||||
//connect visualization to a magix endpoint
|
||||
val visualEndpoint = MagixEndpoint.rSocketWithWebSockets("localhost")
|
||||
visualizer = startDemoDeviceServer(visualEndpoint)
|
||||
fun init() {
|
||||
context.launch {
|
||||
device = deviceManager.install("demo", DemoDevice)
|
||||
//starting magix event loop
|
||||
magixServer = startMagixServer(
|
||||
RSocketMagixFlowPlugin(), //TCP rsocket support
|
||||
ZmqMagixFlowPlugin() //ZMQ support
|
||||
)
|
||||
//Launch a device client and connect it to the server
|
||||
val deviceEndpoint = MagixEndpoint.rSocketWithTcp("localhost")
|
||||
deviceManager.launchMagixService(deviceEndpoint)
|
||||
//connect visualization to a magix endpoint
|
||||
val visualEndpoint = MagixEndpoint.rSocketWithWebSockets("localhost")
|
||||
visualizer = startDemoDeviceServer(visualEndpoint)
|
||||
|
||||
//serve devices as OPC-UA namespace
|
||||
opcUaServer.startup()
|
||||
opcUaServer.serveDevices(deviceManager)
|
||||
//serve devices as OPC-UA namespace
|
||||
opcUaServer.startup()
|
||||
opcUaServer.serveDevices(deviceManager)
|
||||
|
||||
|
||||
val listenerEndpoint = MagixEndpoint.rSocketWithWebSockets("localhost")
|
||||
listenerEndpoint.subscribe(DeviceManager.magixFormat).onEach { (_, deviceMessage) ->
|
||||
// print all messages that are not property change message
|
||||
if (deviceMessage !is PropertyChangedMessage) {
|
||||
println(">> ${Json.encodeToString(DeviceMessage.serializer(), deviceMessage)}")
|
||||
}
|
||||
}.launchIn(this)
|
||||
listenerEndpoint.send(DeviceManager.magixFormat, GetDescriptionMessage(), "listener", "controls-kt")
|
||||
val listenerEndpoint = MagixEndpoint.rSocketWithWebSockets("localhost")
|
||||
listenerEndpoint.subscribe(DeviceManager.magixFormat).onEach { (_, deviceMessage)->
|
||||
// print all messages that are not property change message
|
||||
if(deviceMessage !is PropertyChangedMessage){
|
||||
println(">> ${Json.encodeToString(DeviceMessage.serializer(), deviceMessage)}")
|
||||
}
|
||||
}.launchIn(this)
|
||||
listenerEndpoint.send(DeviceManager.magixFormat, GetDescriptionMessage(), "listener", "controls-kt")
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
fun shutdown(): Job = context.launch {
|
||||
fun shutdown() {
|
||||
logger.info { "Shutting down..." }
|
||||
opcUaServer.shutdown()
|
||||
logger.info { "OpcUa server stopped" }
|
||||
@ -101,82 +101,90 @@ class DemoController : ContextAware {
|
||||
logger.info { "Magix server stopped" }
|
||||
device?.stop()
|
||||
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) }
|
||||
|
||||
Surface(Modifier.padding(5.dp)) {
|
||||
Column {
|
||||
Row(Modifier.fillMaxWidth()) {
|
||||
Text("Time Scale", modifier = Modifier.align(Alignment.CenterVertically).width(100.dp))
|
||||
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)
|
||||
class DemoControllerView : View(title = " Demo controller remote") {
|
||||
private val controller: DemoController by inject()
|
||||
private var timeScaleSlider: Slider by singleAssign()
|
||||
private var xScaleSlider: Slider by singleAssign()
|
||||
private var yScaleSlider: Slider by singleAssign()
|
||||
|
||||
override val root: Parent = vbox {
|
||||
hbox {
|
||||
label("Time scale")
|
||||
pane {
|
||||
hgrow = Priority.ALWAYS
|
||||
}
|
||||
Row(Modifier.fillMaxWidth()) {
|
||||
Text("X scale", modifier = Modifier.align(Alignment.CenterVertically).width(100.dp))
|
||||
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)
|
||||
timeScaleSlider = slider(1000..10000, 5000) {
|
||||
isShowTickLabels = true
|
||||
isShowTickMarks = true
|
||||
}
|
||||
Row(Modifier.fillMaxWidth()) {
|
||||
Text("Y scale", modifier = Modifier.align(Alignment.CenterVertically).width(100.dp))
|
||||
TextField(String.format("%.2f", yScale),{}, enabled = false, modifier = Modifier.align(Alignment.CenterVertically).width(100.dp))
|
||||
Slider(yScale, onValueChange = { yScale = it }, steps = 20, valueRange = 0.1f..2.0f)
|
||||
}
|
||||
hbox {
|
||||
label("X scale")
|
||||
pane {
|
||||
hgrow = Priority.ALWAYS
|
||||
}
|
||||
Row(Modifier.fillMaxWidth()) {
|
||||
Button(
|
||||
onClick = {
|
||||
controller.device?.run {
|
||||
launch {
|
||||
write(DemoDevice.timeScale, timeScale.toDouble())
|
||||
write(DemoDevice.sinScale, xScale.toDouble())
|
||||
write(DemoDevice.cosScale, yScale.toDouble())
|
||||
}
|
||||
}
|
||||
},
|
||||
Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text("Submit")
|
||||
}
|
||||
xScaleSlider = slider(0.1..2.0, 1.0) {
|
||||
isShowTickLabels = true
|
||||
isShowTickMarks = true
|
||||
}
|
||||
Row(Modifier.fillMaxWidth()) {
|
||||
Button(
|
||||
onClick = {
|
||||
controller.visualizer?.run {
|
||||
val host = "localhost"//environment.connectors.first().host
|
||||
val port = environment.connectors.first().port
|
||||
val uri = URI("http", null, host, port, "/", null, null)
|
||||
Desktop.getDesktop().browse(uri)
|
||||
}
|
||||
},
|
||||
Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text("Show plots")
|
||||
}
|
||||
hbox {
|
||||
label("Y scale")
|
||||
pane {
|
||||
hgrow = Priority.ALWAYS
|
||||
}
|
||||
yScaleSlider = slider(0.1..2.0, 1.0) {
|
||||
isShowTickLabels = true
|
||||
isShowTickMarks = true
|
||||
}
|
||||
}
|
||||
button("Submit") {
|
||||
useMaxWidth = true
|
||||
action {
|
||||
controller.device?.run {
|
||||
launch {
|
||||
write(timeScale, timeScaleSlider.value)
|
||||
write(sinScale, xScaleSlider.value)
|
||||
write(cosScale, yScaleSlider.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun main() = application {
|
||||
val controller = remember { DemoController().apply { start() } }
|
||||
|
||||
Window(
|
||||
title = "All things control",
|
||||
onCloseRequest = {
|
||||
controller.shutdown()
|
||||
exitApplication()
|
||||
},
|
||||
state = rememberWindowState(width = 400.dp, height = 320.dp)
|
||||
) {
|
||||
MaterialTheme {
|
||||
DemoControls(controller)
|
||||
button("Show plots") {
|
||||
useMaxWidth = true
|
||||
action {
|
||||
controller.visualizer?.run {
|
||||
val host = "localhost"//environment.connectors.first().host
|
||||
val port = environment.connectors.first().port
|
||||
val uri = URI("http", null, host, port, "/", null, null)
|
||||
Desktop.getDesktop().browse(uri)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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>()
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
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,7 +8,6 @@ import javafx.scene.layout.Priority
|
||||
import javafx.stage.Stage
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import space.kscience.controls.client.launchMagixService
|
||||
import space.kscience.controls.demo.car.IVirtualCar.Companion.acceleration
|
||||
import space.kscience.controls.manager.DeviceManager
|
||||
@ -68,7 +67,7 @@ class VirtualCarController : Controller(), ContextAware {
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun shutdown() {
|
||||
fun shutdown() {
|
||||
logger.info { "Shutting down..." }
|
||||
magixServer?.stop(1000, 5000)
|
||||
logger.info { "Magix server stopped" }
|
||||
@ -138,9 +137,7 @@ class VirtualCarControllerApp : App(VirtualCarControllerView::class) {
|
||||
}
|
||||
|
||||
override fun stop() {
|
||||
runBlocking {
|
||||
controller.shutdown()
|
||||
}
|
||||
controller.shutdown()
|
||||
super.stop()
|
||||
}
|
||||
}
|
||||
|
@ -16,11 +16,9 @@ import androidx.compose.ui.window.Window
|
||||
import androidx.compose.ui.window.application
|
||||
import kotlinx.coroutines.launch
|
||||
import space.kscience.controls.constructor.*
|
||||
import space.kscience.controls.constructor.library.*
|
||||
import space.kscience.controls.manager.ClockManager
|
||||
import space.kscience.controls.manager.DeviceManager
|
||||
import space.kscience.controls.manager.clock
|
||||
import space.kscience.controls.manager.install
|
||||
import space.kscience.controls.spec.doRecurring
|
||||
import space.kscience.controls.spec.name
|
||||
import space.kscience.controls.vision.plot
|
||||
@ -50,14 +48,13 @@ class LinearDrive(
|
||||
val drive by device(VirtualDrive.factory(mass, state))
|
||||
val pid by device(PidRegulator(drive, pidParameters))
|
||||
|
||||
val start by device(LimitSwitch(state.atStartState))
|
||||
val end by device(LimitSwitch(state.atEndState))
|
||||
val start by device(LimitSwitch.factory(state.atStartState))
|
||||
val end by device(LimitSwitch.factory(state.atEndState))
|
||||
|
||||
|
||||
val positionState: DoubleRangeState by property(state)
|
||||
|
||||
private val targetState: MutableDeviceState<Double> by deviceProperty(pid, Regulator.target, 0.0)
|
||||
var target: Double by targetState
|
||||
private val targetState: MutableDeviceState<Double> by property(pid.mutablePropertyAsState(Regulator.target, 0.0))
|
||||
var target by targetState
|
||||
}
|
||||
|
||||
|
||||
@ -76,6 +73,7 @@ private fun Context.launchPidDevice(
|
||||
val timeFromStart = clock.now() - clockStart
|
||||
val t = timeFromStart.toDouble(DurationUnit.SECONDS)
|
||||
val freq = 0.1
|
||||
|
||||
target = 5 * sin(2.0 * PI * freq * t) +
|
||||
sin(2 * PI * 21 * freq * t + 0.02 * (timeFromStart / pidParameters.timeStep))
|
||||
}
|
||||
@ -152,7 +150,7 @@ fun main() = application {
|
||||
Row {
|
||||
Text("kp:", Modifier.align(Alignment.CenterVertically).width(50.dp).padding(5.dp))
|
||||
TextField(
|
||||
String.format("%.2f", pidParameters.kp),
|
||||
String.format("%.2f",pidParameters.kp),
|
||||
{ pidParameters.kp = it.toDouble() },
|
||||
Modifier.width(100.dp),
|
||||
enabled = false
|
||||
@ -167,7 +165,7 @@ fun main() = application {
|
||||
Row {
|
||||
Text("ki:", Modifier.align(Alignment.CenterVertically).width(50.dp).padding(5.dp))
|
||||
TextField(
|
||||
String.format("%.2f", pidParameters.ki),
|
||||
String.format("%.2f",pidParameters.ki),
|
||||
{ pidParameters.ki = it.toDouble() },
|
||||
Modifier.width(100.dp),
|
||||
enabled = false
|
||||
@ -183,7 +181,7 @@ fun main() = application {
|
||||
Row {
|
||||
Text("kd:", Modifier.align(Alignment.CenterVertically).width(50.dp).padding(5.dp))
|
||||
TextField(
|
||||
String.format("%.2f", pidParameters.kd),
|
||||
String.format("%.2f",pidParameters.kd),
|
||||
{ pidParameters.kd = it.toDouble() },
|
||||
Modifier.width(100.dp),
|
||||
enabled = false
|
||||
|
@ -1,6 +1,18 @@
|
||||
plugins {
|
||||
id("space.kscience.gradle.jvm")
|
||||
alias(spclibs.plugins.compose)
|
||||
application
|
||||
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{
|
||||
@ -13,17 +25,5 @@ val dataforgeVersion: String by extra
|
||||
dependencies {
|
||||
implementation(project(":controls-ports-ktor"))
|
||||
implementation(projects.controlsMagix)
|
||||
|
||||
implementation(compose.runtime)
|
||||
implementation(compose.desktop.currentOs)
|
||||
implementation(compose.material3)
|
||||
implementation(spclibs.logback.classic)
|
||||
}
|
||||
|
||||
compose{
|
||||
desktop{
|
||||
application{
|
||||
mainClass = "ru.mipt.npm.devices.pimotionmaster.PiMotionMasterAppKt"
|
||||
}
|
||||
}
|
||||
implementation(libs.tornadofx)
|
||||
}
|
||||
|
@ -1,195 +1,31 @@
|
||||
package ru.mipt.npm.devices.pimotionmaster
|
||||
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.material.Button
|
||||
import androidx.compose.material.OutlinedTextField
|
||||
import androidx.compose.material.Slider
|
||||
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 javafx.beans.property.ReadOnlyProperty
|
||||
import javafx.beans.property.SimpleIntegerProperty
|
||||
import javafx.beans.property.SimpleObjectProperty
|
||||
import javafx.beans.property.SimpleStringProperty
|
||||
import javafx.geometry.Pos
|
||||
import javafx.scene.Parent
|
||||
import javafx.scene.layout.Priority
|
||||
import javafx.scene.layout.VBox
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Job
|
||||
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.installing
|
||||
import space.kscience.controls.spec.read
|
||||
import space.kscience.dataforge.context.Context
|
||||
import space.kscience.dataforge.context.request
|
||||
import tornadofx.*
|
||||
|
||||
//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 PiMotionMasterApp : App(PiMotionMasterView::class)
|
||||
|
||||
@Composable
|
||||
fun ColumnScope.piMotionMasterAxis(
|
||||
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") {
|
||||
class PiMotionMasterController : Controller() {
|
||||
//initialize context
|
||||
val context = Context("piMotionMaster"){
|
||||
plugin(DeviceManager)
|
||||
}
|
||||
|
||||
@ -198,14 +34,131 @@ fun main() = application {
|
||||
|
||||
// install device
|
||||
val motionMaster: PiMotionMasterDevice by deviceManager.installing(PiMotionMasterDevice)
|
||||
}
|
||||
|
||||
Window(
|
||||
title = "Pi motion master demo",
|
||||
onCloseRequest = { exitApplication() },
|
||||
state = rememberWindowState(width = 400.dp, height = 300.dp)
|
||||
) {
|
||||
MaterialTheme {
|
||||
PiMotionMasterApp(motionMaster)
|
||||
fun VBox.piMotionMasterAxis(
|
||||
axisName: String,
|
||||
axis: PiMotionMasterDevice.Axis,
|
||||
coroutineScope: CoroutineScope,
|
||||
) = hbox {
|
||||
alignment = Pos.CENTER
|
||||
label(axisName)
|
||||
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,15 +7,13 @@ import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.toList
|
||||
import kotlinx.coroutines.flow.transformWhile
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withTimeout
|
||||
import space.kscience.controls.api.DeviceHub
|
||||
import space.kscience.controls.api.PropertyDescriptor
|
||||
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.ports.*
|
||||
import space.kscience.controls.spec.*
|
||||
import space.kscience.dataforge.context.*
|
||||
import space.kscience.dataforge.meta.*
|
||||
@ -35,8 +33,10 @@ class PiMotionMasterDevice(
|
||||
//PortProxy { portFactory(address ?: error("The device is not connected"), context) }
|
||||
|
||||
|
||||
suspend fun disconnect() {
|
||||
execute(disconnect)
|
||||
fun disconnect() {
|
||||
runBlocking {
|
||||
execute(disconnect)
|
||||
}
|
||||
}
|
||||
|
||||
var timeoutValue: Duration = 200.milliseconds
|
||||
@ -54,11 +54,13 @@ class PiMotionMasterDevice(
|
||||
if (errorCode != 0) error(message(errorCode))
|
||||
}
|
||||
|
||||
suspend fun connect(host: String, port: Int) {
|
||||
execute(connect, Meta {
|
||||
"host" put host
|
||||
"port" put port
|
||||
})
|
||||
fun connect(host: String, port: Int) {
|
||||
runBlocking {
|
||||
execute(connect, Meta {
|
||||
"host" put host
|
||||
"port" put port
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private val mutex = Mutex()
|
||||
@ -101,7 +103,7 @@ class PiMotionMasterDevice(
|
||||
}.toList()
|
||||
}
|
||||
} catch (ex: Throwable) {
|
||||
logger.error(ex) { "Error during PIMotionMaster request. Requesting error code." }
|
||||
logger.warn { "Error during PIMotionMaster request. Requesting error code." }
|
||||
val errorCode = getErrorCode()
|
||||
dispatchError(errorCode)
|
||||
logger.warn { "Error code $errorCode" }
|
||||
@ -165,12 +167,12 @@ class PiMotionMasterDevice(
|
||||
}
|
||||
//Update port
|
||||
//address = portSpec.node
|
||||
port = portFactory(portSpec, context).apply { open() }
|
||||
port = portFactory(portSpec, context)
|
||||
propertyChanged(connected, true)
|
||||
// connector.open()
|
||||
//Initialize axes
|
||||
val idn = read(identity)
|
||||
failIfError { "Can't connect to $portSpec. Error code: $it" }
|
||||
propertyChanged(connected, true)
|
||||
logger.info { "Connected to $idn on $portSpec" }
|
||||
val ids = request("SAI?").map { it.trim() }
|
||||
if (ids != axes.keys.toList()) {
|
||||
|
@ -1,15 +0,0 @@
|
||||
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)
|
@ -0,0 +1,58 @@
|
||||
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,8 +8,6 @@ import io.ktor.util.InternalAPI
|
||||
import io.ktor.util.moveToByteArray
|
||||
import io.ktor.utils.io.writeAvailable
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import space.kscience.dataforge.context.Context
|
||||
import space.kscience.dataforge.context.Global
|
||||
|
||||
@ -20,40 +18,41 @@ val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
|
||||
@OptIn(InternalAPI::class)
|
||||
fun Context.launchPiDebugServer(port: Int, axes: List<String>): Job = launch(exceptionHandler) {
|
||||
val virtualDevice = PiMotionMasterVirtualDevice(this@launchPiDebugServer, axes)
|
||||
aSocket(ActorSelectorManager(Dispatchers.IO)).tcp().bind("localhost", port).use { server ->
|
||||
println("Started virtual port server at ${server.localAddress}")
|
||||
val server = aSocket(ActorSelectorManager(Dispatchers.IO)).tcp().bind("localhost", port)
|
||||
println("Started virtual port server at ${server.localAddress}")
|
||||
|
||||
while (isActive) {
|
||||
val socket = server.accept()
|
||||
launch(SupervisorJob(coroutineContext[Job])) {
|
||||
println("Socket accepted: ${socket.remoteAddress}")
|
||||
val input = socket.openReadChannel()
|
||||
val output = socket.openWriteChannel()
|
||||
while (isActive) {
|
||||
val socket = server.accept()
|
||||
launch(SupervisorJob(coroutineContext[Job])) {
|
||||
println("Socket accepted: ${socket.remoteAddress}")
|
||||
val input = socket.openReadChannel()
|
||||
val output = socket.openWriteChannel()
|
||||
|
||||
val sendJob = virtualDevice.subscribe().onEach {
|
||||
val sendJob = launch {
|
||||
virtualDevice.subscribe().collect {
|
||||
//println("Sending: ${it.decodeToString()}")
|
||||
output.writeAvailable(it)
|
||||
output.flush()
|
||||
}.launchIn(this)
|
||||
|
||||
|
||||
try {
|
||||
while (isActive) {
|
||||
input.read { buffer ->
|
||||
val array = buffer.moveToByteArray()
|
||||
launch {
|
||||
virtualDevice.send(array)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
e.printStackTrace()
|
||||
sendJob.cancel()
|
||||
socket.close()
|
||||
} finally {
|
||||
println("Client socket closed")
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
while (isActive) {
|
||||
input.read { buffer ->
|
||||
val array = buffer.moveToByteArray()
|
||||
launch {
|
||||
virtualDevice.send(array)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
e.printStackTrace()
|
||||
sendJob.cancel()
|
||||
socket.close()
|
||||
} finally {
|
||||
println("Socket closed")
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,37 +1,35 @@
|
||||
import space.kscience.gradle.Maturity
|
||||
|
||||
plugins {
|
||||
id("space.kscience.gradle.mpp")
|
||||
id("space.kscience.gradle.jvm")
|
||||
`maven-publish`
|
||||
application
|
||||
}
|
||||
|
||||
description = """
|
||||
A magix event loop implementation in Kotlin. Includes HTTP/SSE and RSocket routes.
|
||||
""".trimIndent()
|
||||
|
||||
val dataforgeVersion: String by rootProject.extra
|
||||
val ktorVersion: String = space.kscience.gradle.KScienceVersions.ktorVersion
|
||||
|
||||
kscience {
|
||||
jvm()
|
||||
useSerialization{
|
||||
json()
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
val dataforgeVersion: String by rootProject.extra
|
||||
val ktorVersion: String = space.kscience.gradle.KScienceVersions.ktorVersion
|
||||
|
||||
dependencies{
|
||||
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)
|
||||
}
|
||||
|
||||
readme{
|
||||
maturity = Maturity.EXPERIMENTAL
|
||||
|
@ -18,6 +18,7 @@ pluginManagement {
|
||||
id("space.kscience.gradle.mpp") version toolsVersion
|
||||
id("space.kscience.gradle.jvm") 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