Compare commits

...

4 Commits

70 changed files with 1209 additions and 834 deletions

View File

@ -3,12 +3,11 @@ import space.kscience.gradle.useSPCTeam
plugins { plugins {
id("space.kscience.gradle.project") id("space.kscience.gradle.project")
alias(libs.plugins.versions)
} }
allprojects { allprojects {
group = "space.kscience" group = "space.kscience"
version = "0.3.1-dev-1" version = "0.4.0-dev-1"
repositories{ repositories{
maven("https://maven.pkg.jetbrains.space/spc/p/sci/dev") maven("https://maven.pkg.jetbrains.space/spc/p/sci/dev")
} }

View File

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

View File

@ -0,0 +1,26 @@
package space.kscience.controls.constructor
import space.kscience.controls.api.Device
public sealed interface ConstructorBinding
/**
* A binding that exposes device property as read-only state
*/
public class PropertyBinding<T>(
public val device: Device,
public val propertyName: String,
public val state: DeviceState<T>,
) : ConstructorBinding
/**
* A binding for independent state like a timer
*/
public class StateBinding<T>(
public val state: DeviceState<T>
) : ConstructorBinding
public class ActionBinding(
public val reads: Collection<DeviceState<*>>,
public val writes: Collection<DeviceState<*>>
): ConstructorBinding

View File

@ -1,9 +1,16 @@
package space.kscience.controls.constructor package space.kscience.controls.constructor
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import space.kscience.controls.api.Device import space.kscience.controls.api.Device
import space.kscience.controls.api.PropertyDescriptor import space.kscience.controls.api.PropertyDescriptor
import space.kscience.controls.manager.ClockManager
import space.kscience.controls.spec.DevicePropertySpec
import space.kscience.controls.spec.MutableDevicePropertySpec
import space.kscience.dataforge.context.Context import space.kscience.dataforge.context.Context
import space.kscience.dataforge.context.Factory import space.kscience.dataforge.context.Factory
import space.kscience.dataforge.context.request
import space.kscience.dataforge.meta.Meta import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.MetaConverter import space.kscience.dataforge.meta.MetaConverter
import space.kscience.dataforge.names.Name import space.kscience.dataforge.names.Name
@ -14,22 +21,53 @@ import kotlin.reflect.KProperty
import kotlin.time.Duration import kotlin.time.Duration
/** /**
* A base for strongly typed device constructor blocks. Has additional delegates for type-safe devices * A base for strongly typed device constructor block. Has additional delegates for type-safe devices
*/ */
public abstract class DeviceConstructor( public abstract class DeviceConstructor(
context: Context, context: Context,
meta: Meta, meta: Meta = Meta.EMPTY,
) : DeviceGroup(context, meta) { ) : DeviceGroup(context, meta) {
private val _bindings: MutableList<ConstructorBinding> = mutableListOf()
public val bindings: List<ConstructorBinding> get() = _bindings
public fun registerBinding(binding: ConstructorBinding) {
_bindings.add(binding)
}
override fun registerProperty(descriptor: PropertyDescriptor, state: DeviceState<*>) {
super.registerProperty(descriptor, state)
registerBinding(PropertyBinding(this, descriptor.name, state))
}
/** /**
* Create and register a timer. Timer is not counted as a device property.
*/
public fun timer(tick: Duration): TimerState = TimerState(context.request(ClockManager), tick)
.also { registerBinding(StateBinding(it)) }
/**
* Launch action that is performed on each [DeviceState] value change.
*
* Optionally provide [writes] - a set of states that this change affects.
*/
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 * Register a device, provided by a given [factory] and
*/ */
public fun <D : Device> device( public fun <D : Device> DeviceConstructor.device(
factory: Factory<D>, factory: Factory<D>,
meta: Meta? = null, meta: Meta? = null,
nameOverride: Name? = null, nameOverride: Name? = null,
metaLocation: Name? = null, metaLocation: Name? = null,
): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, D>> = ): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, D>> =
PropertyDelegateProvider { _: DeviceConstructor, property: KProperty<*> -> PropertyDelegateProvider { _: DeviceConstructor, property: KProperty<*> ->
val name = nameOverride ?: property.name.asName() val name = nameOverride ?: property.name.asName()
val device = install(name, factory, meta, metaLocation ?: name) val device = install(name, factory, meta, metaLocation ?: name)
@ -38,10 +76,10 @@ public abstract class DeviceConstructor(
} }
} }
public fun <D : Device> device( public fun <D : Device> DeviceConstructor.device(
device: D, device: D,
nameOverride: Name? = null, nameOverride: Name? = null,
): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, D>> = ): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, D>> =
PropertyDelegateProvider { _: DeviceConstructor, property: KProperty<*> -> PropertyDelegateProvider { _: DeviceConstructor, property: KProperty<*> ->
val name = nameOverride ?: property.name.asName() val name = nameOverride ?: property.name.asName()
install(name, device) install(name, device)
@ -50,15 +88,14 @@ public abstract class DeviceConstructor(
} }
} }
/**
/**
* Register a property and provide a direct reader for it * Register a property and provide a direct reader for it
*/ */
public fun <T, S: DeviceState<T>> property( public fun <T, S : DeviceState<T>> DeviceConstructor.property(
state: S, state: S,
descriptorBuilder: PropertyDescriptor.() -> Unit = {}, descriptorBuilder: PropertyDescriptor.() -> Unit = {},
nameOverride: String? = null, nameOverride: String? = null,
): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, S>> = ): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, S>> =
PropertyDelegateProvider { _: DeviceConstructor, property -> PropertyDelegateProvider { _: DeviceConstructor, property ->
val name = nameOverride ?: property.name val name = nameOverride ?: property.name
val descriptor = PropertyDescriptor(name).apply(descriptorBuilder) val descriptor = PropertyDescriptor(name).apply(descriptorBuilder)
@ -68,26 +105,26 @@ public abstract class DeviceConstructor(
} }
} }
/** /**
* Register external state as a property * Register external state as a property
*/ */
public fun <T : Any> property( public fun <T : Any> DeviceConstructor.property(
metaConverter: MetaConverter<T>, metaConverter: MetaConverter<T>,
reader: suspend () -> T, reader: suspend () -> T,
readInterval: Duration, readInterval: Duration,
initialState: T, initialState: T,
descriptorBuilder: PropertyDescriptor.() -> Unit = {}, descriptorBuilder: PropertyDescriptor.() -> Unit = {},
nameOverride: String? = null, nameOverride: String? = null,
): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, DeviceState<T>>> = property( ): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, DeviceState<T>>> = property(
DeviceState.external(this, metaConverter, readInterval, initialState, reader), DeviceState.external(this, metaConverter, readInterval, initialState, reader),
descriptorBuilder, descriptorBuilder,
nameOverride, nameOverride,
) )
/** /**
* Register a mutable external state as a property * Register a mutable external state as a property
*/ */
public fun <T : Any> mutableProperty( public fun <T : Any> DeviceConstructor.mutableProperty(
metaConverter: MetaConverter<T>, metaConverter: MetaConverter<T>,
reader: suspend () -> T, reader: suspend () -> T,
writer: suspend (T) -> Unit, writer: suspend (T) -> Unit,
@ -95,24 +132,43 @@ public abstract class DeviceConstructor(
initialState: T, initialState: T,
descriptorBuilder: PropertyDescriptor.() -> Unit = {}, descriptorBuilder: PropertyDescriptor.() -> Unit = {},
nameOverride: String? = null, nameOverride: String? = null,
): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, MutableDeviceState<T>>> = property( ): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, MutableDeviceState<T>>> = property(
DeviceState.external(this, metaConverter, readInterval, initialState, reader, writer), DeviceState.external(this, metaConverter, readInterval, initialState, reader, writer),
descriptorBuilder, descriptorBuilder,
nameOverride, nameOverride,
) )
/** /**
* Create and register a virtual mutable property with optional [callback] * Create and register a virtual mutable property with optional [callback]
*/ */
public fun <T> virtualProperty( public fun <T> DeviceConstructor.virtualProperty(
metaConverter: MetaConverter<T>, metaConverter: MetaConverter<T>,
initialState: T, initialState: T,
descriptorBuilder: PropertyDescriptor.() -> Unit = {}, descriptorBuilder: PropertyDescriptor.() -> Unit = {},
nameOverride: String? = null, nameOverride: String? = null,
callback: (T) -> Unit = {}, callback: (T) -> Unit = {},
): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, MutableDeviceState<T>>> = property( ): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, MutableDeviceState<T>>> = property(
DeviceState.virtual(metaConverter, initialState, callback), DeviceState.internal(metaConverter, initialState, callback),
descriptorBuilder, descriptorBuilder,
nameOverride, nameOverride,
) )
}
/**
* Bind existing property provided by specification to this device
*/
public fun <T, D : Device> DeviceConstructor.deviceProperty(
device: D,
property: DevicePropertySpec<D, T>,
initialValue: T,
): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, DeviceState<T>>> =
property(device.propertyAsState(property, initialValue))
/**
* Bind existing property provided by specification to this device
*/
public fun <T, D : Device> DeviceConstructor.deviceProperty(
device: D,
property: MutableDevicePropertySpec<D, T>,
initialValue: T,
): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, MutableDeviceState<T>>> =
property(device.mutablePropertyAsState(property, initialValue))

View File

@ -9,9 +9,7 @@ import space.kscience.controls.api.*
import space.kscience.controls.api.DeviceLifecycleState.* import space.kscience.controls.api.DeviceLifecycleState.*
import space.kscience.controls.manager.DeviceManager import space.kscience.controls.manager.DeviceManager
import space.kscience.controls.manager.install import space.kscience.controls.manager.install
import space.kscience.dataforge.context.Context import space.kscience.dataforge.context.*
import space.kscience.dataforge.context.Factory
import space.kscience.dataforge.context.request
import space.kscience.dataforge.meta.* import space.kscience.dataforge.meta.*
import space.kscience.dataforge.misc.DFExperimental import space.kscience.dataforge.misc.DFExperimental
import space.kscience.dataforge.names.* import space.kscience.dataforge.names.*
@ -57,6 +55,7 @@ public open class DeviceGroup(
) )
) )
} }
logger.error(throwable) { "Exception in device $id" }
} }
) )
@ -69,7 +68,7 @@ public open class DeviceGroup(
* Register and initialize (synchronize child's lifecycle state with group state) a new device in this group * Register and initialize (synchronize child's lifecycle state with group state) a new device in this group
*/ */
@OptIn(DFExperimental::class) @OptIn(DFExperimental::class)
public fun <D : Device> install(token: NameToken, device: D): D { public open fun <D : Device> install(token: NameToken, device: D): D {
require(_devices[token] == null) { "A child device with name $token already exists" } require(_devices[token] == null) { "A child device with name $token already exists" }
//start the child device if needed //start the child device if needed
if (lifecycleState == STARTED || lifecycleState == STARTING) launch { device.start() } if (lifecycleState == STARTED || lifecycleState == STARTING) launch { device.start() }
@ -82,7 +81,7 @@ public open class DeviceGroup(
/** /**
* Register a new property based on [DeviceState]. Properties could be modified dynamically * Register a new property based on [DeviceState]. Properties could be modified dynamically
*/ */
public fun registerProperty(descriptor: PropertyDescriptor, state: DeviceState<*>) { public open fun registerProperty(descriptor: PropertyDescriptor, state: DeviceState<*>) {
val name = descriptor.name.parseAsName() val name = descriptor.name.parseAsName()
require(properties[name] == null) { "Can't add property with name $name. It already exists." } require(properties[name] == null) { "Can't add property with name $name. It already exists." }
properties[name] = Property(state, descriptor) properties[name] = Property(state, descriptor)
@ -126,37 +125,33 @@ public open class DeviceGroup(
return action.invoke(argument) return action.invoke(argument)
} }
@DFExperimental final override var lifecycleState: DeviceLifecycleState = DeviceLifecycleState.STOPPED
override var lifecycleState: DeviceLifecycleState = STOPPED private set
protected set(value) {
if (field != value) {
launch { private suspend fun setLifecycleState(lifecycleState: DeviceLifecycleState) {
this.lifecycleState = lifecycleState
sharedMessageFlow.emit( sharedMessageFlow.emit(
DeviceLifeCycleMessage(value) DeviceLifeCycleMessage(lifecycleState)
) )
} }
}
field = value
}
@OptIn(DFExperimental::class)
override suspend fun start() { override suspend fun start() {
lifecycleState = STARTING setLifecycleState(STARTING)
super.start() super.start()
devices.values.forEach { devices.values.forEach {
it.start() it.start()
} }
lifecycleState = STARTED setLifecycleState(STARTED)
} }
@OptIn(DFExperimental::class) override suspend fun stop() {
override fun stop() {
devices.values.forEach { devices.values.forEach {
it.stop() it.stop()
} }
setLifecycleState(STOPPED)
super.stop() super.stop()
lifecycleState = STOPPED
} }
public companion object { public companion object {
@ -210,13 +205,9 @@ public fun <D : Device> DeviceGroup.install(name: Name, device: D): D {
} }
} }
public fun <D : Device> DeviceGroup.install(name: String, device: D): D = public fun <D : Device> DeviceGroup.install(name: String, device: D): D = install(name.parseAsName(), device)
install(name.parseAsName(), device)
public fun <D : Device> DeviceGroup.install(device: D): D = public fun <D : Device> DeviceGroup.install(device: D): D = install(device.id, device)
install(device.id, device)
public fun <D : Device> Context.install(name: String, device: D): D = request(DeviceManager).install(name, device)
/** /**
* Add a device creating intermediate groups if necessary. If device with given [name] already exists, throws an error. * Add a device creating intermediate groups if necessary. If device with given [name] already exists, throws an error.
@ -292,7 +283,7 @@ public fun <T : Any> DeviceGroup.registerVirtualProperty(
descriptorBuilder: PropertyDescriptor.() -> Unit = {}, descriptorBuilder: PropertyDescriptor.() -> Unit = {},
callback: (T) -> Unit = {}, callback: (T) -> Unit = {},
): MutableDeviceState<T> { ): MutableDeviceState<T> {
val state = DeviceState.virtual<T>(converter, initialValue, callback) val state = DeviceState.internal<T>(converter, initialValue, callback)
registerMutableProperty(name, state, descriptorBuilder) registerMutableProperty(name, state, descriptorBuilder)
return state return state
} }

View File

@ -2,18 +2,13 @@ package space.kscience.controls.constructor
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.launch import kotlinx.coroutines.flow.map
import space.kscience.controls.api.Device import kotlinx.coroutines.flow.onEach
import space.kscience.controls.api.PropertyChangedMessage
import space.kscience.controls.spec.DevicePropertySpec
import space.kscience.controls.spec.MutableDevicePropertySpec
import space.kscience.controls.spec.name
import space.kscience.dataforge.meta.Meta import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.MetaConverter import space.kscience.dataforge.meta.MetaConverter
import kotlin.reflect.KProperty import kotlin.reflect.KProperty
import kotlin.time.Duration
/** /**
* An observable state of a device * An observable state of a device
@ -24,6 +19,8 @@ public interface DeviceState<T> {
public val valueFlow: Flow<T> public val valueFlow: Flow<T>
override fun toString(): String
public companion object public companion object
} }
@ -36,7 +33,7 @@ public operator fun <T> DeviceState<T>.getValue(thisRef: Any?, property: KProper
/** /**
* Collect values in a given [scope] * Collect values in a given [scope]
*/ */
public fun <T> DeviceState<T>.collectValuesIn(scope: CoroutineScope, block: suspend (T)->Unit): Job = public fun <T> DeviceState<T>.collectValuesIn(scope: CoroutineScope, block: suspend (T) -> Unit): Job =
valueFlow.onEach(block).launchIn(scope) valueFlow.onEach(block).launchIn(scope)
/** /**
@ -57,186 +54,46 @@ public var <T> MutableDeviceState<T>.valueAsMeta: Meta
} }
/** /**
* A [MutableDeviceState] that does not correspond to a physical state * Device state with a value that depends on other device states
*
* @param callback a synchronous callback that could be used without a scope
*/ */
private class VirtualDeviceState<T>( public interface DeviceStateWithDependencies<T> : DeviceState<T> {
override val converter: MetaConverter<T>, public val dependencies: Collection<DeviceState<*>>
initialValue: T,
private val callback: (T) -> Unit = {},
) : MutableDeviceState<T> {
private val flow = MutableStateFlow(initialValue)
override val valueFlow: Flow<T> get() = flow
override var value: T
get() = flow.value
set(value) {
flow.value = value
callback(value)
}
}
/**
* A [MutableDeviceState] that does not correspond to a physical state
*
* @param callback a synchronous callback that could be used without a scope
*/
public fun <T> DeviceState.Companion.virtual(
converter: MetaConverter<T>,
initialValue: T,
callback: (T) -> Unit = {},
): MutableDeviceState<T> = VirtualDeviceState(converter, initialValue, callback)
private class StateFlowAsState<T>(
override val converter: MetaConverter<T>,
val flow: MutableStateFlow<T>,
) : MutableDeviceState<T> {
override var value: T by flow::value
override val valueFlow: Flow<T> get() = flow
}
public fun <T> MutableStateFlow<T>.asDeviceState(converter: MetaConverter<T>): DeviceState<T> =
StateFlowAsState(converter, this)
private open class BoundDeviceState<T>(
override val converter: MetaConverter<T>,
val device: Device,
val propertyName: String,
initialValue: T,
) : DeviceState<T> {
override val valueFlow: StateFlow<T> = device.messageFlow.filterIsInstance<PropertyChangedMessage>().filter {
it.property == propertyName
}.mapNotNull {
converter.read(it.value)
}.stateIn(device.context, SharingStarted.Eagerly, initialValue)
override val value: T get() = valueFlow.value
} }
/** /**
* Bind a read-only [DeviceState] to a [Device] property * Create a new read-only [DeviceState] that mirrors receiver state by mapping the value with [mapper].
*/ */
public suspend fun <T> Device.propertyAsState(
propertyName: String,
metaConverter: MetaConverter<T>,
): DeviceState<T> {
val initialValue = metaConverter.readOrNull(readProperty(propertyName)) ?: error("Conversion of property failed")
return BoundDeviceState(metaConverter, this, propertyName, initialValue)
}
public suspend fun <D : Device, T> D.propertyAsState(
propertySpec: DevicePropertySpec<D, T>,
): DeviceState<T> = propertyAsState(propertySpec.name, propertySpec.converter)
public fun <T, R> DeviceState<T>.map( public fun <T, R> DeviceState<T>.map(
converter: MetaConverter<R>, mapper: (T) -> R, converter: MetaConverter<R>, mapper: (T) -> R,
): DeviceState<R> = object : DeviceState<R> { ): DeviceStateWithDependencies<R> = object : DeviceStateWithDependencies<R> {
override val dependencies = listOf(this)
override val converter: MetaConverter<R> = converter override val converter: MetaConverter<R> = converter
override val value: R override val value: R
get() = mapper(this@map.value) get() = mapper(this@map.value)
override val valueFlow: Flow<R> = this@map.valueFlow.map(mapper) override val valueFlow: Flow<R> = this@map.valueFlow.map(mapper)
}
private class MutableBoundDeviceState<T>( override fun toString(): String = "DeviceState.map(arg=${this@map}, converter=$converter)"
converter: MetaConverter<T>,
device: Device,
propertyName: String,
initialValue: T,
) : BoundDeviceState<T>(converter, device, propertyName, initialValue), MutableDeviceState<T> {
override var value: T
get() = valueFlow.value
set(newValue) {
device.launch {
device.writeProperty(propertyName, converter.convert(newValue))
}
}
}
public fun <T> Device.mutablePropertyAsState(
propertyName: String,
metaConverter: MetaConverter<T>,
initialValue: T,
): MutableDeviceState<T> = MutableBoundDeviceState(metaConverter, this, propertyName, initialValue)
public suspend fun <T> Device.mutablePropertyAsState(
propertyName: String,
metaConverter: MetaConverter<T>,
): MutableDeviceState<T> {
val initialValue = metaConverter.readOrNull(readProperty(propertyName)) ?: error("Conversion of property failed")
return mutablePropertyAsState(propertyName, metaConverter, initialValue)
}
public suspend fun <D : Device, T> D.mutablePropertyAsState(
propertySpec: MutableDevicePropertySpec<D, T>,
): MutableDeviceState<T> = mutablePropertyAsState(propertySpec.name, propertySpec.converter)
public fun <D : Device, T> D.mutablePropertyAsState(
propertySpec: MutableDevicePropertySpec<D, T>,
initialValue: T,
): MutableDeviceState<T> = mutablePropertyAsState(propertySpec.name, propertySpec.converter, initialValue)
private open class ExternalState<T>(
val scope: CoroutineScope,
override val converter: MetaConverter<T>,
val readInterval: Duration,
initialValue: T,
val reader: suspend () -> T,
) : DeviceState<T> {
protected val flow: StateFlow<T> = flow {
while (true) {
delay(readInterval)
emit(reader())
}
}.stateIn(scope, SharingStarted.Eagerly, initialValue)
override val value: T get() = flow.value
override val valueFlow: Flow<T> get() = flow
} }
/** /**
* Create a [DeviceState] which is constructed by periodically reading external value * Combine two device states into one read-only [DeviceState]. Only the latest value of each state is used.
*/ */
public fun <T> DeviceState.Companion.external( public fun <T1, T2, R> combine(
scope: CoroutineScope, state1: DeviceState<T1>,
converter: MetaConverter<T>, state2: DeviceState<T2>,
readInterval: Duration, converter: MetaConverter<R>,
initialValue: T, mapper: (T1, T2) -> R,
reader: suspend () -> T, ): DeviceStateWithDependencies<R> = object : DeviceStateWithDependencies<R> {
): DeviceState<T> = ExternalState(scope, converter, readInterval, initialValue, reader) override val dependencies = listOf(state1, state2)
private class MutableExternalState<T>( override val converter: MetaConverter<R> = converter
scope: CoroutineScope,
converter: MetaConverter<T>, override val value: R get() = mapper(state1.value, state2.value)
readInterval: Duration,
initialValue: T, override val valueFlow: Flow<R> = kotlinx.coroutines.flow.combine(state1.valueFlow, state2.valueFlow, mapper)
reader: suspend () -> T,
val writer: suspend (T) -> Unit, override fun toString(): String = "DeviceState.combine(state1=$state1, state2=$state2)"
) : ExternalState<T>(scope, converter, readInterval, initialValue, reader), MutableDeviceState<T> {
override var value: T
get() = super.value
set(value) {
scope.launch {
writer(value)
}
}
} }
/**
* Create a [DeviceState] that regularly reads and caches an external value
*/
public fun <T> DeviceState.Companion.external(
scope: CoroutineScope,
converter: MetaConverter<T>,
readInterval: Duration,
initialValue: T,
reader: suspend () -> T,
writer: suspend (T) -> Unit,
): MutableDeviceState<T> = MutableExternalState(scope, converter, readInterval, initialValue, reader, writer)

View File

@ -0,0 +1,41 @@
package space.kscience.controls.constructor
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.datetime.Instant
import space.kscience.controls.manager.ClockManager
import space.kscience.controls.spec.instant
import space.kscience.dataforge.meta.MetaConverter
import kotlin.time.Duration
/**
* A dedicated [DeviceState] that operates with time.
* The state changes with [tick] interval and always shows the time of the last update.
*
* Both [tick] and current time are computed by [clockManager] enabling time manipulation.
*
* The timer runs indefinitely until the parent context is closed
*/
public class TimerState(
public val clockManager: ClockManager,
public val tick: Duration,
) : DeviceState<Instant> {
override val converter: MetaConverter<Instant> get() = MetaConverter.instant
private val clock = MutableStateFlow(clockManager.clock.now())
private val updateJob = clockManager.context.launch {
while (isActive) {
clockManager.delay(tick)
clock.value = clockManager.clock.now()
}
}
override val valueFlow: Flow<Instant> get() = clock
override val value: Instant get() = clock.value
override fun toString(): String = "TimerState(tick=$tick)"
}

View File

@ -0,0 +1,103 @@
package space.kscience.controls.constructor
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import space.kscience.controls.api.Device
import space.kscience.controls.api.PropertyChangedMessage
import space.kscience.controls.api.id
import space.kscience.controls.spec.DevicePropertySpec
import space.kscience.controls.spec.MutableDevicePropertySpec
import space.kscience.controls.spec.name
import space.kscience.dataforge.meta.MetaConverter
/**
* A copy-free [DeviceState] bound to a device property
*/
private open class BoundDeviceState<T>(
override val converter: MetaConverter<T>,
val device: Device,
val propertyName: String,
initialValue: T,
) : DeviceState<T> {
override val valueFlow: StateFlow<T> = device.messageFlow.filterIsInstance<PropertyChangedMessage>().filter {
it.property == propertyName
}.mapNotNull {
converter.read(it.value)
}.stateIn(device.context, SharingStarted.Eagerly, initialValue)
override val value: T get() = valueFlow.value
override fun toString(): String =
"BoundDeviceState(converter=$converter, device=${device.id}, propertyName='$propertyName')"
}
public fun <T> Device.propertyAsState(
propertyName: String,
metaConverter: MetaConverter<T>,
initialValue: T,
): DeviceState<T> = BoundDeviceState(metaConverter, this, propertyName, initialValue)
/**
* Bind a read-only [DeviceState] to a [Device] property
*/
public suspend fun <T> Device.propertyAsState(
propertyName: String,
metaConverter: MetaConverter<T>,
): DeviceState<T> = propertyAsState(
propertyName,
metaConverter,
metaConverter.readOrNull(readProperty(propertyName)) ?: error("Conversion of property failed")
)
public suspend fun <D : Device, T> D.propertyAsState(
propertySpec: DevicePropertySpec<D, T>,
): DeviceState<T> = propertyAsState(propertySpec.name, propertySpec.converter)
public fun <D : Device, T> D.propertyAsState(
propertySpec: DevicePropertySpec<D, T>,
initialValue: T,
): DeviceState<T> = propertyAsState(propertySpec.name, propertySpec.converter, initialValue)
private class MutableBoundDeviceState<T>(
converter: MetaConverter<T>,
device: Device,
propertyName: String,
initialValue: T,
) : BoundDeviceState<T>(converter, device, propertyName, initialValue), MutableDeviceState<T> {
override var value: T
get() = valueFlow.value
set(newValue) {
device.launch {
device.writeProperty(propertyName, converter.convert(newValue))
}
}
}
public fun <T> Device.mutablePropertyAsState(
propertyName: String,
metaConverter: MetaConverter<T>,
initialValue: T,
): MutableDeviceState<T> = MutableBoundDeviceState(metaConverter, this, propertyName, initialValue)
public suspend fun <T> Device.mutablePropertyAsState(
propertyName: String,
metaConverter: MetaConverter<T>,
): MutableDeviceState<T> {
val initialValue = metaConverter.readOrNull(readProperty(propertyName)) ?: error("Conversion of property failed")
return mutablePropertyAsState(propertyName, metaConverter, initialValue)
}
public suspend fun <D : Device, T> D.mutablePropertyAsState(
propertySpec: MutableDevicePropertySpec<D, T>,
): MutableDeviceState<T> = mutablePropertyAsState(propertySpec.name, propertySpec.converter)
public fun <D : Device, T> D.mutablePropertyAsState(
propertySpec: MutableDevicePropertySpec<D, T>,
initialValue: T,
): MutableDeviceState<T> = mutablePropertyAsState(propertySpec.name, propertySpec.converter, initialValue)

View File

@ -38,6 +38,10 @@ public class DoubleRangeState(
* A state showing that the range is on its higher boundary * A state showing that the range is on its higher boundary
*/ */
public val atEndState: DeviceState<Boolean> = map(MetaConverter.boolean) { it >= range.endInclusive } public val atEndState: DeviceState<Boolean> = map(MetaConverter.boolean) { it >= range.endInclusive }
override fun toString(): String = "DoubleRangeState(range=$range, converter=$converter)"
} }
@Suppress("UnusedReceiverParameter") @Suppress("UnusedReceiverParameter")

View File

@ -0,0 +1,70 @@
package space.kscience.controls.constructor
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import space.kscience.dataforge.meta.MetaConverter
import kotlin.time.Duration
private open class ExternalState<T>(
val scope: CoroutineScope,
override val converter: MetaConverter<T>,
val readInterval: Duration,
initialValue: T,
val reader: suspend () -> T,
) : DeviceState<T> {
protected val flow: StateFlow<T> = flow {
while (true) {
delay(readInterval)
emit(reader())
}
}.stateIn(scope, SharingStarted.Eagerly, initialValue)
override val value: T get() = flow.value
override val valueFlow: Flow<T> get() = flow
override fun toString(): String = "ExternalState(converter=$converter)"
}
/**
* Create a [DeviceState] which is constructed by regularly reading external value
*/
public fun <T> DeviceState.Companion.external(
scope: CoroutineScope,
converter: MetaConverter<T>,
readInterval: Duration,
initialValue: T,
reader: suspend () -> T,
): DeviceState<T> = ExternalState(scope, converter, readInterval, initialValue, reader)
private class MutableExternalState<T>(
scope: CoroutineScope,
converter: MetaConverter<T>,
readInterval: Duration,
initialValue: T,
reader: suspend () -> T,
val writer: suspend (T) -> Unit,
) : ExternalState<T>(scope, converter, readInterval, initialValue, reader), MutableDeviceState<T> {
override var value: T
get() = super.value
set(value) {
scope.launch {
writer(value)
}
}
}
/**
* Create a [MutableDeviceState] which is constructed by regularly reading external value and allows writing
*/
public fun <T> DeviceState.Companion.external(
scope: CoroutineScope,
converter: MetaConverter<T>,
readInterval: Duration,
initialValue: T,
reader: suspend () -> T,
writer: suspend (T) -> Unit,
): MutableDeviceState<T> = MutableExternalState(scope, converter, readInterval, initialValue, reader, writer)

View File

@ -0,0 +1,23 @@
package space.kscience.controls.constructor
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import space.kscience.dataforge.meta.MetaConverter
private class StateFlowAsState<T>(
override val converter: MetaConverter<T>,
val flow: MutableStateFlow<T>,
) : MutableDeviceState<T> {
override var value: T by flow::value
override val valueFlow: Flow<T> get() = flow
override fun toString(): String = "FlowAsState(converter=$converter)"
}
/**
* Create a read-only [DeviceState] that wraps [MutableStateFlow].
* No data copy is performed.
*/
public fun <T> MutableStateFlow<T>.asDeviceState(converter: MetaConverter<T>): DeviceState<T> =
StateFlowAsState(converter, this)

View File

@ -0,0 +1,42 @@
package space.kscience.controls.constructor
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import space.kscience.dataforge.meta.MetaConverter
/**
* A [MutableDeviceState] that does not correspond to a physical state
*
* @param callback a synchronous callback that could be used without a scope
*/
private class VirtualDeviceState<T>(
override val converter: MetaConverter<T>,
initialValue: T,
private val callback: (T) -> Unit = {},
) : MutableDeviceState<T> {
private val flow = MutableStateFlow(initialValue)
override val valueFlow: Flow<T> get() = flow
override var value: T
get() = flow.value
set(value) {
flow.value = value
callback(value)
}
override fun toString(): String = "VirtualDeviceState(converter=$converter)"
}
/**
* A [MutableDeviceState] that does not correspond to a physical state
*
* @param callback a synchronous callback that could be used without a scope
*/
public fun <T> DeviceState.Companion.internal(
converter: MetaConverter<T>,
initialValue: T,
callback: (T) -> Unit = {},
): MutableDeviceState<T> = VirtualDeviceState(converter, initialValue, callback)

View File

@ -1,10 +1,12 @@
package space.kscience.controls.constructor package space.kscience.controls.constructor.library
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import space.kscience.controls.api.Device import space.kscience.controls.api.Device
import space.kscience.controls.constructor.MutableDeviceState
import space.kscience.controls.constructor.mutablePropertyAsState
import space.kscience.controls.manager.clock import space.kscience.controls.manager.clock
import space.kscience.controls.spec.* import space.kscience.controls.spec.*
import space.kscience.dataforge.context.Context import space.kscience.dataforge.context.Context
@ -49,7 +51,7 @@ public class VirtualDrive(
public val positionState: MutableDeviceState<Double>, public val positionState: MutableDeviceState<Double>,
) : Drive, DeviceBySpec<Drive>(Drive, context) { ) : Drive, DeviceBySpec<Drive>(Drive, context) {
private val dt = meta["time.step"].double?.milliseconds ?: 1.milliseconds private val dt = meta["time.step"].double?.milliseconds ?: 5.milliseconds
private val clock = context.clock private val clock = context.clock
override var force: Double = 0.0 override var force: Double = 0.0
@ -82,7 +84,7 @@ public class VirtualDrive(
} }
} }
override fun onStop() { override suspend fun onStop() {
updateJob?.cancel() updateJob?.cancel()
} }

View File

@ -1,8 +1,9 @@
package space.kscience.controls.constructor package space.kscience.controls.constructor.library
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import space.kscience.controls.api.Device import space.kscience.controls.api.Device
import space.kscience.controls.constructor.DeviceState
import space.kscience.controls.spec.DeviceBySpec import space.kscience.controls.spec.DeviceBySpec
import space.kscience.controls.spec.DevicePropertySpec import space.kscience.controls.spec.DevicePropertySpec
import space.kscience.controls.spec.DeviceSpec import space.kscience.controls.spec.DeviceSpec
@ -20,7 +21,7 @@ public interface LimitSwitch : Device {
public companion object : DeviceSpec<LimitSwitch>() { public companion object : DeviceSpec<LimitSwitch>() {
public val locked: DevicePropertySpec<LimitSwitch, Boolean> by booleanProperty { locked } public val locked: DevicePropertySpec<LimitSwitch, Boolean> by booleanProperty { locked }
public fun factory(lockedState: DeviceState<Boolean>): Factory<LimitSwitch> = Factory { context, _ -> public operator fun invoke(lockedState: DeviceState<Boolean>): Factory<LimitSwitch> = Factory { context, _ ->
VirtualLimitSwitch(context, lockedState) VirtualLimitSwitch(context, lockedState)
} }
} }
@ -34,7 +35,7 @@ public class VirtualLimitSwitch(
public val lockedState: DeviceState<Boolean>, public val lockedState: DeviceState<Boolean>,
) : DeviceBySpec<LimitSwitch>(LimitSwitch, context), LimitSwitch { ) : DeviceBySpec<LimitSwitch>(LimitSwitch, context), LimitSwitch {
init { override suspend fun onStart() {
lockedState.valueFlow.onEach { lockedState.valueFlow.onEach {
propertyChanged(LimitSwitch.locked, it) propertyChanged(LimitSwitch.locked, it)
}.launchIn(this) }.launchIn(this)

View File

@ -1,4 +1,4 @@
package space.kscience.controls.constructor package space.kscience.controls.constructor.library
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
@ -7,8 +7,11 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
import kotlinx.datetime.Instant import kotlinx.datetime.Instant
import space.kscience.controls.constructor.DeviceGroup
import space.kscience.controls.constructor.install
import space.kscience.controls.manager.clock import space.kscience.controls.manager.clock
import space.kscience.controls.spec.DeviceBySpec import space.kscience.controls.spec.DeviceBySpec
import space.kscience.controls.spec.write
import kotlin.time.Duration import kotlin.time.Duration
import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.DurationUnit import kotlin.time.DurationUnit
@ -71,15 +74,17 @@ public class PidRegulator(
lastTime = realTime lastTime = realTime
lastPosition = drive.position lastPosition = drive.position
drive.force = pidParameters.kp * delta + pidParameters.ki * integral + pidParameters.kd * derivative drive.write(Drive.force,pidParameters.kp * delta + pidParameters.ki * integral + pidParameters.kd * derivative)
//drive.force = pidParameters.kp * delta + pidParameters.ki * integral + pidParameters.kd * derivative
propertyChanged(Regulator.position, drive.position) propertyChanged(Regulator.position, drive.position)
} }
} }
} }
} }
override fun onStop() { override suspend fun onStop() {
updateJob?.cancel() updateJob?.cancel()
drive.stop()
} }
override val position: Double get() = drive.position override val position: Double get() = drive.position

View File

@ -1,4 +1,4 @@
package space.kscience.controls.constructor package space.kscience.controls.constructor.library
import space.kscience.controls.api.Device import space.kscience.controls.api.Device
import space.kscience.controls.spec.* import space.kscience.controls.spec.*

View File

@ -0,0 +1,43 @@
package space.kscience.controls.constructor
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.runTest
import space.kscience.controls.api.Device
import space.kscience.controls.api.DeviceLifeCycleMessage
import space.kscience.controls.api.DeviceLifecycleState
import space.kscience.controls.manager.DeviceManager
import space.kscience.controls.manager.install
import space.kscience.controls.spec.doRecurring
import space.kscience.dataforge.context.Context
import space.kscience.dataforge.context.Factory
import space.kscience.dataforge.context.Global
import space.kscience.dataforge.context.request
import space.kscience.dataforge.meta.Meta
import kotlin.test.Test
import kotlin.time.Duration.Companion.milliseconds
class DeviceGroupTest {
class TestDevice(context: Context) : DeviceConstructor(context) {
companion object : Factory<Device> {
override fun build(context: Context, meta: Meta): Device = TestDevice(context)
}
}
@Test
fun testRecurringRead() = runTest {
var counter = 10
val testDevice = Global.request(DeviceManager).install("test", TestDevice)
testDevice.doRecurring(1.milliseconds) {
counter--
println(counter)
if (counter <= 0) {
testDevice.stop()
}
error("Error!")
}
testDevice.messageFlow.first { it is DeviceLifeCycleMessage && it.state == DeviceLifecycleState.STOPPED }
println("stopped")
}
}

View File

@ -0,0 +1,22 @@
package space.kscience.controls.constructor
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.take
import kotlinx.coroutines.test.runTest
import space.kscience.controls.manager.ClockManager
import space.kscience.dataforge.context.Global
import space.kscience.dataforge.context.request
import kotlin.test.Test
import kotlin.time.Duration.Companion.milliseconds
class TimerTest {
@Test
fun timer() = runTest {
val timer = TimerState(Global.request(ClockManager), 10.milliseconds)
timer.valueFlow.take(10).onEach {
println(it)
}.collect()
}
}

View File

@ -12,7 +12,6 @@ import space.kscience.dataforge.context.logger
import space.kscience.dataforge.meta.Meta import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.get import space.kscience.dataforge.meta.get
import space.kscience.dataforge.meta.string import space.kscience.dataforge.meta.string
import space.kscience.dataforge.misc.DFExperimental
import space.kscience.dataforge.misc.DfType import space.kscience.dataforge.misc.DfType
import space.kscience.dataforge.names.parseAsName import space.kscience.dataforge.names.parseAsName
@ -100,12 +99,11 @@ public interface Device : ContextAware, CoroutineScope {
/** /**
* Close and terminate the device. This function does not wait for the device to be closed. * Close and terminate the device. This function does not wait for the device to be closed.
*/ */
public fun stop() { public suspend fun stop() {
logger.info { "Device $this is closed" } logger.info { "Device $this is closed" }
cancel("The device is closed") cancel("The device is closed")
} }
@DFExperimental
public val lifecycleState: DeviceLifecycleState public val lifecycleState: DeviceLifecycleState
public companion object { public companion object {

View File

@ -48,7 +48,7 @@ public operator fun DeviceHub.get(nameToken: NameToken): Device =
public fun DeviceHub.getOrNull(name: Name): Device? = when { public fun DeviceHub.getOrNull(name: Name): Device? = when {
name.isEmpty() -> this as? Device name.isEmpty() -> this as? Device
name.length == 1 -> get(name.firstOrNull()!!) name.length == 1 -> devices[name.firstOrNull()!!]
else -> (get(name.firstOrNull()!!) as? DeviceHub)?.getOrNull(name.cutFirst()) else -> (get(name.firstOrNull()!!) as? DeviceHub)?.getOrNull(name.cutFirst())
} }

View File

@ -15,18 +15,23 @@ public class PropertyDescriptor(
public var description: String? = null, public var description: String? = null,
public var metaDescriptor: MetaDescriptor = MetaDescriptor(), public var metaDescriptor: MetaDescriptor = MetaDescriptor(),
public var readable: Boolean = true, public var readable: Boolean = true,
public var mutable: Boolean = false public var mutable: Boolean = false,
) )
public fun PropertyDescriptor.metaDescriptor(block: MetaDescriptorBuilder.()->Unit){ public fun PropertyDescriptor.metaDescriptor(block: MetaDescriptorBuilder.() -> Unit) {
metaDescriptor = MetaDescriptor(block) metaDescriptor = MetaDescriptor {
from(metaDescriptor)
block()
}
} }
/** /**
* A descriptor for property * A descriptor for property
*/ */
@Serializable @Serializable
public class ActionDescriptor(public val name: String) { public class ActionDescriptor(
public var description: String? = null public val name: String,
} public var description: String? = null,
public var inputMetaDescriptor: MetaDescriptor = MetaDescriptor(),
public var outputMetaDescriptor: MetaDescriptor = MetaDescriptor()
)

View File

@ -6,15 +6,21 @@ import space.kscience.dataforge.context.Context
import space.kscience.dataforge.context.PluginFactory import space.kscience.dataforge.context.PluginFactory
import space.kscience.dataforge.context.PluginTag import space.kscience.dataforge.context.PluginTag
import space.kscience.dataforge.meta.Meta import space.kscience.dataforge.meta.Meta
import kotlin.time.Duration
public class ClockManager : AbstractPlugin() { public class ClockManager : AbstractPlugin() {
override val tag: PluginTag get() = DeviceManager.tag override val tag: PluginTag get() = Companion.tag
public val clock: Clock by lazy { public val clock: Clock by lazy {
//TODO add clock customization //TODO add clock customization
Clock.System Clock.System
} }
public suspend fun delay(duration: Duration) {
//TODO add time compression
kotlinx.coroutines.delay(duration)
}
public companion object : PluginFactory<ClockManager> { public companion object : PluginFactory<ClockManager> {
override val tag: PluginTag = PluginTag("clock", group = PluginTag.DATAFORGE_GROUP) override val tag: PluginTag = PluginTag("clock", group = PluginTag.DATAFORGE_GROUP)

View File

@ -49,6 +49,10 @@ public fun <D : Device> DeviceManager.install(name: String, device: D): D {
public fun <D : Device> DeviceManager.install(device: D): D = install(device.id, device) public fun <D : Device> DeviceManager.install(device: D): D = install(device.id, device)
public fun <D : Device> Context.install(name: String, device: D): D = request(DeviceManager).install(name, device)
public fun <D : Device> Context.install(device: D): D = request(DeviceManager).install(device.id, device)
/** /**
* Register and start a device built by [factory] with current [Context] and [meta]. * Register and start a device built by [factory] with current [Context] and [meta].
*/ */
@ -76,4 +80,3 @@ public inline fun <D : Device> DeviceManager.installing(
} }
} }
} }

View File

@ -100,6 +100,7 @@ public abstract class AbstractAsynchronousPort(
* Send a data packet via the port * Send a data packet via the port
*/ */
override suspend fun send(data: ByteArray) { override suspend fun send(data: ByteArray) {
check(isOpen){"The port is not opened"}
outgoing.send(data) outgoing.send(data)
} }

View File

@ -9,11 +9,11 @@ import kotlinx.coroutines.sync.withLock
import space.kscience.controls.api.* import space.kscience.controls.api.*
import space.kscience.dataforge.context.Context import space.kscience.dataforge.context.Context
import space.kscience.dataforge.context.debug import space.kscience.dataforge.context.debug
import space.kscience.dataforge.context.error
import space.kscience.dataforge.context.logger import space.kscience.dataforge.context.logger
import space.kscience.dataforge.meta.Meta import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.get import space.kscience.dataforge.meta.get
import space.kscience.dataforge.meta.int import space.kscience.dataforge.meta.int
import space.kscience.dataforge.misc.DFExperimental
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
/** /**
@ -72,10 +72,10 @@ public abstract class DeviceBase<D : Device>(
onBufferOverflow = BufferOverflow.DROP_OLDEST onBufferOverflow = BufferOverflow.DROP_OLDEST
) )
@OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
override val coroutineContext: CoroutineContext = context.newCoroutineContext( override val coroutineContext: CoroutineContext = context.newCoroutineContext(
SupervisorJob(context.coroutineContext[Job]) + SupervisorJob(context.coroutineContext[Job]) +
CoroutineName("Device $this") + CoroutineName("Device $id") +
CoroutineExceptionHandler { _, throwable -> CoroutineExceptionHandler { _, throwable ->
launch { launch {
sharedMessageFlow.emit( sharedMessageFlow.emit(
@ -86,6 +86,7 @@ public abstract class DeviceBase<D : Device>(
) )
) )
} }
logger.error(throwable) { "Exception in device $id" }
} }
) )
@ -187,43 +188,39 @@ public abstract class DeviceBase<D : Device>(
return spec.executeWithMeta(self, argument ?: Meta.EMPTY) return spec.executeWithMeta(self, argument ?: Meta.EMPTY)
} }
@DFExperimental
final override var lifecycleState: DeviceLifecycleState = DeviceLifecycleState.STOPPED final override var lifecycleState: DeviceLifecycleState = DeviceLifecycleState.STOPPED
private set(value) { private set
if (field != value) {
launch {
private suspend fun setLifecycleState(lifecycleState: DeviceLifecycleState) {
this.lifecycleState = lifecycleState
sharedMessageFlow.emit( sharedMessageFlow.emit(
DeviceLifeCycleMessage(value) DeviceLifeCycleMessage(lifecycleState)
) )
} }
}
field = value
}
protected open suspend fun onStart() { protected open suspend fun onStart() {
} }
@OptIn(DFExperimental::class)
final override suspend fun start() { final override suspend fun start() {
if (lifecycleState == DeviceLifecycleState.STOPPED) { if (lifecycleState == DeviceLifecycleState.STOPPED) {
super.start() super.start()
lifecycleState = DeviceLifecycleState.STARTING setLifecycleState(DeviceLifecycleState.STARTING)
onStart() onStart()
lifecycleState = DeviceLifecycleState.STARTED setLifecycleState(DeviceLifecycleState.STARTED)
} else { } else {
logger.debug { "Device $this is already started" } logger.debug { "Device $this is already started" }
} }
} }
protected open fun onStop() { protected open suspend fun onStop() {
} }
@OptIn(DFExperimental::class) final override suspend fun stop() {
final override fun stop() {
onStop() onStop()
lifecycleState = DeviceLifecycleState.STOPPED setLifecycleState(DeviceLifecycleState.STOPPED)
super.stop() super.stop()
} }

View File

@ -20,7 +20,7 @@ public open class DeviceBySpec<D : Device>(
self.onOpen() self.onOpen()
} }
override fun onStop(): Unit = with(spec){ override suspend fun onStop(): Unit = with(spec){
self.onClose() self.onClose()
} }

View File

@ -4,8 +4,10 @@ import kotlinx.coroutines.withContext
import space.kscience.controls.api.ActionDescriptor import space.kscience.controls.api.ActionDescriptor
import space.kscience.controls.api.Device import space.kscience.controls.api.Device
import space.kscience.controls.api.PropertyDescriptor import space.kscience.controls.api.PropertyDescriptor
import space.kscience.controls.api.metaDescriptor
import space.kscience.dataforge.meta.Meta import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.MetaConverter import space.kscience.dataforge.meta.MetaConverter
import space.kscience.dataforge.meta.descriptors.MetaDescriptor
import kotlin.properties.PropertyDelegateProvider import kotlin.properties.PropertyDelegateProvider
import kotlin.properties.ReadOnlyProperty import kotlin.properties.ReadOnlyProperty
import kotlin.reflect.KProperty import kotlin.reflect.KProperty
@ -54,6 +56,11 @@ public abstract class DeviceSpec<D : Device> {
val deviceProperty = object : DevicePropertySpec<D, T> { val deviceProperty = object : DevicePropertySpec<D, T> {
override val descriptor: PropertyDescriptor = PropertyDescriptor(propertyName).apply { override val descriptor: PropertyDescriptor = PropertyDescriptor(propertyName).apply {
converter.descriptor?.let { converterDescriptor ->
metaDescriptor {
from(converterDescriptor)
}
}
fromSpec(property) fromSpec(property)
descriptorBuilder() descriptorBuilder()
} }
@ -83,6 +90,11 @@ public abstract class DeviceSpec<D : Device> {
propertyName, propertyName,
mutable = true mutable = true
).apply { ).apply {
converter.descriptor?.let { converterDescriptor ->
metaDescriptor {
from(converterDescriptor)
}
}
fromSpec(property) fromSpec(property)
descriptorBuilder() descriptorBuilder()
} }
@ -118,6 +130,19 @@ public abstract class DeviceSpec<D : Device> {
val actionName = name ?: property.name val actionName = name ?: property.name
val deviceAction = object : DeviceActionSpec<D, I, O> { val deviceAction = object : DeviceActionSpec<D, I, O> {
override val descriptor: ActionDescriptor = ActionDescriptor(actionName).apply { override val descriptor: ActionDescriptor = ActionDescriptor(actionName).apply {
inputConverter.descriptor?.let { converterDescriptor ->
inputMetaDescriptor = MetaDescriptor {
from(converterDescriptor)
from(inputMetaDescriptor)
}
}
outputConverter.descriptor?.let { converterDescriptor ->
outputMetaDescriptor = MetaDescriptor {
from(converterDescriptor)
from(outputMetaDescriptor)
}
}
fromSpec(property) fromSpec(property)
descriptorBuilder() descriptorBuilder()
} }

View File

@ -0,0 +1,41 @@
package space.kscience.controls.spec
import kotlinx.datetime.Instant
import space.kscience.dataforge.meta.*
import kotlin.time.Duration
import kotlin.time.DurationUnit
import kotlin.time.toDuration
public fun Double.asMeta(): Meta = Meta(asValue())
/**
* Generate a nullable [MetaConverter] from non-nullable one
*/
public fun <T : Any> MetaConverter<T>.nullable(): MetaConverter<T?> = object : MetaConverter<T?> {
override fun convert(obj: T?): Meta = obj?.let { this@nullable.convert(it) }?: Meta(Null)
override fun readOrNull(source: Meta): T? = if(source.value == Null) null else this@nullable.readOrNull(source)
}
//TODO to be moved to DF
private object DurationConverter : MetaConverter<Duration> {
override fun readOrNull(source: Meta): Duration = source.value?.double?.toDuration(DurationUnit.SECONDS)
?: run {
val unit: DurationUnit = source["unit"].enum<DurationUnit>() ?: DurationUnit.SECONDS
val value = source[Meta.VALUE_KEY].double ?: error("No value present for Duration")
return@run value.toDuration(unit)
}
override fun convert(obj: Duration): Meta = obj.toDouble(DurationUnit.SECONDS).asMeta()
}
public val MetaConverter.Companion.duration: MetaConverter<Duration> get() = DurationConverter
private object InstantConverter : MetaConverter<Instant> {
override fun readOrNull(source: Meta): Instant? = source.string?.let { Instant.parse(it) }
override fun convert(obj: Instant): Meta = Meta(obj.toString())
}
public val MetaConverter.Companion.instant: MetaConverter<Instant> get() = InstantConverter

View File

@ -1,14 +1,31 @@
package space.kscience.controls.spec package space.kscience.controls.spec
import kotlinx.coroutines.Job import kotlinx.coroutines.*
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import space.kscience.controls.api.Device import space.kscience.controls.api.Device
import kotlin.time.Duration import kotlin.time.Duration
/**
* Do a recurring (with a fixed delay) task on a device.
*/
public fun <D : Device> D.doRecurring(
interval: Duration,
debugTaskName: String? = null,
task: suspend D.() -> Unit,
): Job {
val taskName = debugTaskName ?: "task[${task.hashCode().toString(16)}]"
return launch(CoroutineName(taskName)) {
while (isActive) {
delay(interval)
//launch in parent scope to properly evaluate exceptions
this@doRecurring.launch {
task()
}
}
}
}
/** /**
* Perform a recurring asynchronous read action and return a flow of results. * Perform a recurring asynchronous read action and return a flow of results.
* The flow is lazy, so action is not performed unless flow is consumed. * The flow is lazy, so action is not performed unless flow is consumed.
@ -16,23 +33,12 @@ import kotlin.time.Duration
* *
* The flow is canceled when the device scope is canceled * The flow is canceled when the device scope is canceled
*/ */
public fun <D : Device, R> D.readRecurring(interval: Duration, reader: suspend D.() -> R): Flow<R> = flow { public fun <D : Device, R> D.readRecurring(
while (isActive) { interval: Duration,
delay(interval) debugTaskName: String? = null,
launch { reader: suspend D.() -> R,
): Flow<R> = flow {
doRecurring(interval, debugTaskName) {
emit(reader()) 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,8 +4,6 @@ import space.kscience.controls.api.ActionDescriptor
import space.kscience.controls.api.PropertyDescriptor import space.kscience.controls.api.PropertyDescriptor
import kotlin.reflect.KProperty import kotlin.reflect.KProperty
@Target(AnnotationTarget.CLASS, AnnotationTarget.PROPERTY, AnnotationTarget.FIELD)
public annotation class Description(val content: String)
internal expect fun PropertyDescriptor.fromSpec(property: KProperty<*>) internal expect fun PropertyDescriptor.fromSpec(property: KProperty<*>)

View File

@ -1,22 +0,0 @@
package space.kscience.controls.spec
import space.kscience.dataforge.meta.*
import kotlin.time.Duration
import kotlin.time.DurationUnit
import kotlin.time.toDuration
public fun Double.asMeta(): Meta = Meta(asValue())
//TODO to be moved to DF
public object DurationConverter : MetaConverter<Duration> {
override fun readOrNull(source: Meta): Duration = source.value?.double?.toDuration(DurationUnit.SECONDS)
?: run {
val unit: DurationUnit = source["unit"].enum<DurationUnit>() ?: DurationUnit.SECONDS
val value = source[Meta.VALUE_KEY].double ?: error("No value present for Duration")
return@run value.toDuration(unit)
}
override fun convert(obj: Duration): Meta = obj.toDouble(DurationUnit.SECONDS).asMeta()
}
public val MetaConverter.Companion.duration: MetaConverter<Duration> get() = DurationConverter

View File

@ -2,17 +2,18 @@ package space.kscience.controls.spec
import space.kscience.controls.api.ActionDescriptor import space.kscience.controls.api.ActionDescriptor
import space.kscience.controls.api.PropertyDescriptor import space.kscience.controls.api.PropertyDescriptor
import space.kscience.dataforge.descriptors.Description
import kotlin.reflect.KProperty import kotlin.reflect.KProperty
import kotlin.reflect.full.findAnnotation import kotlin.reflect.full.findAnnotation
internal actual fun PropertyDescriptor.fromSpec(property: KProperty<*>) { internal actual fun PropertyDescriptor.fromSpec(property: KProperty<*>) {
property.findAnnotation<Description>()?.let { property.findAnnotation<Description>()?.let {
description = it.content description = it.value
} }
} }
internal actual fun ActionDescriptor.fromSpec(property: KProperty<*>){ internal actual fun ActionDescriptor.fromSpec(property: KProperty<*>){
property.findAnnotation<Description>()?.let { property.findAnnotation<Description>()?.let {
description = it.content description = it.value
} }
} }

View File

@ -1,7 +1,7 @@
import space.kscience.gradle.Maturity import space.kscience.gradle.Maturity
plugins { plugins {
id("space.kscience.gradle.jvm") id("space.kscience.gradle.mpp")
`maven-publish` `maven-publish`
} }
@ -9,10 +9,12 @@ description = """
A plugin for Controls-kt device server on top of modbus-rtu/modbus-tcp protocols A plugin for Controls-kt device server on top of modbus-rtu/modbus-tcp protocols
""".trimIndent() """.trimIndent()
kscience {
dependencies { jvm()
jvmMain {
api(projects.controlsCore) api(projects.controlsCore)
api(libs.j2mod) api(libs.j2mod)
}
} }
readme{ readme{

View File

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

View File

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

View File

@ -1,5 +1,5 @@
plugins { plugins {
id("space.kscience.gradle.jvm") id("space.kscience.gradle.mpp")
`maven-publish` `maven-publish`
} }
@ -7,10 +7,15 @@ description = """
Utils to work with controls-kt on Raspberry pi Utils to work with controls-kt on Raspberry pi
""".trimIndent() """.trimIndent()
dependencies{ kscience {
jvm()
jvmMain {
api(project(":controls-core")) api(project(":controls-core"))
api(libs.pi4j.ktx) // Kotlin DSL api(libs.pi4j.ktx) // Kotlin DSL
api(libs.pi4j.core) api(libs.pi4j.core)
api(libs.pi4j.plugin.raspberrypi) api(libs.pi4j.plugin.raspberrypi)
api(libs.pi4j.plugin.pigpio) api(libs.pi4j.plugin.pigpio)
}
} }

View File

@ -1,7 +1,7 @@
import space.kscience.gradle.Maturity import space.kscience.gradle.Maturity
plugins { plugins {
id("space.kscience.gradle.jvm") id("space.kscience.gradle.mpp")
`maven-publish` `maven-publish`
} }
@ -9,9 +9,12 @@ description = """
Implementation of byte ports on top os ktor-io asynchronous API Implementation of byte ports on top os ktor-io asynchronous API
""".trimIndent() """.trimIndent()
dependencies { kscience {
jvm()
jvmMain {
api(projects.controlsCore) api(projects.controlsCore)
api(spclibs.ktor.network) api(spclibs.ktor.network)
}
} }
readme{ readme{

View File

@ -1,15 +1,18 @@
import space.kscience.gradle.Maturity import space.kscience.gradle.Maturity
plugins { plugins {
id("space.kscience.gradle.jvm") id("space.kscience.gradle.mpp")
`maven-publish` `maven-publish`
} }
description = "Implementation of direct serial port communication with JSerialComm" description = "Implementation of direct serial port communication with JSerialComm"
dependencies{ kscience {
jvm()
jvmMain {
api(project(":controls-core")) api(project(":controls-core"))
implementation(libs.jSerialComm) implementation(libs.jSerialComm)
}
} }
readme{ readme{

View File

@ -1,5 +1,5 @@
plugins { plugins {
id("space.kscience.gradle.jvm") id("space.kscience.gradle.mpp")
`maven-publish` `maven-publish`
} }
@ -7,13 +7,18 @@ description = """
An implementation of controls-storage on top of JetBrains Xodus. An implementation of controls-storage on top of JetBrains Xodus.
""".trimIndent() """.trimIndent()
dependencies { kscience {
jvm()
jvmMain {
api(projects.controlsStorage) api(projects.controlsStorage)
implementation(libs.xodus.entity.store) implementation(libs.xodus.entity.store)
// implementation("org.jetbrains.xodus:xodus-environment:$xodusVersion") // implementation("org.jetbrains.xodus:xodus-environment:$xodusVersion")
// implementation("org.jetbrains.xodus:xodus-vfs:$xodusVersion") // implementation("org.jetbrains.xodus:xodus-vfs:$xodusVersion")
testImplementation(spclibs.kotlinx.coroutines.test) }
jvmTest{
implementation(spclibs.kotlinx.coroutines.test)
}
} }
readme{ readme{

View File

@ -1,7 +1,6 @@
plugins { plugins {
kotlin("jvm") kotlin("jvm")
id("org.openjfx.javafxplugin") version "0.0.13" alias(spclibs.plugins.compose)
application
} }
@ -20,28 +19,46 @@ dependencies {
implementation(projects.controlsOpcua) implementation(projects.controlsOpcua)
implementation(spclibs.ktor.client.cio) implementation(spclibs.ktor.client.cio)
implementation(libs.tornadofx)
implementation(libs.plotlykt.server) implementation(libs.plotlykt.server)
// implementation("com.github.Ricky12Awesome:json-schema-serialization:0.6.6") // implementation("com.github.Ricky12Awesome:json-schema-serialization:0.6.6")
implementation(compose.runtime)
implementation(compose.desktop.currentOs)
implementation(compose.material3)
// implementation("org.pushing-pixels:aurora-window:1.3.0")
// implementation("org.pushing-pixels:aurora-component:1.3.0")
// implementation("org.pushing-pixels:aurora-theming:1.3.0")
implementation(spclibs.logback.classic) implementation(spclibs.logback.classic)
} }
kotlin{ kotlin{
jvmToolchain(11) jvmToolchain(17)
} compilerOptions {
freeCompilerArgs.addAll("-Xjvm-default=all", "-Xopt-in=kotlin.RequiresOptIn")
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach {
kotlinOptions {
freeCompilerArgs = freeCompilerArgs + listOf("-Xjvm-default=all", "-Xopt-in=kotlin.RequiresOptIn")
} }
} }
javafx { compose{
version = "17" desktop{
modules("javafx.controls") application{
} mainClass = "space.kscience.controls.demo.DemoControllerViewKt"
}
application { }
mainClass.set("space.kscience.controls.demo.DemoControllerViewKt")
} }
//
//
//tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach {
// kotlinOptions {
// freeCompilerArgs = freeCompilerArgs + listOf("-Xjvm-default=all", "-Xopt-in=kotlin.RequiresOptIn")
// }
//}
//
//javafx {
// version = "17"
// modules("javafx.controls")
//}
//
//application {
// mainClass.set("space.kscience.controls.demo.DemoControllerViewKt")
//}

View File

@ -1,10 +1,16 @@
package space.kscience.controls.demo package space.kscience.controls.demo
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application
import androidx.compose.ui.window.rememberWindowState
import io.ktor.server.engine.ApplicationEngine import io.ktor.server.engine.ApplicationEngine
import javafx.scene.Parent import kotlinx.coroutines.Job
import javafx.scene.control.Slider
import javafx.scene.layout.Priority
import javafx.stage.Stage
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -16,9 +22,6 @@ import space.kscience.controls.api.GetDescriptionMessage
import space.kscience.controls.api.PropertyChangedMessage import space.kscience.controls.api.PropertyChangedMessage
import space.kscience.controls.client.launchMagixService import space.kscience.controls.client.launchMagixService
import space.kscience.controls.client.magixFormat import space.kscience.controls.client.magixFormat
import space.kscience.controls.demo.DemoDevice.Companion.cosScale
import space.kscience.controls.demo.DemoDevice.Companion.sinScale
import space.kscience.controls.demo.DemoDevice.Companion.timeScale
import space.kscience.controls.manager.DeviceManager import space.kscience.controls.manager.DeviceManager
import space.kscience.controls.manager.install import space.kscience.controls.manager.install
import space.kscience.controls.opcua.server.OpcUaServer import space.kscience.controls.opcua.server.OpcUaServer
@ -34,16 +37,15 @@ import space.kscience.magix.rsocket.rSocketWithWebSockets
import space.kscience.magix.server.RSocketMagixFlowPlugin import space.kscience.magix.server.RSocketMagixFlowPlugin
import space.kscience.magix.server.startMagixServer import space.kscience.magix.server.startMagixServer
import space.kscince.magix.zmq.ZmqMagixFlowPlugin import space.kscince.magix.zmq.ZmqMagixFlowPlugin
import tornadofx.*
import java.awt.Desktop import java.awt.Desktop
import java.net.URI import java.net.URI
class DemoController : Controller(), ContextAware { class DemoController : ContextAware {
var device: DemoDevice? = null var device: DemoDevice? = null
var magixServer: ApplicationEngine? = null var magixServer: ApplicationEngine? = null
var visualizer: ApplicationEngine? = null var visualizer: ApplicationEngine? = null
var opcUaServer: OpcUaServer = OpcUaServer { val opcUaServer: OpcUaServer = OpcUaServer {
setApplicationName(LocalizedText.english("space.kscience.controls.opcua")) setApplicationName(LocalizedText.english("space.kscience.controls.opcua"))
endpoint { endpoint {
@ -59,8 +61,7 @@ class DemoController : Controller(), ContextAware {
private val deviceManager = context.request(DeviceManager) private val deviceManager = context.request(DeviceManager)
fun init() { fun start(): Job = context.launch {
context.launch {
device = deviceManager.install("demo", DemoDevice) device = deviceManager.install("demo", DemoDevice)
//starting magix event loop //starting magix event loop
magixServer = startMagixServer( magixServer = startMagixServer(
@ -80,18 +81,17 @@ class DemoController : Controller(), ContextAware {
val listenerEndpoint = MagixEndpoint.rSocketWithWebSockets("localhost") val listenerEndpoint = MagixEndpoint.rSocketWithWebSockets("localhost")
listenerEndpoint.subscribe(DeviceManager.magixFormat).onEach { (_, deviceMessage)-> listenerEndpoint.subscribe(DeviceManager.magixFormat).onEach { (_, deviceMessage) ->
// print all messages that are not property change message // print all messages that are not property change message
if(deviceMessage !is PropertyChangedMessage){ if (deviceMessage !is PropertyChangedMessage) {
println(">> ${Json.encodeToString(DeviceMessage.serializer(), deviceMessage)}") println(">> ${Json.encodeToString(DeviceMessage.serializer(), deviceMessage)}")
} }
}.launchIn(this) }.launchIn(this)
listenerEndpoint.send(DeviceManager.magixFormat, GetDescriptionMessage(), "listener", "controls-kt") listenerEndpoint.send(DeviceManager.magixFormat, GetDescriptionMessage(), "listener", "controls-kt")
} }
}
fun shutdown() { fun shutdown(): Job = context.launch {
logger.info { "Shutting down..." } logger.info { "Shutting down..." }
opcUaServer.shutdown() opcUaServer.shutdown()
logger.info { "OpcUa server stopped" } logger.info { "OpcUa server stopped" }
@ -101,90 +101,82 @@ class DemoController : Controller(), ContextAware {
logger.info { "Magix server stopped" } logger.info { "Magix server stopped" }
device?.stop() device?.stop()
logger.info { "Device server stopped" } logger.info { "Device server stopped" }
context.close()
} }
} }
@Composable
fun DemoControls(controller: DemoController) {
var timeScale by remember { mutableStateOf(5000f) }
var xScale by remember { mutableStateOf(1f) }
var yScale by remember { mutableStateOf(1f) }
class DemoControllerView : View(title = " Demo controller remote") { Surface(Modifier.padding(5.dp)) {
private val controller: DemoController by inject() Column {
private var timeScaleSlider: Slider by singleAssign() Row(Modifier.fillMaxWidth()) {
private var xScaleSlider: Slider by singleAssign() Text("Time Scale", modifier = Modifier.align(Alignment.CenterVertically).width(100.dp))
private var yScaleSlider: Slider by singleAssign() TextField(String.format("%.2f", timeScale),{}, enabled = false, modifier = Modifier.align(Alignment.CenterVertically).width(100.dp))
Slider(timeScale, onValueChange = { timeScale = it }, steps = 20, valueRange = 1000f..5000f)
override val root: Parent = vbox {
hbox {
label("Time scale")
pane {
hgrow = Priority.ALWAYS
} }
timeScaleSlider = slider(1000..10000, 5000) { Row(Modifier.fillMaxWidth()) {
isShowTickLabels = true Text("X scale", modifier = Modifier.align(Alignment.CenterVertically).width(100.dp))
isShowTickMarks = true TextField(String.format("%.2f", xScale),{}, enabled = false, modifier = Modifier.align(Alignment.CenterVertically).width(100.dp))
Slider(xScale, onValueChange = { xScale = it }, steps = 20, valueRange = 0.1f..2.0f)
} }
Row(Modifier.fillMaxWidth()) {
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 { Row(Modifier.fillMaxWidth()) {
label("X scale") Button(
pane { onClick = {
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 { controller.device?.run {
launch { launch {
write(timeScale, timeScaleSlider.value) write(DemoDevice.timeScale, timeScale.toDouble())
write(sinScale, xScaleSlider.value) write(DemoDevice.sinScale, xScale.toDouble())
write(cosScale, yScaleSlider.value) write(DemoDevice.cosScale, yScale.toDouble())
} }
} }
},
Modifier.fillMaxWidth()
) {
Text("Submit")
} }
} }
button("Show plots") { Row(Modifier.fillMaxWidth()) {
useMaxWidth = true Button(
action { onClick = {
controller.visualizer?.run { controller.visualizer?.run {
val host = "localhost"//environment.connectors.first().host val host = "localhost"//environment.connectors.first().host
val port = environment.connectors.first().port val port = environment.connectors.first().port
val uri = URI("http", null, host, port, "/", null, null) val uri = URI("http", null, host, port, "/", null, null)
Desktop.getDesktop().browse(uri) Desktop.getDesktop().browse(uri)
} }
},
Modifier.fillMaxWidth()
) {
Text("Show plots")
} }
} }
} }
}
} }
class DemoControllerApp : App(DemoControllerView::class) { fun main() = application {
private val controller: DemoController by inject() val controller = remember { DemoController().apply { start() } }
override fun start(stage: Stage) { Window(
super.start(stage) title = "All things control",
controller.init() onCloseRequest = {
}
override fun stop() {
controller.shutdown() controller.shutdown()
super.stop() exitApplication()
},
state = rememberWindowState(width = 400.dp, height = 320.dp)
) {
MaterialTheme {
DemoControls(controller)
}
} }
} }
fun main() {
launch<DemoControllerApp>()
}

View File

@ -1,10 +0,0 @@
package space.kscience.controls.demo
//import com.github.ricky12awesome.jss.encodeToSchema
//import com.github.ricky12awesome.jss.globalJson
//import space.kscience.controls.api.DeviceMessage
//fun main() {
// val schema = globalJson.encodeToSchema(DeviceMessage.serializer(), generateDefinitions = false)
// println(schema)
//}

View File

@ -8,6 +8,7 @@ import javafx.scene.layout.Priority
import javafx.stage.Stage import javafx.stage.Stage
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import space.kscience.controls.client.launchMagixService import space.kscience.controls.client.launchMagixService
import space.kscience.controls.demo.car.IVirtualCar.Companion.acceleration import space.kscience.controls.demo.car.IVirtualCar.Companion.acceleration
import space.kscience.controls.manager.DeviceManager import space.kscience.controls.manager.DeviceManager
@ -67,7 +68,7 @@ class VirtualCarController : Controller(), ContextAware {
} }
} }
fun shutdown() { suspend fun shutdown() {
logger.info { "Shutting down..." } logger.info { "Shutting down..." }
magixServer?.stop(1000, 5000) magixServer?.stop(1000, 5000)
logger.info { "Magix server stopped" } logger.info { "Magix server stopped" }
@ -137,7 +138,9 @@ class VirtualCarControllerApp : App(VirtualCarControllerView::class) {
} }
override fun stop() { override fun stop() {
runBlocking {
controller.shutdown() controller.shutdown()
}
super.stop() super.stop()
} }
} }

View File

@ -16,9 +16,11 @@ import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application import androidx.compose.ui.window.application
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import space.kscience.controls.constructor.* import space.kscience.controls.constructor.*
import space.kscience.controls.constructor.library.*
import space.kscience.controls.manager.ClockManager import space.kscience.controls.manager.ClockManager
import space.kscience.controls.manager.DeviceManager import space.kscience.controls.manager.DeviceManager
import space.kscience.controls.manager.clock import space.kscience.controls.manager.clock
import space.kscience.controls.manager.install
import space.kscience.controls.spec.doRecurring import space.kscience.controls.spec.doRecurring
import space.kscience.controls.spec.name import space.kscience.controls.spec.name
import space.kscience.controls.vision.plot import space.kscience.controls.vision.plot
@ -48,13 +50,14 @@ class LinearDrive(
val drive by device(VirtualDrive.factory(mass, state)) val drive by device(VirtualDrive.factory(mass, state))
val pid by device(PidRegulator(drive, pidParameters)) val pid by device(PidRegulator(drive, pidParameters))
val start by device(LimitSwitch.factory(state.atStartState)) val start by device(LimitSwitch(state.atStartState))
val end by device(LimitSwitch.factory(state.atEndState)) val end by device(LimitSwitch(state.atEndState))
val positionState: DoubleRangeState by property(state) val positionState: DoubleRangeState by property(state)
private val targetState: MutableDeviceState<Double> by property(pid.mutablePropertyAsState(Regulator.target, 0.0))
var target by targetState private val targetState: MutableDeviceState<Double> by deviceProperty(pid, Regulator.target, 0.0)
var target: Double by targetState
} }
@ -73,7 +76,6 @@ private fun Context.launchPidDevice(
val timeFromStart = clock.now() - clockStart val timeFromStart = clock.now() - clockStart
val t = timeFromStart.toDouble(DurationUnit.SECONDS) val t = timeFromStart.toDouble(DurationUnit.SECONDS)
val freq = 0.1 val freq = 0.1
target = 5 * sin(2.0 * PI * freq * t) + target = 5 * sin(2.0 * PI * freq * t) +
sin(2 * PI * 21 * freq * t + 0.02 * (timeFromStart / pidParameters.timeStep)) sin(2 * PI * 21 * freq * t + 0.02 * (timeFromStart / pidParameters.timeStep))
} }
@ -150,7 +152,7 @@ fun main() = application {
Row { Row {
Text("kp:", Modifier.align(Alignment.CenterVertically).width(50.dp).padding(5.dp)) Text("kp:", Modifier.align(Alignment.CenterVertically).width(50.dp).padding(5.dp))
TextField( TextField(
String.format("%.2f",pidParameters.kp), String.format("%.2f", pidParameters.kp),
{ pidParameters.kp = it.toDouble() }, { pidParameters.kp = it.toDouble() },
Modifier.width(100.dp), Modifier.width(100.dp),
enabled = false enabled = false
@ -165,7 +167,7 @@ fun main() = application {
Row { Row {
Text("ki:", Modifier.align(Alignment.CenterVertically).width(50.dp).padding(5.dp)) Text("ki:", Modifier.align(Alignment.CenterVertically).width(50.dp).padding(5.dp))
TextField( TextField(
String.format("%.2f",pidParameters.ki), String.format("%.2f", pidParameters.ki),
{ pidParameters.ki = it.toDouble() }, { pidParameters.ki = it.toDouble() },
Modifier.width(100.dp), Modifier.width(100.dp),
enabled = false enabled = false
@ -181,7 +183,7 @@ fun main() = application {
Row { Row {
Text("kd:", Modifier.align(Alignment.CenterVertically).width(50.dp).padding(5.dp)) Text("kd:", Modifier.align(Alignment.CenterVertically).width(50.dp).padding(5.dp))
TextField( TextField(
String.format("%.2f",pidParameters.kd), String.format("%.2f", pidParameters.kd),
{ pidParameters.kd = it.toDouble() }, { pidParameters.kd = it.toDouble() },
Modifier.width(100.dp), Modifier.width(100.dp),
enabled = false enabled = false

View File

@ -1,18 +1,6 @@
plugins { plugins {
id("space.kscience.gradle.jvm") id("space.kscience.gradle.jvm")
application alias(spclibs.plugins.compose)
id("org.openjfx.javafxplugin")
}
//TODO to be moved to a separate project
javafx {
version = "17"
modules = listOf("javafx.controls")
}
application{
mainClass.set("ru.mipt.npm.devices.pimotionmaster.PiMotionMasterAppKt")
} }
kotlin{ kotlin{
@ -25,5 +13,17 @@ val dataforgeVersion: String by extra
dependencies { dependencies {
implementation(project(":controls-ports-ktor")) implementation(project(":controls-ports-ktor"))
implementation(projects.controlsMagix) implementation(projects.controlsMagix)
implementation(libs.tornadofx)
implementation(compose.runtime)
implementation(compose.desktop.currentOs)
implementation(compose.material3)
implementation(spclibs.logback.classic)
}
compose{
desktop{
application{
mainClass = "ru.mipt.npm.devices.pimotionmaster.PiMotionMasterAppKt"
}
}
} }

View File

@ -1,31 +1,195 @@
package ru.mipt.npm.devices.pimotionmaster package ru.mipt.npm.devices.pimotionmaster
import javafx.beans.property.ReadOnlyProperty
import javafx.beans.property.SimpleIntegerProperty import androidx.compose.foundation.layout.Column
import javafx.beans.property.SimpleObjectProperty import androidx.compose.foundation.layout.ColumnScope
import javafx.beans.property.SimpleStringProperty import androidx.compose.foundation.layout.Row
import javafx.geometry.Pos import androidx.compose.foundation.layout.fillMaxWidth
import javafx.scene.Parent import androidx.compose.material.Button
import javafx.scene.layout.Priority import androidx.compose.material.OutlinedTextField
import javafx.scene.layout.VBox import androidx.compose.material.Slider
import kotlinx.coroutines.CoroutineScope import androidx.compose.material.Text
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application
import androidx.compose.ui.window.rememberWindowState
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import ru.mipt.npm.devices.pimotionmaster.PiMotionMasterDevice.Axis.Companion.maxPosition
import ru.mipt.npm.devices.pimotionmaster.PiMotionMasterDevice.Axis.Companion.minPosition
import ru.mipt.npm.devices.pimotionmaster.PiMotionMasterDevice.Axis.Companion.position
import space.kscience.controls.manager.DeviceManager import space.kscience.controls.manager.DeviceManager
import space.kscience.controls.manager.installing import space.kscience.controls.manager.installing
import space.kscience.controls.spec.read import space.kscience.controls.spec.read
import space.kscience.dataforge.context.Context import space.kscience.dataforge.context.Context
import space.kscience.dataforge.context.request import space.kscience.dataforge.context.request
import tornadofx.*
class PiMotionMasterApp : App(PiMotionMasterView::class) //class PiMotionMasterApp : App(PiMotionMasterView::class)
//
//class PiMotionMasterController : Controller() {
// //initialize context
// val context = Context("piMotionMaster") {
// plugin(DeviceManager)
// }
//
// //initialize deviceManager plugin
// val deviceManager: DeviceManager = context.request(DeviceManager)
//
// // install device
// val motionMaster: PiMotionMasterDevice by deviceManager.installing(PiMotionMasterDevice)
//}
class PiMotionMasterController : Controller() { @Composable
//initialize context fun ColumnScope.piMotionMasterAxis(
val context = Context("piMotionMaster"){ axisName: String,
axis: PiMotionMasterDevice.Axis,
) {
var min by remember { mutableStateOf(0f) }
var max by remember { mutableStateOf(1f) }
var targetPosition by remember { mutableStateOf(0f) }
val position: Double by axis.composeState(PiMotionMasterDevice.Axis.position, 0.0)
val scope = rememberCoroutineScope()
LaunchedEffect(axis) {
min = axis.read(PiMotionMasterDevice.Axis.minPosition).toFloat()
max = axis.read(PiMotionMasterDevice.Axis.maxPosition).toFloat()
targetPosition = axis.read(PiMotionMasterDevice.Axis.position).toFloat()
}
Row {
Text(axisName)
Column {
Slider(
value = position.toFloat(),
enabled = false,
onValueChange = { },
valueRange = min..max
)
Slider(
value = targetPosition,
onValueChange = { newPosition ->
targetPosition = newPosition
scope.launch {
axis.move(newPosition.toDouble())
}
},
valueRange = min..max
)
}
}
}
@Composable
fun AxisPane(axes: Map<String, PiMotionMasterDevice.Axis>) {
Column {
axes.forEach { (name, axis) ->
this.piMotionMasterAxis(name, axis)
}
}
}
@Composable
fun PiMotionMasterApp(device: PiMotionMasterDevice) {
val scope = rememberCoroutineScope()
val connected by device.composeState(PiMotionMasterDevice.connected, false)
var debugServerJob by remember { mutableStateOf<Job?>(null) }
var axes by remember { mutableStateOf<Map<String, PiMotionMasterDevice.Axis>?>(null) }
//private val axisList = FXCollections.observableArrayList<Map.Entry<String, PiMotionMasterDevice.Axis>>()
var host by remember { mutableStateOf("127.0.0.1") }
var port by remember { mutableStateOf(10024) }
Scaffold {
Column {
Text("Address:")
Row {
OutlinedTextField(
value = host,
onValueChange = { host = it },
label = { Text("Host") },
enabled = debugServerJob == null,
modifier = Modifier.weight(1f)
)
var portError by remember { mutableStateOf(false) }
OutlinedTextField(
value = port.toString(),
onValueChange = {
it.toIntOrNull()?.let { value ->
port = value
portError = false
} ?: run {
portError = true
}
},
label = { Text("Port") },
enabled = debugServerJob == null,
isError = portError,
modifier = Modifier.weight(1f),
)
}
Row {
Button(
onClick = {
if (debugServerJob == null) {
debugServerJob = device.context.launchPiDebugServer(port, listOf("1", "2", "3", "4"))
} else {
debugServerJob?.cancel()
debugServerJob = null
}
},
modifier = Modifier.fillMaxWidth()
) {
if (debugServerJob == null) {
Text("Start debug server")
} else {
Text("Stop debug server")
}
}
}
Row {
Button(
onClick = {
if (!connected) {
device.launch {
device.connect(host, port)
axes = device.axes
}
} else {
device.launch {
device.disconnect()
axes = null
}
}
},
modifier = Modifier.fillMaxWidth()
) {
if (!connected) {
Text("Connect")
} else {
Text("Disconnect")
}
}
}
axes?.let { axes ->
AxisPane(axes)
}
}
}
}
fun main() = application {
val context = Context("piMotionMaster") {
plugin(DeviceManager) plugin(DeviceManager)
} }
@ -34,131 +198,14 @@ class PiMotionMasterController : Controller() {
// install device // install device
val motionMaster: PiMotionMasterDevice by deviceManager.installing(PiMotionMasterDevice) val motionMaster: PiMotionMasterDevice by deviceManager.installing(PiMotionMasterDevice)
}
fun VBox.piMotionMasterAxis( Window(
axisName: String, title = "Pi motion master demo",
axis: PiMotionMasterDevice.Axis, onCloseRequest = { exitApplication() },
coroutineScope: CoroutineScope, state = rememberWindowState(width = 400.dp, height = 300.dp)
) = hbox { ) {
alignment = Pos.CENTER MaterialTheme {
label(axisName) PiMotionMasterApp(motionMaster)
coroutineScope.launch {
with(axis) {
val min: Double = read(minPosition)
val max: Double = read(maxPosition)
val positionProperty = fxProperty(position)
val startPosition = read(position)
runLater {
vbox {
hgrow = Priority.ALWAYS
slider(min..max, startPosition) {
minWidth = 300.0
isShowTickLabels = true
isShowTickMarks = true
minorTickCount = 10
majorTickUnit = 1.0
valueProperty().onChange {
coroutineScope.launch {
axis.move(value)
}
}
}
slider(min..max) {
isDisable = true
valueProperty().bind(positionProperty)
}
}
}
} }
} }
} }
fun Parent.axisPane(axes: Map<String, PiMotionMasterDevice.Axis>, coroutineScope: CoroutineScope) {
vbox {
axes.forEach { (name, axis) ->
this.piMotionMasterAxis(name, axis, coroutineScope)
}
}
}
class PiMotionMasterView : View() {
private val controller: PiMotionMasterController by inject()
val device = controller.motionMaster
private val connectedProperty: ReadOnlyProperty<Boolean> = device.fxProperty(PiMotionMasterDevice.connected)
private val debugServerJobProperty = SimpleObjectProperty<Job>()
private val debugServerStarted = debugServerJobProperty.booleanBinding { it != null }
//private val axisList = FXCollections.observableArrayList<Map.Entry<String, PiMotionMasterDevice.Axis>>()
override val root: Parent = borderpane {
top {
form {
val host = SimpleStringProperty("127.0.0.1")
val port = SimpleIntegerProperty(10024)
fieldset("Address:") {
field("Host:") {
textfield(host) {
enableWhen(debugServerStarted.not())
}
}
field("Port:") {
textfield(port) {
stripNonNumeric()
}
button {
hgrow = Priority.ALWAYS
textProperty().bind(debugServerStarted.stringBinding {
if (it != true) {
"Start debug server"
} else {
"Stop debug server"
}
})
action {
if (!debugServerStarted.get()) {
debugServerJobProperty.value =
controller.context.launchPiDebugServer(port.get(), listOf("1", "2", "3", "4"))
} else {
debugServerJobProperty.get().cancel()
debugServerJobProperty.value = null
}
}
}
}
}
button {
hgrow = Priority.ALWAYS
textProperty().bind(connectedProperty.stringBinding {
if (it == false) {
"Connect"
} else {
"Disconnect"
}
})
action {
if (!connectedProperty.value) {
device.connect(host.get(), port.get())
center {
axisPane(device.axes,controller.context)
}
} else {
this@borderpane.center = null
device.disconnect()
}
}
}
}
}
}
}
fun main() {
launch<PiMotionMasterApp>()
}

View File

@ -7,13 +7,15 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.toList import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.flow.transformWhile import kotlinx.coroutines.flow.transformWhile
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withTimeout import kotlinx.coroutines.withTimeout
import space.kscience.controls.api.DeviceHub import space.kscience.controls.api.DeviceHub
import space.kscience.controls.api.PropertyDescriptor import space.kscience.controls.api.PropertyDescriptor
import space.kscience.controls.ports.* import space.kscience.controls.ports.AsynchronousPort
import space.kscience.controls.ports.KtorTcpPort
import space.kscience.controls.ports.send
import space.kscience.controls.ports.withStringDelimiter
import space.kscience.controls.spec.* import space.kscience.controls.spec.*
import space.kscience.dataforge.context.* import space.kscience.dataforge.context.*
import space.kscience.dataforge.meta.* import space.kscience.dataforge.meta.*
@ -33,11 +35,9 @@ class PiMotionMasterDevice(
//PortProxy { portFactory(address ?: error("The device is not connected"), context) } //PortProxy { portFactory(address ?: error("The device is not connected"), context) }
fun disconnect() { suspend fun disconnect() {
runBlocking {
execute(disconnect) execute(disconnect)
} }
}
var timeoutValue: Duration = 200.milliseconds var timeoutValue: Duration = 200.milliseconds
@ -54,14 +54,12 @@ class PiMotionMasterDevice(
if (errorCode != 0) error(message(errorCode)) if (errorCode != 0) error(message(errorCode))
} }
fun connect(host: String, port: Int) { suspend fun connect(host: String, port: Int) {
runBlocking {
execute(connect, Meta { execute(connect, Meta {
"host" put host "host" put host
"port" put port "port" put port
}) })
} }
}
private val mutex = Mutex() private val mutex = Mutex()
@ -103,7 +101,7 @@ class PiMotionMasterDevice(
}.toList() }.toList()
} }
} catch (ex: Throwable) { } catch (ex: Throwable) {
logger.warn { "Error during PIMotionMaster request. Requesting error code." } logger.error(ex) { "Error during PIMotionMaster request. Requesting error code." }
val errorCode = getErrorCode() val errorCode = getErrorCode()
dispatchError(errorCode) dispatchError(errorCode)
logger.warn { "Error code $errorCode" } logger.warn { "Error code $errorCode" }
@ -167,12 +165,12 @@ class PiMotionMasterDevice(
} }
//Update port //Update port
//address = portSpec.node //address = portSpec.node
port = portFactory(portSpec, context) port = portFactory(portSpec, context).apply { open() }
propertyChanged(connected, true)
// connector.open() // connector.open()
//Initialize axes //Initialize axes
val idn = read(identity) val idn = read(identity)
failIfError { "Can't connect to $portSpec. Error code: $it" } failIfError { "Can't connect to $portSpec. Error code: $it" }
propertyChanged(connected, true)
logger.info { "Connected to $idn on $portSpec" } logger.info { "Connected to $idn on $portSpec" }
val ids = request("SAI?").map { it.trim() } val ids = request("SAI?").map { it.trim() }
if (ids != axes.keys.toList()) { if (ids != axes.keys.toList()) {

View File

@ -0,0 +1,15 @@
package ru.mipt.npm.devices.pimotionmaster
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState
import space.kscience.controls.api.Device
import space.kscience.controls.spec.DevicePropertySpec
import space.kscience.controls.spec.propertyFlow
@Composable
fun <D : Device, T : Any> D.composeState(
spec: DevicePropertySpec<D, T>,
initialState: T,
): State<T> = propertyFlow(spec).collectAsState(initialState)

View File

@ -1,58 +0,0 @@
package ru.mipt.npm.devices.pimotionmaster
import javafx.beans.property.ObjectPropertyBase
import javafx.beans.property.Property
import javafx.beans.property.ReadOnlyProperty
import space.kscience.controls.api.Device
import space.kscience.controls.spec.*
import space.kscience.dataforge.context.info
import space.kscience.dataforge.context.logger
import tornadofx.*
/**
* Bind a FX property to a device property with a given [spec]
*/
fun <D : Device, T : Any> D.fxProperty(
spec: DevicePropertySpec<D, T>,
): ReadOnlyProperty<T> = object : ObjectPropertyBase<T>() {
override fun getBean(): Any = this
override fun getName(): String = spec.name
init {
//Read incoming changes
onPropertyChange(spec) {
runLater {
try {
set(it)
} catch (ex: Throwable) {
logger.info { "Failed to set property $name to $it" }
}
}
}
}
}
fun <D : Device, T : Any> D.fxProperty(spec: MutableDevicePropertySpec<D, T>): Property<T> =
object : ObjectPropertyBase<T>() {
override fun getBean(): Any = this
override fun getName(): String = spec.name
init {
//Read incoming changes
onPropertyChange(spec) {
runLater {
try {
set(it)
} catch (ex: Throwable) {
logger.info { "Failed to set property $name to $it" }
}
}
}
onChange { newValue ->
if (newValue != null) {
writeAsync(spec, newValue)
}
}
}
}

View File

@ -8,6 +8,8 @@ import io.ktor.util.InternalAPI
import io.ktor.util.moveToByteArray import io.ktor.util.moveToByteArray
import io.ktor.utils.io.writeAvailable import io.ktor.utils.io.writeAvailable
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import space.kscience.dataforge.context.Context import space.kscience.dataforge.context.Context
import space.kscience.dataforge.context.Global import space.kscience.dataforge.context.Global
@ -18,7 +20,7 @@ val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
@OptIn(InternalAPI::class) @OptIn(InternalAPI::class)
fun Context.launchPiDebugServer(port: Int, axes: List<String>): Job = launch(exceptionHandler) { fun Context.launchPiDebugServer(port: Int, axes: List<String>): Job = launch(exceptionHandler) {
val virtualDevice = PiMotionMasterVirtualDevice(this@launchPiDebugServer, axes) val virtualDevice = PiMotionMasterVirtualDevice(this@launchPiDebugServer, axes)
val server = aSocket(ActorSelectorManager(Dispatchers.IO)).tcp().bind("localhost", port) aSocket(ActorSelectorManager(Dispatchers.IO)).tcp().bind("localhost", port).use { server ->
println("Started virtual port server at ${server.localAddress}") println("Started virtual port server at ${server.localAddress}")
while (isActive) { while (isActive) {
@ -28,13 +30,12 @@ fun Context.launchPiDebugServer(port: Int, axes: List<String>): Job = launch(exc
val input = socket.openReadChannel() val input = socket.openReadChannel()
val output = socket.openWriteChannel() val output = socket.openWriteChannel()
val sendJob = launch { val sendJob = virtualDevice.subscribe().onEach {
virtualDevice.subscribe().collect {
//println("Sending: ${it.decodeToString()}") //println("Sending: ${it.decodeToString()}")
output.writeAvailable(it) output.writeAvailable(it)
output.flush() output.flush()
} }.launchIn(this)
}
try { try {
while (isActive) { while (isActive) {
@ -50,9 +51,9 @@ fun Context.launchPiDebugServer(port: Int, axes: List<String>): Job = launch(exc
sendJob.cancel() sendJob.cancel()
socket.close() socket.close()
} finally { } finally {
println("Socket closed") println("Client socket closed")
}
} }
} }
} }
} }

View File

@ -1,25 +1,24 @@
import space.kscience.gradle.Maturity import space.kscience.gradle.Maturity
plugins { plugins {
id("space.kscience.gradle.jvm") id("space.kscience.gradle.mpp")
`maven-publish` `maven-publish`
application
} }
description = """ description = """
A magix event loop implementation in Kotlin. Includes HTTP/SSE and RSocket routes. A magix event loop implementation in Kotlin. Includes HTTP/SSE and RSocket routes.
""".trimIndent() """.trimIndent()
kscience {
useSerialization{
json()
}
}
val dataforgeVersion: String by rootProject.extra val dataforgeVersion: String by rootProject.extra
val ktorVersion: String = space.kscience.gradle.KScienceVersions.ktorVersion val ktorVersion: String = space.kscience.gradle.KScienceVersions.ktorVersion
dependencies{ kscience {
jvm()
useSerialization{
json()
}
jvmMain{
api(projects.magix.magixApi) api(projects.magix.magixApi)
api("io.ktor:ktor-server-cio:$ktorVersion") api("io.ktor:ktor-server-cio:$ktorVersion")
api("io.ktor:ktor-server-websockets:$ktorVersion") api("io.ktor:ktor-server-websockets:$ktorVersion")
@ -29,8 +28,11 @@ dependencies{
api(libs.rsocket.ktor.server) api(libs.rsocket.ktor.server)
api(libs.rsocket.transport.ktor.tcp) api(libs.rsocket.transport.ktor.tcp)
}
} }
readme{ readme{
maturity = Maturity.EXPERIMENTAL maturity = Maturity.EXPERIMENTAL
} }

View File

@ -18,7 +18,6 @@ pluginManagement {
id("space.kscience.gradle.mpp") version toolsVersion id("space.kscience.gradle.mpp") version toolsVersion
id("space.kscience.gradle.jvm") version toolsVersion id("space.kscience.gradle.jvm") version toolsVersion
id("space.kscience.gradle.js") version toolsVersion id("space.kscience.gradle.js") version toolsVersion
id("org.openjfx.javafxplugin") version "0.0.13"
} }
} }