Remove stand-alone properties and use specs instead
This commit is contained in:
parent
1b021ca5ca
commit
ed2a2a29af
@ -57,7 +57,7 @@ public interface Device : Closeable, ContextAware, CoroutineScope {
|
||||
|
||||
/**
|
||||
* A subscription-based [Flow] of [DeviceMessage] provided by device. The flow is guaranteed to be readable
|
||||
* multiple times
|
||||
* multiple times.
|
||||
*/
|
||||
public val messageFlow: Flow<DeviceMessage>
|
||||
|
||||
@ -67,6 +67,14 @@ public interface Device : Closeable, ContextAware, CoroutineScope {
|
||||
*/
|
||||
public suspend fun execute(action: String, argument: Meta? = null): Meta?
|
||||
|
||||
/**
|
||||
* Initialize the device. This function suspends until the device is finished initialization
|
||||
*/
|
||||
public suspend fun open(): Unit = Unit
|
||||
|
||||
/**
|
||||
* Close and terminate the device. This function does not wait for device to be closed.
|
||||
*/
|
||||
override fun close() {
|
||||
cancel("The device is closed")
|
||||
}
|
||||
@ -85,7 +93,7 @@ public suspend fun Device.getOrReadProperty(propertyName: String): Meta =
|
||||
/**
|
||||
* Get a snapshot of logical state of the device
|
||||
*
|
||||
* TODO currently this
|
||||
* TODO currently this
|
||||
*/
|
||||
public fun Device.getProperties(): Meta = Meta {
|
||||
for (descriptor in propertyDescriptors) {
|
||||
@ -97,4 +105,4 @@ public fun Device.getProperties(): Meta = Meta {
|
||||
* Subscribe on property changes for the whole device
|
||||
*/
|
||||
public fun Device.onPropertyChange(callback: suspend PropertyChangedMessage.() -> Unit): Job =
|
||||
messageFlow.filterIsInstance<PropertyChangedMessage>().onEach(callback).launchIn(this)
|
||||
messageFlow.filterIsInstance<PropertyChangedMessage>().onEach(callback).launchIn(this)
|
||||
|
@ -1,10 +0,0 @@
|
||||
package ru.mipt.npm.controls.base
|
||||
|
||||
import ru.mipt.npm.controls.api.ActionDescriptor
|
||||
import space.kscience.dataforge.meta.Meta
|
||||
|
||||
public interface DeviceAction {
|
||||
public val name: String
|
||||
public val descriptor: ActionDescriptor
|
||||
public suspend operator fun invoke(arg: Meta? = null): Meta?
|
||||
}
|
@ -1,252 +0,0 @@
|
||||
package ru.mipt.npm.controls.base
|
||||
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import ru.mipt.npm.controls.api.*
|
||||
import space.kscience.dataforge.context.Context
|
||||
import space.kscience.dataforge.meta.Meta
|
||||
import space.kscience.dataforge.misc.DFExperimental
|
||||
import kotlin.collections.set
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
//TODO move to DataForge-core
|
||||
@DFExperimental
|
||||
public data class LogEntry(val content: String, val priority: Int = 0)
|
||||
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
private open class BasicReadOnlyDeviceProperty(
|
||||
val device: DeviceBase,
|
||||
override val name: String,
|
||||
default: Meta?,
|
||||
override val descriptor: PropertyDescriptor,
|
||||
private val getter: suspend (before: Meta?) -> Meta,
|
||||
) : ReadOnlyDeviceProperty {
|
||||
|
||||
override val scope: CoroutineScope get() = device
|
||||
|
||||
private val state: MutableStateFlow<Meta?> = MutableStateFlow(default)
|
||||
override val value: Meta? get() = state.value
|
||||
|
||||
override suspend fun invalidate() {
|
||||
state.value = null
|
||||
}
|
||||
|
||||
override fun updateLogical(item: Meta) {
|
||||
state.value = item
|
||||
scope.launch {
|
||||
device.sharedMessageFlow.emit(
|
||||
PropertyChangedMessage(
|
||||
property = name,
|
||||
value = item,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun read(force: Boolean): Meta {
|
||||
//backup current value
|
||||
val currentValue = value
|
||||
return if (force || currentValue == null) {
|
||||
//all device operations should be run on device context
|
||||
//propagate error, but do not fail scope
|
||||
val res = withContext(scope.coroutineContext + SupervisorJob(scope.coroutineContext[Job])) {
|
||||
getter(currentValue)
|
||||
}
|
||||
updateLogical(res)
|
||||
res
|
||||
} else {
|
||||
currentValue
|
||||
}
|
||||
}
|
||||
|
||||
override fun flow(): StateFlow<Meta?> = state
|
||||
}
|
||||
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
private class BasicDeviceProperty(
|
||||
device: DeviceBase,
|
||||
name: String,
|
||||
default: Meta?,
|
||||
descriptor: PropertyDescriptor,
|
||||
getter: suspend (Meta?) -> Meta,
|
||||
private val setter: suspend (oldValue: Meta?, newValue: Meta) -> Meta?,
|
||||
) : BasicReadOnlyDeviceProperty(device, name, default, descriptor, getter), DeviceProperty {
|
||||
|
||||
override var value: Meta?
|
||||
get() = super.value
|
||||
set(value) {
|
||||
scope.launch {
|
||||
if (value == null) {
|
||||
invalidate()
|
||||
} else {
|
||||
write(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val writeLock = Mutex()
|
||||
|
||||
override suspend fun write(item: Meta) {
|
||||
writeLock.withLock {
|
||||
//fast return if value is not changed
|
||||
if (item == value) return@withLock
|
||||
val oldValue = value
|
||||
//all device operations should be run on device context
|
||||
withContext(scope.coroutineContext + SupervisorJob(scope.coroutineContext[Job])) {
|
||||
setter(oldValue, item)?.let {
|
||||
updateLogical(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Baseline implementation of [Device] interface
|
||||
*/
|
||||
@Suppress("EXPERIMENTAL_API_USAGE")
|
||||
public abstract class DeviceBase(final override val context: Context) : Device {
|
||||
|
||||
override val coroutineContext: CoroutineContext =
|
||||
context.coroutineContext + SupervisorJob(context.coroutineContext[Job])
|
||||
|
||||
private val _properties = HashMap<String, ReadOnlyDeviceProperty>()
|
||||
public val properties: Map<String, ReadOnlyDeviceProperty> get() = _properties
|
||||
private val _actions = HashMap<String, DeviceAction>()
|
||||
public val actions: Map<String, DeviceAction> get() = _actions
|
||||
|
||||
internal val sharedMessageFlow = MutableSharedFlow<DeviceMessage>()
|
||||
|
||||
override val messageFlow: SharedFlow<DeviceMessage> get() = sharedMessageFlow
|
||||
private val sharedLogFlow = MutableSharedFlow<LogEntry>()
|
||||
|
||||
/**
|
||||
* The [SharedFlow] of log messages
|
||||
*/
|
||||
@DFExperimental
|
||||
public val logFlow: SharedFlow<LogEntry>
|
||||
get() = sharedLogFlow
|
||||
|
||||
protected suspend fun log(message: String, priority: Int = 0) {
|
||||
sharedLogFlow.emit(LogEntry(message, priority))
|
||||
}
|
||||
|
||||
override val propertyDescriptors: Collection<PropertyDescriptor>
|
||||
get() = _properties.values.map { it.descriptor }
|
||||
|
||||
override val actionDescriptors: Collection<ActionDescriptor>
|
||||
get() = _actions.values.map { it.descriptor }
|
||||
|
||||
private fun <P : ReadOnlyDeviceProperty> registerProperty(name: String, property: P) {
|
||||
if (_properties.contains(name)) error("Property with name $name already registered")
|
||||
_properties[name] = property
|
||||
}
|
||||
|
||||
internal fun registerAction(name: String, action: DeviceAction) {
|
||||
if (_actions.contains(name)) error("Action with name $name already registered")
|
||||
_actions[name] = action
|
||||
}
|
||||
|
||||
override suspend fun readProperty(propertyName: String): Meta =
|
||||
(_properties[propertyName] ?: error("Property with name $propertyName not defined")).read()
|
||||
|
||||
override fun getProperty(propertyName: String): Meta? =
|
||||
(_properties[propertyName] ?: error("Property with name $propertyName not defined")).value
|
||||
|
||||
override suspend fun invalidate(propertyName: String) {
|
||||
(_properties[propertyName] ?: error("Property with name $propertyName not defined")).invalidate()
|
||||
}
|
||||
|
||||
override suspend fun writeProperty(propertyName: String, value: Meta) {
|
||||
(_properties[propertyName] as? DeviceProperty ?: error("Property with name $propertyName not defined")).write(
|
||||
value
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun execute(action: String, argument: Meta?): Meta? =
|
||||
(_actions[action] ?: error("Request with name $action not defined")).invoke(argument)
|
||||
|
||||
/**
|
||||
* Create a bound read-only property with given [getter]
|
||||
*/
|
||||
public fun createReadOnlyProperty(
|
||||
name: String,
|
||||
default: Meta?,
|
||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||
getter: suspend (Meta?) -> Meta,
|
||||
): ReadOnlyDeviceProperty {
|
||||
val property = BasicReadOnlyDeviceProperty(
|
||||
this,
|
||||
name,
|
||||
default,
|
||||
PropertyDescriptor(name).apply(descriptorBuilder),
|
||||
getter
|
||||
)
|
||||
registerProperty(name, property)
|
||||
return property
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Create a bound mutable property with given [getter] and [setter]
|
||||
*/
|
||||
internal fun createMutableProperty(
|
||||
name: String,
|
||||
default: Meta?,
|
||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||
getter: suspend (Meta?) -> Meta,
|
||||
setter: suspend (oldValue: Meta?, newValue: Meta) -> Meta?,
|
||||
): DeviceProperty {
|
||||
val property = BasicDeviceProperty(
|
||||
this,
|
||||
name,
|
||||
default,
|
||||
PropertyDescriptor(name).apply(descriptorBuilder),
|
||||
getter,
|
||||
setter
|
||||
)
|
||||
registerProperty(name, property)
|
||||
return property
|
||||
}
|
||||
|
||||
/**
|
||||
* A stand-alone action
|
||||
*/
|
||||
private inner class BasicDeviceAction(
|
||||
override val name: String,
|
||||
override val descriptor: ActionDescriptor,
|
||||
private val block: suspend (Meta?) -> Meta?,
|
||||
) : DeviceAction {
|
||||
override suspend fun invoke(arg: Meta?): Meta? =
|
||||
withContext(coroutineContext) {
|
||||
block(arg)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new bound action
|
||||
*/
|
||||
internal fun createAction(
|
||||
name: String,
|
||||
descriptorBuilder: ActionDescriptor.() -> Unit = {},
|
||||
block: suspend (Meta?) -> Meta?,
|
||||
): DeviceAction {
|
||||
val action = BasicDeviceAction(name, ActionDescriptor(name).apply(descriptorBuilder), block)
|
||||
registerAction(name, action)
|
||||
return action
|
||||
}
|
||||
|
||||
public companion object {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -1,74 +0,0 @@
|
||||
package ru.mipt.npm.controls.base
|
||||
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import ru.mipt.npm.controls.api.PropertyDescriptor
|
||||
import space.kscience.dataforge.meta.Meta
|
||||
import kotlin.time.Duration
|
||||
|
||||
/**
|
||||
* Read-only device property
|
||||
*/
|
||||
public interface ReadOnlyDeviceProperty {
|
||||
/**
|
||||
* Property name, should be unique in device
|
||||
*/
|
||||
public val name: String
|
||||
|
||||
/**
|
||||
* Property descriptor
|
||||
*/
|
||||
public val descriptor: PropertyDescriptor
|
||||
|
||||
public val scope: CoroutineScope
|
||||
|
||||
/**
|
||||
* Erase logical value and force re-read from device on next [read]
|
||||
*/
|
||||
public suspend fun invalidate()
|
||||
|
||||
/**
|
||||
* Directly update property logical value and notify listener without writing it to device
|
||||
*/
|
||||
public fun updateLogical(item: Meta)
|
||||
|
||||
/**
|
||||
* Get cached value and return null if value is invalid or not initialized
|
||||
*/
|
||||
public val value: Meta?
|
||||
|
||||
/**
|
||||
* Read value either from cache if cache is valid or directly from physical device.
|
||||
* If [force], reread from physical state even if the logical state is set.
|
||||
*/
|
||||
public suspend fun read(force: Boolean = false): Meta
|
||||
|
||||
/**
|
||||
* The [Flow] representing future logical states of the property.
|
||||
* Produces null when the state is invalidated
|
||||
*/
|
||||
public fun flow(): Flow<Meta?>
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Launch recurring force re-read job on a property scope with given [duration] between reads.
|
||||
*/
|
||||
public fun ReadOnlyDeviceProperty.readEvery(duration: Duration): Job = scope.launch {
|
||||
while (isActive) {
|
||||
read(true)
|
||||
delay(duration)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A writeable device property with non-suspended write
|
||||
*/
|
||||
public interface DeviceProperty : ReadOnlyDeviceProperty {
|
||||
override var value: Meta?
|
||||
|
||||
/**
|
||||
* Write value to physical device. Invalidates logical value, but does not update it automatically
|
||||
*/
|
||||
public suspend fun write(item: Meta)
|
||||
}
|
@ -1,58 +0,0 @@
|
||||
package ru.mipt.npm.controls.base
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import space.kscience.dataforge.meta.Meta
|
||||
import space.kscience.dataforge.meta.transformations.MetaConverter
|
||||
|
||||
/**
|
||||
* A type-safe wrapper on top of read-only property
|
||||
*/
|
||||
public open class TypedReadOnlyDeviceProperty<T : Any>(
|
||||
private val property: ReadOnlyDeviceProperty,
|
||||
protected val converter: MetaConverter<T>,
|
||||
) : ReadOnlyDeviceProperty by property {
|
||||
|
||||
public fun updateLogical(obj: T) {
|
||||
property.updateLogical(converter.objectToMeta(obj))
|
||||
}
|
||||
|
||||
public open val typedValue: T? get() = value?.let { converter.metaToObject(it) }
|
||||
|
||||
public suspend fun readTyped(force: Boolean = false): T {
|
||||
val meta = read(force)
|
||||
return converter.metaToObject(meta)
|
||||
?: error("Meta $meta could not be converted by $converter")
|
||||
}
|
||||
|
||||
public fun flowTyped(): Flow<T?> = flow().map { it?.let { converter.metaToObject(it) } }
|
||||
}
|
||||
|
||||
/**
|
||||
* A type-safe wrapper for a read-write device property
|
||||
*/
|
||||
public class TypedDeviceProperty<T : Any>(
|
||||
private val property: DeviceProperty,
|
||||
converter: MetaConverter<T>,
|
||||
) : TypedReadOnlyDeviceProperty<T>(property, converter), DeviceProperty {
|
||||
|
||||
override var value: Meta?
|
||||
get() = property.value
|
||||
set(arg) {
|
||||
property.value = arg
|
||||
}
|
||||
|
||||
public override var typedValue: T?
|
||||
get() = value?.let { converter.metaToObject(it) }
|
||||
set(arg) {
|
||||
property.value = arg?.let { converter.objectToMeta(arg) }
|
||||
}
|
||||
|
||||
override suspend fun write(item: Meta) {
|
||||
property.write(item)
|
||||
}
|
||||
|
||||
public suspend fun write(obj: T) {
|
||||
property.write(converter.objectToMeta(obj))
|
||||
}
|
||||
}
|
@ -1,58 +0,0 @@
|
||||
package ru.mipt.npm.controls.base
|
||||
|
||||
import ru.mipt.npm.controls.api.ActionDescriptor
|
||||
import space.kscience.dataforge.meta.Meta
|
||||
import space.kscience.dataforge.meta.MutableMeta
|
||||
import space.kscience.dataforge.values.Value
|
||||
import kotlin.properties.PropertyDelegateProvider
|
||||
import kotlin.properties.ReadOnlyProperty
|
||||
import kotlin.reflect.KProperty
|
||||
|
||||
|
||||
private fun <D : DeviceBase> D.provideAction(): ReadOnlyProperty<D, DeviceAction> =
|
||||
ReadOnlyProperty { _: D, property: KProperty<*> ->
|
||||
val name = property.name
|
||||
return@ReadOnlyProperty actions[name]!!
|
||||
}
|
||||
|
||||
public typealias ActionDelegate = ReadOnlyProperty<DeviceBase, DeviceAction>
|
||||
|
||||
private class ActionProvider<D : DeviceBase>(
|
||||
val owner: D,
|
||||
val descriptorBuilder: ActionDescriptor.() -> Unit = {},
|
||||
val block: suspend (Meta?) -> Meta?,
|
||||
) : PropertyDelegateProvider<D, ActionDelegate> {
|
||||
override operator fun provideDelegate(thisRef: D, property: KProperty<*>): ActionDelegate {
|
||||
val name = property.name
|
||||
owner.createAction(name, descriptorBuilder, block)
|
||||
return owner.provideAction()
|
||||
}
|
||||
}
|
||||
|
||||
public fun DeviceBase.requesting(
|
||||
descriptorBuilder: ActionDescriptor.() -> Unit = {},
|
||||
action: suspend (Meta?) -> Meta?,
|
||||
): PropertyDelegateProvider<DeviceBase, ActionDelegate> = ActionProvider(this, descriptorBuilder, action)
|
||||
|
||||
public fun <D : DeviceBase> D.requestingValue(
|
||||
descriptorBuilder: ActionDescriptor.() -> Unit = {},
|
||||
action: suspend (Meta?) -> Any?,
|
||||
): PropertyDelegateProvider<D, ActionDelegate> = ActionProvider(this, descriptorBuilder) {
|
||||
val res = action(it)
|
||||
Meta(Value.of(res))
|
||||
}
|
||||
|
||||
public fun <D : DeviceBase> D.requestingMeta(
|
||||
descriptorBuilder: ActionDescriptor.() -> Unit = {},
|
||||
action: suspend MutableMeta.(Meta?) -> Unit,
|
||||
): PropertyDelegateProvider<D, ActionDelegate> = ActionProvider(this, descriptorBuilder) {
|
||||
Meta { action(it) }
|
||||
}
|
||||
|
||||
public fun DeviceBase.acting(
|
||||
descriptorBuilder: ActionDescriptor.() -> Unit = {},
|
||||
action: suspend (Meta?) -> Unit,
|
||||
): PropertyDelegateProvider<DeviceBase, ActionDelegate> = ActionProvider(this, descriptorBuilder) {
|
||||
action(it)
|
||||
null
|
||||
}
|
@ -1,283 +0,0 @@
|
||||
package ru.mipt.npm.controls.base
|
||||
|
||||
import ru.mipt.npm.controls.api.PropertyDescriptor
|
||||
import space.kscience.dataforge.meta.Meta
|
||||
import space.kscience.dataforge.meta.MutableMeta
|
||||
import space.kscience.dataforge.meta.boolean
|
||||
import space.kscience.dataforge.meta.double
|
||||
import space.kscience.dataforge.meta.transformations.MetaConverter
|
||||
import space.kscience.dataforge.values.Null
|
||||
import space.kscience.dataforge.values.Value
|
||||
import space.kscience.dataforge.values.asValue
|
||||
import kotlin.properties.PropertyDelegateProvider
|
||||
import kotlin.properties.ReadOnlyProperty
|
||||
import kotlin.reflect.KProperty
|
||||
|
||||
private fun <D : DeviceBase> D.provideProperty(name: String): ReadOnlyProperty<D, ReadOnlyDeviceProperty> =
|
||||
ReadOnlyProperty { _: D, _: KProperty<*> ->
|
||||
return@ReadOnlyProperty properties.getValue(name)
|
||||
}
|
||||
|
||||
private fun <D : DeviceBase, T : Any> D.provideProperty(
|
||||
name: String,
|
||||
converter: MetaConverter<T>,
|
||||
): ReadOnlyProperty<D, TypedReadOnlyDeviceProperty<T>> =
|
||||
ReadOnlyProperty { _: D, _: KProperty<*> ->
|
||||
return@ReadOnlyProperty TypedReadOnlyDeviceProperty(properties.getValue(name), converter)
|
||||
}
|
||||
|
||||
|
||||
public typealias ReadOnlyPropertyDelegate = ReadOnlyProperty<DeviceBase, ReadOnlyDeviceProperty>
|
||||
public typealias TypedReadOnlyPropertyDelegate<T> = ReadOnlyProperty<DeviceBase, TypedReadOnlyDeviceProperty<T>>
|
||||
|
||||
private class ReadOnlyDevicePropertyProvider<D : DeviceBase>(
|
||||
val owner: D,
|
||||
val default: Meta?,
|
||||
val descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||
private val getter: suspend (Meta?) -> Meta,
|
||||
) : PropertyDelegateProvider<D, ReadOnlyPropertyDelegate> {
|
||||
|
||||
override operator fun provideDelegate(thisRef: D, property: KProperty<*>): ReadOnlyPropertyDelegate {
|
||||
val name = property.name
|
||||
owner.createReadOnlyProperty(name, default, descriptorBuilder, getter)
|
||||
return owner.provideProperty(name)
|
||||
}
|
||||
}
|
||||
|
||||
private class TypedReadOnlyDevicePropertyProvider<D : DeviceBase, T : Any>(
|
||||
val owner: D,
|
||||
val default: Meta?,
|
||||
val converter: MetaConverter<T>,
|
||||
val descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||
private val getter: suspend (Meta?) -> Meta,
|
||||
) : PropertyDelegateProvider<D, TypedReadOnlyPropertyDelegate<T>> {
|
||||
|
||||
override operator fun provideDelegate(thisRef: D, property: KProperty<*>): TypedReadOnlyPropertyDelegate<T> {
|
||||
val name = property.name
|
||||
owner.createReadOnlyProperty(name, default, descriptorBuilder, getter)
|
||||
return owner.provideProperty(name, converter)
|
||||
}
|
||||
}
|
||||
|
||||
public fun DeviceBase.reading(
|
||||
default: Meta? = null,
|
||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||
getter: suspend (Meta?) -> Meta,
|
||||
): PropertyDelegateProvider<DeviceBase, ReadOnlyPropertyDelegate> = ReadOnlyDevicePropertyProvider(
|
||||
this,
|
||||
default,
|
||||
descriptorBuilder,
|
||||
getter
|
||||
)
|
||||
|
||||
public fun DeviceBase.readingValue(
|
||||
default: Value? = null,
|
||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||
getter: suspend () -> Any?,
|
||||
): PropertyDelegateProvider<DeviceBase, ReadOnlyPropertyDelegate> = ReadOnlyDevicePropertyProvider(
|
||||
this,
|
||||
default?.let { Meta(it) },
|
||||
descriptorBuilder,
|
||||
getter = { Meta(Value.of(getter())) }
|
||||
)
|
||||
|
||||
public fun DeviceBase.readingNumber(
|
||||
default: Number? = null,
|
||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||
getter: suspend () -> Number,
|
||||
): PropertyDelegateProvider<DeviceBase, TypedReadOnlyPropertyDelegate<Number>> = TypedReadOnlyDevicePropertyProvider(
|
||||
this,
|
||||
default?.let { Meta(it.asValue()) },
|
||||
MetaConverter.number,
|
||||
descriptorBuilder,
|
||||
getter = {
|
||||
val number = getter()
|
||||
Meta(number.asValue())
|
||||
}
|
||||
)
|
||||
|
||||
public fun DeviceBase.readingDouble(
|
||||
default: Number? = null,
|
||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||
getter: suspend () -> Double,
|
||||
): PropertyDelegateProvider<DeviceBase, TypedReadOnlyPropertyDelegate<Double>> = TypedReadOnlyDevicePropertyProvider(
|
||||
this,
|
||||
default?.let { Meta(it.asValue()) },
|
||||
MetaConverter.double,
|
||||
descriptorBuilder,
|
||||
getter = {
|
||||
val number = getter()
|
||||
Meta(number.asValue())
|
||||
}
|
||||
)
|
||||
|
||||
public fun DeviceBase.readingString(
|
||||
default: String? = null,
|
||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||
getter: suspend () -> String,
|
||||
): PropertyDelegateProvider<DeviceBase, TypedReadOnlyPropertyDelegate<String>> = TypedReadOnlyDevicePropertyProvider(
|
||||
this,
|
||||
default?.let { Meta(it.asValue()) },
|
||||
MetaConverter.string,
|
||||
descriptorBuilder,
|
||||
getter = {
|
||||
val number = getter()
|
||||
Meta(number.asValue())
|
||||
}
|
||||
)
|
||||
|
||||
public fun DeviceBase.readingBoolean(
|
||||
default: Boolean? = null,
|
||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||
getter: suspend () -> Boolean,
|
||||
): PropertyDelegateProvider<DeviceBase, TypedReadOnlyPropertyDelegate<Boolean>> = TypedReadOnlyDevicePropertyProvider(
|
||||
this,
|
||||
default?.let { Meta(it.asValue()) },
|
||||
MetaConverter.boolean,
|
||||
descriptorBuilder,
|
||||
getter = {
|
||||
val boolean = getter()
|
||||
Meta(boolean.asValue())
|
||||
}
|
||||
)
|
||||
|
||||
public fun DeviceBase.readingMeta(
|
||||
default: Meta? = null,
|
||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||
getter: suspend MutableMeta.() -> Unit,
|
||||
): PropertyDelegateProvider<DeviceBase, TypedReadOnlyPropertyDelegate<Meta>> = TypedReadOnlyDevicePropertyProvider(
|
||||
this,
|
||||
default,
|
||||
MetaConverter.meta,
|
||||
descriptorBuilder,
|
||||
getter = {
|
||||
Meta { getter() }
|
||||
}
|
||||
)
|
||||
|
||||
private fun DeviceBase.provideMutableProperty(name: String): ReadOnlyProperty<DeviceBase, DeviceProperty> =
|
||||
ReadOnlyProperty { _: DeviceBase, _: KProperty<*> ->
|
||||
return@ReadOnlyProperty properties[name] as DeviceProperty
|
||||
}
|
||||
|
||||
private fun <T : Any> DeviceBase.provideMutableProperty(
|
||||
name: String,
|
||||
converter: MetaConverter<T>,
|
||||
): ReadOnlyProperty<DeviceBase, TypedDeviceProperty<T>> =
|
||||
ReadOnlyProperty { _: DeviceBase, _: KProperty<*> ->
|
||||
return@ReadOnlyProperty TypedDeviceProperty(properties[name] as DeviceProperty, converter)
|
||||
}
|
||||
|
||||
public typealias PropertyDelegate = ReadOnlyProperty<DeviceBase, DeviceProperty>
|
||||
public typealias TypedPropertyDelegate<T> = ReadOnlyProperty<DeviceBase, TypedDeviceProperty<T>>
|
||||
|
||||
private class DevicePropertyProvider<D : DeviceBase>(
|
||||
val owner: D,
|
||||
val default: Meta?,
|
||||
val descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||
private val getter: suspend (Meta?) -> Meta,
|
||||
private val setter: suspend (oldValue: Meta?, newValue: Meta) -> Meta?,
|
||||
) : PropertyDelegateProvider<D, PropertyDelegate> {
|
||||
|
||||
override operator fun provideDelegate(thisRef: D, property: KProperty<*>): PropertyDelegate {
|
||||
val name = property.name
|
||||
owner.createMutableProperty(name, default, descriptorBuilder, getter, setter)
|
||||
return owner.provideMutableProperty(name)
|
||||
}
|
||||
}
|
||||
|
||||
private class TypedDevicePropertyProvider<D : DeviceBase, T : Any>(
|
||||
val owner: D,
|
||||
val default: Meta?,
|
||||
val converter: MetaConverter<T>,
|
||||
val descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||
private val getter: suspend (Meta?) -> Meta,
|
||||
private val setter: suspend (oldValue: Meta?, newValue: Meta) -> Meta?,
|
||||
) : PropertyDelegateProvider<D, TypedPropertyDelegate<T>> {
|
||||
|
||||
override operator fun provideDelegate(thisRef: D, property: KProperty<*>): TypedPropertyDelegate<T> {
|
||||
val name = property.name
|
||||
owner.createMutableProperty(name, default, descriptorBuilder, getter, setter)
|
||||
return owner.provideMutableProperty(name, converter)
|
||||
}
|
||||
}
|
||||
|
||||
public fun DeviceBase.writing(
|
||||
default: Meta? = null,
|
||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||
getter: suspend (Meta?) -> Meta,
|
||||
setter: suspend (oldValue: Meta?, newValue: Meta) -> Meta?,
|
||||
): PropertyDelegateProvider<DeviceBase, PropertyDelegate> = DevicePropertyProvider(
|
||||
this,
|
||||
default,
|
||||
descriptorBuilder,
|
||||
getter,
|
||||
setter
|
||||
)
|
||||
|
||||
public fun DeviceBase.writingVirtual(
|
||||
default: Meta,
|
||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||
): PropertyDelegateProvider<DeviceBase, PropertyDelegate> = writing(
|
||||
default,
|
||||
descriptorBuilder,
|
||||
getter = { it ?: default },
|
||||
setter = { _, newItem -> newItem }
|
||||
)
|
||||
|
||||
public fun DeviceBase.writingVirtual(
|
||||
default: Value,
|
||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||
): PropertyDelegateProvider<DeviceBase, PropertyDelegate> = writing(
|
||||
Meta(default),
|
||||
descriptorBuilder,
|
||||
getter = { it ?: Meta(default) },
|
||||
setter = { _, newItem -> newItem }
|
||||
)
|
||||
|
||||
public fun <D : DeviceBase> D.writingDouble(
|
||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||
getter: suspend (Double) -> Double,
|
||||
setter: suspend (oldValue: Double?, newValue: Double) -> Double?,
|
||||
): PropertyDelegateProvider<D, TypedPropertyDelegate<Double>> {
|
||||
val innerGetter: suspend (Meta?) -> Meta = {
|
||||
Meta(getter(it.double ?: Double.NaN).asValue())
|
||||
}
|
||||
|
||||
val innerSetter: suspend (oldValue: Meta?, newValue: Meta) -> Meta? = { oldValue, newValue ->
|
||||
setter(oldValue.double, newValue.double ?: Double.NaN)?.asMeta()
|
||||
}
|
||||
|
||||
return TypedDevicePropertyProvider(
|
||||
this,
|
||||
Meta(Double.NaN.asValue()),
|
||||
MetaConverter.double,
|
||||
descriptorBuilder,
|
||||
innerGetter,
|
||||
innerSetter
|
||||
)
|
||||
}
|
||||
|
||||
public fun <D : DeviceBase> D.writingBoolean(
|
||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||
getter: suspend (Boolean?) -> Boolean,
|
||||
setter: suspend (oldValue: Boolean?, newValue: Boolean) -> Boolean?,
|
||||
): PropertyDelegateProvider<D, TypedPropertyDelegate<Boolean>> {
|
||||
val innerGetter: suspend (Meta?) -> Meta = {
|
||||
Meta(getter(it.boolean).asValue())
|
||||
}
|
||||
|
||||
val innerSetter: suspend (oldValue: Meta?, newValue: Meta) -> Meta? = { oldValue, newValue ->
|
||||
setter(oldValue.boolean, newValue.boolean ?: error("Can't convert $newValue to boolean"))?.asValue()
|
||||
?.let { Meta(it) }
|
||||
}
|
||||
|
||||
return TypedDevicePropertyProvider(
|
||||
this,
|
||||
Meta(Null),
|
||||
MetaConverter.boolean,
|
||||
descriptorBuilder,
|
||||
innerGetter,
|
||||
innerSetter
|
||||
)
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package ru.mipt.npm.controls.properties
|
||||
package ru.mipt.npm.controls.spec
|
||||
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
@ -13,24 +13,15 @@ import space.kscience.dataforge.context.Global
|
||||
import space.kscience.dataforge.meta.Meta
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
/**
|
||||
* A device generated from specification
|
||||
* @param D recursive self-type for properties and actions
|
||||
*/
|
||||
|
||||
@OptIn(InternalDeviceAPI::class)
|
||||
public open class DeviceBySpec<D : DeviceBySpec<D>>(
|
||||
public val spec: DeviceSpec<D>,
|
||||
context: Context = Global,
|
||||
meta: Meta = Meta.EMPTY
|
||||
public abstract class DeviceBase<D : DeviceBase<D>>(
|
||||
override val context: Context = Global,
|
||||
public val meta: Meta = Meta.EMPTY
|
||||
) : Device {
|
||||
override var context: Context = context
|
||||
internal set
|
||||
|
||||
public var meta: Meta = meta
|
||||
internal set
|
||||
|
||||
public val properties: Map<String, DevicePropertySpec<D, *>> get() = spec.properties
|
||||
public val actions: Map<String, DeviceActionSpec<D, *, *>> get() = spec.actions
|
||||
public abstract val properties: Map<String, DevicePropertySpec<D, *>> //get() = spec.properties
|
||||
public abstract val actions: Map<String, DeviceActionSpec<D, *, *>> //get() = spec.actions
|
||||
|
||||
override val propertyDescriptors: Collection<PropertyDescriptor>
|
||||
get() = properties.values.map { it.descriptor }
|
||||
@ -68,6 +59,13 @@ public open class DeviceBySpec<D : DeviceBySpec<D>>(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update logical state using given [spec] and its convertor
|
||||
*/
|
||||
protected suspend fun <T> updateLogical(spec: DevicePropertySpec<D, T>, value: T) {
|
||||
updateLogical(spec.name, spec.converter.objectToMeta(value))
|
||||
}
|
||||
|
||||
/**
|
||||
* Force read physical value and push an update if it is changed. It does not matter if logical state is present.
|
||||
* The logical state is updated after read
|
||||
@ -98,7 +96,7 @@ public open class DeviceBySpec<D : DeviceBySpec<D>>(
|
||||
}
|
||||
|
||||
override suspend fun execute(action: String, argument: Meta?): Meta? =
|
||||
actions[action]?.executeMeta(self, argument)
|
||||
actions[action]?.executeWithMeta(self, argument)
|
||||
|
||||
/**
|
||||
* Read typed value and update/push event if needed
|
||||
@ -123,19 +121,20 @@ public open class DeviceBySpec<D : DeviceBySpec<D>>(
|
||||
}
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
with(spec) { self.onShutdown() }
|
||||
super.close()
|
||||
}
|
||||
public suspend operator fun <I, O> DeviceActionSpec<D, I, O>.invoke(input: I? = null): O? = execute(self, input)
|
||||
|
||||
}
|
||||
|
||||
public suspend fun <D : DeviceBySpec<D>, T : Any> D.read(
|
||||
propertySpec: DevicePropertySpec<D, T>
|
||||
): T = propertySpec.read()
|
||||
|
||||
public fun <D : DeviceBySpec<D>, T> D.write(
|
||||
propertySpec: WritableDevicePropertySpec<D, T>,
|
||||
value: T
|
||||
): Job = launch {
|
||||
propertySpec.write(value)
|
||||
/**
|
||||
* A device generated from specification
|
||||
* @param D recursive self-type for properties and actions
|
||||
*/
|
||||
public open class DeviceBySpec<D : DeviceBySpec<D>>(
|
||||
public val spec: DeviceSpec<D>,
|
||||
context: Context = Global,
|
||||
meta: Meta = Meta.EMPTY
|
||||
) : DeviceBase<D>(context, meta) {
|
||||
override val properties: Map<String, DevicePropertySpec<D, *>> get() = spec.properties
|
||||
override val actions: Map<String, DeviceActionSpec<D, *, *>> get() = spec.actions
|
||||
}
|
||||
|
@ -1,7 +1,14 @@
|
||||
package ru.mipt.npm.controls.properties
|
||||
package ru.mipt.npm.controls.spec
|
||||
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.filterIsInstance
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
import ru.mipt.npm.controls.api.ActionDescriptor
|
||||
import ru.mipt.npm.controls.api.Device
|
||||
import ru.mipt.npm.controls.api.PropertyChangedMessage
|
||||
import ru.mipt.npm.controls.api.PropertyDescriptor
|
||||
import space.kscience.dataforge.meta.Meta
|
||||
import space.kscience.dataforge.meta.transformations.MetaConverter
|
||||
@ -75,11 +82,48 @@ public interface DeviceActionSpec<in D : Device, I, O> {
|
||||
public suspend fun execute(device: D, input: I?): O?
|
||||
}
|
||||
|
||||
public suspend fun <D : Device, I, O> DeviceActionSpec<D, I, O>.executeMeta(
|
||||
public suspend fun <D : Device, I, O> DeviceActionSpec<D, I, O>.executeWithMeta(
|
||||
device: D,
|
||||
item: Meta?
|
||||
): Meta? {
|
||||
val arg = item?.let { inputConverter.metaToObject(item) }
|
||||
val res = execute(device, arg)
|
||||
return res?.let { outputConverter.objectToMeta(res) }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public suspend fun <D : DeviceBase<D>, T : Any> D.read(
|
||||
propertySpec: DevicePropertySpec<D, T>
|
||||
): T = propertySpec.read()
|
||||
|
||||
public suspend fun <D : Device, T : Any> D.read(
|
||||
propertySpec: DevicePropertySpec<D, T>
|
||||
): T = propertySpec.converter.metaToObject(readProperty(propertySpec.name))
|
||||
?: error("Property meta converter returned null")
|
||||
|
||||
public fun <D : Device, T> D.write(
|
||||
propertySpec: WritableDevicePropertySpec<D, T>,
|
||||
value: T
|
||||
): Job = launch {
|
||||
writeProperty(propertySpec.name, propertySpec.converter.objectToMeta(value))
|
||||
}
|
||||
|
||||
public fun <D : DeviceBase<D>, T> D.write(
|
||||
propertySpec: WritableDevicePropertySpec<D, T>,
|
||||
value: T
|
||||
): Job = launch {
|
||||
propertySpec.write(value)
|
||||
}
|
||||
|
||||
/**
|
||||
* A type safe property change listener
|
||||
*/
|
||||
public fun <D : Device, T> Device.onPropertyChange(
|
||||
spec: DevicePropertySpec<D, T>,
|
||||
callback: suspend PropertyChangedMessage.(T?) -> Unit
|
||||
): Job = messageFlow
|
||||
.filterIsInstance<PropertyChangedMessage>()
|
||||
.filter { it.property == spec.name }
|
||||
.onEach { change ->
|
||||
change.callback(spec.converter.metaToObject(change.value))
|
||||
}.launchIn(this)
|
@ -1,10 +1,9 @@
|
||||
package ru.mipt.npm.controls.properties
|
||||
package ru.mipt.npm.controls.spec
|
||||
|
||||
import kotlinx.coroutines.withContext
|
||||
import ru.mipt.npm.controls.api.ActionDescriptor
|
||||
import ru.mipt.npm.controls.api.Device
|
||||
import ru.mipt.npm.controls.api.PropertyDescriptor
|
||||
import space.kscience.dataforge.context.Context
|
||||
import space.kscience.dataforge.context.Factory
|
||||
import space.kscience.dataforge.meta.Meta
|
||||
import space.kscience.dataforge.meta.transformations.MetaConverter
|
||||
import kotlin.properties.PropertyDelegateProvider
|
||||
@ -14,9 +13,7 @@ import kotlin.reflect.KProperty
|
||||
import kotlin.reflect.KProperty1
|
||||
|
||||
@OptIn(InternalDeviceAPI::class)
|
||||
public abstract class DeviceSpec<D : DeviceBySpec<D>>(
|
||||
private val buildDevice: () -> D
|
||||
) : Factory<D> {
|
||||
public abstract class DeviceSpec<D : Device> {
|
||||
private val _properties = HashMap<String, DevicePropertySpec<D, *>>()
|
||||
public val properties: Map<String, DevicePropertySpec<D, *>> get() = _properties
|
||||
|
||||
@ -75,8 +72,8 @@ public abstract class DeviceSpec<D : DeviceBySpec<D>>(
|
||||
|
||||
public fun <T : Any> property(
|
||||
converter: MetaConverter<T>,
|
||||
name: String? = null,
|
||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||
name: String? = null,
|
||||
read: suspend D.() -> T
|
||||
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DevicePropertySpec<D, T>>> =
|
||||
PropertyDelegateProvider { _: DeviceSpec<D>, property ->
|
||||
@ -96,8 +93,8 @@ public abstract class DeviceSpec<D : DeviceBySpec<D>>(
|
||||
|
||||
public fun <T : Any> property(
|
||||
converter: MetaConverter<T>,
|
||||
name: String? = null,
|
||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||
name: String? = null,
|
||||
read: suspend D.() -> T,
|
||||
write: suspend D.(T) -> Unit
|
||||
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, WritableDevicePropertySpec<D, T>>> =
|
||||
@ -129,8 +126,8 @@ public abstract class DeviceSpec<D : DeviceBySpec<D>>(
|
||||
public fun <I : Any, O : Any> action(
|
||||
inputConverter: MetaConverter<I>,
|
||||
outputConverter: MetaConverter<O>,
|
||||
name: String? = null,
|
||||
descriptorBuilder: ActionDescriptor.() -> Unit = {},
|
||||
name: String? = null,
|
||||
execute: suspend D.(I?) -> O?
|
||||
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DeviceActionSpec<D, I, O>>> =
|
||||
PropertyDelegateProvider { _: DeviceSpec<D>, property ->
|
||||
@ -153,19 +150,35 @@ public abstract class DeviceSpec<D : DeviceBySpec<D>>(
|
||||
}
|
||||
|
||||
/**
|
||||
* The function is executed right after device initialization is finished
|
||||
* An action that takes [Meta] and returns [Meta]. No conversions are done
|
||||
*/
|
||||
public open fun D.onStartup() {}
|
||||
public fun metaAction(
|
||||
descriptorBuilder: ActionDescriptor.() -> Unit = {},
|
||||
name: String? = null,
|
||||
execute: suspend D.(Meta?) -> Meta?
|
||||
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DeviceActionSpec<D, Meta, Meta>>> = action(
|
||||
MetaConverter.Companion.meta,
|
||||
MetaConverter.Companion.meta,
|
||||
descriptorBuilder,
|
||||
name
|
||||
){
|
||||
execute(it)
|
||||
}
|
||||
|
||||
/**
|
||||
* The function is executed before device is shut down
|
||||
* An action that takes no parameters and returns no values
|
||||
*/
|
||||
public open fun D.onShutdown() {}
|
||||
|
||||
|
||||
override fun invoke(meta: Meta, context: Context): D = buildDevice().apply {
|
||||
this.context = context
|
||||
this.meta = meta
|
||||
onStartup()
|
||||
public fun unitAction(
|
||||
descriptorBuilder: ActionDescriptor.() -> Unit = {},
|
||||
name: String? = null,
|
||||
execute: suspend D.() -> Unit
|
||||
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DeviceActionSpec<D, Meta, Meta>>> = action(
|
||||
MetaConverter.Companion.meta,
|
||||
MetaConverter.Companion.meta,
|
||||
descriptorBuilder,
|
||||
name
|
||||
){
|
||||
execute()
|
||||
null
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package ru.mipt.npm.controls.properties
|
||||
package ru.mipt.npm.controls.spec
|
||||
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
@ -14,7 +14,7 @@ import kotlin.time.Duration
|
||||
*
|
||||
* The flow is canceled when the device scope is canceled
|
||||
*/
|
||||
public fun <D : DeviceBySpec<D>, R> D.readRecurring(interval: Duration, reader: suspend D.() -> R): Flow<R> = flow {
|
||||
public fun <D : DeviceBase<D>, R> D.readRecurring(interval: Duration, reader: suspend D.() -> R): Flow<R> = flow {
|
||||
while (isActive) {
|
||||
kotlinx.coroutines.delay(interval)
|
||||
emit(reader())
|
||||
@ -24,7 +24,7 @@ public fun <D : DeviceBySpec<D>, R> D.readRecurring(interval: Duration, reader:
|
||||
/**
|
||||
* Do a recurring task on a device. The task could
|
||||
*/
|
||||
public fun <D : DeviceBySpec<D>> D.doRecurring(interval: Duration, task: suspend D.() -> Unit): Job = launch {
|
||||
public fun <D : DeviceBase<D>> D.doRecurring(interval: Duration, task: suspend D.() -> Unit): Job = launch {
|
||||
while (isActive) {
|
||||
kotlinx.coroutines.delay(interval)
|
||||
task()
|
@ -1,4 +1,4 @@
|
||||
package ru.mipt.npm.controls.base
|
||||
package ru.mipt.npm.controls.spec
|
||||
|
||||
import space.kscience.dataforge.meta.Meta
|
||||
import space.kscience.dataforge.meta.double
|
@ -1,4 +1,4 @@
|
||||
package ru.mipt.npm.controls.properties
|
||||
package ru.mipt.npm.controls.spec
|
||||
|
||||
import ru.mipt.npm.controls.api.PropertyDescriptor
|
||||
import ru.mipt.npm.controls.api.metaDescriptor
|
||||
@ -10,19 +10,19 @@ import kotlin.properties.ReadOnlyProperty
|
||||
|
||||
//read only delegates
|
||||
|
||||
public fun <D : DeviceBySpec<D>> DeviceSpec<D>.booleanProperty(
|
||||
name: String? = null,
|
||||
public fun <D : DeviceBase<D>> DeviceSpec<D>.booleanProperty(
|
||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||
name: String? = null,
|
||||
read: suspend D.() -> Boolean
|
||||
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DevicePropertySpec<D, Boolean>>> = property(
|
||||
MetaConverter.boolean,
|
||||
name,
|
||||
{
|
||||
metaDescriptor {
|
||||
type(ValueType.BOOLEAN)
|
||||
}
|
||||
descriptorBuilder()
|
||||
},
|
||||
name,
|
||||
read
|
||||
)
|
||||
|
||||
@ -35,110 +35,110 @@ private inline fun numberDescriptor(
|
||||
descriptorBuilder()
|
||||
}
|
||||
|
||||
public fun <D : DeviceBySpec<D>> DeviceSpec<D>.numberProperty(
|
||||
public fun <D : DeviceBase<D>> DeviceSpec<D>.numberProperty(
|
||||
name: String? = null,
|
||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||
read: suspend D.() -> Number
|
||||
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DevicePropertySpec<D, Number>>> = property(
|
||||
MetaConverter.number,
|
||||
name,
|
||||
numberDescriptor(descriptorBuilder),
|
||||
name,
|
||||
read
|
||||
)
|
||||
|
||||
public fun <D : DeviceBySpec<D>> DeviceSpec<D>.doubleProperty(
|
||||
name: String? = null,
|
||||
public fun <D : DeviceBase<D>> DeviceSpec<D>.doubleProperty(
|
||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||
name: String? = null,
|
||||
read: suspend D.() -> Double
|
||||
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DevicePropertySpec<D, Double>>> = property(
|
||||
MetaConverter.double,
|
||||
name,
|
||||
numberDescriptor(descriptorBuilder),
|
||||
name,
|
||||
read
|
||||
)
|
||||
|
||||
public fun <D : DeviceBySpec<D>> DeviceSpec<D>.stringProperty(
|
||||
name: String? = null,
|
||||
public fun <D : DeviceBase<D>> DeviceSpec<D>.stringProperty(
|
||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||
name: String? = null,
|
||||
read: suspend D.() -> String
|
||||
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DevicePropertySpec<D, String>>> = property(
|
||||
MetaConverter.string,
|
||||
name,
|
||||
{
|
||||
metaDescriptor {
|
||||
type(ValueType.STRING)
|
||||
}
|
||||
descriptorBuilder()
|
||||
},
|
||||
name,
|
||||
read
|
||||
)
|
||||
|
||||
public fun <D : DeviceBySpec<D>> DeviceSpec<D>.metaProperty(
|
||||
name: String? = null,
|
||||
public fun <D : DeviceBase<D>> DeviceSpec<D>.metaProperty(
|
||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||
name: String? = null,
|
||||
read: suspend D.() -> Meta
|
||||
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DevicePropertySpec<D, Meta>>> = property(
|
||||
MetaConverter.meta,
|
||||
name,
|
||||
{
|
||||
metaDescriptor {
|
||||
type(ValueType.STRING)
|
||||
}
|
||||
descriptorBuilder()
|
||||
},
|
||||
name,
|
||||
read
|
||||
)
|
||||
|
||||
//read-write delegates
|
||||
|
||||
public fun <D : DeviceBySpec<D>> DeviceSpec<D>.booleanProperty(
|
||||
name: String? = null,
|
||||
public fun <D : DeviceBase<D>> DeviceSpec<D>.booleanProperty(
|
||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||
name: String? = null,
|
||||
read: suspend D.() -> Boolean,
|
||||
write: suspend D.(Boolean) -> Unit
|
||||
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, WritableDevicePropertySpec<D, Boolean>>> =
|
||||
property(
|
||||
MetaConverter.boolean,
|
||||
name,
|
||||
{
|
||||
metaDescriptor {
|
||||
type(ValueType.BOOLEAN)
|
||||
}
|
||||
descriptorBuilder()
|
||||
},
|
||||
name,
|
||||
read,
|
||||
write
|
||||
)
|
||||
|
||||
|
||||
public fun <D : DeviceBySpec<D>> DeviceSpec<D>.numberProperty(
|
||||
name: String? = null,
|
||||
public fun <D : DeviceBase<D>> DeviceSpec<D>.numberProperty(
|
||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||
name: String? = null,
|
||||
read: suspend D.() -> Number,
|
||||
write: suspend D.(Number) -> Unit
|
||||
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, WritableDevicePropertySpec<D, Number>>> =
|
||||
property(MetaConverter.number, name, numberDescriptor(descriptorBuilder), read, write)
|
||||
property(MetaConverter.number, numberDescriptor(descriptorBuilder), name, read, write)
|
||||
|
||||
public fun <D : DeviceBySpec<D>> DeviceSpec<D>.doubleProperty(
|
||||
name: String? = null,
|
||||
public fun <D : DeviceBase<D>> DeviceSpec<D>.doubleProperty(
|
||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||
name: String? = null,
|
||||
read: suspend D.() -> Double,
|
||||
write: suspend D.(Double) -> Unit
|
||||
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, WritableDevicePropertySpec<D, Double>>> =
|
||||
property(MetaConverter.double, name, numberDescriptor(descriptorBuilder), read, write)
|
||||
property(MetaConverter.double, numberDescriptor(descriptorBuilder), name, read, write)
|
||||
|
||||
public fun <D : DeviceBySpec<D>> DeviceSpec<D>.stringProperty(
|
||||
name: String? = null,
|
||||
public fun <D : DeviceBase<D>> DeviceSpec<D>.stringProperty(
|
||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||
name: String? = null,
|
||||
read: suspend D.() -> String,
|
||||
write: suspend D.(String) -> Unit
|
||||
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, WritableDevicePropertySpec<D, String>>> =
|
||||
property(MetaConverter.string, name, descriptorBuilder, read, write)
|
||||
property(MetaConverter.string, descriptorBuilder, name, read, write)
|
||||
|
||||
public fun <D : DeviceBySpec<D>> DeviceSpec<D>.metaProperty(
|
||||
name: String? = null,
|
||||
public fun <D : DeviceBase<D>> DeviceSpec<D>.metaProperty(
|
||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||
name: String? = null,
|
||||
read: suspend D.() -> Meta,
|
||||
write: suspend D.(Meta) -> Unit
|
||||
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, WritableDevicePropertySpec<D, Meta>>> =
|
||||
property(MetaConverter.meta, name, descriptorBuilder, read, write)
|
||||
property(MetaConverter.meta, descriptorBuilder, name, read, write)
|
@ -1,89 +0,0 @@
|
||||
package ru.mipt.npm.controls.controllers
|
||||
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import ru.mipt.npm.controls.base.*
|
||||
import space.kscience.dataforge.meta.Meta
|
||||
import space.kscience.dataforge.meta.transformations.MetaConverter
|
||||
import kotlin.properties.ReadOnlyProperty
|
||||
import kotlin.properties.ReadWriteProperty
|
||||
import kotlin.reflect.KProperty
|
||||
import kotlin.time.Duration
|
||||
|
||||
/**
|
||||
* Blocking read of the value
|
||||
*/
|
||||
public operator fun ReadOnlyDeviceProperty.getValue(thisRef: Any?, property: KProperty<*>): Meta =
|
||||
runBlocking(scope.coroutineContext) {
|
||||
read()
|
||||
}
|
||||
|
||||
public operator fun <T: Any> TypedReadOnlyDeviceProperty<T>.getValue(thisRef: Any?, property: KProperty<*>): T =
|
||||
runBlocking(scope.coroutineContext) {
|
||||
readTyped()
|
||||
}
|
||||
|
||||
public operator fun DeviceProperty.setValue(thisRef: Any?, property: KProperty<*>, value: Meta) {
|
||||
this.value = value
|
||||
}
|
||||
|
||||
public operator fun <T: Any> TypedDeviceProperty<T>.setValue(thisRef: Any?, property: KProperty<*>, value: T) {
|
||||
this.typedValue = value
|
||||
}
|
||||
|
||||
public fun <T : Any> ReadOnlyDeviceProperty.convert(
|
||||
metaConverter: MetaConverter<T>,
|
||||
forceRead: Boolean,
|
||||
): ReadOnlyProperty<Any?, T> {
|
||||
return ReadOnlyProperty { _, _ ->
|
||||
runBlocking(scope.coroutineContext) {
|
||||
val meta = read(forceRead)
|
||||
metaConverter.metaToObject(meta)?: error("Meta $meta could not be converted by $metaConverter")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public fun <T : Any> DeviceProperty.convert(
|
||||
metaConverter: MetaConverter<T>,
|
||||
forceRead: Boolean,
|
||||
): ReadWriteProperty<Any?, T> {
|
||||
return object : ReadWriteProperty<Any?, T> {
|
||||
override fun getValue(thisRef: Any?, property: KProperty<*>): T = runBlocking(scope.coroutineContext) {
|
||||
val meta = read(forceRead)
|
||||
metaConverter.metaToObject(meta)?: error("Meta $meta could not be converted by $metaConverter")
|
||||
}
|
||||
|
||||
override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
|
||||
this@convert.setValue(thisRef, property, value.let { metaConverter.objectToMeta(it) })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public fun ReadOnlyDeviceProperty.double(forceRead: Boolean = false): ReadOnlyProperty<Any?, Double> =
|
||||
convert(MetaConverter.double, forceRead)
|
||||
|
||||
public fun DeviceProperty.double(forceRead: Boolean = false): ReadWriteProperty<Any?, Double> =
|
||||
convert(MetaConverter.double, forceRead)
|
||||
|
||||
public fun ReadOnlyDeviceProperty.int(forceRead: Boolean = false): ReadOnlyProperty<Any?, Int> =
|
||||
convert(MetaConverter.int, forceRead)
|
||||
|
||||
public fun DeviceProperty.int(forceRead: Boolean = false): ReadWriteProperty<Any?, Int> =
|
||||
convert(MetaConverter.int, forceRead)
|
||||
|
||||
public fun ReadOnlyDeviceProperty.string(forceRead: Boolean = false): ReadOnlyProperty<Any?, String> =
|
||||
convert(MetaConverter.string, forceRead)
|
||||
|
||||
public fun DeviceProperty.string(forceRead: Boolean = false): ReadWriteProperty<Any?, String> =
|
||||
convert(MetaConverter.string, forceRead)
|
||||
|
||||
public fun ReadOnlyDeviceProperty.boolean(forceRead: Boolean = false): ReadOnlyProperty<Any?, Boolean> =
|
||||
convert(MetaConverter.boolean, forceRead)
|
||||
|
||||
public fun DeviceProperty.boolean(forceRead: Boolean = false): ReadWriteProperty<Any?, Boolean> =
|
||||
convert(MetaConverter.boolean, forceRead)
|
||||
|
||||
public fun ReadOnlyDeviceProperty.duration(forceRead: Boolean = false): ReadOnlyProperty<Any?, Duration> =
|
||||
convert(DurationConverter, forceRead)
|
||||
|
||||
public fun DeviceProperty.duration(forceRead: Boolean = false): ReadWriteProperty<Any?, Duration> =
|
||||
convert(DurationConverter, forceRead)
|
@ -1,10 +0,0 @@
|
||||
package ru.mipt.npm.controls.properties
|
||||
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
||||
/**
|
||||
* Blocking property get call
|
||||
*/
|
||||
public operator fun <D : DeviceBySpec<D>, T : Any> D.get(
|
||||
propertySpec: DevicePropertySpec<D, T>
|
||||
): T = runBlocking { read(propertySpec) }
|
@ -0,0 +1,10 @@
|
||||
package ru.mipt.npm.controls.spec
|
||||
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
||||
/**
|
||||
* Blocking property get call
|
||||
*/
|
||||
public operator fun <D : DeviceBase<D>, T : Any> D.get(
|
||||
propertySpec: DevicePropertySpec<D, T>
|
||||
): T = runBlocking { read(propertySpec) }
|
@ -4,8 +4,8 @@ import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.eclipse.milo.opcua.sdk.client.OpcUaClient
|
||||
import org.eclipse.milo.opcua.stack.core.types.builtin.NodeId
|
||||
import ru.mipt.npm.controls.properties.DeviceBySpec
|
||||
import ru.mipt.npm.controls.properties.DeviceSpec
|
||||
import ru.mipt.npm.controls.spec.DeviceBySpec
|
||||
import ru.mipt.npm.controls.spec.DeviceSpec
|
||||
import space.kscience.dataforge.context.Context
|
||||
import space.kscience.dataforge.context.Global
|
||||
import space.kscience.dataforge.meta.Meta
|
||||
|
@ -5,7 +5,7 @@ plugins {
|
||||
}
|
||||
|
||||
|
||||
repositories{
|
||||
repositories {
|
||||
mavenCentral()
|
||||
jcenter()
|
||||
maven("https://repo.kotlin.link")
|
||||
@ -15,7 +15,7 @@ repositories{
|
||||
val ktorVersion: String by rootProject.extra
|
||||
val rsocketVersion: String by rootProject.extra
|
||||
|
||||
dependencies{
|
||||
dependencies {
|
||||
implementation(projects.controlsCore)
|
||||
//implementation(projects.controlsServer)
|
||||
implementation(projects.magix.magixServer)
|
||||
@ -34,15 +34,15 @@ dependencies{
|
||||
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach {
|
||||
kotlinOptions {
|
||||
jvmTarget = "11"
|
||||
freeCompilerArgs = freeCompilerArgs + "-Xjvm-default=all"
|
||||
freeCompilerArgs = freeCompilerArgs + listOf("-Xjvm-default=all", "-Xopt-in=kotlin.RequiresOptIn")
|
||||
}
|
||||
}
|
||||
|
||||
javafx{
|
||||
javafx {
|
||||
version = "14"
|
||||
modules("javafx.controls")
|
||||
}
|
||||
|
||||
application{
|
||||
application {
|
||||
mainClass.set("ru.mipt.npm.controls.demo.DemoControllerViewKt")
|
||||
}
|
@ -1,22 +1,47 @@
|
||||
package ru.mipt.npm.controls.demo
|
||||
|
||||
import kotlinx.coroutines.launch
|
||||
import ru.mipt.npm.controls.properties.*
|
||||
import ru.mipt.npm.controls.api.metaDescriptor
|
||||
import ru.mipt.npm.controls.spec.*
|
||||
import space.kscience.dataforge.context.Context
|
||||
import space.kscience.dataforge.context.Factory
|
||||
import space.kscience.dataforge.meta.Meta
|
||||
import space.kscience.dataforge.meta.descriptors.value
|
||||
import space.kscience.dataforge.meta.transformations.MetaConverter
|
||||
import space.kscience.dataforge.values.ValueType
|
||||
import java.time.Instant
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.ExperimentalTime
|
||||
|
||||
|
||||
class DemoDevice : DeviceBySpec<DemoDevice>(DemoDevice) {
|
||||
class DemoDevice(context: Context, meta: Meta) : DeviceBySpec<DemoDevice>(DemoDevice, context, meta) {
|
||||
private var timeScaleState = 5000.0
|
||||
private var sinScaleState = 1.0
|
||||
private var cosScaleState = 1.0
|
||||
|
||||
companion object : DeviceSpec<DemoDevice>(::DemoDevice) {
|
||||
@OptIn(ExperimentalTime::class)
|
||||
override suspend fun open() {
|
||||
super.open()
|
||||
launch {
|
||||
sinScale.read()
|
||||
cosScale.read()
|
||||
timeScale.read()
|
||||
}
|
||||
doRecurring(Duration.milliseconds(50)) {
|
||||
coordinates.read()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
companion object : DeviceSpec<DemoDevice>(), Factory<DemoDevice> {
|
||||
// register virtual properties based on actual object state
|
||||
val timeScale by property(MetaConverter.double, DemoDevice::timeScaleState)
|
||||
val timeScale by property(MetaConverter.double, DemoDevice::timeScaleState) {
|
||||
metaDescriptor {
|
||||
type(ValueType.NUMBER)
|
||||
}
|
||||
info = "Real to virtual time scale"
|
||||
}
|
||||
|
||||
val sinScale by property(MetaConverter.double, DemoDevice::sinScaleState)
|
||||
val cosScale by property(MetaConverter.double, DemoDevice::cosScaleState)
|
||||
|
||||
@ -30,7 +55,13 @@ class DemoDevice : DeviceBySpec<DemoDevice>(DemoDevice) {
|
||||
kotlin.math.cos(time.toEpochMilli().toDouble() / timeScaleState) * sinScaleState
|
||||
}
|
||||
|
||||
val coordinates by metaProperty {
|
||||
val coordinates by metaProperty(
|
||||
descriptorBuilder = {
|
||||
metaDescriptor {
|
||||
value("time", ValueType.NUMBER)
|
||||
}
|
||||
}
|
||||
) {
|
||||
Meta {
|
||||
val time = Instant.now()
|
||||
"time" put time.toEpochMilli()
|
||||
@ -46,15 +77,6 @@ class DemoDevice : DeviceBySpec<DemoDevice>(DemoDevice) {
|
||||
null
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalTime::class)
|
||||
override fun DemoDevice.onStartup() {
|
||||
launch {
|
||||
sinScale.read()
|
||||
cosScale.read()
|
||||
}
|
||||
doRecurring(Duration.milliseconds(50)){
|
||||
coordinates.read()
|
||||
}
|
||||
}
|
||||
override fun invoke(meta: Meta, context: Context): DemoDevice = DemoDevice(context, meta)
|
||||
}
|
||||
}
|
@ -13,6 +13,9 @@ import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import ru.mipt.npm.controls.controllers.DeviceManager
|
||||
import ru.mipt.npm.controls.controllers.installing
|
||||
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.dataforge.context.Global
|
||||
import space.kscience.dataforge.context.fetch
|
||||
import tornadofx.*
|
||||
@ -40,28 +43,30 @@ fun VBox.piMotionMasterAxis(
|
||||
alignment = Pos.CENTER
|
||||
label(axisName)
|
||||
coroutineScope.launch {
|
||||
val min = axis.minPosition.readTyped(true)
|
||||
val max = axis.maxPosition.readTyped(true)
|
||||
val positionProperty = axis.position.fxProperty(axis)
|
||||
val startPosition = axis.position.readTyped(true)
|
||||
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)
|
||||
with(axis) {
|
||||
val min = minPosition.read()
|
||||
val max = maxPosition.read()
|
||||
val positionProperty = fxProperty(position)
|
||||
val startPosition = position.read()
|
||||
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)
|
||||
slider(min..max) {
|
||||
isDisable = true
|
||||
valueProperty().bind(positionProperty)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -82,7 +87,7 @@ class PiMotionMasterView : View() {
|
||||
private val controller: PiMotionMasterController by inject()
|
||||
val device = controller.motionMaster
|
||||
|
||||
private val connectedProperty: ReadOnlyProperty<Boolean> = device.connected.fxProperty(device)
|
||||
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>>()
|
||||
|
@ -13,13 +13,13 @@ import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withTimeout
|
||||
import ru.mipt.npm.controls.api.DeviceHub
|
||||
import ru.mipt.npm.controls.api.PropertyDescriptor
|
||||
import ru.mipt.npm.controls.base.*
|
||||
import ru.mipt.npm.controls.controllers.duration
|
||||
import ru.mipt.npm.controls.ports.*
|
||||
import ru.mipt.npm.controls.spec.*
|
||||
import space.kscience.dataforge.context.*
|
||||
import space.kscience.dataforge.meta.Meta
|
||||
import space.kscience.dataforge.meta.double
|
||||
import space.kscience.dataforge.meta.get
|
||||
import space.kscience.dataforge.meta.transformations.MetaConverter
|
||||
import space.kscience.dataforge.names.NameToken
|
||||
import space.kscience.dataforge.values.asValue
|
||||
import kotlin.collections.component1
|
||||
@ -29,70 +29,20 @@ import kotlin.time.Duration
|
||||
class PiMotionMasterDevice(
|
||||
context: Context,
|
||||
private val portFactory: PortFactory = KtorTcpPort,
|
||||
) : DeviceBase(context), DeviceHub {
|
||||
) : DeviceBySpec<PiMotionMasterDevice>(PiMotionMasterDevice, context), DeviceHub {
|
||||
|
||||
private var port: Port? = null
|
||||
//TODO make proxy work
|
||||
//PortProxy { portFactory(address ?: error("The device is not connected"), context) }
|
||||
|
||||
|
||||
val connected by readingBoolean(false, descriptorBuilder = {
|
||||
info = "True if the connection address is defined and the device is initialized"
|
||||
}) {
|
||||
port != null
|
||||
}
|
||||
|
||||
|
||||
val connect: DeviceAction by acting({
|
||||
info = "Connect to specific port and initialize axis"
|
||||
}) { portSpec ->
|
||||
//Clear current actions if present
|
||||
if (port != null) {
|
||||
disconnect()
|
||||
}
|
||||
//Update port
|
||||
//address = portSpec.node
|
||||
port = portFactory(portSpec ?: Meta.EMPTY, context)
|
||||
connected.updateLogical(true)
|
||||
// connector.open()
|
||||
//Initialize axes
|
||||
if (portSpec != null) {
|
||||
val idn = identity.read()
|
||||
failIfError { "Can't connect to $portSpec. Error code: $it" }
|
||||
logger.info { "Connected to $idn on $portSpec" }
|
||||
val ids = request("SAI?").map { it.trim() }
|
||||
if (ids != axes.keys.toList()) {
|
||||
//re-define axes if needed
|
||||
axes = ids.associateWith { Axis(it) }
|
||||
}
|
||||
Meta(ids.map { it.asValue() }.asValue())
|
||||
initialize()
|
||||
failIfError()
|
||||
}
|
||||
}
|
||||
|
||||
val disconnect: DeviceAction by acting({
|
||||
info = "Disconnect the program from the device if it is connected"
|
||||
}) {
|
||||
if (port != null) {
|
||||
stop()
|
||||
port?.close()
|
||||
}
|
||||
port = null
|
||||
connected.updateLogical(false)
|
||||
}
|
||||
|
||||
fun disconnect() {
|
||||
runBlocking {
|
||||
disconnect.invoke()
|
||||
}
|
||||
}
|
||||
|
||||
val timeout: DeviceProperty by writingVirtual(200.asValue()) {
|
||||
info = "Timeout"
|
||||
}
|
||||
|
||||
var timeoutValue: Duration by timeout.duration()
|
||||
var timeoutValue: Duration = Duration.microseconds(200)
|
||||
|
||||
/**
|
||||
* Name-friendly accessor for axis
|
||||
@ -182,166 +132,225 @@ class PiMotionMasterDevice(
|
||||
}
|
||||
}
|
||||
|
||||
val initialize: DeviceAction by acting {
|
||||
send("INI")
|
||||
}
|
||||
companion object : DeviceSpec<PiMotionMasterDevice>(), Factory<PiMotionMasterDevice> {
|
||||
|
||||
val identity: ReadOnlyDeviceProperty by readingString {
|
||||
request("*IDN?").first()
|
||||
}
|
||||
override fun invoke(meta: Meta, context: Context): PiMotionMasterDevice = PiMotionMasterDevice(context)
|
||||
|
||||
val firmwareVersion: ReadOnlyDeviceProperty by readingString {
|
||||
request("VER?").first()
|
||||
}
|
||||
val connected by booleanProperty(descriptorBuilder = {
|
||||
info = "True if the connection address is defined and the device is initialized"
|
||||
}) {
|
||||
port != null
|
||||
}
|
||||
|
||||
val stop: DeviceAction by acting(
|
||||
descriptorBuilder = {
|
||||
|
||||
val initialize by unitAction {
|
||||
send("INI")
|
||||
}
|
||||
|
||||
val identity by stringProperty {
|
||||
request("*IDN?").first()
|
||||
}
|
||||
|
||||
val firmwareVersion by stringProperty {
|
||||
request("VER?").first()
|
||||
}
|
||||
|
||||
val stop by unitAction({
|
||||
info = "Stop all axis"
|
||||
},
|
||||
action = { send("STP") }
|
||||
)
|
||||
}) {
|
||||
send("STP")
|
||||
}
|
||||
|
||||
inner class Axis(val axisId: String) : DeviceBase(context) {
|
||||
val connect by metaAction(descriptorBuilder = {
|
||||
info = "Connect to specific port and initialize axis"
|
||||
}) { portSpec ->
|
||||
//Clear current actions if present
|
||||
if (port != null) {
|
||||
disconnect()
|
||||
}
|
||||
//Update port
|
||||
//address = portSpec.node
|
||||
port = portFactory(portSpec ?: Meta.EMPTY, context)
|
||||
updateLogical(connected, true)
|
||||
// connector.open()
|
||||
//Initialize axes
|
||||
if (portSpec != null) {
|
||||
val idn = identity.read()
|
||||
failIfError { "Can't connect to $portSpec. Error code: $it" }
|
||||
logger.info { "Connected to $idn on $portSpec" }
|
||||
val ids = request("SAI?").map { it.trim() }
|
||||
if (ids != axes.keys.toList()) {
|
||||
//re-define axes if needed
|
||||
axes = ids.associateWith { Axis(this, it) }
|
||||
}
|
||||
Meta(ids.map { it.asValue() }.asValue())
|
||||
initialize()
|
||||
failIfError()
|
||||
}
|
||||
null
|
||||
}
|
||||
|
||||
val disconnect by metaAction({
|
||||
info = "Disconnect the program from the device if it is connected"
|
||||
}) {
|
||||
if (port != null) {
|
||||
stop()
|
||||
port?.close()
|
||||
}
|
||||
port = null
|
||||
updateLogical(connected, false)
|
||||
null
|
||||
}
|
||||
|
||||
|
||||
val timeout by property(MetaConverter.duration, PiMotionMasterDevice::timeoutValue) {
|
||||
info = "Timeout"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class Axis(
|
||||
val mm: PiMotionMasterDevice,
|
||||
val axisId: String
|
||||
) : DeviceBySpec<Axis>(Axis, mm.context) {
|
||||
|
||||
/**
|
||||
* TODO Move to head device and abstract
|
||||
*/
|
||||
private suspend fun readAxisBoolean(command: String): Boolean =
|
||||
requestAndParse(command, axisId)[axisId]?.toIntOrNull()
|
||||
?: error("Malformed $command response. Should include integer value for $axisId") != 0
|
||||
(mm.requestAndParse(command, axisId)[axisId]?.toIntOrNull()
|
||||
?: error("Malformed $command response. Should include integer value for $axisId")) != 0
|
||||
|
||||
/**
|
||||
* TODO Move to head device and abstract
|
||||
*/
|
||||
private suspend fun writeAxisBoolean(command: String, value: Boolean): Boolean {
|
||||
val boolean = if (value) {
|
||||
"1"
|
||||
} else {
|
||||
"0"
|
||||
}
|
||||
send(command, axisId, boolean)
|
||||
failIfError()
|
||||
mm.send(command, axisId, boolean)
|
||||
mm.failIfError()
|
||||
return value
|
||||
}
|
||||
|
||||
private fun axisBooleanProperty(command: String, descriptorBuilder: PropertyDescriptor.() -> Unit = {}) =
|
||||
writingBoolean(
|
||||
getter = { readAxisBoolean("$command?") },
|
||||
setter = { _, newValue ->
|
||||
writeAxisBoolean(command, newValue)
|
||||
},
|
||||
descriptorBuilder = descriptorBuilder
|
||||
)
|
||||
|
||||
private fun axisNumberProperty(command: String, descriptorBuilder: PropertyDescriptor.() -> Unit = {}) =
|
||||
writingDouble(
|
||||
getter = {
|
||||
requestAndParse("$command?", axisId)[axisId]?.toDoubleOrNull()
|
||||
?: error("Malformed $command response. Should include float value for $axisId")
|
||||
},
|
||||
setter = { _, newValue ->
|
||||
send(command, axisId, newValue.toString())
|
||||
failIfError()
|
||||
newValue
|
||||
},
|
||||
descriptorBuilder = descriptorBuilder
|
||||
)
|
||||
|
||||
val enabled by axisBooleanProperty("EAX") {
|
||||
info = "Motor enable state."
|
||||
}
|
||||
|
||||
val halt: DeviceAction by acting {
|
||||
send("HLT", axisId)
|
||||
}
|
||||
|
||||
val targetPosition by axisNumberProperty("MOV") {
|
||||
info = """
|
||||
Sets a new absolute target position for the specified axis.
|
||||
Servo mode must be switched on for the commanded axis prior to using this command (closed-loop operation).
|
||||
""".trimIndent()
|
||||
}
|
||||
|
||||
val onTarget: TypedReadOnlyDeviceProperty<Boolean> by readingBoolean(
|
||||
descriptorBuilder = {
|
||||
info = "Queries the on-target state of the specified axis."
|
||||
},
|
||||
getter = {
|
||||
readAxisBoolean("ONT?")
|
||||
}
|
||||
)
|
||||
|
||||
val reference: ReadOnlyDeviceProperty by readingBoolean(
|
||||
descriptorBuilder = {
|
||||
info = "Get Referencing Result"
|
||||
},
|
||||
getter = {
|
||||
readAxisBoolean("FRF?")
|
||||
}
|
||||
)
|
||||
|
||||
val moveToReference by acting {
|
||||
send("FRF", axisId)
|
||||
}
|
||||
|
||||
val minPosition by readingDouble(
|
||||
descriptorBuilder = {
|
||||
info = "Minimal position value for the axis"
|
||||
},
|
||||
getter = {
|
||||
requestAndParse("TMN?", axisId)[axisId]?.toDoubleOrNull()
|
||||
?: error("Malformed `TMN?` response. Should include float value for $axisId")
|
||||
}
|
||||
)
|
||||
|
||||
val maxPosition by readingDouble(
|
||||
descriptorBuilder = {
|
||||
info = "Maximal position value for the axis"
|
||||
},
|
||||
getter = {
|
||||
requestAndParse("TMX?", axisId)[axisId]?.toDoubleOrNull()
|
||||
?: error("Malformed `TMX?` response. Should include float value for $axisId")
|
||||
}
|
||||
)
|
||||
|
||||
val position by readingDouble(
|
||||
descriptorBuilder = {
|
||||
info = "The current axis position."
|
||||
},
|
||||
getter = {
|
||||
requestAndParse("POS?", axisId)[axisId]?.toDoubleOrNull()
|
||||
?: error("Malformed `POS?` response. Should include float value for $axisId")
|
||||
}
|
||||
)
|
||||
|
||||
val openLoopTarget: DeviceProperty by axisNumberProperty("OMA") {
|
||||
info = "Position for open-loop operation."
|
||||
}
|
||||
|
||||
val closedLoop: TypedDeviceProperty<Boolean> by axisBooleanProperty("SVO") {
|
||||
info = "Servo closed loop mode"
|
||||
}
|
||||
|
||||
val velocity: TypedDeviceProperty<Double> by axisNumberProperty("VEL") {
|
||||
info = "Velocity value for closed-loop operation"
|
||||
}
|
||||
|
||||
val move by acting {
|
||||
val target = it.double ?: it?.get("target").double ?: error("Unacceptable target value $it")
|
||||
closedLoop.write(true)
|
||||
//optionally set velocity
|
||||
it?.get("velocity").double?.let { v ->
|
||||
velocity.write(v)
|
||||
}
|
||||
targetPosition.write(target)
|
||||
//read `onTarget` and `position` properties in a cycle until movement is complete
|
||||
while (!onTarget.readTyped(true)) {
|
||||
position.read(true)
|
||||
delay(200)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun move(target: Double) {
|
||||
move(target.asMeta())
|
||||
}
|
||||
}
|
||||
|
||||
companion object : Factory<PiMotionMasterDevice> {
|
||||
override fun invoke(meta: Meta, context: Context): PiMotionMasterDevice = PiMotionMasterDevice(context)
|
||||
companion object : DeviceSpec<Axis>() {
|
||||
|
||||
private fun axisBooleanProperty(
|
||||
command: String,
|
||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {}
|
||||
) = booleanProperty(
|
||||
read = {
|
||||
readAxisBoolean("$command?")
|
||||
},
|
||||
write = {
|
||||
writeAxisBoolean(command, it)
|
||||
},
|
||||
descriptorBuilder = descriptorBuilder
|
||||
)
|
||||
|
||||
private fun axisNumberProperty(
|
||||
command: String,
|
||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {}
|
||||
) = doubleProperty(
|
||||
read = {
|
||||
mm.requestAndParse("$command?", axisId)[axisId]?.toDoubleOrNull()
|
||||
?: error("Malformed $command response. Should include float value for $axisId")
|
||||
},
|
||||
write = { newValue ->
|
||||
mm.send(command, axisId, newValue.toString())
|
||||
mm.failIfError()
|
||||
},
|
||||
descriptorBuilder = descriptorBuilder
|
||||
)
|
||||
|
||||
val enabled by axisBooleanProperty("EAX") {
|
||||
info = "Motor enable state."
|
||||
}
|
||||
|
||||
val halt by unitAction {
|
||||
mm.send("HLT", axisId)
|
||||
}
|
||||
|
||||
val targetPosition by axisNumberProperty("MOV") {
|
||||
info = """
|
||||
Sets a new absolute target position for the specified axis.
|
||||
Servo mode must be switched on for the commanded axis prior to using this command (closed-loop operation).
|
||||
""".trimIndent()
|
||||
}
|
||||
|
||||
val onTarget by booleanProperty({
|
||||
info = "Queries the on-target state of the specified axis."
|
||||
}) {
|
||||
readAxisBoolean("ONT?")
|
||||
}
|
||||
|
||||
val reference by booleanProperty({
|
||||
info = "Get Referencing Result"
|
||||
}) {
|
||||
readAxisBoolean("FRF?")
|
||||
}
|
||||
|
||||
val moveToReference by unitAction {
|
||||
mm.send("FRF", axisId)
|
||||
}
|
||||
|
||||
val minPosition by doubleProperty({
|
||||
info = "Minimal position value for the axis"
|
||||
}) {
|
||||
mm.requestAndParse("TMN?", axisId)[axisId]?.toDoubleOrNull()
|
||||
?: error("Malformed `TMN?` response. Should include float value for $axisId")
|
||||
}
|
||||
|
||||
val maxPosition by doubleProperty({
|
||||
info = "Maximal position value for the axis"
|
||||
}) {
|
||||
mm.requestAndParse("TMX?", axisId)[axisId]?.toDoubleOrNull()
|
||||
?: error("Malformed `TMX?` response. Should include float value for $axisId")
|
||||
}
|
||||
|
||||
val position by doubleProperty({
|
||||
info = "The current axis position."
|
||||
}) {
|
||||
mm.requestAndParse("POS?", axisId)[axisId]?.toDoubleOrNull()
|
||||
?: error("Malformed `POS?` response. Should include float value for $axisId")
|
||||
}
|
||||
|
||||
val openLoopTarget by axisNumberProperty("OMA") {
|
||||
info = "Position for open-loop operation."
|
||||
}
|
||||
|
||||
val closedLoop by axisBooleanProperty("SVO") {
|
||||
info = "Servo closed loop mode"
|
||||
}
|
||||
|
||||
val velocity by axisNumberProperty("VEL") {
|
||||
info = "Velocity value for closed-loop operation"
|
||||
}
|
||||
|
||||
val move by metaAction {
|
||||
val target = it.double ?: it?.get("target").double ?: error("Unacceptable target value $it")
|
||||
closedLoop.write(true)
|
||||
//optionally set velocity
|
||||
it?.get("velocity").double?.let { v ->
|
||||
velocity.write(v)
|
||||
}
|
||||
targetPosition.write(target)
|
||||
//read `onTarget` and `position` properties in a cycle until movement is complete
|
||||
while (!onTarget.read()) {
|
||||
position.read()
|
||||
delay(200)
|
||||
}
|
||||
null
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -3,58 +3,67 @@ package ru.mipt.npm.devices.pimotionmaster
|
||||
import javafx.beans.property.ObjectPropertyBase
|
||||
import javafx.beans.property.Property
|
||||
import javafx.beans.property.ReadOnlyProperty
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import ru.mipt.npm.controls.api.Device
|
||||
import ru.mipt.npm.controls.base.TypedDeviceProperty
|
||||
import ru.mipt.npm.controls.base.TypedReadOnlyDeviceProperty
|
||||
import ru.mipt.npm.controls.spec.DevicePropertySpec
|
||||
import ru.mipt.npm.controls.spec.WritableDevicePropertySpec
|
||||
import ru.mipt.npm.controls.spec.onPropertyChange
|
||||
import ru.mipt.npm.controls.spec.write
|
||||
import space.kscience.dataforge.context.info
|
||||
import space.kscience.dataforge.context.logger
|
||||
import tornadofx.*
|
||||
|
||||
fun <T : Any> TypedReadOnlyDeviceProperty<T>.fxProperty(ownerDevice: Device?): ReadOnlyProperty<T> =
|
||||
object : ObjectPropertyBase<T>() {
|
||||
override fun getBean(): Any? = ownerDevice
|
||||
override fun getName(): String = this@fxProperty.name
|
||||
/**
|
||||
* Bind a FX property to a device property with a given [spec]
|
||||
*/
|
||||
fun <D : Device, T : Any> Device.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
|
||||
flowTyped().onEach {
|
||||
if (it != null) {
|
||||
runLater {
|
||||
init {
|
||||
//Read incoming changes
|
||||
onPropertyChange(spec) {
|
||||
if (it != null) {
|
||||
runLater {
|
||||
try {
|
||||
set(it)
|
||||
} catch (ex: Throwable) {
|
||||
logger.info { "Failed to set property $name to $it" }
|
||||
}
|
||||
} else {
|
||||
invalidated()
|
||||
}
|
||||
}.catch {
|
||||
ownerDevice?.logger?.info { "Failed to set property $name to $it" }
|
||||
}.launchIn(scope)
|
||||
}
|
||||
}
|
||||
|
||||
fun <T : Any> TypedDeviceProperty<T>.fxProperty(ownerDevice: Device?): Property<T> =
|
||||
object : ObjectPropertyBase<T>() {
|
||||
override fun getBean(): Any? = ownerDevice
|
||||
override fun getName(): String = this@fxProperty.name
|
||||
|
||||
init {
|
||||
//Read incoming changes
|
||||
flowTyped().onEach {
|
||||
if (it != null) {
|
||||
runLater {
|
||||
set(it)
|
||||
}
|
||||
} else {
|
||||
invalidated()
|
||||
}
|
||||
}.catch {
|
||||
ownerDevice?.logger?.info { "Failed to set property $name to $it" }
|
||||
}.launchIn(scope)
|
||||
|
||||
onChange {
|
||||
typedValue = it
|
||||
} else {
|
||||
invalidated()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun <D : Device, T : Any> D.fxProperty(spec: WritableDevicePropertySpec<D, T>): Property<T> =
|
||||
object : ObjectPropertyBase<T>() {
|
||||
override fun getBean(): Any = this
|
||||
override fun getName(): String = spec.name
|
||||
|
||||
init {
|
||||
//Read incoming changes
|
||||
onPropertyChange(spec) {
|
||||
if (it != null) {
|
||||
runLater {
|
||||
try {
|
||||
set(it)
|
||||
} catch (ex: Throwable) {
|
||||
logger.info { "Failed to set property $name to $it" }
|
||||
}
|
||||
}
|
||||
} else {
|
||||
invalidated()
|
||||
}
|
||||
}
|
||||
|
||||
onChange { newValue ->
|
||||
if (newValue != null) {
|
||||
write(spec, newValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4,7 +4,7 @@ enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")
|
||||
enableFeaturePreview("VERSION_CATALOGS")
|
||||
|
||||
pluginManagement {
|
||||
val toolsVersion = "0.10.4"
|
||||
val toolsVersion = "0.10.5"
|
||||
|
||||
repositories {
|
||||
maven("https://repo.kotlin.link")
|
||||
@ -28,7 +28,7 @@ dependencyResolutionManagement {
|
||||
|
||||
versionCatalogs {
|
||||
create("npm") {
|
||||
from("ru.mipt.npm:version-catalog:0.10.4")
|
||||
from("ru.mipt.npm:version-catalog:0.10.5")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user