Compare commits

..

No commits in common. "a9592d0372e07a32f9461a8493d2fb6573fca5ca" and "4b05f46fa72dad09e4eb103bc39326eed63289fe" have entirely different histories.

70 changed files with 843 additions and 1218 deletions

View File

@ -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")
}

View File

@ -10,14 +10,9 @@ description = """
kscience{
jvm()
js()
useCoroutines()
commonMain {
dependencies {
api(projects.controlsCore)
}
commonTest{
implementation(spclibs.logback.classic)
}
}
readme{

View File

@ -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

View File

@ -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)))
}
}
/**
* 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 <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
}
}
}
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 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 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 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,
)
/**
* 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))
/**
* 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,
)
}

View File

@ -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
}

View File

@ -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)

View File

@ -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()
}

View File

@ -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)

View File

@ -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

View File

@ -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.*

View File

@ -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)"
}

View File

@ -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)

View File

@ -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")

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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")
}
}

View File

@ -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()
}
}

View File

@ -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 {

View File

@ -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())
}

View File

@ -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
}

View File

@ -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)

View File

@ -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].
*/
@ -79,4 +75,5 @@ public inline fun <D : Device> DeviceManager.installing(
current as D
}
}
}
}

View File

@ -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)
}

View File

@ -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()
}

View File

@ -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()
}

View File

@ -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()
}

View File

@ -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

View File

@ -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()
}
}
}

View File

@ -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<*>)

View File

@ -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

View File

@ -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
}
}

View File

@ -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{

View File

@ -24,7 +24,7 @@ public open class ModbusDeviceBySpec<D: Device>(
master.connect()
}
override suspend fun onStop() {
override fun onStop() {
if(disposeMasterOnClose){
master.disconnect()
}

View File

@ -63,7 +63,7 @@ public open class OpcUaDeviceBySpec<D : Device>(
}
}
override suspend fun onStop() {
override fun onStop() {
client.disconnect()
}
}

View File

@ -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)
}

View File

@ -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{

View File

@ -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{

View File

@ -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{

View File

@ -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")
}
//
//
//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")
//}
application {
mainClass.set("space.kscience.controls.demo.DemoControllerViewKt")
}

View File

@ -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" }
}
}
@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)
}
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)
}
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)
}
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")
}
}
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")
}
}
}
context.close()
}
}
fun main() = application {
val controller = remember { DemoController().apply { start() } }
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()
Window(
title = "All things control",
onCloseRequest = {
controller.shutdown()
exitApplication()
},
state = rememberWindowState(width = 400.dp, height = 320.dp)
) {
MaterialTheme {
DemoControls(controller)
override val root: Parent = vbox {
hbox {
label("Time scale")
pane {
hgrow = Priority.ALWAYS
}
timeScaleSlider = slider(1000..10000, 5000) {
isShowTickLabels = true
isShowTickMarks = true
}
}
hbox {
label("X scale")
pane {
hgrow = Priority.ALWAYS
}
xScaleSlider = slider(0.1..2.0, 1.0) {
isShowTickLabels = true
isShowTickMarks = true
}
}
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)
}
}
}
}
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>()
}

View File

@ -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)
//}

View File

@ -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()
}
}

View File

@ -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

View File

@ -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)
}

View File

@ -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>()
}

View File

@ -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()) {

View File

@ -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)

View File

@ -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)
}
}
}
}

View File

@ -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")
}
}
}
}

View File

@ -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

View File

@ -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"
}
}